hzhang 7cb046d785 feat(triage): 3-state delivery + admin observer + admin cache
Triage channels now compute a 3-state delivery decision per recipient
(wake / observer / skip) instead of the binary wakeup flag, and route
according to:

  1. author never gets back their own message            → skip
  2. wake_mapping member (on-duty)                       → wake
  3. mention (NEW: was 'skip' for triage before)         → wake
  4. Center-scoped admin (at most 1)                     → observer
  5. anyone else                                         → skip
                                                         (was 'deliver wake=false')

Skipping means the websocket emit is omitted entirely — the recipient's
openclaw plugin never sees the message and the agent's session stays
free of background noise. Observer means delivered with wakeup=false
(silent UI / no model dispatch on the plugin side).

## What this PR ships

### realtime/realtime.gateway.ts
- new `computeDelivery()` returns DeliveryDecision = 'wake'|'observer'|'skip'
- old `computeWakeup()` kept as a deprecated wrapper for callers that
  still want the boolean answer (treats observer + skip as false)
- `emitMessageCreated` accepts `adminUserId?: string|null` and now
  short-circuits on 'skip' (no socket emit at all)
- general kept its current behavior; custom kept its current behavior
  (members not in wake_mapping become observer instead of `wake=false`)
  — the user-visible bit is just that the response field is the same
  `wakeup: boolean`; the explicit 'skip' is new for triage

### common/center-auth.ts
- `fetchAdminEmail()` calls GET `${center}/auth/admin-email` with the
  existing x-api-key (same auth as introspect/resolve-names). Returns
  `{email, userId}` or `null` on either "no admin" or any error

### common/admin-cache.service.ts (NEW)
- `AdminCacheService` — in-memory cache, 1-day TTL, lazy refresh.
  `get(force=true)` bypasses TTL for cli-triggered refresh
- exposed by MessagingModule

### messaging/messaging.controller.ts
- non-rotating branch threads `adminUserId` into emitMessageCreated

### cli/admin-refresh.ts (NEW)
- `node dist/cli/admin-refresh.js` — force-refresh cache and print
  before/after JSON. Use after a Center `user set-admin` so triage
  delivery picks up the new admin without waiting for 24h TTL

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 22:14:05 +01:00
2026-05-16 16:15:04 +01:00
2026-05-15 18:47:36 +01:00

Fabric.Backend.Guild

A guild node for Fabric (NestJS, ES modules, MySQL/TypeORM, socket.io). Default port 7002, global prefix /api. Many independent guild nodes can run; each registers with Fabric.Backend.Center and introspects the user/guild tokens Center issues.

Responsibilities

  • Guilds / channels / messaging — per-channel seq ordering, edit window, soft delete, reply, <@id> mentions (backtick-aware) plus <@user.name:NAME><@userId> translation via Center.
  • Channel x_type (required on create): general, work, report, discuss, triage, custom. Plus isPublic and closed (closed → history readable, posting returns 409).
  • wake_mapping — explicit wake list for triage (on-duty) and custom (listeners) channels.
  • Per-recipient wakeupmessage.created is emitted per socket with its own wakeup flag (author=false; general→all; report→none; triage/custom→wake_mapping; discuss/work→the current speaker only). This is push-only metadata for the OpenClaw plugin; UIs ignore it.
  • discuss/work turn engine (channel_turn_state): speaking order and a disjoint bypass list (bypass members aren't woken unless @-mentioned); activation from idle, queue-jump, cross-round /no-reply pause, /force-proceed, end-of-round shuffle, guild /ack, and a mention sub-frame stack with a 5-level nesting cap (root + 4). moveToBypass mid-rotation.
  • FilesPOST /files (multipart, configurable max size, default 100 MB), GET /files/:id (Bearer or ?access_token= for browser <img>/<a>), automatic retention sweep (default 7 days). Messages carry attachments[].
  • Channel canvas — one pinned document per channel (md/html/text), re-share replaces, only the original sharer may update/remove; emits canvas.updated / canvas.removed.
  • Slash-command registry — guild-global catalog: PUT /api/commands (the OpenClaw plugin syncs OpenClaw's native-command specs here), GET /api/commands (frontend / autocomplete). Stored verbatim; execution is unchanged (a /<cmd> message flows normally to the plugin → OpenClaw command system; only /no-reply,/force-proceed are server-intercepted).
  • Realtime — socket.io /realtime; join_channel/leave_channel, message.created/updated/deleted, canvas.*, presence, typing.

Required env (hard-checked at startup)

  • FABRIC_BACKEND_GUILD_CENTER_BASE_URL
  • FABRIC_BACKEND_GUILD_CENTER_API_KEY
  • FABRIC_BACKEND_GUILD_NODE_ID

Missing any of these aborts startup.

Other env

  • FABRIC_BACKEND_GUILD_PORT (default 7002)
  • FABRIC_BACKEND_GUILD_DB_*, FABRIC_BACKEND_GUILD_DB_SYNC
  • FABRIC_BACKEND_GUILD_FILE_DIR (storage root), FABRIC_BACKEND_GUILD_FILE_MAX_BYTES (default 100 MB), FABRIC_BACKEND_GUILD_FILE_TTL_DAYS (default 7)
  • FABRIC_BACKEND_GUILD_CORS_ORIGINS (empty = allow all; null origin — file:// desktop — is always allowed)

Run

npm install
npm run build && npm start          # or: npm run start:dev

Usually run via the root docker-compose.local.yml (backend-guild1 test-guild1 :7002, backend-guild2 test-guild2 :7003). Schema is auto-managed (DB_SYNC). ES modules (NodeNext).

Description
No description provided
Readme 483 KiB
Languages
TypeScript 99.3%
JavaScript 0.4%
Dockerfile 0.3%