@fitchmultz/pi-subagents 0.46.0 → 0.46.2

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.
Files changed (92) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/agents/agent-management.js +8 -2
  3. package/dist/agents/agent-selection.js +5 -17
  4. package/dist/agents/agents.js +10 -10
  5. package/dist/extension/build-info.js +1 -1
  6. package/dist/extension/index.js +11 -5
  7. package/dist/extension/result-snapshot.js +0 -18
  8. package/dist/history/ingest.js +40 -31
  9. package/dist/history/prune.js +1 -1
  10. package/dist/history/run-projection.js +1 -2
  11. package/dist/history/search-queries.js +8 -2
  12. package/dist/history/store.js +20 -4
  13. package/dist/history/text-chunks.js +1 -2
  14. package/dist/history/transcript-queries.js +12 -6
  15. package/dist/intercom/result-intercom.js +0 -74
  16. package/dist/pi-intercom/broker/broker.js +33 -16
  17. package/dist/pi-intercom/broker/framing.js +67 -17
  18. package/dist/pi-intercom/broker/snapshot-reader.js +29 -8
  19. package/dist/pi-intercom/broker/snapshots.js +144 -41
  20. package/dist/pi-intercom/session-targets.js +0 -1
  21. package/dist/runs/background/async-run-discovery.js +4 -6
  22. package/dist/runs/background/completion-dedupe.js +4 -55
  23. package/dist/runs/background/notify.js +1 -9
  24. package/dist/runs/background/result-watcher.js +1 -1
  25. package/dist/runs/background/runner-child-observer.js +12 -7
  26. package/dist/runs/background/runner-monitor.js +2 -0
  27. package/dist/runs/background/runner-parallel.js +2 -3
  28. package/dist/runs/background/runner-step-output.js +1 -3
  29. package/dist/runs/shared/acceptance-finalization.js +2 -2
  30. package/dist/runs/shared/acceptance.js +1 -1
  31. package/dist/runs/shared/child-observation-spool.js +8 -3
  32. package/dist/runs/shared/model-fallback.js +3 -0
  33. package/dist/runs/shared/parallel-utils.js +1 -21
  34. package/dist/runs/shared/run-restoration.js +4 -5
  35. package/dist/runs/shared/single-output.js +1 -4
  36. package/dist/runs/shared/subagent-prompt-runtime.js +2 -2
  37. package/dist/runs/shared/supervisor-questions.js +7 -4
  38. package/dist/runs/shared/workflow-policy.js +2 -1
  39. package/dist/shared/journal-frames.js +1 -1
  40. package/dist/shared/settings.js +1 -4
  41. package/dist/slash/command-discovery.js +13 -12
  42. package/dist/tui/agent-history.js +0 -14
  43. package/dist/tui/agent-view.js +0 -1
  44. package/dist/tui/conversation-history.js +10 -1
  45. package/dist/tui/history-cards.js +8 -22
  46. package/docs/intercom.md +5 -1
  47. package/docs/performance.md +72 -3
  48. package/docs/reference.md +1 -1
  49. package/package.json +1 -1
  50. package/src/agents/agent-management.ts +8 -4
  51. package/src/agents/agent-selection.ts +8 -19
  52. package/src/agents/agents.ts +19 -14
  53. package/src/extension/index.ts +12 -5
  54. package/src/extension/result-snapshot.ts +1 -27
  55. package/src/history/ingest.ts +57 -49
  56. package/src/history/prune.ts +1 -1
  57. package/src/history/run-projection.ts +2 -4
  58. package/src/history/search-queries.ts +9 -2
  59. package/src/history/store.ts +21 -5
  60. package/src/history/text-chunks.ts +7 -2
  61. package/src/history/transcript-queries.ts +16 -10
  62. package/src/intercom/result-intercom.ts +0 -103
  63. package/src/pi-intercom/broker/broker.ts +34 -20
  64. package/src/pi-intercom/broker/framing.ts +71 -20
  65. package/src/pi-intercom/broker/snapshot-reader.ts +47 -9
  66. package/src/pi-intercom/broker/snapshots.ts +184 -51
  67. package/src/pi-intercom/session-targets.ts +0 -2
  68. package/src/runs/background/async-run-discovery.ts +4 -10
  69. package/src/runs/background/completion-dedupe.ts +6 -69
  70. package/src/runs/background/notify.ts +2 -12
  71. package/src/runs/background/result-watcher.ts +1 -1
  72. package/src/runs/background/runner-child-observer.ts +19 -11
  73. package/src/runs/background/runner-monitor.ts +2 -0
  74. package/src/runs/background/runner-parallel.ts +2 -3
  75. package/src/runs/background/runner-step-output.ts +1 -3
  76. package/src/runs/shared/acceptance-finalization.ts +2 -2
  77. package/src/runs/shared/acceptance.ts +0 -2
  78. package/src/runs/shared/child-observation-spool.ts +8 -3
  79. package/src/runs/shared/model-fallback.ts +3 -0
  80. package/src/runs/shared/parallel-utils.ts +1 -25
  81. package/src/runs/shared/run-restoration.ts +4 -4
  82. package/src/runs/shared/single-output.ts +0 -4
  83. package/src/runs/shared/subagent-prompt-runtime.ts +2 -2
  84. package/src/runs/shared/supervisor-questions.ts +10 -10
  85. package/src/runs/shared/workflow-policy.ts +1 -1
  86. package/src/shared/journal-frames.ts +1 -1
  87. package/src/shared/settings.ts +1 -4
  88. package/src/slash/command-discovery.ts +13 -12
  89. package/src/tui/agent-history.ts +1 -18
  90. package/src/tui/agent-view.ts +0 -1
  91. package/src/tui/conversation-history.ts +15 -1
  92. package/src/tui/history-cards.ts +8 -26
