Adds a free-form 'purpose' text field on Channel so agents (or anyone
creating a channel via API) can describe what the channel is for —
'debate broadcasts', 'security alerts', etc. — and other agents can
later find the right channel by intent rather than channel id.
Wire:
- Channel.purpose (text, nullable; TypeORM synchronize auto-adds)
- POST /api/channels accepts optional 'purpose' in body
- GET /api/channels returns purpose on every row (already returns the
full entity via {...c})
- PATCH /api/channels/:id { purpose } — member-or-public auth (mirrors
the close() rule). Today only 'purpose' is patchable; other fields
would get their own typed branch.
Frontend create form continues to omit the field — purpose stays optional.
This pairs with Fabric.OpenclawPlugin's fabric-channel-set-purpose tool +
fabric-channel-list returning purpose, so agent workflows can say 'find
an announce channel about X' instead of pinning a UUID.
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.
channel enum + X_TYPES + realtime XType gain 'dm'. dm channels are
forced private (never public) and non-unique (no dedup; create()
always makes a fresh one). computeWakeup: dm wakes every non-author
participant unconditionally (no rotation / no wake_mapping). The
message.created realtime payload now carries xType so the plugin can
treat dm specially.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Channel.closed; POST /channels/:id/close (member-only); message/command
posts on closed channel -> 409 {error:channel_closed}; GET history still
allowed; listForUser carries closed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Channel.x_type enum(general|work|report|discuss|triage|custom); required
and validated on channel creation (400 if missing/invalid).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- new channel_members table; creator always added, plus selected members
- Channel.isPublic (default false): public channels visible to all guild
members; non-public only to explicit members
- GET /channels filters to channels visible to the requesting user
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>