The presence-sync tick iterates accounts serially with await on each
agent-login + PUT round-trip — a single tick can easily run 20+s when
there are several accounts. setInterval(intervalMs) does NOT wait for
the previous tick to finish, so on a busy gateway the next tick fires
on top of a still-running one and two parallel iterations each PUT
the same agentId within ~10 ms. That tipped the guild backend's
first-time-insert race (separate fix in nav/Fabric.Backend.Guild) into
500s on prod (caught in t2 gateway 2026-05-25 23:23:35Z; 6 of 6 agents
showed paired log lines 4-10 ms apart for the same agent → idle).
Fix: a simple `inflight` boolean. tick() returns immediately if
already running; the next interval beat catches up. lastStatus !==
bridge.get gating already means status changes catch the next tick
anyway, so skipping a beat costs nothing the next beat won't fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two layered bugs in the presence-sync loop, both causing every PUT to
fail forever in prod:
1. **Missing /api prefix.** URL was `${guildBaseUrl}/agents/<id>/presence`
but the guild backend sets a global prefix 'api' in main.ts
`setGlobalPrefix('api')`. Every other REST call in this plugin
(channel.ts channels list, fabric-client.ts postMessage, canvas)
already prepends /api/ — only presence-sync missed it. Returned 404
"Cannot PUT /agents/...".
2. **Wrong auth scheme.** Plugin sent `x-api-key: <fabricApiKey>`, but
the endpoint sits behind the global APP_GUARD = ApiKeyGuard, which
actually expects `Authorization: Bearer <guildAccessToken>` (despite
its name — confusing naming on the backend side). With /api added,
error became 401 "missing bearer token". Confirmed by `docker exec
fabric-backend-guild grep APP_GUARD /app/dist/app.module.js` and
manual curl: Bearer guild token → 200 OK.
**Fix**
- presence-sync.ts: do agent-login on demand to obtain a fresh
guildAccessToken, cache it per-agent for 13 min (under the 15-min
JWT TTL), use it as Bearer for the PUT. 401 response invalidates
the cache so the next tick re-logs-in. Pushes are gated on status
changes (rare), so the login overhead is negligible.
- inbound.ts: firstGuildEndpointByAgent → firstGuildByAgent storing
both endpoint and nodeId (presence-sync needs nodeId to pick the
right token out of guildAccessTokens[]).
- index.ts: pass FabricClient to PresenceSync constructor.
**Verified in sim**
After restart, gateway log shows `fabric: presence-sync recruiter →
idle` (200 OK), zero failed PUTs, where previously it would log a 404
every ~5s per agent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two agent-facing tools that close the discoverability loop:
- fabric-guild-list — enumerates guilds the agent belongs to with
name + purpose + status (no api calls beyond the existing agentLogin
response). Optional nameFilter/purposeFilter for narrowing.
- fabric-channel-set-purpose — PATCH /api/channels/:id { purpose }
so agents can backfill or update an existing channel's purpose.
Extends existing tools:
- fabric-channel-list now returns purpose on each row.
- create-{chat,work,report,discussion}-channel accept optional purpose.
FabricClient + FabricSession type changes carry the new field through.
Manifest contracts.tools updated (jiti loader needs both manifest entry
and onStartup activation to register).
Lets workflows that previously needed hardcoded channel ids instead say
'find a guild whose purpose mentions debate, then a channel of x_type
announce whose purpose covers public debate broadcasts.'