feat: durable command lifecycle logging in the executor (command/run + command/done)
CommandService.execute appends the log-only pair around every resolved handler — run before invocation, done at settlement, including thrown and aborted handlers (kind:'error'); admission misses log nothing. commandId is minted monotonically per instance; per-session appends serialize through a tail queue over SessionStore.appendOutOfBand (zero-step wrap on an idle log, direct join inside an open turn). The invariant companion now asserts the pairing relation (unique run ids; a done requires a prior in-log run). CommandSource is a minimal merge-extensible map (user variant only). Dependent benches mount SessionStore; TUI/e2e snapshots re-recorded for the executor's durable-append timing and the /status event counts.
This commit is contained in:
@@ -1281,6 +1281,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
})
|
||||
result.terminal.send('/clear')
|
||||
result.terminal.send('\r')
|
||||
await tick() // the executor logs command/run durably before the handler clears
|
||||
appendAssistant(result.session, [{ type: 'text', text: 'answer after clear' }], undefined, { turn: 3, step: 1 })
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('answer after clear')
|
||||
@@ -1758,7 +1759,8 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
expect(result.terminal.output).toContain('/workspace/status')
|
||||
expect(result.terminal.output).toContain('deepseek/deepseek-v4-pro (effort default; reasoning blocks')
|
||||
expect(result.terminal.output).toContain('hidden)')
|
||||
expect(result.terminal.output).toContain('running · 6 events · 1 turn · 1 step · 2 tool calls')
|
||||
// 6 domain events + the /status invocation's own command/run (open turn: joined directly).
|
||||
expect(result.terminal.output).toContain('running · 7 events · 1 turn · 1 step · 2 tool calls')
|
||||
expect(result.terminal.output).toContain('1,250 input + 340 output')
|
||||
expect(result.terminal.output).toContain('[███████████░░░░░] 67% hit (3,000 read + 250 write)')
|
||||
expect(result.terminal.output).toContain('[█████░░░░░░░░░░░] 33% used (42,000 / 128,000)')
|
||||
@@ -1794,7 +1796,8 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
|
||||
expect(result.terminal.output).toContain('untitled')
|
||||
expect(result.terminal.output).toContain('unset (effort unset; reasoning blocks shown)')
|
||||
expect(result.terminal.output).toContain('idle · 0 events · 0 turns · 0 steps · 0 tool calls')
|
||||
// An empty log gains the /status invocation's zero-step wrap: turn/start + command/run + turn/end.
|
||||
expect(result.terminal.output).toContain('idle · 3 events · 1 turn · 0 steps · 0 tool calls')
|
||||
expect(result.terminal.output).toContain('n/a (0 read + 0 write)')
|
||||
expect(result.terminal.output).toContain('7 used · capacity unknown')
|
||||
expect(result.terminal.output).toContain('2026-07-22 10:11:12 UTC')
|
||||
@@ -1834,8 +1837,8 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
for (const command of ['/clear', '/wat']) {
|
||||
result.terminal.send(command)
|
||||
result.terminal.send('\r')
|
||||
await tick() // /clear's handler runs after the durable command/run append; keep it from wiping the next notice
|
||||
}
|
||||
await tick()
|
||||
result.terminal.send('draft')
|
||||
result.terminal.send('\x03')
|
||||
result.terminal.send('\x04')
|
||||
|
||||
Reference in New Issue
Block a user