mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 10:59:32 +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
@@ -4,7 +4,9 @@
|
||||
* - Delete (`killMux=true`) drops the inbox; detach and shutdown (`killMux=false`)
|
||||
* keep it, because a detached session comes back.
|
||||
* - Boot pruning of orphaned inboxes runs only after a successful restore, and keeps
|
||||
* every session state.json still holds. A failed restore leaves all mail alone.
|
||||
* every session state.json still holds plus every session on the reboot-restore
|
||||
* offer (its record is already pruned, and a rebuild reuses the id). A failed
|
||||
* restore leaves all mail alone.
|
||||
*
|
||||
* Uses a bare `WebServer.prototype` object with just the fields these paths touch,
|
||||
* like tab-layout-restore-gate.test.ts.
|
||||
@@ -12,6 +14,9 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { WebServer } from '../src/web/server.js';
|
||||
import { agentInbox } from '../src/web/agent-inbox.js';
|
||||
import { rebootRestoreRegistry } from '../src/web/reboot-restore-registry.js';
|
||||
import type { RebootRestoreEntry } from '../src/reboot-restore.js';
|
||||
import type { SessionState } from '../src/types.js';
|
||||
|
||||
type CleanupServer = {
|
||||
_doCleanupSession(sessionId: string, killMux: boolean, reason?: string): Promise<void>;
|
||||
@@ -48,9 +53,23 @@ function bareServer(live: string[] = [], persisted: string[] = []): CleanupServe
|
||||
return server;
|
||||
}
|
||||
|
||||
/** A reboot-restore offer entry; the prune reads only its `sessionId`. */
|
||||
function offerEntry(sessionId: string): RebootRestoreEntry {
|
||||
return {
|
||||
sessionId,
|
||||
workingDir: `/tmp/codeman-agent-inbox-offer/${sessionId}`,
|
||||
mode: 'claude',
|
||||
resumeConversationId: `conv-${sessionId}`,
|
||||
state: { id: sessionId, workingDir: `/tmp/codeman-agent-inbox-offer/${sessionId}` } as SessionState,
|
||||
};
|
||||
}
|
||||
|
||||
describe('agent inbox server wiring', () => {
|
||||
beforeEach(() => agentInbox.resetForTests());
|
||||
afterEach(() => agentInbox.resetForTests());
|
||||
afterEach(() => {
|
||||
agentInbox.resetForTests();
|
||||
rebootRestoreRegistry.reset();
|
||||
});
|
||||
|
||||
it('a delete drops the inbox; a detach (killMux=false) keeps it', async () => {
|
||||
const server = bareServer();
|
||||
@@ -77,4 +96,17 @@ describe('agent inbox server wiring', () => {
|
||||
expect(agentInbox.pendingCount('kept-for-recovery')).toBe(1);
|
||||
expect(agentInbox.pendingCount('orphan')).toBe(0);
|
||||
});
|
||||
|
||||
it('a successful restore keeps the inbox of a session on the reboot-restore offer', async () => {
|
||||
// After a host reboot the dead session is in neither `this.sessions` nor state.json
|
||||
// (cleanupStaleSessions already pruned its record), only in the offer, and the
|
||||
// rebuild reuses its id, so its mail must still be there when it comes back.
|
||||
const server = bareServer();
|
||||
rebootRestoreRegistry.set([offerEntry('on-offer')]);
|
||||
for (const id of ['on-offer', 'orphan']) agentInbox.post(id, null, 'm');
|
||||
await server.finalizeRestoredState(true);
|
||||
expect(rebootRestoreRegistry.list(() => true).map((e) => e.sessionId)).toEqual(['on-offer']);
|
||||
expect(agentInbox.pendingCount('on-offer')).toBe(1);
|
||||
expect(agentInbox.pendingCount('orphan')).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
registerAgentCommands,
|
||||
resolveAgentContext,
|
||||
stripAnsi,
|
||||
terminalSafeText,
|
||||
waitExitCode,
|
||||
type AgentContext,
|
||||
type AgentDeps,
|
||||
@@ -707,6 +708,35 @@ describe('agent inbox verbs', () => {
|
||||
expect(deps.err.join('')).toMatch(/codeman agent ack <id>/);
|
||||
});
|
||||
|
||||
it('inbox prints message text with escape sequences and control characters neutralized', async () => {
|
||||
const hostile = {
|
||||
...MSG,
|
||||
text: 'title \x1b]0;pwned\x07ok\r\nred \x1b[31mtext\x1b[0m\rover\bwrite\ttab\x9b2J\nend',
|
||||
};
|
||||
const deps = fakeDeps([ok({ receiver: true, messages: [hostile], pending: 1, unseen: 0 })]);
|
||||
expect(await agentInboxRead(deps, {})).toBe(EXIT.ok);
|
||||
// out[0] is the `── <id> from …` header (styled by the palette), out[1] the text.
|
||||
expect(deps.out).toHaveLength(2);
|
||||
expect(deps.out[1]).not.toMatch(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/);
|
||||
expect(deps.out[1]).toBe('title ok\nred text\\x0dover\\x08write\ttab\\x9b2J\nend');
|
||||
});
|
||||
|
||||
it('terminalSafeText keeps newline and tab and escapes every other control', () => {
|
||||
expect(terminalSafeText('a\nb\tc')).toBe('a\nb\tc');
|
||||
expect(terminalSafeText('a\r\nb')).toBe('a\nb');
|
||||
expect(terminalSafeText('\x1b[2J\x1bc\x00\x7f')).toBe('\\x1bc\\x00\\x7f');
|
||||
expect(terminalSafeText('ünïcödé ✓')).toBe('ünïcödé ✓');
|
||||
});
|
||||
|
||||
it('inbox --json hands the text out as stored', async () => {
|
||||
const raw = { ...MSG, text: 'red \x1b[31mtext' };
|
||||
const deps = fakeDeps([ok({ receiver: true, messages: [raw], pending: 1, unseen: 0 })], true);
|
||||
await agentInboxRead(deps, {});
|
||||
expect((JSON.parse(deps.out.join('')) as { messages: Array<{ text: string }> }).messages[0].text).toBe(
|
||||
'red \x1b[31mtext'
|
||||
);
|
||||
});
|
||||
|
||||
it('inbox <id> is a peek and says so', async () => {
|
||||
const deps = fakeDeps([ok([{ id: OTHER }]), ok({ receiver: false, messages: [MSG], pending: 1, unseen: 1 })]);
|
||||
expect(await agentInboxRead(deps, { id: OTHER.slice(0, 8) })).toBe(EXIT.ok);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { createRouteTestHarness, type RouteTestHarness } from './_route-test-utils.js';
|
||||
import { registerSystemRoutes } from '../../src/web/routes/system-routes.js';
|
||||
import { agentInbox } from '../../src/web/agent-inbox.js';
|
||||
|
||||
// ── Mocks ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -343,6 +344,29 @@ describe('system-routes', () => {
|
||||
expect(harness.ctx.store.cleanupSessionsByIds).toHaveBeenCalledWith(new Set());
|
||||
expect(runStaleSessionCleanup).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('drops the agent inbox of every record it removes, and only those', async () => {
|
||||
agentInbox.resetForTests();
|
||||
agentInbox.post('stale-detached', null, 'mail for a detached session');
|
||||
agentInbox.post('still-kept', null, 'mail for a session that stays');
|
||||
harness.ctx.tabLayouts.runStaleSessionCleanup = vi.fn(
|
||||
async (_activeIds: Set<string>, action: (ids: ReadonlySet<string>) => unknown) =>
|
||||
action(new Set(['stale-detached']))
|
||||
);
|
||||
vi.mocked(harness.ctx.store.cleanupSessionsByIds).mockReturnValueOnce({
|
||||
count: 1,
|
||||
cleaned: [{ id: 'stale-detached', name: 'w9-gone' }],
|
||||
});
|
||||
try {
|
||||
const res = await harness.app.inject({ method: 'POST', url: '/api/cleanup-state' });
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(JSON.parse(res.body).cleanedSessions).toBe(1);
|
||||
expect(agentInbox.pendingCount('stale-detached')).toBe(0);
|
||||
expect(agentInbox.pendingCount('still-kept')).toBe(1);
|
||||
} finally {
|
||||
agentInbox.resetForTests();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ========== GET /api/subagents ==========
|
||||
|
||||
Reference in new issue
Block a user