feat(presence): F-5b presence-sync — mirror sm.Machine into Fabric

Adds an internal/presence package that ticks every 30s (configurable
via presence_interval_seconds), reads each bound agent's sm.Machine
state through host.ReadAgentState, maps to Fabric's 6-status enum,
and PUTs diffs to /api/agents/:userId/presence on every guild the
agent belongs to.

Semantic mapping (the part flagged "needed" in the prior README):
  idle    → idle
  working → on_call
  busy    → busy
  offline → offline
exhausted/unknown reserved for backend-side fallbacks; we don't push.

Tick is mutex-guarded (avoids the upsert race the openclaw incident
called out in agent-presence.service.ts) and diff-gated so writes are
sparse. Token-cache invalidation on PUT failure handles guild JWT
rotation.

fabric.Client gains SetAgentPresence helper. README marks F-5b .
This commit is contained in:
h z
2026-06-01 08:40:17 +01:00
parent 76a3bfbedb
commit 7911cc6320
5 changed files with 291 additions and 5 deletions

View File

@@ -325,6 +325,23 @@ func (c *Client) RemoveCanvas(ctx context.Context, guildEndpoint, guildToken, ch
return err
}
// SetAgentPresence pushes one agent's presence to the guild. Body
// shape mirrors PUT /api/agents/:userId/presence:
//
// { "status": "idle"|"on_call"|"busy"|"exhausted"|"offline"|"unknown",
// "source": "<debug-tag>" }
//
// userID is the agent's Fabric Center user UUID (not the Plexum agent
// id). The guild-side ApiKeyGuard accepts the per-guild access JWT —
// caller must already hold a fresh one.
func (c *Client) SetAgentPresence(ctx context.Context, guildEndpoint, guildToken, userID, status, source string) error {
body := map[string]any{"status": status, "source": source}
_, err := c.do(ctx, http.MethodPut,
guildEndpoint+"/api/agents/"+url.PathEscape(userID)+"/presence",
guildToken, body, nil)
return err
}
// SyncCommands PUTs the agent's slash-command catalog onto the guild
// (idempotent full replace). Needs the guild's commands-sync key, which
// the operator sources from the guild config.