@@ -2,29 +2,8 @@ import { createHash } from "node:crypto";
2
2
  import { isRecord, hasText } from "./async-value.ts";
3
3
 
4
4
  interface CompletionDataLike {
5
- completionId?: unknown;
6
- id?: unknown;
7
- agent?: unknown;
8
- timestamp?: unknown;
9
- sessionId?: unknown;
10
- taskIndex?: unknown;
11
- totalTasks?: unknown;
12
- success?: unknown;
13
- }
14
-
15
- function asNonEmptyString(value: unknown): string | undefined {
16
- if (typeof value !== "string") {
17
- return undefined;
18
- }
19
- const trimmed = value.trim();
20
- return trimmed.length > 0 ? trimmed : undefined;
21
- }
22
-
23
- function asFiniteNumber(value: unknown): number | undefined {
24
- if (typeof value !== "number") {
25
- return undefined;
26
- }
27
- return Number.isFinite(value) ? value : undefined;
5
+ completionId?: string;
6
+ id: string;
28
7
  }
29
8
 
30
9
  function stableStringify(value: unknown): string {
@@ -48,32 +27,11 @@ function payloadDigest(value: unknown): string {
48
27
  return createHash("sha256").update(stableStringify(value)).digest("hex").slice(0, 16);
49
28
  }
50
29
 
51
- export function buildCompletionKey(data: Readonly<CompletionDataLike>, fallback: string): string {
52
- const completionId = asNonEmptyString(data.completionId);
53
- if (hasText(completionId)) {
54
- return `completion:${completionId}`;
55
- }
56
- const id = asNonEmptyString(data.id);
57
- if (hasText(id)) {
58
- return `id:${id}:${payloadDigest(data)}`;
30
+ export function buildCompletionKey(data: Readonly<CompletionDataLike>): string {
31
+ if (hasText(data.completionId)) {
32
+ return `completion:${data.completionId.trim()}`;
59
33
  }
60
- const sessionId = asNonEmptyString(data.sessionId) ?? "no-session";
61
- const agent = asNonEmptyString(data.agent) ?? "unknown";
62
- const timestamp = asFiniteNumber(data.timestamp);
63
- const taskIndex = asFiniteNumber(data.taskIndex);
64
- const totalTasks = asFiniteNumber(data.totalTasks);
65
- const success = data.success === true ? "1" : "?";
66
- const successIdentity = data.success === false ? "0" : success;
67
- return [
68
- "meta",
69
- sessionId,
70
- agent,
71
- timestamp !== undefined ? String(timestamp) : "no-ts",
72
- taskIndex !== undefined ? String(taskIndex) : "-",
73
- totalTasks !== undefined ? String(totalTasks) : "-",
74
- successIdentity,
75
- fallback,
76
- ].join(":");
34
+ return `id:${data.id.trim()}:${payloadDigest(data)}`;
77
35
  }
78
36
 
79
37
  /** Mutable identity store owned by the completion-admission lifecycle. */
@@ -105,24 +63,3 @@ export function markSeenWithTtl(
105
63
  seen.set(key, now);
106
64
  return false;
107
65
  }
108
-
109
- function isSeenMap(value: unknown): value is Map<string, number> {
110
- if (!(value instanceof Map)) {
111
- return false;
112
- }
113
- const entries: Map<unknown, unknown> = value;
114
- return [...entries].every(
115
- ([key, timestamp]) => typeof key === "string" && typeof timestamp === "number",
116
- );
117
- }
118
-
119
- export function getGlobalSeenMap(storeKey: string): Map<string, number> {
120
- const globalStore = globalThis as Record<string, unknown>;
121
- const existing = globalStore[storeKey];
122
- if (isSeenMap(existing)) {
123
- return existing;
124
- }
125
- const map = new Map<string, number>();
126
- globalStore[storeKey] = map;
127
- return map;
128
- }
@@ -1,6 +1,5 @@
1
1
  /** Subagent completion notifications. */
2
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
- import { buildCompletionKey, getGlobalSeenMap, markSeenWithTtl } from "./completion-dedupe.ts";
4
3
  import { SUBAGENT_ASYNC_COMPLETE_EVENT } from "../../shared/types.ts";
5
4
  import { hasText, isRecord } from "./async-value.ts";
6
5
  import type { ReadonlyInput } from "../../shared/types/inputs.ts";
@@ -144,7 +143,7 @@ function isUnsubscribe(value: unknown): value is () => void {
144
143
 
145
144
  export default function registerSubagentNotify(
146
145
  pi: ReadonlyInput<ExtensionAPI>,
147
- onQueued?: (completionKey: string, runId?: string) => void,
146
+ onQueued: (completionKey: string, runId?: string) => void,
148
147
  ): () => void {
149
148
  const unsubscribeStoreKey = "__pi_subagents_notify_unsubscribe__";
150
149
  const globalStore = globalThis as Record<string, unknown>;
@@ -156,7 +155,6 @@ export default function registerSubagentNotify(
156
155
  // Best effort cleanup for stale handlers from an older reload.
157
156
  }
158
157
  }
159
- const seen = getGlobalSeenMap("__pi_subagents_notify_seen__");
160
158
  const handleComplete = (data: unknown): void => {
161
159
  if (
162
160
  !isResult(data) ||
@@ -165,17 +163,9 @@ export default function registerSubagentNotify(
165
163
  ) {
166
164
  return;
167
165
  }
168
- const key = buildCompletionKey(data, "notify");
169
- // Owned completion keys are governed by the shared queue/journal lifecycle, not a TTL.
170
- if (
171
- (onQueued === undefined || !hasText(data.completionKey)) &&
172
- markSeenWithTtl(seen, key, Date.now(), 10 * 60 * 1000)
173
- ) {
174
- return;
175
- }
176
166
  pi.sendMessage(buildNotification(data), { triggerTurn: true });
177
167
  if (hasText(data.completionKey)) {
178
- onQueued?.(data.completionKey, data.runId);
168
+ onQueued(data.completionKey, data.runId);
179
169
  }
180
170
  };
181
171
  const unsubscribe = pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, handleComplete);
@@ -292,7 +292,7 @@ class ResultWatcher {
292
292
  if (this.foreign(envelope)) {
293
293
  return;
294
294
  }
295
- const key = buildCompletionKey({ ...envelope.data, id: envelope.runId }, "result");
295
+ const key = buildCompletionKey({ ...envelope.data, id: envelope.runId });
296
296
  if (this.alreadyHandled(envelope, key)) {
297
297
  return;
298
298
  }
@@ -59,6 +59,7 @@ export class RunnerChildObserver {
59
59
  this.id = config.id;
60
60
  this.hooks = hooks;
61
61
  this.mutatingFailureStates = statusPayload.steps.map(() => createMutatingFailureState());
62
+ this.syncTopLevelCurrentTool();
62
63
  }
63
64
 
64
65
  expandChildren(flatIndex: number, count: number): void {
@@ -69,16 +70,21 @@ export class RunnerChildObserver {
69
70
  );
70
71
  }
71
72
 
72
- private syncTopLevelCurrentTool(): void {
73
- const active = this.statusPayload.steps
74
- .filter(
75
- (step) =>
76
- step.status === "running" &&
77
- typeof step.currentTool === "string" &&
78
- step.currentTool.length > 0,
79
- )
80
- .sort((left, right) => (right.currentToolStartedAt ?? 0) - (left.currentToolStartedAt ?? 0))
81
- .at(0);
73
+ syncTopLevelCurrentTool(): void {
74
+ const active =
75
+ this.statusPayload.state === "running" || this.statusPayload.state === "queued"
76
+ ? this.statusPayload.steps
77
+ .filter(
78
+ (step) =>
79
+ step.status === "running" &&
80
+ typeof step.currentTool === "string" &&
81
+ step.currentTool.length > 0,
82
+ )
83
+ .sort(
84
+ (left, right) => (right.currentToolStartedAt ?? 0) - (left.currentToolStartedAt ?? 0),
85
+ )
86
+ .at(0)
87
+ : undefined;
82
88
  this.statusPayload.currentTool = active?.currentTool;
83
89
  this.statusPayload.currentToolStartedAt = active?.currentToolStartedAt;
84
90
  this.statusPayload.currentPath = active?.currentPath;
@@ -106,7 +112,9 @@ export class RunnerChildObserver {
106
112
  }
107
113
  step.streamingText = updateStreamingText(step.streamingText, event);
108
114
  this.recordChildActivity(flatIndex, event, toolSnapshot, now);
109
- this.syncTopLevelCurrentTool();
115
+ if (event.type === "tool_execution_start" || event.type === "tool_execution_end") {
116
+ this.syncTopLevelCurrentTool();
117
+ }
110
118
  step.lastActivityAt = now;
111
119
  this.statusPayload.lastActivityAt = now;
112
120
  this.statusPayload.lastUpdate = now;
@@ -166,6 +166,8 @@ export class RunnerMonitor {
166
166
  clearTimeout(this.statusWriteTimer);
167
167
  this.statusWriteTimer = undefined;
168
168
  }
169
+ // Step completion, interruption and expansion can change activity without a tool-end event.
170
+ this.childObserver.syncTopLevelCurrentTool();
169
171
  this.refreshWorkflowGraph();
170
172
  writeAtomicJson(this.statusPath, this.statusPayload);
171
173
  if (this.config.runtimeVersion !== 2) {
@@ -2,7 +2,6 @@ import * as path from "node:path";
2
2
  import { inspect } from "node:util";
3
3
  import type { ReadonlyDeep } from "type-fest";
4
4
  import { appendJsonl } from "../../shared/artifacts.ts";
5
- import { writeAtomicJson } from "../../shared/atomic-json.ts";
6
5
  import { writeInitialProgressFile } from "../../shared/settings.ts";
7
6
  import { completeWorkflowStep, runParallelTasks } from "../shared/workflow-policy.ts";
8
7
  import {
@@ -194,7 +193,7 @@ export class RunnerParallelGroup {
194
193
  status.currentStep = position.flatIndex;
195
194
  status.lastUpdate = failedAt;
196
195
  status.outputFile = path.join(this.config.asyncDir, `output-${position.flatIndex}.log`);
197
- writeAtomicJson(path.join(this.config.asyncDir, "status.json"), status);
196
+ this.monitor.writeStatusPayload();
198
197
  this.append({
199
198
  type: "subagent.parallel.completed",
200
199
  ts: failedAt,
@@ -224,7 +223,7 @@ export class RunnerParallelGroup {
224
223
  status.lastActivityAt = startedAt;
225
224
  status.lastUpdate = startedAt;
226
225
  status.outputFile = path.join(this.config.asyncDir, `output-${position.flatIndex}.log`);
227
- writeAtomicJson(path.join(this.config.asyncDir, "status.json"), status);
226
+ this.monitor.writeStatusPayload();
228
227
  this.append({
229
228
  type: "subagent.parallel.started",
230
229
  ts: startedAt,
@@ -110,9 +110,7 @@ export function publishStepOutput(
110
110
  originalPath.length > 0 &&
111
111
  step.outputMode !== "file-only" &&
112
112
  step.outputPathFromAgentDefault === true;
113
- const cleanup = shouldConsume
114
- ? cleanupSingleOutputFile(originalPath, output, undefined)
115
- : undefined;
113
+ const cleanup = shouldConsume ? cleanupSingleOutputFile(originalPath, output) : undefined;
116
114
  const savedPath = relocateOutput(ctx, originalPath, cleanup);
117
115
  const reference = outputReference(savedPath, state.resolvedOutput.fullOutput, cleanup);
118
116
  const displayOutput = finalizeSingleOutput({
@@ -402,7 +402,7 @@ export function formatAcceptanceFinalizationPrompt(input: {
402
402
  return lines.join("\n");
403
403
  }
404
404
 
405
- export function createFinalizationTurn(input: {
405
+ function createFinalizationTurn(input: {
406
406
  readonly turn: number;
407
407
  readonly prompt: string;
408
408
  readonly rawOutput: string;
@@ -447,7 +447,7 @@ export function createFinalizationProcessFailureTurn(input: {
447
447
  };
448
448
  }
449
449
 
450
- export function attachFinalizationToLedger(input: {
450
+ function attachFinalizationToLedger(input: {
451
451
  readonly initialLedger: AcceptanceLedger;
452
452
  readonly authoritativeLedger: AcceptanceLedger;
453
453
  readonly turns: readonly AcceptanceFinalizationTurn[];
@@ -18,10 +18,8 @@ export {
18
18
  evaluateAcceptanceReport,
19
19
  } from "./acceptance-evaluation.ts";
20
20
  export {
21
- attachFinalizationToLedger,
22
21
  buildFinalizationProcessFailureLedger,
23
22
  createFinalizationProcessFailureTurn,
24
- createFinalizationTurn,
25
23
  createFinalizationReportRuntime,
26
24
  type FinalizationReportSubmission,
27
25
  readFinalizationReport,
@@ -51,8 +51,11 @@ export class ChildObservationSpool {
51
51
  fs.writeFileSync(this.stderr, text);
52
52
  }
53
53
  resetText(): void {
54
- this.textOffset = 0;
54
+ if (this.textOffset === 0) {
55
+ return;
56
+ }
55
57
  fs.ftruncateSync(this.text, 0);
58
+ this.textOffset = 0;
56
59
  }
57
60
  appendText(text: string): void {
58
61
  const bytes = Buffer.from(text);
@@ -72,14 +75,16 @@ export class ChildObservationSpool {
72
75
  this.textOffset += bytes.length;
73
76
  }
74
77
  replayText(consume: (text: string) => void): void {
75
- this.decode(this.text, 0, this.textOffset, consume);
78
+ if (this.textOffset > 0) {
79
+ this.decode(this.text, 0, this.textOffset, consume);
80
+ }
76
81
  }
77
82
  replayWire(start: number, end: number, consume: (text: string) => void): void {
78
83
  this.decode(this.wire, start, end, consume);
79
84
  }
80
85
 
81
86
  private copy(fd: number, start: number, end: number, consume: (bytes: Buffer) => void): void {
82
- const buffer = Buffer.allocUnsafe(64 * 1024);
87
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, end - start));
83
88
  for (let position = start; position < end;) {
84
89
  const count = fs.readSync(fd, buffer, 0, Math.min(buffer.length, end - position), position);
85
90
  if (count === 0) {
@@ -154,6 +154,9 @@ const SAME_MODEL_RECOVERY_PATTERNS = [
154
154
  /socket/i,
155
155
  /connection/i,
156
156
  /fetch failed/i,
157
+ // Node fetch reports a response body cut off mid-stream as bare "terminated"; Pi retries it too.
158
+ // Match the whole message: process failures embed stderr and paths that can contain the word.
159
+ /^\s*terminated\s*$/i,
157
160
  /network/i,
158
161
  /http idle/i,
159
162
  /timed? out/i,
@@ -44,8 +44,6 @@ export function flattenSteps(
44
44
  return flat;
45
45
  }
46
46
 
47
- export { mapConcurrent } from "../../shared/concurrency.ts";
48
-
49
47
  export interface ParallelTaskResult {
50
48
  agent: string;
51
49
  taskIndex?: number;
@@ -54,8 +52,6 @@ export interface ParallelTaskResult {
54
52
  error?: string;
55
53
  model?: string;
56
54
  attemptedModels?: string[];
57
- outputTargetPath?: string;
58
- outputTargetExists?: boolean;
59
55
  }
60
56
 
61
57
  function parallelResultStatus(
@@ -63,30 +59,10 @@ function parallelResultStatus(
63
59
  hasOutput: boolean,
64
60
  ): string {
65
61
  const error = result.error ?? "";
66
- const suffix = error.length > 0 ? `: ${error}` : "";
67
- if (result.exitCode === -1) {
68
- return `SKIPPED${suffix}`;
69
- }
70
- if (result.exitCode !== 0 && result.exitCode !== null) {
71
- return `FAILED (exit code ${result.exitCode})${suffix}`;
72
- }
73
62
  if (error.length > 0) {
74
63
  return `WARNING: ${error}`;
75
64
  }
76
- return emptyOutputStatus(result, hasOutput);
77
- }
78
-
79
- function emptyOutputStatus(result: ReadonlyInput<ParallelTaskResult>, hasOutput: boolean): string {
80
- if (hasOutput) {
81
- return "";
82
- }
83
- const target = result.outputTargetPath ?? "";
84
- if (target.length === 0) {
85
- return "EMPTY OUTPUT (no textual response returned)";
86
- }
87
- return result.outputTargetExists === false
88
- ? `EMPTY OUTPUT (expected output file missing: ${target})`
89
- : "";
65
+ return hasOutput ? "" : "EMPTY OUTPUT (no textual response returned)";
90
66
  }
91
67
 
92
68
  export function aggregateParallelOutputs(
@@ -22,6 +22,7 @@ import {
22
22
  import {
23
23
  getRunMetadataDir,
24
24
  migrateSupervisorQuestionSteps,
25
+ readQuestionOwner,
25
26
  readRunJson,
26
27
  saveQuestionOwner,
27
28
  saveRunStatus,
@@ -254,14 +255,13 @@ function nativeLaunchCwd(run: OwnedRun, details: ReadonlyDetails): OwnedRun {
254
255
  }
255
256
 
256
257
  function repairQuestionOwner(run: OwnedRun): void {
257
- let owner: unknown;
258
+ let sessionId: string | undefined;
258
259
  try {
259
- owner = readRunJson(path.join(getRunMetadataDir(run.runId), "question-owner.json"));
260
+ sessionId = readQuestionOwner(run.runId)?.sessionId;
260
261
  } catch {
261
262
  // A genuine receipt can repair unusable supplemental metadata.
262
263
  }
263
- const sessionId = isRecord(owner) ? owner.sessionId : undefined;
264
- if (typeof sessionId !== "string" || sessionId.trim() === "") {
264
+ if (sessionId === undefined || sessionId.trim() === "") {
265
265
  saveQuestionOwner(run.runId, run.ownerSessionId);
266
266
  }
267
267
  }
@@ -325,7 +325,6 @@ function unchangedOutputIdentity(fd: number, outputPath: string, stat: fs.Stats)
325
325
  export function cleanupSingleOutputFile(
326
326
  outputPath: string | undefined,
327
327
  fullOutput: string,
328
- beforeRun: SingleOutputSnapshot | undefined,
329
328
  ): SingleOutputCleanupResult | undefined {
330
329
  if (!nonempty(outputPath)) {
331
330
  return undefined;
@@ -345,9 +344,6 @@ export function cleanupSingleOutputFile(
345
344
  if (!equal) {
346
345
  return { path: absolutePath, action: "skipped", reason: "file changed after capture" };
347
346
  }
348
- if (matchesSnapshot(stat, beforeRun)) {
349
- return { path: absolutePath, action: "skipped", reason: "file preexisted and was unchanged" };
350
- }
351
347
  fs.unlinkSync(outputPath);
352
348
  return { path: absolutePath, action: "deleted" };
353
349
  } catch (error) {
@@ -24,7 +24,7 @@ const STRUCTURED_OUTPUT_INSTRUCTIONS = [
24
24
  "Do not rely on prose-only completion; if you do not call `structured_output`, the parent will fail this step.",
25
25
  ].join("\n");
26
26
 
27
- export const CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS = [
27
+ const CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS = [
28
28
  "You are a child subagent, not the parent orchestrator.",
29
29
  "The parent session owns delegation, orchestration, review fanout, and follow-up worker launches.",
30
30
  "Ignore prior parent-only orchestration instructions in inherited conversation history.",
@@ -32,7 +32,7 @@ export const CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS = [
32
32
  "If you need to edit files, call the actual edit/write tools. Do not print tool-call syntax, patches, or pseudo-tool calls as text.",
33
33
  ].join("\n");
34
34
 
35
- export const CHILD_FANOUT_BOUNDARY_INSTRUCTIONS = [
35
+ const CHILD_FANOUT_BOUNDARY_INSTRUCTIONS = [
36
36
  "You are a child subagent with delegation enabled for your assigned task.",
37
37
  "You may delegate useful helper work within that task when it saves time or improves quality, using the available delegation tools.",
38
38
  "You remain responsible for your assigned result. The original parent owns integration, review synthesis, and final delivery.",
@@ -216,7 +216,10 @@ export function createSupervisorQuestion(
216
216
  return question;
217
217
  }
218
218
 
219
- export function listRunQuestions(runDir: string): SupervisorQuestionView[] {
219
+ export function listRunQuestions(
220
+ runDir: string,
221
+ ownerSessionId?: string,
222
+ ): SupervisorQuestionView[] {
220
223
  let entries: fs.Dirent[];
221
224
  try {
222
225
  entries = fs.readdirSync(path.join(runDir, "questions"), { withFileTypes: true });
@@ -233,7 +236,10 @@ export function listRunQuestions(runDir: string): SupervisorQuestionView[] {
233
236
  path.join(runDir, "questions", entry.name, "question.json"),
234
237
  parseSupervisorQuestion,
235
238
  );
236
- return question ? [readQuestionState(question, path.dirname(runDir))] : [];
239
+ return question &&
240
+ (ownerSessionId === undefined || question.ownerSessionId === ownerSessionId)
241
+ ? [readQuestionState(question, path.dirname(runDir))]
242
+ : [];
237
243
  });
238
244
  }
239
245
 
@@ -243,9 +249,7 @@ export function listOwnedRunQuestions(
243
249
  runId: string,
244
250
  ): SupervisorQuestionView[] {
245
251
  migrateSupervisorQuestions(ownerSessionId, runId);
246
- return listRunQuestions(getRunMetadataDir(runId)).filter(
247
- (question) => question.ownerSessionId === ownerSessionId,
248
- );
252
+ return listRunQuestions(getRunMetadataDir(runId), ownerSessionId);
249
253
  }
250
254
 
251
255
  export function pendingSupervisorQuestion(input: {
@@ -321,11 +325,7 @@ function* supervisorQuestionSteps(
321
325
  for (const entry of runs) {
322
326
  yield;
323
327
  try {
324
- questions.push(
325
- ...listRunQuestions(path.join(root, entry.name)).filter(
326
- (question) => question.ownerSessionId === ownerSessionId,
327
- ),
328
- );
328
+ questions.push(...listRunQuestions(path.join(root, entry.name), ownerSessionId));
329
329
  } catch (error) {
330
330
  if (runId !== undefined) {
331
331
  throw error;
@@ -1,4 +1,5 @@
1
1
  import { errorMessage } from "../../shared/unknown.ts";
2
+ import { mapConcurrent } from "../../shared/concurrency.ts";
2
3
  import type { DynamicParallelStep } from "../../shared/types/workflow.ts";
3
4
  import type {
4
5
  AcceptanceLedger,
@@ -17,7 +18,6 @@ import {
17
18
  import {
18
19
  aggregateParallelOutputs,
19
20
  FAIL_FAST_REASON,
20
- mapConcurrent,
21
21
  type ParallelTaskResult,
22
22
  } from "./parallel-utils.ts";
23
23
 
@@ -172,7 +172,7 @@ export function readJsonProjection(file: string, select: Projection): UnknownRec
172
172
  const projection = new JsonProjection(select, undefined, () => Infinity);
173
173
  try {
174
174
  const end = fs.fstatSync(fd).size;
175
- const bytes = Buffer.allocUnsafe(64 * 1024);
175
+ const bytes = Buffer.allocUnsafe(Math.min(64 * 1024, end));
176
176
  for (let offset = 0; offset < end;) {
177
177
  const count = fs.readSync(fd, bytes, 0, Math.min(bytes.length, end - offset), offset);
178
178
  if (count === 0) {
@@ -126,7 +126,7 @@ export function resolveStepBehavior(
126
126
  function resolveChainPath(filePath: string, chainDir: string): string {
127
127
  return path.isAbsolute(filePath) ? filePath : path.join(chainDir, filePath);
128
128
  }
129
- /** Prepend read/output paths so task prose cannot accidentally override the chosen destination. */
129
+ /** Compose read paths and progress instructions for a chain child. */
130
130
  export function buildChainInstructions(
131
131
  behavior: ResolvedStepBehavior,
132
132
  chainDir: string,
@@ -139,9 +139,6 @@ export function buildChainInstructions(
139
139
  `[Read from: ${behavior.reads.map((file) => resolveChainPath(file, chainDir)).join(", ")}]`,
140
140
  );
141
141
  }
142
- if (behavior.output !== false && behavior.output.length > 0) {
143
- prefixParts.push(`[Write to: ${resolveChainPath(behavior.output, chainDir)}]`);
144
- }
145
142
  if (behavior.progress) {
146
143
  const progressPath = path.join(chainDir, "progress.md");
147
144
  suffixParts.push(
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { discoverAgents, discoverAgentsAll } from "../agents/agents.ts";
2
+ import { discoverAgents, discoverChains } from "../agents/agents.ts";
3
3
  import type { ChainConfig } from "../shared/types/config.ts";
4
4
  import { resolveExecutionCwd } from "../shared/execution-cwd.ts";
5
5
 
@@ -51,21 +51,19 @@ export function makeAgentCompletions(
51
51
  multiAgent: boolean,
52
52
  ): (prefix: string) => Completion[] | null {
53
53
  return (prefix) => {
54
+ const position = multiAgent ? completionPosition(prefix) : { word: prefix, before: "" };
55
+ if (!position || (!multiAgent && prefix.includes(" "))) {
56
+ return null;
57
+ }
54
58
  const cwd = completionCwd(pi, state);
55
59
  if (cwd === null || cwd.length === 0) {
56
60
  return null;
57
61
  }
58
62
  const agents = discoverAgents(cwd, "both", { projectTrusted: projectTrusted(state) }).agents;
59
63
  if (!multiAgent) {
60
- return prefix.includes(" ")
61
- ? null
62
- : agents
63
- .filter((agent) => agent.name.startsWith(prefix))
64
- .map((agent) => ({ value: agent.name, label: agent.name }));
65
- }
66
- const position = completionPosition(prefix);
67
- if (!position) {
68
- return null;
64
+ return agents
65
+ .filter((agent) => agent.name.startsWith(prefix))
66
+ .map((agent) => ({ value: agent.name, label: agent.name }));
69
67
  }
70
68
  const { word: lastWord, before: beforeLastWord } = position;
71
69
  if (lastWord === "->") {
@@ -79,7 +77,7 @@ export function makeAgentCompletions(
79
77
 
80
78
  export function discoverSavedChains(cwd: string, trusted: boolean): ChainConfig[] {
81
79
  const chains = new Map<string, ChainConfig>();
82
- for (const chain of discoverAgentsAll(cwd, { projectTrusted: trusted }).chains) {
80
+ for (const chain of discoverChains(cwd, { projectTrusted: trusted }).chains) {
83
81
  const existing = chains.get(chain.name);
84
82
  const projectOverride = existing?.source === "user" && chain.source === "project";
85
83
  const jsonOverride =
@@ -98,8 +96,11 @@ export function makeChainCompletions(
98
96
  state: SlashDiscoveryState,
99
97
  ): (prefix: string) => Completion[] | null {
100
98
  return (prefix) => {
99
+ if (prefix.includes(" ")) {
100
+ return null;
101
+ }
101
102
  const cwd = completionCwd(pi, state);
102
- if (prefix.includes(" ") || cwd === null || cwd.length === 0) {
103
+ if (cwd === null || cwd.length === 0) {
103
104
  return null;
104
105
  }
105
106
  return discoverSavedChains(cwd, projectTrusted(state))
@@ -1,27 +1,10 @@
1
- import type { SessionEntry } from "@earendil-works/pi-coding-agent";
2
1
  import { stripAcceptanceReport } from "../runs/shared/acceptance-reports.ts";
3
- import { displayHistory, type AgentHistory, type AgentHistoryItem } from "./history-display.ts";
4
- import type { DisplayEntry } from "./history-record.ts";
2
+ import type { AgentHistory, AgentHistoryItem } from "./history-display.ts";
5
3
  import { readableText } from "./history-text.ts";
6
4
  export { readableText } from "./history-text.ts";
7
5
  export { indexedHistory } from "./indexed-history.ts";
8
6
  export type { AgentHistory, AgentHistoryItem } from "./history-display.ts";
9
7
 
10
- /** Read native entries, not a compacted model context: earlier messages remain inspectable. */
11
- export function historyItems(entries: readonly SessionEntry[]): AgentHistory {
12
- const visible: DisplayEntry[] = [];
13
- for (const entry of entries) {
14
- if (
15
- entry.type === "message" ||
16
- entry.type === "custom_message" ||
17
- entry.type === "compaction" ||
18
- entry.type === "branch_summary"
19
- ) {
20
- visible.push(entry.type === "message" ? { ...entry, native: entry.message } : entry);
21
- }
22
- }
23
- return displayHistory(visible);
24
- }
25
8
  function finalMatch(history: AgentHistory, text: string): string | undefined {
26
9
  if (history.finalId !== undefined) {
27
10
  return history.finalId;
@@ -400,5 +400,4 @@ export class AgentViewController {
400
400
  this.lastSaved = "";
401
401
  }
402
402
  }
403
- export { AgentConversation } from "./agent-conversation.ts";
404
403
  export { agentTaskLabel, type AgentTask, type AgentVisit } from "./view-model.ts";
@@ -1,4 +1,5 @@
1
1
  import { errorText } from "./text-values.ts";
2
+ import { isDeepStrictEqual } from "node:util";
2
3
  import {
3
4
  historyStatus,
4
5
  pageNotices,
@@ -183,9 +184,22 @@ export class ConversationHistory {
183
184
  ? { ...item, load: () => this.controller.savedResult(this.key, item.id) }
184
185
  : item,
185
186
  );
187
+ const previous = task.page,
188
+ sameSource =
189
+ previous !== undefined &&
190
+ previous.sourceId === result.page.sourceId &&
191
+ previous.generation === result.page.generation &&
192
+ previous.sessionId === result.page.sessionId &&
193
+ previous.sourceState === result.page.sourceState &&
194
+ isDeepStrictEqual(
195
+ previous.entries.map((entry) => entry.ref),
196
+ result.page.entries.map((entry) => entry.ref),
197
+ );
186
198
  this.seenSource ||= result.page.count > 0;
187
199
  this.controller.applyHistoryPage(this.key, { ...history, items }, result.page, this.seenSource);
188
- this.source.clearCards();
200
+ if (!sameSource) {
201
+ this.source.clearCards();
202
+ }
189
203
  }
190
204
 
191
205
  assignment(): AgentHistoryItem {