fix(agent-inbox): review round 1

- codeman agent post: accept text that starts with "-" (markdown from a
  file), --json and -h stay options.
- DELETE /api/sessions/:id on a persisted-only row drops the inbox a detach
  kept.
- inbox --wait: wait.ended is exit 3 only when the session list no longer has
  the session; a restart or detach ending the wait is exit 2 (wait again).
- A read whose client hung up during ?wait= marks nothing seen.
- InboxPostSchema bound is 4x the byte cap, so every oversize message gets
  the store's "post a file path" answer.
- Store: restore sorts oldest first; the TTL sweep checks every message,
  not only the first.
- Docs: admins can act as any session of the receiver's owner (now tested
  for admin and non-admin); a peek with ?wait= does not block; exit codes.
- Tests: partial PUT /api/settings keeps persistence on.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
RandalixandClaude Opus 5.5 committed 2026-10-23 12:45:48 +02:00
1 parent c6dfaa2cac
commit a1a33cf050
15 files changed
+209 -31

No files matched your search

+43 -3
View File
@@ -714,12 +714,32 @@ describe('agent inbox verbs', () => {
expect(deps.err.join('')).toMatch(/peek: nothing was marked seen/);
});
it('inbox --wait passes the budget and maps timeout to exit 2, a vanished session to exit 3', async () => {
it('inbox --wait passes the budget and maps timeout to exit 2', async () => {
const timedOut = fakeDeps([ok({ receiver: true, messages: [], pending: 0, unseen: 0, wait: { timedOut: true } })]);
expect(await agentInboxRead(timedOut, { waitMs: 30_000 })).toBe(EXIT.timeout);
expect(timedOut.calls[0]).toMatchObject({ query: { wait: 30_000 }, timeoutMs: 40_000 });
const ended = fakeDeps([ok({ receiver: true, messages: [], pending: 0, unseen: 0, wait: { ended: true } })]);
expect(await agentInboxRead(ended, { waitMs: 30_000 })).toBe(EXIT.dead);
});
describe('a wait the server ended', () => {
const ended = ok({ receiver: true, messages: [], pending: 0, unseen: 0, wait: { ended: true } });
it('is exit 3 only when the session list confirms the session is gone', async () => {
const deps = fakeDeps([ended, ok([{ id: OTHER }])]);
expect(await agentInboxRead(deps, { waitMs: 30_000 })).toBe(EXIT.dead);
expect(deps.calls[1]).toMatchObject({ method: 'GET', path: '/api/v1/sessions' });
expect(deps.err.join('')).toMatch(/went away/);
});
it('is exit 2 (wait again) while the session is still listed: a restart or detach', async () => {
const deps = fakeDeps([ended, ok([{ id: SELF }])]);
expect(await agentInboxRead(deps, { waitMs: 30_000 })).toBe(EXIT.timeout);
expect(deps.err.join('')).toMatch(/ended the wait early/);
});
it('is exit 2 when the list cannot be had (the server is still coming back)', async () => {
const deps = fakeDeps([ended, apiError(503, 'INTERNAL_ERROR', 'restarting')]);
expect(await agentInboxRead(deps, { waitMs: 30_000 })).toBe(EXIT.timeout);
});
});
it('ack posts the ids to this session’s own inbox', async () => {
@@ -767,6 +787,26 @@ describe('help texts carry the traps the skill documents', () => {
expect(sub('send').description()).toContain('send <id> -- "- fix the bug"');
});
it('post takes text that starts with "-" (markdown from a file) without --', async () => {
const program = new Command();
program.exitOverride();
const post = registerAgentCommands(program).commands.find((c) => c.name() === 'post')!;
const got: Array<{ words: string[]; json: boolean }> = [];
post.action((_id: string, words: string[], o: { json?: boolean }) => {
got.push({ words, json: Boolean(o.json) });
});
for (const text of ['- [ ] item\nnext', '---\ntitle: a', '-v is great']) {
await program.parseAsync(['node', 'codeman', 'agent', 'post', 'abcdefgh', text]);
}
await program.parseAsync(['node', 'codeman', 'agent', 'post', 'abcdefgh', 'hi', '--json']);
expect(got).toEqual([
{ words: ['- [ ] item\nnext'], json: false },
{ words: ['---\ntitle: a'], json: false },
{ words: ['-v is great'], json: false },
{ words: ['hi'], json: true },
]);
});
it('rm does not claim a lineage check it does not make', () => {
expect(sub('rm').description()).toMatch(/^Delete any session except this one/);
});