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>
This commit is contained in:
39
src/cli/admin-refresh.ts
Normal file
39
src/cli/admin-refresh.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// Operator convenience: force-refresh the in-memory Center admin cache
|
||||
// without waiting for the 1-day TTL. Used after `center user set-admin`
|
||||
// to make new admin visible immediately to triage delivery.
|
||||
//
|
||||
// Usage (inside the deployed container):
|
||||
// docker exec fabric-backend-guild node dist/cli/admin-refresh.js
|
||||
//
|
||||
// Prints the (possibly null) result as JSON. Exit 0 always — a "no
|
||||
// admin" outcome is a valid state, not an error.
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from '../app.module.js';
|
||||
import { AdminCacheService } from '../common/admin-cache.service.js';
|
||||
|
||||
async function main() {
|
||||
const app = await NestFactory.createApplicationContext(AppModule, { logger: ['error', 'warn'] });
|
||||
try {
|
||||
const cache = app.get(AdminCacheService);
|
||||
const before = cache.snapshot();
|
||||
const after = await cache.get(true);
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
before,
|
||||
after,
|
||||
changed: JSON.stringify(before) !== JSON.stringify(after),
|
||||
}) + '\n',
|
||||
);
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
}
|
||||
|
||||
void main().catch((error: unknown) => {
|
||||
const message = error instanceof Error ? error.message : 'unknown error';
|
||||
process.stderr.write(JSON.stringify({ ok: false, error: message }) + '\n');
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user