mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
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:
1 parent
a1a33cf050
commit
9ba24a02df
11 files changed
+150
-10
No files matched your search
+17
-1
@@ -191,6 +191,22 @@ export function stripAnsi(text: string): string {
|
||||
return stripAnsiSequences(text).replace(/\x1b[()][AB0]/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Inbox message text for a terminal. Another session wrote it, so an escape sequence
|
||||
* in it would otherwise reach the reader's terminal (retitle it, or move the cursor
|
||||
* over the `from` line above it). ANSI sequences go as in `read --tail`, and every
|
||||
* control character left (C0 other than newline and tab, DEL, C1) is shown as `\xHH`,
|
||||
* so a bare `\r` or backspace cannot overwrite the header either. A CRLF line end
|
||||
* stays a line end. `--json` prints the text as stored.
|
||||
*/
|
||||
export function terminalSafeText(text: string): string {
|
||||
return (
|
||||
stripAnsi(text.replace(/\r\n/g, '\n'))
|
||||
// eslint-disable-next-line no-control-regex
|
||||
.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`)
|
||||
);
|
||||
}
|
||||
|
||||
/** Parse a positive-integer option (`--timeout` ms, `--tail` bytes); the server rejects anything else. */
|
||||
export function parsePositiveInt(raw: string | undefined, fallback: number, flag = '--timeout'): number {
|
||||
if (raw === undefined) return fallback;
|
||||
@@ -950,7 +966,7 @@ export async function agentInboxRead(deps: AgentDeps, options: InboxReadOptions)
|
||||
const sender = m.from ? m.from.slice(0, 8) : 'user';
|
||||
const tag = m.seen ? palette.muted('seen') : palette.emph('new');
|
||||
deps.io.out(palette.muted(`── ${m.id} from ${sender}, ${age(m.createdAt, now)} ago, `) + tag);
|
||||
deps.io.out(m.text);
|
||||
deps.io.out(terminalSafeText(m.text));
|
||||
}
|
||||
if (gone) {
|
||||
deps.io.err(palette.err(`${GLYPH.fail} the session went away during the wait`));
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user