fix: apply the review items for #690 at merge

Boot prune keeps the inboxes of sessions on the reboot-restore offer (their records are already pruned and a rebuild reuses the id), with a case in test/agent-inbox-server-wiring.test.ts.
docs/wiki: post, inbox and ack in the codeman agent command block of Driving-Codeman-From-An-Agent, and "Keep agent inbox across restarts" in Settings-Reference.
codeman agent inbox prints message text through terminalSafeText (ANSI stripped, other C0, DEL and C1 controls shown as \xHH); --json stays as stored.
Shutdown flushes the inbox snapshot again after app.close(), for a post that lands after the first flush.
POST /api/cleanup-state drops the inboxes of the records it removes.
InboxPostSchema derives its fence from MAX_MESSAGE_BYTES instead of the literal.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-24 14:15:49 +02:00
1 parent a1a33cf050
commit 9ba24a02df
11 files changed
+150 -10

No files matched your search

+4
View File
@@ -65,6 +65,7 @@ import { QR_AUTH_FAILURE_MAX } from '../../config/tunnel-config.js';
import { AUTH_SESSION_TTL_MS } from '../../config/auth-config.js';
import { resolveTerminalHistoryConfig } from '../../config/terminal-history.js';
import { agentInboxPersistence } from '../agent-inbox-persistence.js';
import { agentInbox } from '../agent-inbox.js';
/**
* Defaults for `POST /api/deepseek/install-profile`.
@@ -802,6 +803,9 @@ export function registerSystemRoutes(
const lifecycleLog = getLifecycleLog();
for (const s of result.cleaned) {
lifecycleLog.log({ event: 'stale_cleaned', sessionId: s.id, name: s.name });
// A detached session kept its agent inbox; removing its record is the delete,
// so the mail goes with it (as on the persisted-only DELETE /api/sessions/:id).
agentInbox.drop(s.id);
}
return { cleanedSessions: result.count };
});
+2 -1
View File
@@ -23,6 +23,7 @@ import { WEBHOOK_KINDS, WEBHOOK_SCOPES } from '../types/push.js';
import { MIN_MATCH_LENGTH, MAX_MATCH_LENGTH } from '../config/agent-wait.js';
import { MAX_WAKE_MACS } from '../config/remote-wake-limits.js';
import { MAX_INPUT_LENGTH } from '../config/terminal-limits.js';
import { MAX_MESSAGE_BYTES } from './agent-inbox.js';
import { enabledCliIds, enabledClis } from '../config/cli-registry/registry.js';
import type { SessionMode } from '../types.js';
import { isAdvisorModel } from '../types/session.js';
@@ -1857,7 +1858,7 @@ export const InboxPostSchema = z
text: z
.string()
.min(1, 'text must not be empty')
.max(4 * 16_384),
.max(4 * MAX_MESSAGE_BYTES),
})
.strict();
+18 -4
View File
@@ -4036,8 +4036,15 @@ export class WebServer extends EventEmitter {
// Same gate as the tab layouts: only a restore that succeeded may decide which
// inboxes are orphans. A failed one returns above and leaves every message alone.
// Kept: live sessions plus every record state.json still holds (a session that
// was not re-adopted this boot but is kept for recovery keeps its mail).
const keep = new Set([...this.sessions.keys(), ...Object.keys(this.store.getSessions())]);
// was not re-adopted this boot but is kept for recovery keeps its mail), plus the
// sessions on the reboot-restore offer. `cleanupStaleSessions()` above has already
// pruned their records, and a rebuild reuses the old id, so without them a restored
// session would come back to an empty inbox. The 12 h TTL bounds what is never rebuilt.
const keep = new Set([
...this.sessions.keys(),
...Object.keys(this.store.getSessions()),
...rebootRestoreRegistry.list(() => true).map((entry) => entry.sessionId),
]);
const pruned = agentInbox.pruneExcept(keep);
if (pruned > 0) console.log(`[AgentInbox] dropped ${pruned} inbox(es) of sessions that did not come back`);
}
@@ -4217,9 +4224,10 @@ export class WebServer extends EventEmitter {
approvalInbox.stop();
this.keepWarmService.stop();
// A post within the debounce window before shutdown would otherwise not reach disk.
// Flushed again after `app.close()` below, for a post that lands in between.
agentInboxPersistence.flush();
// Same reason as `cancelEverything` above: an in-flight wake is awaited by a request,
// and `app.close()` (the last line of this method) does not abort in-flight requests —
// and `app.close()` (at the end of this method) does not abort in-flight requests,
// so without this a restart during a wake waits out the readiness poll.
this.remoteWake?.stop();
@@ -4360,7 +4368,13 @@ export class WebServer extends EventEmitter {
// Dispose push store (flush pending saves)
this.pushStore.dispose();
await this.app.close();
try {
await this.app.close();
} finally {
// The routes answer until `app.close()` returns, so a post after the flush above
// would otherwise sit in an unref'd debounce timer and never reach disk.
agentInboxPersistence.flush();
}
}
}