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:
imccyu
2026-07-27 17:37:41 +08:00
parent e900ebd4c6
commit 4e50369eb6
15 changed files with 384 additions and 110 deletions
@@ -11,46 +11,46 @@ buffer
2| " deepseek-v4-flash • main-session"
style 1-34 dim
3| <blank>
4| " Keyboard shortcuts "
style 1-18 fg=bright-blue bold
5| " Enter send • Shift/Alt+Enter newline • Up/Down prompt history "
style 1-61 fg=bright-black
6| " Esc cancel active turn • Ctrl+O toggle tool cards • Ctrl+R toggle reasoning "
style 1-75 fg=bright-black
7| " Ctrl+C cancel while running; clear input or exit while idle • Ctrl+D exit "
style 1-73 fg=bright-black
8| " "
9| " /clear — Clear the transcript view (session history is unchanged) "
style 1-65 fg=bright-black
10| " /exit — Exit after the active turn reaches idle "
style 1-47 fg=bright-black
11| " /help — Show keyboard shortcuts and commands "
style 1-44 fg=bright-black
12| " /model [[provider/]model] — Show or switch this session's model "
style 1-63 fg=bright-black
13| " /reasoning — Toggle reasoning blocks "
style 1-36 fg=bright-black
14| " /redraw — Invalidate components and redraw the terminal "
style 1-55 fg=bright-black
15| " /reload — EXPERIMENTAL (dev): re-read loader config files and apply the diff (idle only) "
style 1-88 fg=bright-black
16| " /resume — List this workspace's resumable sessions "
style 1-50 fg=bright-black
17| " /status — Show detailed session diagnostics "
style 1-43 fg=bright-black
18| " /tools — Expand or collapse all tool cards "
style 1-42 fg=bright-black
19| " /skill:<name> [instructions] — load a skill into the conversation "
style 1-65 fg=bright-black
20| <blank>
21| " provider stream failed after partial output "
4| " provider stream failed after partial output "
style 1-43 fg=red
22| <blank>
23| " The previous process ended during this turn. "
5| <blank>
6| " The previous process ended during this turn. "
style 1-44 fg=yellow
24| <blank>
25| " Unknown command: /unknown-advanced-command "
7| <blank>
8| " Unknown command: /unknown-advanced-command "
style 1-42 fg=yellow
9| <blank>
10| " Keyboard shortcuts "
style 1-18 fg=bright-blue bold
11| " Enter send • Shift/Alt+Enter newline • Up/Down prompt history "
style 1-61 fg=bright-black
12| " Esc cancel active turn • Ctrl+O toggle tool cards • Ctrl+R toggle reasoning "
style 1-75 fg=bright-black
13| " Ctrl+C cancel while running; clear input or exit while idle • Ctrl+D exit "
style 1-73 fg=bright-black
14| " "
15| " /clear — Clear the transcript view (session history is unchanged) "
style 1-65 fg=bright-black
16| " /exit — Exit after the active turn reaches idle "
style 1-47 fg=bright-black
17| " /help — Show keyboard shortcuts and commands "
style 1-44 fg=bright-black
18| " /model [[provider/]model] — Show or switch this session's model "
style 1-63 fg=bright-black
19| " /reasoning — Toggle reasoning blocks "
style 1-36 fg=bright-black
20| " /redraw — Invalidate components and redraw the terminal "
style 1-55 fg=bright-black
21| " /reload — EXPERIMENTAL (dev): re-read loader config files and apply the diff (idle only) "
style 1-88 fg=bright-black
22| " /resume — List this workspace's resumable sessions "
style 1-50 fg=bright-black
23| " /status — Show detailed session diagnostics "
style 1-43 fg=bright-black
24| " /tools — Expand or collapse all tool cards "
style 1-42 fg=bright-black
25| " /skill:<name> [instructions] — load a skill into the conversation "
style 1-65 fg=bright-black
26| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim
27| " "
@@ -11,46 +11,46 @@ buffer
2| " deepseek-v4-flash • main-session"
style 1-34 dim
3| <blank>
4| " Keyboard shortcuts "
style 1-18 fg=bright-blue bold
5| " Enter send • Shift/Alt+Enter newline • Up/Down prompt history "
style 1-61 fg=bright-black
6| " Esc cancel active turn • Ctrl+O toggle tool cards • Ctrl+R toggle reasoning "
style 1-75 fg=bright-black
7| " Ctrl+C cancel while running; clear input or exit while idle • Ctrl+D exit "
style 1-73 fg=bright-black
8| " "
9| " /clear — Clear the transcript view (session history is unchanged) "
style 1-65 fg=bright-black
10| " /exit — Exit after the active turn reaches idle "
style 1-47 fg=bright-black
11| " /help — Show keyboard shortcuts and commands "
style 1-44 fg=bright-black
12| " /model [[provider/]model] — Show or switch this session's model "
style 1-63 fg=bright-black
13| " /reasoning — Toggle reasoning blocks "
style 1-36 fg=bright-black
14| " /redraw — Invalidate components and redraw the terminal "
style 1-55 fg=bright-black
15| " /reload — EXPERIMENTAL (dev): re-read loader config files and apply the diff (idle only) "
style 1-88 fg=bright-black
16| " /resume — List this workspace's resumable sessions "
style 1-50 fg=bright-black
17| " /status — Show detailed session diagnostics "
style 1-43 fg=bright-black
18| " /tools — Expand or collapse all tool cards "
style 1-42 fg=bright-black
19| " /skill:<name> [instructions] — load a skill into the conversation "
style 1-65 fg=bright-black
20| <blank>
21| " provider stream failed after partial output "
4| " provider stream failed after partial output "
style 1-43 fg=red
22| <blank>
23| " The previous process ended during this turn. "
5| <blank>
6| " The previous process ended during this turn. "
style 1-44 fg=yellow
24| <blank>
25| " Unknown command: /unknown-advanced-command "
7| <blank>
8| " Unknown command: /unknown-advanced-command "
style 1-42 fg=yellow
9| <blank>
10| " Keyboard shortcuts "
style 1-18 fg=bright-blue bold
11| " Enter send • Shift/Alt+Enter newline • Up/Down prompt history "
style 1-61 fg=bright-black
12| " Esc cancel active turn • Ctrl+O toggle tool cards • Ctrl+R toggle reasoning "
style 1-75 fg=bright-black
13| " Ctrl+C cancel while running; clear input or exit while idle • Ctrl+D exit "
style 1-73 fg=bright-black
14| " "
15| " /clear — Clear the transcript view (session history is unchanged) "
style 1-65 fg=bright-black
16| " /exit — Exit after the active turn reaches idle "
style 1-47 fg=bright-black
17| " /help — Show keyboard shortcuts and commands "
style 1-44 fg=bright-black
18| " /model [[provider/]model] — Show or switch this session's model "
style 1-63 fg=bright-black
19| " /reasoning — Toggle reasoning blocks "
style 1-36 fg=bright-black
20| " /redraw — Invalidate components and redraw the terminal "
style 1-55 fg=bright-black
21| " /reload — EXPERIMENTAL (dev): re-read loader config files and apply the diff (idle only) "
style 1-88 fg=bright-black
22| " /resume — List this workspace's resumable sessions "
style 1-50 fg=bright-black
23| " /status — Show detailed session diagnostics "
style 1-43 fg=bright-black
24| " /tools — Expand or collapse all tool cards "
style 1-42 fg=bright-black
25| " /skill:<name> [instructions] — load a skill into the conversation "
style 1-65 fg=bright-black
26| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim
27| " "
@@ -52,7 +52,7 @@ buffer
18| "│ │"
style 0-0 dim
style 55-55 dim
19| "│ Agent: idle · 6 events · 1 turn · 1 step · 1 │"
19| "│ Agent: idle · 7 events · 1 turn · 1 step · 1 │"
style 0-0 dim
style 3-12 fg=bright-black
style 55-55 dim
@@ -49,7 +49,7 @@ buffer
17| "│ │"
style 0-0 dim
style 81-81 dim
18| "│ Agent: idle · 6 events · 1 turn · 1 step · 1 tool call │"
18| "│ Agent: idle · 7 events · 1 turn · 1 step · 1 tool call │"
style 0-0 dim
style 3-12 fg=bright-black
style 81-81 dim
+6 -3
View File
@@ -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')