pi-crew 0.9.41 → 0.9.42

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 (147) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/dist/build-meta.json +7087 -6726
  3. package/dist/index.mjs +43043 -42426
  4. package/dist/index.mjs.map +4 -4
  5. package/docs/migration/atomic-write-v2-migration.md +2 -0
  6. package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +2 -0
  7. package/package.json +4 -2
  8. package/scripts/README.md +6 -0
  9. package/scripts/check-all-skills.ts +3 -3
  10. package/scripts/dev-runner.mjs +106 -0
  11. package/scripts/postinstall.mjs +6 -2
  12. package/scripts/verify-skill.ts +2 -2
  13. package/src/config/config.ts +19 -0
  14. package/src/config/drift-detector.ts +97 -15
  15. package/src/config/resilient-parser.ts +4 -4
  16. package/src/extension/crew-cleanup.ts +17 -3
  17. package/src/extension/crew-vibes/index.ts +1 -1
  18. package/src/extension/cross-extension-rpc.ts +18 -5
  19. package/src/extension/management.ts +6 -5
  20. package/src/extension/register.ts +58 -1573
  21. package/src/extension/registration/command-registration.ts +57 -0
  22. package/src/extension/registration/commands.ts +2 -1
  23. package/src/extension/registration/context-builder.ts +131 -0
  24. package/src/extension/registration/crash-recovery-cache.ts +54 -0
  25. package/src/extension/registration/foreground-run-controller.ts +210 -0
  26. package/src/extension/registration/hook-registration.ts +119 -0
  27. package/src/extension/registration/lazy-configurers.ts +124 -0
  28. package/src/extension/registration/lifecycle-handlers.ts +731 -0
  29. package/src/extension/registration/observability.ts +33 -4
  30. package/src/extension/registration/registration-types.ts +149 -0
  31. package/src/extension/registration/runtime-cleanup.ts +230 -0
  32. package/src/extension/registration/subagent-manager-setup.ts +211 -0
  33. package/src/extension/registration/tool-registration.ts +50 -0
  34. package/src/extension/registration/wire-cross-extension.ts +37 -0
  35. package/src/extension/rpc-hmac.ts +21 -1
  36. package/src/extension/run-import.ts +3 -3
  37. package/src/extension/team-tool/auto-summarize.ts +4 -4
  38. package/src/extension/team-tool/cancel.ts +5 -5
  39. package/src/extension/team-tool/chain-executor.ts +5 -4
  40. package/src/extension/team-tool/context.ts +11 -1
  41. package/src/extension/team-tool/doctor.ts +2 -1
  42. package/src/extension/team-tool/goal-wrap.ts +2 -2
  43. package/src/extension/team-tool/goal.ts +1 -1
  44. package/src/extension/team-tool/handle-schedule.ts +25 -36
  45. package/src/extension/team-tool/orchestrate.ts +0 -1
  46. package/src/extension/team-tool/parallel-dispatch.ts +5 -0
  47. package/src/extension/team-tool/run.ts +220 -214
  48. package/src/extension/team-tool/workflow-manage.ts +1 -1
  49. package/src/extension/team-tool.ts +64 -16
  50. package/src/hooks/registry.ts +7 -2
  51. package/src/observability/correlation.ts +4 -3
  52. package/src/observability/event-bus.ts +10 -1
  53. package/src/observability/event-to-metric.ts +8 -7
  54. package/src/observability/exporters/adapter.ts +6 -9
  55. package/src/observability/exporters/otlp-exporter.ts +81 -0
  56. package/src/observability/metric-retention.ts +4 -0
  57. package/src/observability/metric-sink.ts +14 -4
  58. package/src/runtime/adaptive-plan.ts +1 -1
  59. package/src/runtime/async-runner.ts +13 -4
  60. package/src/runtime/background-runner.ts +32 -23
  61. package/src/runtime/capability-inventory.ts +1 -1
  62. package/src/runtime/chain-runner.ts +5 -5
  63. package/src/runtime/checkpoint.ts +6 -16
  64. package/src/runtime/child-pi.ts +22 -2
  65. package/src/runtime/coalesce-tasks.ts +1 -1
  66. package/src/runtime/crash-recovery.ts +63 -47
  67. package/src/runtime/delivery-coordinator.ts +1 -1
  68. package/src/runtime/deterministic-ast.ts +2 -2
  69. package/src/runtime/diagnostic-export.ts +2 -1
  70. package/src/runtime/foreground-control.ts +3 -2
  71. package/src/runtime/goal-loop-runner.ts +6 -2
  72. package/src/runtime/goal-state-store.ts +1 -1
  73. package/src/runtime/handoff-manager.ts +0 -2
  74. package/src/runtime/intercom-bridge.ts +0 -2
  75. package/src/runtime/live-agent-manager.ts +12 -5
  76. package/src/runtime/live-extension-bridge.ts +0 -2
  77. package/src/runtime/live-session-runtime.ts +65 -57
  78. package/src/runtime/manifest-cache.ts +59 -0
  79. package/src/runtime/mcp-proxy.ts +1 -1
  80. package/src/runtime/orphan-worker-registry.ts +3 -3
  81. package/src/runtime/output-validator.ts +1 -1
  82. package/src/runtime/path-overlap.ts +1 -1
  83. package/src/runtime/pi-args.ts +3 -3
  84. package/src/runtime/pi-json-output.ts +2 -2
  85. package/src/runtime/pipeline-runner.ts +1 -4
  86. package/src/runtime/post-checks.ts +0 -1
  87. package/src/runtime/retry-runner.ts +1 -1
  88. package/src/runtime/run-coalesced-task-group.ts +0 -13
  89. package/src/runtime/scheduler.ts +2 -0
  90. package/src/runtime/session-resources.ts +1 -1
  91. package/src/runtime/settings-store.ts +31 -1
  92. package/src/runtime/skill-effectiveness.ts +2 -2
  93. package/src/runtime/stale-reconciler.ts +23 -53
  94. package/src/runtime/subagent-manager.ts +19 -3
  95. package/src/runtime/task-output-context.ts +2 -1
  96. package/src/runtime/task-runner/prompt-builder.ts +1 -1
  97. package/src/runtime/task-runner/run-projection.ts +3 -1
  98. package/src/runtime/task-runner/state-helpers.ts +13 -1
  99. package/src/runtime/task-runner.ts +31 -6
  100. package/src/runtime/team-runner.ts +3 -2
  101. package/src/runtime/verification-gates.ts +54 -5
  102. package/src/schema/config-schema.ts +19 -1
  103. package/src/schema/team-tool-schema.ts +2 -0
  104. package/src/skills/discover-skills.ts +25 -15
  105. package/src/state/active-run-registry.ts +0 -1
  106. package/src/state/atomic-write.ts +37 -11
  107. package/src/state/blob-store.ts +3 -9
  108. package/src/state/contracts.ts +5 -1
  109. package/src/state/crew-init.ts +3 -2
  110. package/src/state/decision-ledger.ts +2 -2
  111. package/src/state/event-log-rotation.ts +1 -1
  112. package/src/state/event-log.ts +3 -3
  113. package/src/state/gitignore-manager.ts +2 -1
  114. package/src/state/health-store.ts +5 -2
  115. package/src/state/instinct-store.ts +11 -2
  116. package/src/state/mailbox.ts +3 -4
  117. package/src/state/observation-store.ts +1 -13
  118. package/src/state/run-cache.ts +12 -12
  119. package/src/state/run-graph.ts +2 -1
  120. package/src/state/run-metrics.ts +1 -1
  121. package/src/state/schedule.ts +7 -1
  122. package/src/state/state-store.ts +9 -3
  123. package/src/state/tiered-eval.ts +1 -1
  124. package/src/ui/dynamic-border.ts +13 -3
  125. package/src/ui/live-conversation-overlay.ts +7 -27
  126. package/src/ui/live-duration.ts +2 -2
  127. package/src/ui/live-run-sidebar.ts +56 -32
  128. package/src/ui/mascot.ts +19 -13
  129. package/src/ui/powerbar-publisher.ts +244 -178
  130. package/src/ui/render-coalescer.ts +44 -3
  131. package/src/ui/render-diff.ts +2 -2
  132. package/src/ui/run-dashboard.ts +122 -21
  133. package/src/ui/run-event-bus.ts +66 -0
  134. package/src/ui/run-snapshot-cache.ts +84 -81
  135. package/src/ui/settings-overlay.ts +3 -3
  136. package/src/ui/status-colors.ts +24 -0
  137. package/src/ui/tool-progress-formatter.ts +1 -1
  138. package/src/ui/tool-render.ts +3 -3
  139. package/src/ui/transcript-cache.ts +5 -10
  140. package/src/ui/transcript-entries.ts +68 -72
  141. package/src/ui/transcript-viewer.ts +54 -14
  142. package/src/ui/widget/index.ts +26 -15
  143. package/src/ui/widget/widget-model.ts +18 -9
  144. package/src/ui/widget/widget-renderer.ts +2 -5
  145. package/src/utils/fingerprint.ts +1 -1
  146. package/src/utils/project-detector.ts +1 -1
  147. package/src/state/atomic-write-v2.ts +0 -108
