From 6ad4346b97d247ba6e00b7baca610afeb5ec7ab7 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 22 Oct 2026 18:28:36 -0400 Subject: [PATCH 1/3] fix(channel): record a kill as a stop and label tool calls that never ran Why: packages/channel/src/channel.ts#function events | tool calls in an aborted or failed assistant message never ran: pi runs calls only from a finished reply, so they carry unrun --- apps/app/src/timeline.ts | 7 ++++++- apps/app/src/transcript.ts | 9 ++++++++- apps/host/src/cli.ts | 1 + packages/channel/src/channel.ts | 18 +++++++++++++----- packages/channel/src/protocol.ts | 2 ++ 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/apps/app/src/timeline.ts b/apps/app/src/timeline.ts index de08562e..44cdd0ee 100644 --- a/apps/app/src/timeline.ts +++ b/apps/app/src/timeline.ts @@ -97,6 +97,8 @@ function text(value: string): Event.Message.Content.Text { return parts.length ? { type: "text", text: value, parts } : { type: "text", text: value }; } +const NOT_RUN = "Not run"; + function tool( tool: Tool, busy: boolean, @@ -110,8 +112,11 @@ function tool( name: tool.name, arguments: (tool.args ?? {}) as Record, status, - ...(caption ? { caption } : {}), + ...(tool.unrun ? { caption: NOT_RUN } : caption ? { caption } : {}), ...(recap ? { recap } : {}), + ...(tool.unrun + ? { result: { content: `${NOT_RUN}: the run ended while the agent was writing this call.` } } + : {}), ...(tool.result ? { result: { diff --git a/apps/app/src/transcript.ts b/apps/app/src/transcript.ts index b2375f3f..12fec524 100644 --- a/apps/app/src/transcript.ts +++ b/apps/app/src/transcript.ts @@ -19,6 +19,8 @@ export type Tool = { }; /** The running call's live output tail, until its result replaces it. */ progress?: { text: string; truncated?: boolean }; + /** The reply ended while the agent was still writing this call, so it never ran. */ + unrun?: boolean; }; export type Item = @@ -148,7 +150,12 @@ export function apply(state: Transcript, event: Event): Transcript { return { ...state, draft: "", items: [...state.items, item] }; } case "tool": { - const tool: Tool = { call: event.call, name: event.name, args: event.args }; + const tool: Tool = { + call: event.call, + name: event.name, + args: event.args, + ...(event.unrun ? { unrun: true } : {}), + }; const last = state.items.at(-1); if (last?.kind === "reply" && !last.text) { return { diff --git a/apps/host/src/cli.ts b/apps/host/src/cli.ts index b8705a54..f53f1719 100755 --- a/apps/host/src/cli.ts +++ b/apps/host/src/cli.ts @@ -185,6 +185,7 @@ function render(event: Event, info: ChannelInfo) { return console.log(`${tag}${bold(event.model)}: ${event.text}${ending}`); } case "tool": { + if (event.unrun) return console.log(dim(`${tag} → ${event.name} (not run)`)); if (event.name !== "question") { return console.log(dim(`${tag} → ${event.name} ${clip(JSON.stringify(event.args), 100)}`)); } diff --git a/packages/channel/src/channel.ts b/packages/channel/src/channel.ts index 92d1eeed..e0318731 100644 --- a/packages/channel/src/channel.ts +++ b/packages/channel/src/channel.ts @@ -432,7 +432,7 @@ export class Channel { case "kill": this.#author(request.author); await this.#mayInvoke(request.author); - return this.kill(); + return this.kill(request.author); case "share": return this.#share(request); case "desktop": @@ -637,15 +637,21 @@ export class Channel { return importSeed(this.#harness, value, shift); } - async kill(): Promise { + /** `author`'s kill records a Stop in each chat it ended, so their next runs know. */ + async kill(author?: string): Promise { const { chats } = await this.info(); - this.#log("warn", "channel.kill", { - chats: chats.filter((chat) => chat.busy).map((c) => c.id), - }); + const busy = chats.filter((chat) => chat.busy); + this.#log("warn", "channel.kill", { chats: busy.map((c) => c.id) }); await Promise.all(chats.map(async (chat) => { const conversation = await this.#harness.conversation(chat.id as ConversationId, context); await conversation?.abort(context, { background: true }); })); + if (!author) return; + const at = Date.now(); + await Promise.all(busy.map(async (chat) => { + const conversation = await this.#conversation(chat.id); + await conversation.submit({ type: "write", entry: room.stop(author, at) }, context); + })); } async close(): Promise { @@ -1235,6 +1241,8 @@ function events(entry: EntryRecord): Event[] { call: block.id, name: block.name, args: block.arguments, + // pi runs calls only from a finished reply; a cut-off one's calls were never run. + ...(error || stopped ? { unrun: true as const } : {}), }); } return out; diff --git a/packages/channel/src/protocol.ts b/packages/channel/src/protocol.ts index cef57d60..ae930f0c 100644 --- a/packages/channel/src/protocol.ts +++ b/packages/channel/src/protocol.ts @@ -353,6 +353,8 @@ export type Event = call: string; name: string; args: unknown; + /** The reply ended while the agent was still writing this call, so it never ran. */ + unrun?: true; } /** * The bounded tail of a running tool call's output, sent on attachment and as it grows, at most From 532490336dea6da4c619791d81f0bc51b8838924 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 22 Oct 2026 18:28:50 -0400 Subject: [PATCH 2/3] chore: confirm notes the kill change left true --- ...XQKND0900E94CG3BXBEJP7-a-run-s-end-is-read-from-the.md | 6 +++--- ...4YFFTK7TWGKENJAD1W10HJJ-the-desktop-default-changes.md | 6 +++--- ...4YPJQB6KKEW1YTRD08YCYJY-only-gateways-admit-clients.md | 6 +++--- ...ZH31DXZK0Y57C97DP84ZKX-handoffs-learns-queued-hando.md | 6 +++--- ...3CAGW2RVRKCYF5BCJRTZX2-a-fork-s-starting-lane-is-re.md | 6 +++--- ...5JVP06WMT6GF0BNWADVRA43-tool-calls-in-an-aborted-or.md | 8 ++++++++ 6 files changed, 23 insertions(+), 15 deletions(-) create mode 100644 .lane/memory/packages/channel/src/channel.ts/01M5JVP06WMT6GF0BNWADVRA43-tool-calls-in-an-aborted-or.md diff --git a/.lane/memory/packages/channel/src/channel.ts/01M4XQKND0900E94CG3BXBEJP7-a-run-s-end-is-read-from-the.md b/.lane/memory/packages/channel/src/channel.ts/01M4XQKND0900E94CG3BXBEJP7-a-run-s-end-is-read-from-the.md index 9dcf883e..d3e1879c 100644 --- a/.lane/memory/packages/channel/src/channel.ts/01M4XQKND0900E94CG3BXBEJP7-a-run-s-end-is-read-from-the.md +++ b/.lane/memory/packages/channel/src/channel.ts/01M4XQKND0900E94CG3BXBEJP7-a-run-s-end-is-read-from-the.md @@ -4,9 +4,9 @@ anchor: class Channel created: 2026-10-14T17:33:22Z norm: '1' sig: c905677fa4be33c1 -body_hash: edc1045f02d80405 -raw_hash: 95969dbe14ea6781 -vouched: 2026-10-22T21:41:26Z +body_hash: a3c1f1281c5b3997 +raw_hash: 10f2879f7a794315 +vouched: 2026-10-22T22:28:50Z lines: 114-792 --- diff --git a/.lane/memory/packages/channel/src/channel.ts/01M4YFFTK7TWGKENJAD1W10HJJ-the-desktop-default-changes.md b/.lane/memory/packages/channel/src/channel.ts/01M4YFFTK7TWGKENJAD1W10HJJ-the-desktop-default-changes.md index 3ff6cb0a..d7b9ee08 100644 --- a/.lane/memory/packages/channel/src/channel.ts/01M4YFFTK7TWGKENJAD1W10HJJ-the-desktop-default-changes.md +++ b/.lane/memory/packages/channel/src/channel.ts/01M4YFFTK7TWGKENJAD1W10HJJ-the-desktop-default-changes.md @@ -4,9 +4,9 @@ anchor: class Channel created: 2026-10-15T00:30:42Z norm: '1' sig: c905677fa4be33c1 -body_hash: edc1045f02d80405 -raw_hash: 95969dbe14ea6781 -vouched: 2026-10-22T21:41:26Z +body_hash: a3c1f1281c5b3997 +raw_hash: 10f2879f7a794315 +vouched: 2026-10-22T22:28:50Z lines: 114-792 --- diff --git a/.lane/memory/packages/channel/src/channel.ts/01M4YPJQB6KKEW1YTRD08YCYJY-only-gateways-admit-clients.md b/.lane/memory/packages/channel/src/channel.ts/01M4YPJQB6KKEW1YTRD08YCYJY-only-gateways-admit-clients.md index 2b9704cf..45744ae4 100644 --- a/.lane/memory/packages/channel/src/channel.ts/01M4YPJQB6KKEW1YTRD08YCYJY-only-gateways-admit-clients.md +++ b/.lane/memory/packages/channel/src/channel.ts/01M4YPJQB6KKEW1YTRD08YCYJY-only-gateways-admit-clients.md @@ -4,9 +4,9 @@ anchor: class Channel created: 2026-10-15T02:34:37Z norm: '1' sig: c905677fa4be33c1 -body_hash: edc1045f02d80405 -raw_hash: 95969dbe14ea6781 -vouched: 2026-10-22T21:41:26Z +body_hash: a3c1f1281c5b3997 +raw_hash: 10f2879f7a794315 +vouched: 2026-10-22T22:28:50Z lines: 114-794 --- diff --git a/.lane/memory/packages/channel/src/channel.ts/01M4ZH31DXZK0Y57C97DP84ZKX-handoffs-learns-queued-hando.md b/.lane/memory/packages/channel/src/channel.ts/01M4ZH31DXZK0Y57C97DP84ZKX-handoffs-learns-queued-hando.md index 245767af..b5ce2ee6 100644 --- a/.lane/memory/packages/channel/src/channel.ts/01M4ZH31DXZK0Y57C97DP84ZKX-handoffs-learns-queued-hando.md +++ b/.lane/memory/packages/channel/src/channel.ts/01M4ZH31DXZK0Y57C97DP84ZKX-handoffs-learns-queued-hando.md @@ -4,9 +4,9 @@ anchor: class Channel created: 2026-10-15T10:17:55Z norm: '1' sig: c905677fa4be33c1 -body_hash: edc1045f02d80405 -raw_hash: 95969dbe14ea6781 -vouched: 2026-10-22T21:41:26Z +body_hash: a3c1f1281c5b3997 +raw_hash: 10f2879f7a794315 +vouched: 2026-10-22T22:28:50Z lines: 121-817 --- diff --git a/.lane/memory/packages/channel/src/channel.ts/01M53CAGW2RVRKCYF5BCJRTZX2-a-fork-s-starting-lane-is-re.md b/.lane/memory/packages/channel/src/channel.ts/01M53CAGW2RVRKCYF5BCJRTZX2-a-fork-s-starting-lane-is-re.md index 628d202d..1354d7a4 100644 --- a/.lane/memory/packages/channel/src/channel.ts/01M53CAGW2RVRKCYF5BCJRTZX2-a-fork-s-starting-lane-is-re.md +++ b/.lane/memory/packages/channel/src/channel.ts/01M53CAGW2RVRKCYF5BCJRTZX2-a-fork-s-starting-lane-is-re.md @@ -4,9 +4,9 @@ anchor: class Channel created: 2026-10-16T22:11:35Z norm: '1' sig: c905677fa4be33c1 -body_hash: edc1045f02d80405 -raw_hash: 95969dbe14ea6781 -vouched: 2026-10-22T21:41:26Z +body_hash: a3c1f1281c5b3997 +raw_hash: 10f2879f7a794315 +vouched: 2026-10-22T22:28:50Z lines: 127-873 --- diff --git a/.lane/memory/packages/channel/src/channel.ts/01M5JVP06WMT6GF0BNWADVRA43-tool-calls-in-an-aborted-or.md b/.lane/memory/packages/channel/src/channel.ts/01M5JVP06WMT6GF0BNWADVRA43-tool-calls-in-an-aborted-or.md new file mode 100644 index 00000000..b1778460 --- /dev/null +++ b/.lane/memory/packages/channel/src/channel.ts/01M5JVP06WMT6GF0BNWADVRA43-tool-calls-in-an-aborted-or.md @@ -0,0 +1,8 @@ +--- +id: 01M5JVP06WMT6GF0BNWADVRA43 +anchor: function events +created: 2026-10-22T22:28:36Z +norm: '1' +--- + +tool calls in an aborted or failed assistant message never ran: pi runs calls only from a finished reply, so they carry unrun From 2e3433949e046bbfcbcc0e453f4559153b00fd1a Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 22 Oct 2026 18:28:51 -0400 Subject: [PATCH 3/3] lane: sync fix-kill-resume memory --- .../01M5JVP06WMT6GF0BNWADVRA43-tool-calls-in-an-aborted-or.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.lane/memory/packages/channel/src/channel.ts/01M5JVP06WMT6GF0BNWADVRA43-tool-calls-in-an-aborted-or.md b/.lane/memory/packages/channel/src/channel.ts/01M5JVP06WMT6GF0BNWADVRA43-tool-calls-in-an-aborted-or.md index b1778460..553b3365 100644 --- a/.lane/memory/packages/channel/src/channel.ts/01M5JVP06WMT6GF0BNWADVRA43-tool-calls-in-an-aborted-or.md +++ b/.lane/memory/packages/channel/src/channel.ts/01M5JVP06WMT6GF0BNWADVRA43-tool-calls-in-an-aborted-or.md @@ -3,6 +3,10 @@ id: 01M5JVP06WMT6GF0BNWADVRA43 anchor: function events created: 2026-10-22T22:28:36Z norm: '1' +sig: 9791b5aa2005ab6b +body_hash: 6d8754eb6e1f376b +raw_hash: 150cd53f05e61cea +lines: 1201-1270 --- tool calls in an aborted or failed assistant message never ran: pi runs calls only from a finished reply, so they carry unrun