pi-cohort 5.3.2 → 5.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [5.3.4] - 2026-09-07
6
+
7
+ ### Fixed
8
+
9
+ - A fanout lead no longer loses its own `subagent` call/result before its next turn: the child-context filter now only strips `subagent` history inherited from a forked parent (captured at `session_start`), not the child's own delegation made during its own session. Previously a deterministic provider would see the delegation as never having happened and repeat it instead of finishing.
10
+
11
+ ## [5.3.3] - 2026-09-07
12
+
13
+ ### Fixed
14
+
15
+ - Nested foreground delegation no longer crashes when a grandchild reports cost progress because child-safe state now initializes the required grand-total accumulator. ([#13](https://github.com/jjuraszek/pi-cohort/issues/13))
16
+
3
17
  ## [5.3.2] - 2026-09-06
4
18
 
5
19
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cohort",
3
- "version": "5.3.2",
3
+ "version": "5.3.4",
4
4
  "description": "Delegate Pi work to focused child agents: code review, scouting, implementation, parallel audits, saved chains, and background jobs.",
5
5
  "author": "Jacek Juraszek",
6
6
  "license": "MIT",
@@ -13,6 +13,7 @@ import { SubagentParams } from "./schemas.ts";
13
13
  import { loadConfig } from "./config.ts";
14
14
  import { deriveForwardedFlags } from "../runs/shared/forward-flags.ts";
15
15
  import type { Details, SubagentState } from "../shared/types.ts";
16
+ import { emptyGrandTotal } from "./grand-total.ts";
16
17
 
17
18
  function getSubagentSessionRoot(parentSessionFile: string | null): string {
18
19
  if (parentSessionFile) {
@@ -32,6 +33,7 @@ function createChildSafeState(): SubagentState {
32
33
  baseCwd: "",
33
34
  currentSessionId: null,
34
35
  asyncJobs: new Map(),
36
+ grandTotal: emptyGrandTotal(),
35
37
  foregroundRuns: new Map(),
36
38
  foregroundControls: new Map(),
37
39
  lastForegroundControlId: null,
@@ -115,34 +115,44 @@ function isParentOnlySubagentMessage(message: unknown): boolean {
115
115
  && PARENT_ONLY_CUSTOM_MESSAGE_TYPES.has(m.customType);
116
116
  }
117
117
 
118
- function isSubagentToolResultMessage(message: unknown): boolean {
119
- const m = message as { role?: string; toolName?: string };
120
- return m?.role === "toolResult" && m.toolName === "subagent";
118
+ function isSubagentToolResultMessage(message: unknown, inheritedToolCallIds: ReadonlySet<string>): boolean {
119
+ const m = message as { role?: string; toolName?: string; toolCallId?: string };
120
+ if (m?.role !== "toolResult" || m.toolName !== "subagent") return false;
121
+ return typeof m.toolCallId === "string" && inheritedToolCallIds.has(m.toolCallId);
121
122
  }
122
123
 
123
- function isSubagentToolCallBlock(block: unknown): boolean {
124
- const b = block as { type?: string; name?: string };
125
- return b?.type === "toolCall" && b.name === "subagent";
124
+ function isInheritedSubagentToolCallBlock(block: unknown, inheritedToolCallIds: ReadonlySet<string>): boolean {
125
+ const b = block as { type?: string; name?: string; id?: string };
126
+ if (b?.type !== "toolCall" || b.name !== "subagent") return false;
127
+ return typeof b.id === "string" && inheritedToolCallIds.has(b.id);
126
128
  }
127
129
 
128
- function stripAssistantSubagentToolCallBlocks(message: unknown): unknown | undefined {
130
+ function stripAssistantSubagentToolCallBlocks(message: unknown, inheritedToolCallIds: ReadonlySet<string>): unknown | undefined {
129
131
  const m = message as { role?: string; content?: unknown };
130
132
  if (m?.role !== "assistant" || !Array.isArray(m.content)) return message;
131
- const filteredContent = m.content.filter((block) => !isSubagentToolCallBlock(block));
133
+ const filteredContent = m.content.filter((block) => !isInheritedSubagentToolCallBlock(block, inheritedToolCallIds));
132
134
  if (filteredContent.length === m.content.length) return message;
133
135
  if (filteredContent.length === 0) return undefined;
134
136
  return { ...m, content: filteredContent };
135
137
  }
136
138
 
137
- export function stripParentOnlySubagentMessages(messages: unknown[]): unknown[] {
139
+ /**
140
+ * Strips parent-only orchestration artifacts from a child's context.
141
+ *
142
+ * `inheritedToolCallIds` distinguishes history inherited from a forked parent
143
+ * session (ids captured before the child's own first turn) from the child's
144
+ * own `subagent` calls/results made during its own session - only the former
145
+ * is stripped.
146
+ */
147
+ export function stripParentOnlySubagentMessages(messages: unknown[], inheritedToolCallIds: ReadonlySet<string>): unknown[] {
138
148
  let changed = false;
139
149
  const filtered: unknown[] = [];
140
150
  for (const message of messages) {
141
- if (isParentOnlySubagentMessage(message) || isSubagentToolResultMessage(message)) {
151
+ if (isParentOnlySubagentMessage(message) || isSubagentToolResultMessage(message, inheritedToolCallIds)) {
142
152
  changed = true;
143
153
  continue;
144
154
  }
145
- const stripped = stripAssistantSubagentToolCallBlocks(message);
155
+ const stripped = stripAssistantSubagentToolCallBlocks(message, inheritedToolCallIds);
146
156
  if (stripped === undefined) {
147
157
  changed = true;
148
158
  continue;
@@ -153,6 +163,19 @@ export function stripParentOnlySubagentMessages(messages: unknown[]): unknown[]
153
163
  return changed ? filtered : messages;
154
164
  }
155
165
 
166
+ function collectSubagentToolCallIds(entries: readonly unknown[]): Set<string> {
167
+ const ids = new Set<string>();
168
+ for (const entry of entries) {
169
+ const e = entry as { type?: string; message?: { role?: string; content?: unknown } };
170
+ if (e?.type !== "message" || e.message?.role !== "assistant" || !Array.isArray(e.message.content)) continue;
171
+ for (const block of e.message.content) {
172
+ const b = block as { type?: string; name?: string; id?: string };
173
+ if (b?.type === "toolCall" && b.name === "subagent" && typeof b.id === "string") ids.add(b.id);
174
+ }
175
+ }
176
+ return ids;
177
+ }
178
+
156
179
  export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
157
180
  const structuredOutputPath = process.env[STRUCTURED_OUTPUT_CAPTURE_ENV];
158
181
  const structuredSchemaPath = process.env[STRUCTURED_OUTPUT_SCHEMA_ENV];
@@ -192,11 +215,21 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
192
215
  });
193
216
  }
194
217
 
195
- const onRuntimeEvent = pi.on as unknown as (event: string, handler: (event: unknown) => unknown) => void;
196
- onRuntimeEvent("context", (event: { messages: unknown[] }) => {
197
- const messages = stripParentOnlySubagentMessages(event.messages);
198
- if (messages === event.messages) return undefined;
199
- return { messages };
218
+ let inheritedSubagentToolCallIds: ReadonlySet<string> = new Set<string>();
219
+ const onRuntimeEvent = pi.on as unknown as (
220
+ event: string,
221
+ handler: (event: unknown, ctx?: { sessionManager?: { getBranch(): unknown[] } }) => unknown,
222
+ ) => void;
223
+ onRuntimeEvent("session_start", (_event: unknown, ctx?: { sessionManager?: { getBranch(): unknown[] } }) => {
224
+ // Establishes the inherited/own boundary once, before the child's own first turn: any
225
+ // `subagent` call already in the branch at this point came from the (forked) parent.
226
+ inheritedSubagentToolCallIds = collectSubagentToolCallIds(ctx?.sessionManager?.getBranch() ?? []);
227
+ });
228
+ onRuntimeEvent("context", (event: unknown) => {
229
+ const { messages } = event as { messages: unknown[] };
230
+ const stripped = stripParentOnlySubagentMessages(messages, inheritedSubagentToolCallIds);
231
+ if (stripped === messages) return undefined;
232
+ return { messages: stripped };
200
233
  });
201
234
 
202
235
  onRuntimeEvent("before_agent_start", async (event: { systemPrompt: string }) => {