fix inbox lifecycle downstream contracts
This commit is contained in:
@@ -459,7 +459,7 @@ function resumeTurnLabel(snapshot: SessionLogSnapshot): string {
|
||||
const reason = event.data.reason
|
||||
switch (reason.kind) {
|
||||
case 'completed': return `turn ${event.data.turn}: completed`
|
||||
case 'aborted': return `turn ${event.data.turn}: cancelled`
|
||||
case 'aborted': return `turn ${event.data.turn}: ${reason.reason.kind === 'disposed' ? 'disposed' : 'cancelled'}`
|
||||
case 'error': return `turn ${event.data.turn}: error`
|
||||
case 'max-tokens': return `turn ${event.data.turn}: max tokens`
|
||||
case 'interrupted': return `turn ${event.data.turn}: interrupted`
|
||||
|
||||
@@ -664,6 +664,16 @@ export function createTuiChat(
|
||||
chat.addChild(streaming.timing)
|
||||
}
|
||||
|
||||
const trailAssistantStep = (): void => {
|
||||
if (streaming === undefined) return
|
||||
for (const child of [streaming, streaming.timing]) {
|
||||
const index = chat.children.indexOf(child)
|
||||
/* v8 ignore next -- an open step keeps both assistant children attached until it settles or retracts. */
|
||||
if (index >= 0) chat.children.splice(index, 1)
|
||||
chat.addChild(child)
|
||||
}
|
||||
}
|
||||
|
||||
const renderEvent = (
|
||||
event: SessionEvent,
|
||||
options: {
|
||||
@@ -682,6 +692,7 @@ export function createTuiChat(
|
||||
if (references !== undefined) {
|
||||
chat.addChild(new Spacer(1))
|
||||
chat.addChild(new Text(palette.dim(`Referenced sessions · ${references.map(displayText).join(', ')}`), 0, 0))
|
||||
trailAssistantStep()
|
||||
break
|
||||
}
|
||||
const text = contentText(event.data.content).trim()
|
||||
@@ -701,6 +712,7 @@ export function createTuiChat(
|
||||
chat.addChild(new Spacer(1))
|
||||
chat.addChild(card)
|
||||
}
|
||||
trailAssistantStep()
|
||||
break
|
||||
}
|
||||
const text = displayText(contentText(event.data.content).trim())
|
||||
@@ -709,6 +721,7 @@ export function createTuiChat(
|
||||
chat.addChild(new UserMessageComponent(text, palette, mdTheme))
|
||||
if (options.addHistory) editor.addToHistory(text)
|
||||
}
|
||||
trailAssistantStep()
|
||||
break
|
||||
}
|
||||
case 'steering/message': {
|
||||
@@ -799,7 +812,9 @@ export function createTuiChat(
|
||||
break
|
||||
}
|
||||
case 'aborted':
|
||||
appendNotice('Turn cancelled.', 'warning')
|
||||
appendNotice(reason.reason.kind === 'disposed'
|
||||
? 'Turn stopped: the agent was disposed.'
|
||||
: 'Turn cancelled.', 'warning')
|
||||
break
|
||||
case 'max-tokens':
|
||||
appendNotice('The model reached its output-token limit.', 'warning')
|
||||
|
||||
@@ -11,26 +11,26 @@ viewport
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
5| "Reasoning "
|
||||
style 0-8 dim italic
|
||||
6| "Inspecting width and styles. "
|
||||
style 0-27 dim italic
|
||||
7| "Streaming visible state… "
|
||||
style 10-22 bold
|
||||
8| " "
|
||||
9| "ts "
|
||||
style 0-1 dim
|
||||
10| " const visible = true "
|
||||
style 2-21 fg=cyan
|
||||
11| " "
|
||||
12| "Model wait 1.0s · Thinking 2.0s "
|
||||
style 0-30 dim
|
||||
13| <blank>
|
||||
14| "You "
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
15| "Show the live update. "
|
||||
5| "Show the live update. "
|
||||
6| <blank>
|
||||
7| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
8| "Reasoning "
|
||||
style 0-8 dim italic
|
||||
9| "Inspecting width and styles. "
|
||||
style 0-27 dim italic
|
||||
10| "Streaming visible state… "
|
||||
style 10-22 bold
|
||||
11| " "
|
||||
12| "ts "
|
||||
style 0-1 dim
|
||||
13| " const visible = true "
|
||||
style 2-21 fg=cyan
|
||||
14| " "
|
||||
15| "Model wait 1.0s · Thinking 2.0s "
|
||||
style 0-30 dim
|
||||
16| <blank>
|
||||
17| "/workspace/project (tui-staging) deepseek-v4-flash ↑0 ↓0 0% context"
|
||||
style 0-17 fg=bright-magenta bold
|
||||
|
||||
@@ -11,15 +11,15 @@ buffer
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
5| "Session inspected. "
|
||||
6| "Model wait 0.0s "
|
||||
style 0-14 dim
|
||||
7| <blank>
|
||||
8| "You "
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
9| "inspect this session "
|
||||
5| "inspect this session "
|
||||
6| <blank>
|
||||
7| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
8| "Session inspected. "
|
||||
9| "Model wait 0.0s "
|
||||
style 0-14 dim
|
||||
10| <blank>
|
||||
11| "╭─ Session status ─────────────────────────────────────╮"
|
||||
style 0-2 dim
|
||||
|
||||
@@ -11,15 +11,15 @@ buffer
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
5| "Session inspected. "
|
||||
6| "Model wait 0.0s "
|
||||
style 0-14 dim
|
||||
7| <blank>
|
||||
8| "You "
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
9| "inspect this session "
|
||||
5| "inspect this session "
|
||||
6| <blank>
|
||||
7| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
8| "Session inspected. "
|
||||
9| "Model wait 0.0s "
|
||||
style 0-14 dim
|
||||
10| <blank>
|
||||
11| "╭─ Session status ────────────────────────────────────────────────────────────────────────╮"
|
||||
style 0-2 dim
|
||||
|
||||
@@ -11,13 +11,13 @@ buffer
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
5| <blank>
|
||||
6| "You "
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
7| "Old prompt with a long line that exercises "
|
||||
8| "wrapping and stays visible after compaction."
|
||||
5| "Old prompt with a long line that exercises "
|
||||
6| "wrapping and stays visible after compaction."
|
||||
7| <blank>
|
||||
8| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
9| <blank>
|
||||
10| "● Tool / bash / Run the coverage gate"
|
||||
style 0-36 fg=green
|
||||
|
||||
@@ -11,12 +11,12 @@ buffer
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
5| <blank>
|
||||
6| "You "
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
7| "Old prompt with a long line that exercises wrapping and stays visible after compaction. "
|
||||
5| "Old prompt with a long line that exercises wrapping and stays visible after compaction. "
|
||||
6| <blank>
|
||||
7| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
8| <blank>
|
||||
9| "● Tool / bash / Run the coverage gate"
|
||||
style 0-36 fg=green
|
||||
|
||||
@@ -11,13 +11,13 @@ buffer
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
5| <blank>
|
||||
6| "You "
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
7| "Old prompt with a long line that exercises wrapping and stays visible after "
|
||||
8| "compaction. "
|
||||
5| "Old prompt with a long line that exercises wrapping and stays visible after "
|
||||
6| "compaction. "
|
||||
7| <blank>
|
||||
8| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
9| <blank>
|
||||
10| "● Tool / bash / Run the coverage gate"
|
||||
style 0-36 fg=green
|
||||
|
||||
@@ -11,12 +11,12 @@ buffer
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
5| <blank>
|
||||
6| "You "
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
7| "Old prompt with a long line that exercises wrapping and stays visible after compaction. "
|
||||
5| "Old prompt with a long line that exercises wrapping and stays visible after compaction. "
|
||||
6| <blank>
|
||||
7| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
8| <blank>
|
||||
9| "● Tool / bash / Run the coverage gate"
|
||||
style 0-36 fg=green
|
||||
|
||||
@@ -11,15 +11,15 @@ viewport
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
5| "Tracking the steps. "
|
||||
6| "Model wait 0.0s · Completed 2026-07-21 14:45:00 "
|
||||
style 0-46 dim
|
||||
7| <blank>
|
||||
8| "You "
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
9| "Plan the work. "
|
||||
5| "Plan the work. "
|
||||
6| <blank>
|
||||
7| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
8| "Tracking the steps. "
|
||||
9| "Model wait 0.0s · Completed 2026-07-21 14:45:00 "
|
||||
style 0-46 dim
|
||||
10| <blank>
|
||||
11| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
|
||||
@@ -11,30 +11,30 @@ buffer
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
5| <blank>
|
||||
6| "You "
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
7| "Unsafe user \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
|
||||
8| <blank>
|
||||
9| "● Tool / unsafe / Unsafe description \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m"
|
||||
style 0-81 fg=green
|
||||
10| "$ Unsafe title \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
|
||||
style 0-59 dim
|
||||
11| "/unsafe/\\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
|
||||
style 0-52 dim
|
||||
12| "Unsafe output \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
|
||||
style 0-58 dim
|
||||
13| "[signal SIG\\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m] "
|
||||
style 0-56 fg=red
|
||||
14| "Model wait 0.0s · Completed 2026-07-21 15:00:00 "
|
||||
style 0-46 dim
|
||||
15| <blank>
|
||||
16| "Context · unsafe-\\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m"
|
||||
5| "Unsafe user \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
|
||||
6| <blank>
|
||||
7| "Context · unsafe-\\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m"
|
||||
style 0-61 dim
|
||||
17| "Unsafe context \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
|
||||
8| "Unsafe context \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
|
||||
style 0-59 dim
|
||||
9| <blank>
|
||||
10| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
11| <blank>
|
||||
12| "● Tool / unsafe / Unsafe description \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m"
|
||||
style 0-81 fg=green
|
||||
13| "$ Unsafe title \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
|
||||
style 0-59 dim
|
||||
14| "/unsafe/\\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
|
||||
style 0-52 dim
|
||||
15| "Unsafe output \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
|
||||
style 0-58 dim
|
||||
16| "[signal SIG\\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m] "
|
||||
style 0-56 fg=red
|
||||
17| "Model wait 0.0s · Completed 2026-07-21 15:00:00 "
|
||||
style 0-46 dim
|
||||
18| <blank>
|
||||
19| "Unsafe turn error \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
|
||||
style 0-62 fg=red
|
||||
|
||||
@@ -633,6 +633,10 @@ describe('TUI terminal-state snapshots', () => {
|
||||
},
|
||||
beforeMount(session) {
|
||||
appendUser(session, `Unsafe user ${CONTROL_PROBE}`)
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: `Unsafe context ${CONTROL_PROBE}` }],
|
||||
source: { kind: 'plugin', plugin: `unsafe-${CONTROL_PROBE}` },
|
||||
}), { surfaceOp: 'append' })
|
||||
appendAssistant(session, [
|
||||
{ type: 'reasoning', text: `Unsafe reasoning ${CONTROL_PROBE}` },
|
||||
{ type: 'text', text: `Unsafe assistant ${CONTROL_PROBE}` },
|
||||
@@ -642,10 +646,6 @@ describe('TUI terminal-state snapshots', () => {
|
||||
session.append('todo/write', {
|
||||
todos: [{ content: `Unsafe todo ${CONTROL_PROBE}`, status: 'in_progress' }],
|
||||
})
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: `Unsafe context ${CONTROL_PROBE}` }],
|
||||
source: { kind: 'plugin', plugin: `unsafe-${CONTROL_PROBE}` },
|
||||
}), { surfaceOp: 'append' })
|
||||
session.append('step/end', { turn: 1, step: 1 })
|
||||
session.append('turn/end', {
|
||||
turn: 1,
|
||||
|
||||
@@ -460,7 +460,7 @@ describe('goodbye message and /resume', () => {
|
||||
it.each([
|
||||
[{ kind: 'aborted', reason: { kind: 'user' } }, 'cancelled'],
|
||||
[{ kind: 'error', error: 'failed' }, 'error'],
|
||||
[{ kind: 'aborted', reason: { kind: 'disposed' } }, 'cancelled'],
|
||||
[{ kind: 'aborted', reason: { kind: 'disposed' } }, 'disposed'],
|
||||
[{ kind: 'max-tokens' }, 'max tokens'],
|
||||
[{ kind: 'interrupted' }, 'interrupted'],
|
||||
[{ kind: 'future-result' } as unknown as TurnEndReason, 'unknown result'],
|
||||
@@ -1232,7 +1232,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
}
|
||||
const result = await setup({
|
||||
beforeMount(session) {
|
||||
session.append('user/message', createUserMessage({
|
||||
const message = createUserMessage({
|
||||
content: renderGoalChange(change),
|
||||
source: {
|
||||
kind: 'goal',
|
||||
@@ -1241,7 +1241,10 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
round: 0,
|
||||
change,
|
||||
},
|
||||
}), { surfaceOp: 'append' })
|
||||
})
|
||||
session.append('agent/inbox/spliced', {
|
||||
target: 'next-step', start: 0, inserted: [message],
|
||||
})
|
||||
},
|
||||
})
|
||||
expect(result.terminal.output).toContain('Goal restored (active) with automatic continuation disarmed')
|
||||
@@ -3772,6 +3775,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
expect(events.terminal.output).toContain('live failure')
|
||||
expect(events.terminal.output).toContain('durable failure')
|
||||
expect(events.terminal.output).toContain('Turn cancelled')
|
||||
expect(events.terminal.output).toContain('Turn stopped: the agent was disposed')
|
||||
expect(events.terminal.output).toContain('structured provider failure')
|
||||
expect(events.terminal.output).not.toContain('[object Object]')
|
||||
expect(events.terminal.output).toContain('output-token limit')
|
||||
|
||||
@@ -520,6 +520,25 @@ describe('approval policy (the approval/policy fold)', () => {
|
||||
expect(narrations(session)).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('preserves a rejected pre-step without adding policy narration', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(ApprovalService)
|
||||
const { agent, session } = sessionAgent('sess-narr-rejected')
|
||||
appendHeader(session, ASK_MARKER)
|
||||
setApprovalPolicy(session, 'never')
|
||||
const signal = new AbortController().signal
|
||||
|
||||
const decision = await agentEvents(ctx, agent).waterfall(
|
||||
'agent/pre-step',
|
||||
[],
|
||||
{ turn: 1, step: 1, signal },
|
||||
() => Promise.resolve({ kind: 'reject' as const }),
|
||||
)
|
||||
|
||||
expect(decision).toEqual({ kind: 'reject' })
|
||||
expect(narrations(session)).toEqual([])
|
||||
})
|
||||
|
||||
it('reads what the model was told back from the folded header text after a restart', async () => {
|
||||
// A session whose last request carried the never sentence resumes under
|
||||
// an ask default: the narrator attributes the change to the operator.
|
||||
|
||||
Reference in New Issue
Block a user