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

@@ -29,6 +29,7 @@ import (
"git.hangman-lab.top/hzhang/Plexum-fabric-channel-plugin/internal/fabric"
"git.hangman-lab.top/hzhang/Plexum-fabric-channel-plugin/internal/identity"
"git.hangman-lab.top/hzhang/Plexum-fabric-channel-plugin/internal/inbound"
"git.hangman-lab.top/hzhang/Plexum-fabric-channel-plugin/internal/presence"
"git.hangman-lab.top/hzhang/Plexum-fabric-channel-plugin/internal/subdisc"
"git.hangman-lab.top/hzhang/Plexum-fabric-channel-plugin/internal/tokens"
"git.hangman-lab.top/hzhang/Plexum-fabric-channel-plugin/internal/tools"
@@ -43,9 +44,10 @@ import (
// "sync_commands": ["new","stop"] // optional override; defaults to ["new","stop"]
// }
type HostConfig struct {
CenterAPIBase string `json:"center_api_base"`
CommandsSyncKey string `json:"commands_sync_key,omitempty"`
SyncCommands []string `json:"sync_commands,omitempty"`
CenterAPIBase string `json:"center_api_base"`
CommandsSyncKey string `json:"commands_sync_key,omitempty"`
SyncCommands []string `json:"sync_commands,omitempty"`
PresenceIntervalSeconds int `json:"presence_interval_seconds,omitempty"` // F-5b; 0 → default 30s
}
type fabricPlugin struct {
@@ -237,6 +239,21 @@ func (p *fabricPlugin) Init(ctx context.Context, host plugin.HostAPI) error {
}()
host.Log("info", "fabric inbound supervisor started",
map[string]any{"agents": sup.AgentIDs, "bindings": len(p.bindings)})
// F-5b: presence-sync. Shares the inbound supervisor's context
// so it dies together. Tick interval honours config override.
interval := time.Duration(p.cfg.PresenceIntervalSeconds) * time.Second
ps := &presence.Sync{
Host: host,
Client: p.client,
Tokens: p.tokens,
Identities: p.identities,
Bindings: p.bindings,
Interval: interval,
}
ps.Start(ctxBg)
host.Log("info", "fabric presence-sync started",
map[string]any{"interval": ps.Interval.String()})
}
return nil