Previous setStatus() did read-modify-write:
findOne → if-exists save / else create+save
Two concurrent first-time writes for the same userId both saw no row,
both INSERT'd, second hit unique-key (agent_presences.PRIMARY) and 500'd
with "Duplicate entry '<userId>' for key 'agent_presences.PRIMARY'" —
visible in prod (2026-05-25 23:23:35Z) when Fabric.OpenclawPlugin's
presence-sync emitted two PUTs ~10 ms apart for the same agent (its
tick-overlap is being fixed separately in nav/Fabric.OpenclawPlugin).
Replace with repo.upsert(values, ['userId']) — compiles to MySQL
`INSERT … ON DUPLICATE KEY UPDATE`, atomic at the storage engine,
no read needed, no race window. Synthesize the returned entity from
the values we just wrote rather than a SELECT round-trip; controller
only reads {userId, status} off it.
Sim verified with 5 parallel PUTs to a fresh userId: all 200, no
Duplicate errors in guild log (was: 1 × 200 + 4 × 500 with the
old code).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of DIALECTIC-V2 — adds Fabric infrastructure for
system-broadcast channels with HF-status-aware delivery filtering.
New channel x_type 'announce':
- channels.entity.ts + channels.service.ts + realtime.gateway.ts
enum + union extended.
- computeDelivery() adds an 'announce' case: recipient with
presence='busy' → 'skip' (discarded silently); other presences →
'observer' (delivered, no wake). System-broadcast semantics —
agents proactively check their announce inbox when they're ready,
not interrupted out of band.
- messaging.controller POST guard: announce-type channels reject
posts that don't present x-fabric-system-key header matching
FABRIC_BACKEND_GUILD_SYSTEM_API_KEY env. Empty env = no system
caller is valid (closed-by-default).
New entity + module agent_presences:
- agent-presence.entity.ts: per-user (userId PK) status enum
(idle/on_call/busy/exhausted/offline/unknown), source tag, updatedAt
- agent-presence.service.ts: getStatus/getStatusMap (bulk for
delivery-time fanout) + setStatus (upsert)
- agent-presence.controller.ts: GET + PUT /agents/:userId/presence
- agent-presence.module.ts: TypeORM forFeature + wired into AppModule
- buildTypeOrmConfig() entities list extended
RealtimeGateway wiring:
- New optional field on the gateway (typed loosely to avoid
circular import). RealtimeModule.onModuleInit() assigns from the
injected AgentPresenceService — degrades gracefully (no busy-discard,
treat all as 'unknown') if presence wiring is ever removed.
- emitMessageCreated pre-loads presence per fanout only when xType is
'announce' (other xTypes bypass the lookup entirely).
Note: actual presence data writes come from Fabric.OpenclawPlugin's
presence-sync loop (separate commit on that submodule); without it,
all rows are 'unknown' and announce delivery falls through to the
default observer behavior (no busy filtering). System-only POST gate
is independent and works immediately.
See /home/hzhang/arch/DIALECTIC-V2-DESIGN.md sections 7 + 10 Phase 1.