@@ -21,7 +21,6 @@ export interface DeliveryCoordinatorDeps {
21
21
  const PENDING_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
22
22
 
23
23
  export class DeliveryCoordinator {
24
- private ownerSessionId: string | undefined;
25
24
  private active = false;
26
25
  private generation = 0;
27
26
  private pending: PendingDelivery[] = [];
@@ -29,6 +28,7 @@ export class DeliveryCoordinator {
29
28
  private readonly deps: DeliveryCoordinatorDeps;
30
29
  private ttlTimer: ReturnType<typeof setInterval> | undefined;
31
30
  private timerStarted = false;
31
+ private ownerSessionId: string | undefined = undefined;
32
32
 
33
33
  constructor(deps: DeliveryCoordinatorDeps) {
34
34
  this.deps = deps;
@@ -120,9 +120,9 @@ function isNewDateExpression(node: AstNode): boolean {
120
120
  */
121
121
  function isMemberExpression(node: AstNode, childKey: string, objectName: string, propertyName: string): boolean {
122
122
  const child = asAstNode(node[childKey]);
123
- if (!child || child.type !== "MemberExpression") return false;
123
+ if (child?.type !== "MemberExpression") return false;
124
124
  const object = asAstNode(child.object);
125
- if (!object || object.type !== "Identifier" || object.name !== objectName) return false;
125
+ if (object?.type !== "Identifier" || object.name !== objectName) return false;
126
126
  return propertyNameOf(child) === propertyName;
127
127
  }
128
128
 
@@ -3,6 +3,7 @@ import * as path from "node:path";
3
3
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
4
4
  import type { MetricRegistry } from "../observability/metric-registry.ts";
5
5
  import type { MetricSnapshot } from "../observability/metrics-primitives.ts";
6
+ import { atomicWriteFile } from "../state/atomic-write.ts";
6
7
  import { readEvents, type TeamEvent } from "../state/event-log.ts";
7
8
  import { loadRunManifestById } from "../state/state-store.ts";
8
9
  import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
@@ -148,7 +149,7 @@ export async function exportDiagnostic(
148
149
  const dir = path.join(loaded.manifest.artifactsRoot, "diagnostic");
149
150
  fs.mkdirSync(dir, { recursive: true });
150
151
  const filePath = path.join(dir, `diagnostic-${safeTimestamp}.json`);
151
- fs.writeFileSync(filePath, `${JSON.stringify(report, null, 2)}\n`, "utf-8");
152
+ atomicWriteFile(filePath, `${JSON.stringify(report, null, 2)}\n`);
152
153
  return { path: filePath, report };
153
154
  }
154
155
 
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { atomicWriteFile } from "../state/atomic-write.ts";
3
4
  import { appendEvent } from "../state/event-log.ts";
4
5
  import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
5
6
  import { logInternalError } from "../utils/internal-error.ts";
@@ -82,7 +83,7 @@ export function writeForegroundInterruptRequest(
82
83
  try {
83
84
  fs.mkdirSync(lockDir, { recursive: true });
84
85
  try {
85
- fs.writeFileSync(pidFile, String(process.pid), "utf-8");
86
+ atomicWriteFile(pidFile, String(process.pid));
86
87
  } catch {
87
88
  /* best-effort */
88
89
  }
@@ -178,7 +179,7 @@ export function writeForegroundInterruptRequest(
178
179
  acknowledged: false,
179
180
  };
180
181
  fs.mkdirSync(path.dirname(controlPath), { recursive: true });
181
- fs.writeFileSync(controlPath, `${JSON.stringify({ requests: [...requests, request] }, null, 2)}\n`, "utf-8");
182
+ atomicWriteFile(controlPath, `${JSON.stringify({ requests: [...requests, request] }, null, 2)}\n`);
182
183
  appendEvent(manifest.eventsPath, {
183
184
  type: "foreground.interrupt_requested",
184
185
  runId: manifest.runId,
@@ -16,6 +16,7 @@
16
16
 
17
17
  import { randomBytes } from "node:crypto";
18
18
  import { existsSync, readdirSync } from "node:fs";
19
+ import { createRequire } from "node:module";
19
20
  import type { AgentConfig } from "../agents/agent-config.ts";
20
21
  import { loadConfig } from "../config/config.ts";
21
22
  import { effectiveRunConfig } from "../extension/team-tool/config-patch.ts";
@@ -260,8 +261,11 @@ function resolveGoalTurnWorkflow(goal: GoalLoopState): WorkflowConfig {
260
261
  const wrapName = (goal as GoalLoopState & { goalWrapWorkflow?: string }).goalWrapWorkflow;
261
262
  if (!wrapName) return buildTurnWorkflow();
262
263
  try {
263
- const { discoverWorkflows, allWorkflows } =
264
- require("../workflows/discover-workflows.ts") as typeof import("../workflows/discover-workflows.ts");
264
+ // LAZY: createRequire for synchronous module resolution in ESM context
265
+ const requireFromHere = createRequire(import.meta.url);
266
+ const { discoverWorkflows, allWorkflows } = requireFromHere(
267
+ "../workflows/discover-workflows.ts",
268
+ ) as typeof import("../workflows/discover-workflows.ts");
265
269
  const found = allWorkflows(discoverWorkflows(goal.cwd)).find((w) => w.name === wrapName && w.source === "builtin");
266
270
  if (found) return found;
267
271
  logInternalError(
@@ -13,7 +13,7 @@
13
13
  * per-turn usage stays on each turn's TeamRunManifest/tasks.json.
14
14
  */
15
15
 
16
- import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
16
+ import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
17
17
  import { dirname } from "node:path";
18
18
  import { atomicWriteJson } from "../state/atomic-write.ts";
19
19
  import { appendEvent } from "../state/event-log.ts";
@@ -54,8 +54,6 @@ export function isValidHandoffSummary(value: unknown): value is HandoffSummary {
54
54
  * @see docs/pi-boomerang-integration-plan.md
55
55
  */
56
56
 
57
- import type { TeamEvent } from "../state/event-log.ts";
58
-
59
57
  /**
60
58
  * Represents a key decision made during task execution.
61
59
  */
@@ -8,8 +8,6 @@
8
8
  * The actual tool registration happens in task-runner.ts.
9
9
  */
10
10
 
11
- import { logInternalError } from "../utils/internal-error.ts";
12
-
13
11
  // ── Types ────────────────────────────────────────────────────────────────
14
12
 
15
13
  export type IntercomUrgency = "low" | "medium" | "high" | "critical";
@@ -2,6 +2,7 @@ import type { appendEvent } from "../state/event-log.ts";
2
2
  import { logInternalError } from "../utils/internal-error.ts";
3
3
  import type { CrewAgentRecord } from "./crew-agent-runtime.ts";
4
4
  import type { IrcMessage } from "./live-irc.ts";
5
+ import { checkProcessLiveness } from "./process-status.ts";
5
6
 
6
7
  const MAX_PENDING_MESSAGES = 1000;
7
8
 
@@ -263,10 +264,16 @@ export function evictStaleLiveAgentHandles(now = Date.now()): number {
263
264
  evicted++;
264
265
  }
265
266
  } else if (age > STALE_RUNNING_HANDLE_MS) {
266
- // Active-status handle with no update for 30min — presumed dead
267
- liveAgents.delete(agentId);
268
- safeDisposeLiveSession(handle);
269
- evicted++;
267
+ // Active-status handle with no update for 30min — check process liveness
268
+ // before evicting. Only evict if the process is dead, to avoid evicting
269
+ // slow-but-alive agents.
270
+ const sessionPid = (handle.session as Record<string, unknown>)?.pid as number | undefined;
271
+ const liveness = checkProcessLiveness(sessionPid);
272
+ if (!liveness.alive) {
273
+ liveAgents.delete(agentId);
274
+ safeDisposeLiveSession(handle);
275
+ evicted++;
276
+ }
270
277
  }
271
278
  }
272
279
  return evicted;
@@ -282,7 +289,7 @@ export function listActiveLiveAgents(): LiveAgentHandle[] {
282
289
 
283
290
  export function getLiveAgentContextPercent(agentIdOrTaskId: string): number | null {
284
291
  const handle = getLiveAgent(agentIdOrTaskId);
285
- if (!handle || handle.status !== "running") return null;
292
+ if (handle?.status !== "running") return null;
286
293
  try {
287
294
  return handle.session.getSessionStats?.().contextUsage?.percent ?? null;
288
295
  } catch {
@@ -29,8 +29,6 @@
29
29
  * - setActiveTools / getActiveTools / getAllTools
30
30
  */
31
31
 
32
- import type { YieldResult } from "./yield-handler.ts";
33
-
34
32
  export interface ExtensionBridgeApis {
35
33
  sendMessage: (message: unknown, options?: Record<string, unknown>) => void;
36
34
  sendUserMessage: (content: unknown, options?: Record<string, unknown>) => void;
@@ -27,7 +27,7 @@ import {
27
27
  } from "./live-agent-manager.ts";
28
28
  import { subscribeLiveControlRealtime } from "./live-control-realtime.ts";
29
29
  import { buildExtensionBridge } from "./live-extension-bridge.ts";
30
- import { collectLiveSessionHealth, formatLiveSessionDiagnostics, type LiveSessionHealth } from "./live-session-health.ts";
30
+ import { collectLiveSessionHealth, formatLiveSessionDiagnostics } from "./live-session-health.ts";
31
31
  import { buildMcpProxyFromSession } from "./mcp-proxy.ts";
32
32
  import { buildConfiguredModelRouting } from "./model-fallback.ts";
33
33
  import { readEnabledModelsPatterns } from "./model-scope.ts";
@@ -231,7 +231,7 @@ function eventText(event: unknown): string[] {
231
231
 
232
232
  function finalAssistantText(event: unknown): string[] {
233
233
  const obj = asRecord(event);
234
- if (!obj || obj.type !== "message_end") return [];
234
+ if (obj?.type !== "message_end") return [];
235
235
  const message = asRecord(obj.message);
236
236
  if (message?.role !== "assistant") return [];
237
237
  return textFromContent(message.content);
@@ -266,7 +266,7 @@ async function resolveScopeModelsPatterns(cwd: string, agentDir?: string): Promi
266
266
  }
267
267
 
268
268
  function modelFromRegistry(modelRegistry: unknown, modelId: string | undefined): unknown {
269
- if (!modelId || !modelId.includes("/")) return undefined;
269
+ if (!modelId?.includes("/")) return undefined;
270
270
  const registry = asRecord(modelRegistry);
271
271
  const find = registry?.find;
272
272
  if (typeof find !== "function") return undefined;
@@ -450,7 +450,7 @@ function filterActiveTools(session: LiveSessionLike, agent: AgentConfig, role?:
450
450
  const allowed = policy.tools?.length ? new Set(policy.tools) : undefined;
451
451
  const active = session
452
452
  .getActiveToolNames()
453
- .filter((name) => !recursiveTools.has(name) && (!disallowed || !disallowed.has(name)) && (!allowed || allowed.has(name)));
453
+ .filter((name) => !recursiveTools.has(name) && !disallowed?.has(name) && (!allowed || allowed.has(name)));
454
454
  session.setActiveToolsByName(active);
455
455
  }
456
456
 
@@ -822,61 +822,69 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
822
822
  });
823
823
  if (typeof session.subscribe === "function") {
824
824
  unsubscribe = session.subscribe((event) => {
825
- if (!isCurrent()) return;
826
- jsonEvents += 1;
827
- appendTranscript(input.transcriptPath, event);
828
- const sidechainType = eventToSidechainType(event);
829
- if (sidechainType)
830
- writeSidechainEntry(sidechainPath, {
831
- agentId,
832
- type: sidechainType,
833
- message: event,
834
- cwd: input.task.cwd,
835
- });
836
- const obj = asRecord(event);
837
- if (obj?.type === "turn_end") {
838
- turnCount += 1;
839
- trackLiveAgentTurnEnd(agentId);
840
- if (maxTurns !== undefined && !softLimitReached && turnCount >= maxTurns) {
841
- softLimitReached = true;
842
- void session?.steer?.("You have reached your turn limit. Wrap up immediately — provide your final answer now.");
843
- } else if (maxTurns !== undefined && softLimitReached && turnCount >= maxTurns + graceTurns) {
844
- void session?.abort?.();
845
- }
846
- }
847
- // Accumulate lifetime usage that survives compaction
848
- if (obj?.type === "message_end" && extractMessageRole(obj) === "assistant") {
849
- const u = extractMessageUsage(obj);
850
- if (u) {
851
- trackTaskUsage(input.task.id, {
852
- input: typeof u.input === "number" ? u.input : 0,
853
- output: typeof u.output === "number" ? u.output : 0,
854
- cacheWrite: typeof u.cacheWrite === "number" ? u.cacheWrite : 0,
825
+ // Wrap callback body in try/catch to prevent unguarded exceptions from
826
+ // propagating into the session internals and crashing the live session.
827
+ try {
828
+ if (!isCurrent()) return;
829
+ jsonEvents += 1;
830
+ appendTranscript(input.transcriptPath, event);
831
+ const sidechainType = eventToSidechainType(event);
832
+ if (sidechainType)
833
+ writeSidechainEntry(sidechainPath, {
834
+ agentId,
835
+ type: sidechainType,
836
+ message: event,
837
+ cwd: input.task.cwd,
855
838
  });
839
+ const obj = asRecord(event);
840
+ if (obj?.type === "turn_end") {
841
+ turnCount += 1;
842
+ trackLiveAgentTurnEnd(agentId);
843
+ if (maxTurns !== undefined && !softLimitReached && turnCount >= maxTurns) {
844
+ softLimitReached = true;
845
+ void session?.steer?.("You have reached your turn limit. Wrap up immediately — provide your final answer now.");
846
+ } else if (maxTurns !== undefined && softLimitReached && turnCount >= maxTurns + graceTurns) {
847
+ void session?.abort?.();
848
+ }
856
849
  }
857
- }
858
- input.onEvent?.(event);
859
- const text = [...eventText(event), ...finalAssistantText(event)].join("\n");
860
- if (text.trim()) {
861
- stdout += `${text}\n`;
862
- streamOut?.write(text + "\n");
863
- trackLiveAgentResponseText(agentId, text);
864
- input.onOutput?.(text);
865
- }
866
- // G2: Track tool start/end for activity display
867
- if (obj?.type === "tool_use" || obj?.type === "tool_execution_start") {
868
- const toolName = extractToolName(obj) ?? "unknown";
869
- trackLiveAgentToolStart(agentId, toolName);
870
- }
871
- if (obj?.type === "tool_result" || obj?.type === "tool_execution_end") {
872
- const toolName = extractToolName(obj) ?? "unknown";
873
- trackLiveAgentToolEnd(agentId, toolName);
874
- }
875
- // Phase 1: collect events for yield detection
876
- if (collectedJsonEvents && event && typeof event === "object" && !Array.isArray(event)) {
877
- collectedJsonEvents.push(event as Record<string, unknown>);
878
- if (collectedJsonEvents.length > maxCollectedJsonEvents)
879
- collectedJsonEvents.splice(0, collectedJsonEvents.length - maxCollectedJsonEvents);
850
+ // Accumulate lifetime usage that survives compaction
851
+ if (obj?.type === "message_end" && extractMessageRole(obj) === "assistant") {
852
+ const u = extractMessageUsage(obj);
853
+ if (u) {
854
+ trackTaskUsage(input.task.id, {
855
+ input: typeof u.input === "number" ? u.input : 0,
856
+ output: typeof u.output === "number" ? u.output : 0,
857
+ cacheWrite: typeof u.cacheWrite === "number" ? u.cacheWrite : 0,
858
+ });
859
+ }
860
+ }
861
+ input.onEvent?.(event);
862
+ const text = [...eventText(event), ...finalAssistantText(event)].join("\n");
863
+ if (text.trim()) {
864
+ stdout += `${text}\n`;
865
+ streamOut?.write(text + "\n");
866
+ trackLiveAgentResponseText(agentId, text);
867
+ input.onOutput?.(text);
868
+ }
869
+ // G2: Track tool start/end for activity display
870
+ if (obj?.type === "tool_use" || obj?.type === "tool_execution_start") {
871
+ const toolName = extractToolName(obj) ?? "unknown";
872
+ trackLiveAgentToolStart(agentId, toolName);
873
+ }
874
+ if (obj?.type === "tool_result" || obj?.type === "tool_execution_end") {
875
+ const toolName = extractToolName(obj) ?? "unknown";
876
+ trackLiveAgentToolEnd(agentId, toolName);
877
+ }
878
+ // Phase 1: collect events for yield detection
879
+ if (collectedJsonEvents && event && typeof event === "object" && !Array.isArray(event)) {
880
+ collectedJsonEvents.push(event as Record<string, unknown>);
881
+ if (collectedJsonEvents.length > maxCollectedJsonEvents)
882
+ collectedJsonEvents.splice(0, collectedJsonEvents.length - maxCollectedJsonEvents);
883
+ }
884
+ } catch (error) {
885
+ // Prevent unguarded exceptions in the subscribe callback from crashing
886
+ // the live session or propagating into the session internals.
887
+ logInternalError("live-session.subscribe-callback", error, `agentId=${agentId}`);
880
888
  }
881
889
  });
882
890
  }
@@ -9,6 +9,15 @@ import { isSafePathId, resolveContainedRelativePath, resolveRealContainedPath }
9
9
 
10
10
  export interface ManifestCache {
11
11
  list(limit?: number): TeamRunManifest[];
12
+ /**
13
+ * Return ONLY running manifests, capped only by `limit`.
14
+ *
15
+ * RT-F3: unlike `list(limit)`, this filters `status === "running"` BEFORE
16
+ * the limit is applied so stale orphaned runs that have been pushed past
17
+ * the top-N by recent activity are not silently hidden from crash-recovery
18
+ * and zombie-detection scans.
19
+ */
20
+ listActive(limit: number): TeamRunManifest[];
12
21
  get(runId: string): TeamRunManifest | undefined;
13
22
  clear(runId?: string): void;
14
23
  dispose(): void;
@@ -203,6 +212,15 @@ export function createManifestCache(cwd: string, options: ManifestCacheOptions =
203
212
  return undefined;
204
213
  }
205
214
 
215
+ /**
216
+ * NOTE (RT-F10): This function performs a full FS scan + JSON.parse of all
217
+ * manifests on every TTL expiry (default 500ms). This is a known performance
218
+ * tradeoff: fs.watch provides real-time invalidation (see constructor above)
219
+ * so the TTL cache is only consulted when the watcher fires, but the list()
220
+ * call itself still re-reads all entries to pick up changes. Incremental
221
+ * updates via fs.watch-driven delta tracking are planned for a future
222
+ * iteration to avoid the full scan on each expiry.
223
+ */
206
224
  function list(limit = DEFAULT_CACHE.manifestMaxEntries): TeamRunManifest[] {
207
225
  const now = Date.now();
208
226
  const cached = listCache.get(limit);
@@ -248,6 +266,46 @@ export function createManifestCache(cwd: string, options: ManifestCacheOptions =
248
266
  return undefined;
249
267
  }
250
268
 
269
+ /**
270
+ * RT-F3: filter to `status === "running"` BEFORE applying the limit so an
271
+ * orphaned run that has been pushed past the top-N by recent successful
272
+ * runs is still surfaced to crash-recovery. The cap is the LIMIT, not the
273
+ * "top-N most recent"; callers (e.g. zombie detection) explicitly want
274
+ * EVERY currently-running run, in any order.
275
+ *
276
+ * Internally performs a full scan (NOT via list()) so the cap doesn't
277
+ * silently drop "running" runs that fell past the top-N createdAt cutoff.
278
+ * Still goes through parseManifestIfChanged for stat+size memoization, so
279
+ * the per-run I/O cost is the same as list().
280
+ */
281
+ function listActive(limit: number): TeamRunManifest[] {
282
+ const cap = Math.max(0, limit);
283
+ const parsedEntries = [
284
+ ...roots.flatMap((root) => collectRoots(root)),
285
+ ...activeRunEntries().map((entry) => ({
286
+ runId: entry.runId,
287
+ path: entry.manifestPath,
288
+ })),
289
+ ];
290
+ const unique = new Map<string, CachedManifest | undefined>();
291
+ for (const entry of parsedEntries) {
292
+ if (entry.runId.length === 0) continue;
293
+ let cached = manifestIndex.get(entry.runId);
294
+ const root = path.dirname(path.dirname(entry.path));
295
+ const parsed = parseManifestIfChanged(root, entry.runId, entry.path, cached);
296
+ if (parsed) {
297
+ cached = parsed;
298
+ manifestIndex.set(entry.runId, cached);
299
+ }
300
+ if (cached) unique.set(entry.runId, cached);
301
+ }
302
+ const running = [...unique.values()]
303
+ .filter((value): value is CachedManifest => value !== undefined)
304
+ .map((value) => value.manifest)
305
+ .filter((manifest) => manifest.status === "running");
306
+ return running.slice(0, cap);
307
+ }
308
+
251
309
  if (options.watch ?? true) {
252
310
  for (const root of roots) {
253
311
  const watcher = watchWithErrorHandler(
@@ -268,6 +326,7 @@ export function createManifestCache(cwd: string, options: ManifestCacheOptions =
268
326
 
269
327
  return {
270
328
  list,
329
+ listActive,
271
330
  get,
272
331
  clear(runId) {
273
332
  invalidate(runId);
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
20
- import type { Static, TSchema } from "@sinclair/typebox";
20
+ import type { TSchema } from "@sinclair/typebox";
21
21
 
22
22
  export interface McpProxyConfig {
23
23
  /** Whether to enable MCP in the child session. */
@@ -22,10 +22,10 @@
22
22
  * writes from causing lost updates.
23
23
  */
24
24
 
25
- import { execFileSync, execSync } from "node:child_process";
25
+ import { execFileSync } from "node:child_process";
26
26
  import * as fs from "node:fs";
27
27
  import * as path from "node:path";
28
- import { isSymlinkSafePath } from "../state/atomic-write.ts";
28
+ import { atomicWriteFile, isSymlinkSafePath } from "../state/atomic-write.ts";
29
29
  import { withFileLockSync } from "../state/locks.ts";
30
30
  import { logInternalError } from "../utils/internal-error.ts";
31
31
  import { userPiRoot } from "../utils/paths.ts";
@@ -252,7 +252,7 @@ function writeRegistry(entries: OrphanWorkerEntry[]): void {
252
252
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
253
253
  }
254
254
  try {
255
- fs.writeFileSync(p, JSON.stringify(entries, null, 2), { mode: 0o600 });
255
+ atomicWriteFile(p, JSON.stringify(entries, null, 2), { mode: 0o600 });
256
256
  } catch (error) {
257
257
  logInternalError("orphan-worker-registry.write", error, `path=${p} entries=${entries.length}`);
258
258
  }
@@ -42,7 +42,7 @@ export function validateWorkerOutput(role: string, output: string): OutputValida
42
42
  const issues: string[] = [];
43
43
 
44
44
  // Empty output always fails
45
- if (!output || !output.trim()) {
45
+ if (!output?.trim()) {
46
46
  return {
47
47
  valid: false,
48
48
  formatMatch: false,
@@ -124,7 +124,7 @@ export function filterReadyByWriteOverlap(
124
124
  for (const taskId of readyTaskIds) {
125
125
  if (pickedTaskIds.length >= maxCount) break;
126
126
  const task = taskById.get(taskId);
127
- if (!task || !task.stepId) {
127
+ if (!task?.stepId) {
128
128
  // No step reference: treat as no declared write path → never conflicts.
129
129
  pickedTaskIds.push(taskId);
130
130
  continue;
@@ -1,9 +1,9 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
4
  import type { AgentConfig } from "../agents/agent-config.ts";
6
5
  import { resolveToolPolicy } from "../agents/agent-config.ts";
6
+ import { atomicWriteFile } from "../state/atomic-write.ts";
7
7
  import { packageRoot, userPiRoot } from "../utils/paths.ts";
8
8
 
9
9
  const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
@@ -323,7 +323,7 @@ export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerA
323
323
  const tmpBase = getPiTempBase();
324
324
  tempDir = createSafeTempDir(tmpBase, `pi-crew-${process.pid}-`);
325
325
  const promptPath = path.join(tempDir, `${input.agent.name.replace(/[^\w.-]/g, "_")}.md`);
326
- fs.writeFileSync(promptPath, input.agent.systemPrompt, { mode: 0o600 });
326
+ atomicWriteFile(promptPath, input.agent.systemPrompt, { mode: 0o600 });
327
327
  args.push(input.agent.systemPromptMode === "append" ? "--append-system-prompt" : "--system-prompt", promptPath);
328
328
  }
329
329
 
@@ -333,7 +333,7 @@ export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerA
333
333
  tempDir = createSafeTempDir(tmpBase, `pi-crew-${process.pid}-`);
334
334
  }
335
335
  const taskPath = path.join(tempDir, "task.md");
336
- fs.writeFileSync(taskPath, input.task, { mode: 0o600 });
336
+ atomicWriteFile(taskPath, input.task, { mode: 0o600 });
337
337
  args.push(`@${taskPath}`);
338
338
  } else {
339
339
  args.push(`Task: ${input.task}`);
@@ -151,14 +151,14 @@ function extractErrorMessage(event: unknown): string | undefined {
151
151
  */
152
152
  function extractPatch(event: unknown, patches: string[]): void {
153
153
  const obj = asRecord(event);
154
- if (!obj || obj.type !== "tool_result") return;
154
+ if (obj?.type !== "tool_result") return;
155
155
 
156
156
  const content = obj.content;
157
157
  if (!Array.isArray(content)) return;
158
158
 
159
159
  for (const item of content) {
160
160
  const part = asRecord(item);
161
- if (!part || part.type !== "text") continue;
161
+ if (part?.type !== "text") continue;
162
162
  const text = typeof part.text === "string" ? part.text : "";
163
163
 
164
164
  // Check if this looks like a unified patch (starts with "---" or "+++")
@@ -1,9 +1,6 @@
1
- import type { AgentConfig } from "../agents/agent-config.ts";
2
1
  import { errors } from "../errors.ts";
3
2
  import { appendEventAsync, flushEventLogBuffer } from "../state/event-log.ts";
4
- import type { TeamTaskState } from "../state/types.ts";
5
- import type { TeamConfig } from "../teams/team-config.ts";
6
- import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
3
+ import type { WorkflowConfig } from "../workflows/workflow-config.ts";
7
4
  import { mapConcurrent } from "./parallel-utils.ts";
8
5
 
9
6
  /**
@@ -5,7 +5,6 @@
5
5
  * Distilled from pi-autoresearch's post-check / backpressure pattern.
6
6
  */
7
7
  import { execFileSync } from "node:child_process";
8
- import * as path from "node:path";
9
8
  import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
10
9
  import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
11
10
  import { resolveShellForScript } from "../utils/resolve-shell.ts";
@@ -70,10 +70,10 @@ export interface TaskRunnerLike {
70
70
  */
71
71
  export class RetryRunner {
72
72
  private _disposed = false;
73
- private _handoffs: HandoffSummary[] = [];
74
73
 
75
74
  private taskRunner: TaskRunnerLike;
76
75
  private handoffManager: HandoffManager;
76
+ private _handoffs: HandoffSummary[] = [];
77
77
 
78
78
  constructor(taskRunner: TaskRunnerLike, handoffManager: HandoffManager) {
79
79
  this.taskRunner = taskRunner;
@@ -1,16 +1,3 @@
1
- /**
2
- * run-coalesced-task-group.ts — M6 real-dispatch MVP worker spawn.
3
- *
4
- * Spawns ONE child Pi process for an entire coalesced group of N tasks
5
- * (sharing role + cwd per `planCoalescedGroups`). The combined prompt
6
- * instructs the worker to wrap each task's result in `<<<TASK_RESULT:id>>>`
7
- * ... `<<<END_TASK_RESULT>>>` delimiters. Output is split back into N
8
- * per-task results via `splitCoalescedOutput`, each written to its own
9
- * result artifact + state update.
10
- */
11
-
12
- import { writeFile } from "node:fs/promises";
13
- import path from "node:path";
14
1
  import type { AgentConfig } from "../agents/agent-config.ts";
15
2
  import { writeArtifact } from "../state/artifact-store.ts";
16
3
  import { appendEventAsync } from "../state/event-log.ts";
@@ -111,6 +111,7 @@ export class CrewScheduler {
111
111
  if (this.timers.has(job.id)) return;
112
112
  if (job.scheduleType === "interval" && job.intervalMs) {
113
113
  const t = setInterval(() => this.fire(job.id), job.intervalMs);
114
+ t.unref();
114
115
  this.timers.set(job.id, t);
115
116
  } else if (job.scheduleType === "once") {
116
117
  const target = new Date(job.schedule).getTime();
@@ -120,6 +121,7 @@ export class CrewScheduler {
120
121
  this.fire(job.id);
121
122
  this.update(job.id, { enabled: false });
122
123
  }, delay);
124
+ t.unref();
123
125
  this.timers.set(job.id, t);
124
126
  } else {
125
127
  this.update(job.id, { enabled: false, lastStatus: "error" });
@@ -9,7 +9,7 @@ import { logInternalError } from "../utils/internal-error.ts";
9
9
  */
10
10
  export function tryRegisterSessionCleanup(pi: ExtensionAPI, cleanup: () => void): (() => void) | undefined {
11
11
  const api = pi as unknown as Record<string, unknown>;
12
- const registerFn = api["registerSessionResourceCleanup"];
12
+ const registerFn = api.registerSessionResourceCleanup;
13
13
  if (typeof registerFn === "function") {
14
14
  try {
15
15
  const unregister = (registerFn as (fn: () => void) => (() => void) | void)(cleanup);