pi-crew 0.9.38 → 0.9.40

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,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.40] — stale-reconciler fix + async dispatch + gitignore cleanup (2026-07-16)
4
+
5
+ ### Fixes
6
+
7
+ - **Stale-reconciler `needs_attention` status inference** — `src/runtime/stale-reconciler.ts`. When all tasks end in `needs_attention` (no explicit failure), `reconcileStaleRun` previously marked the run `completed`. This contradicted the codebase convention in `chain-executor.ts:147` (treating `needs_attention` as a non-clean terminal). Now includes `needs_attention` in the `hasFailed` predicate, marking the run `failed` instead. Tests cover: all-`needs_attention` → failed; mix `needs_attention`+completed → failed.
8
+
9
+ ### Performance
10
+
11
+ - **Async `saveRunTasks` in `runCoalescedTaskGroup`** — `src/runtime/run-coalesced-task-group.ts`. Replaced 4 synchronous `saveRunTasks` calls with `await saveRunTasksAsync` (from `state-store.ts:493`). The heartbeat `setInterval` callback was promoted to `async () =>` to support the await. With many coalesced groups running in parallel these sync writes blocked the event loop briefly; the async version uses `withRunLock` (non-blocking). No behavioral change.
12
+
13
+ ### Cleanup
14
+
15
+ - **`src/**/*.js` gitignored** — `.gitignore`. 290 strip-types companion files in `src/` were untracked noise in `git status`. Added `src/**/*.js` rule. No runtime impact.
16
+
17
+ ## [0.9.39] — foreground abort fix + security hardening (2026-07-15)
18
+
19
+ ### Fixes
20
+
21
+ - **Foreground abort semantics** — `src/extension/register.ts`. The `session_shutdown` handler now checks `event.reason` before aborting foreground team runs. Session switch (`reason="resume"/"new"/"fork"`) preserves foreground runs per P0 fix intent; only actual shutdown (`reason="quit"/"reload"`) aborts them. Extracted `cleanupSessionResourcesOnly()` for session-switch cleanup path.
22
+ - **`createRunPaths` cwd validation** — `src/state/state-store.ts`. Added defensive check rejecting `undefined`/empty/null `cwd` to prevent accidental `undefined/` directory creation.
23
+ - **EPERM lock handling documented** — `src/state/locks.ts`. Added detailed JSDoc explaining the EPERM-as-stealable trade-off rationale. Added `SEC-008` entry in `SECURITY-ISSUES.md` as accepted risk.
24
+
25
+ ### Cleanup
26
+
27
+ - Removed stale `undefined/` directory and added to `.gitignore`.
28
+
3
29
  ## [0.9.38] — cold context activation + UI polish (2026-07-15)
4
30
 
5
31
  Fixes and UX polish following v0.9.37's cold-context groundwork:
@@ -6175,7 +6175,7 @@
6175
6175
  "format": "esm"
6176
6176
  },
6177
6177
  "src/state/locks.ts": {
6178
- "bytes": 15906,
6178
+ "bytes": 17045,
6179
6179
  "imports": [
6180
6180
  {
6181
6181
  "path": "node:crypto",
@@ -6598,7 +6598,7 @@
6598
6598
  "format": "esm"
6599
6599
  },
6600
6600
  "src/state/state-store.ts": {
6601
- "bytes": 40339,
6601
+ "bytes": 40424,
6602
6602
  "imports": [
6603
6603
  {
6604
6604
  "path": "node:fs",
@@ -6794,7 +6794,7 @@
6794
6794
  "format": "esm"
6795
6795
  },
6796
6796
  "src/runtime/stale-reconciler.ts": {
6797
- "bytes": 28660,
6797
+ "bytes": 28694,
6798
6798
  "imports": [
6799
6799
  {
6800
6800
  "path": "node:fs",
@@ -14343,7 +14343,7 @@
14343
14343
  "format": "esm"
14344
14344
  },
14345
14345
  "src/runtime/run-coalesced-task-group.ts": {
14346
- "bytes": 9226,
14346
+ "bytes": 9281,
14347
14347
  "imports": [
14348
14348
  {
14349
14349
  "path": "node:fs/promises",
@@ -18003,7 +18003,7 @@
18003
18003
  "format": "esm"
18004
18004
  },
18005
18005
  "src/extension/register.ts": {
18006
- "bytes": 68063,
18006
+ "bytes": 70731,
18007
18007
  "imports": [
18008
18008
  {
18009
18009
  "path": "node:fs",
@@ -18425,7 +18425,7 @@
18425
18425
  "imports": [],
18426
18426
  "exports": [],
18427
18427
  "inputs": {},
18428
- "bytes": 6704580
18428
+ "bytes": 6709812
18429
18429
  },
18430
18430
  "dist/index.mjs": {
18431
18431
  "imports": [
@@ -20704,7 +20704,7 @@
20704
20704
  "bytesInOutput": 1803
20705
20705
  },
20706
20706
  "src/state/state-store.ts": {
20707
- "bytesInOutput": 20386
20707
+ "bytesInOutput": 20475
20708
20708
  },
20709
20709
  "src/utils/file-coalescer.ts": {
20710
20710
  "bytesInOutput": 1204
@@ -20725,7 +20725,7 @@
20725
20725
  "bytesInOutput": 4162
20726
20726
  },
20727
20727
  "src/runtime/stale-reconciler.ts": {
20728
- "bytesInOutput": 16497
20728
+ "bytesInOutput": 16532
20729
20729
  },
20730
20730
  "src/runtime/worker-heartbeat.ts": {
20731
20731
  "bytesInOutput": 580
@@ -21733,7 +21733,7 @@
21733
21733
  "bytesInOutput": 7286
21734
21734
  },
21735
21735
  "src/runtime/run-coalesced-task-group.ts": {
21736
- "bytesInOutput": 6313
21736
+ "bytesInOutput": 6363
21737
21737
  },
21738
21738
  "src/runtime/runtime-policy.ts": {
21739
21739
  "bytesInOutput": 575
@@ -22030,7 +22030,7 @@
22030
22030
  "bytesInOutput": 3150
22031
22031
  },
22032
22032
  "src/extension/register.ts": {
22033
- "bytesInOutput": 45555
22033
+ "bytesInOutput": 47211
22034
22034
  },
22035
22035
  "src/runtime/batch-barrier.ts": {
22036
22036
  "bytesInOutput": 3060
@@ -22135,7 +22135,7 @@
22135
22135
  "bytesInOutput": 81
22136
22136
  }
22137
22137
  },
22138
- "bytes": 3175900
22138
+ "bytes": 3177730
22139
22139
  }
22140
22140
  }
22141
22141
  }
package/dist/index.mjs CHANGED
@@ -13483,6 +13483,9 @@ function validateRunManifestPaths(cwd, runId, manifest, stateRoot, tasksPath) {
13483
13483
  return true;
13484
13484
  }
13485
13485
  function createRunPaths(cwd, runId = createRunId()) {
13486
+ if (!cwd || typeof cwd !== "string") {
13487
+ throw new Error(`Invalid cwd: ${cwd}`);
13488
+ }
13486
13489
  assertSafePathId("runId", runId);
13487
13490
  const baseRoot = scopeBaseRoot(cwd);
13488
13491
  const stateRoot = resolveContainedRelativePath(path11.join(baseRoot, DEFAULT_PATHS.state.runsSubdir), runId, "runId");
@@ -14998,7 +15001,7 @@ function checkResultFile(manifest, tasks) {
14998
15001
  (t2) => t2.status === "completed" || t2.status === "failed" || t2.status === "cancelled" || t2.status === "skipped" || t2.status === "needs_attention"
14999
15002
  );
15000
15003
  if (allTerminal) {
15001
- const hasFailed = tasks.some((t2) => t2.status === "failed");
15004
+ const hasFailed = tasks.some((t2) => t2.status === "failed" || t2.status === "needs_attention");
15002
15005
  const onlyCancelledOrSkipped = tasks.every((t2) => t2.status === "cancelled" || t2.status === "skipped");
15003
15006
  manifest.status = hasFailed ? "failed" : onlyCancelledOrSkipped ? "cancelled" : "completed";
15004
15007
  saveRunManifest(manifest);
@@ -55557,7 +55560,7 @@ async function runCoalescedTaskGroup(input) {
55557
55560
  }
55558
55561
  return t2;
55559
55562
  });
55560
- saveRunTasks(manifest, updatedTasks);
55563
+ await saveRunTasksAsync(manifest, updatedTasks);
55561
55564
  await appendEventAsync(manifest.eventsPath, {
55562
55565
  type: "task.coalesced_dispatch_start",
55563
55566
  runId: manifest.runId,
@@ -55572,14 +55575,14 @@ async function runCoalescedTaskGroup(input) {
55572
55575
  heartbeat: t2.heartbeat ?? createWorkerHeartbeat(t2.id)
55573
55576
  };
55574
55577
  });
55575
- saveRunTasks(manifest, updatedTasks);
55578
+ await saveRunTasksAsync(manifest, updatedTasks);
55576
55579
  let rawOutput = "";
55577
55580
  let success = false;
55578
55581
  if (!executeWorkers) {
55579
55582
  rawOutput = buildScaffoldOutput(groupTasks);
55580
55583
  success = true;
55581
55584
  } else {
55582
- const heartbeatTimer = setInterval(() => {
55585
+ const heartbeatTimer = setInterval(async () => {
55583
55586
  const now = (/* @__PURE__ */ new Date()).toISOString();
55584
55587
  updatedTasks = updatedTasks.map((t2) => {
55585
55588
  if (!taskIds.includes(t2.id)) return t2;
@@ -55589,7 +55592,7 @@ async function runCoalescedTaskGroup(input) {
55589
55592
  };
55590
55593
  });
55591
55594
  try {
55592
- saveRunTasks(manifest, updatedTasks);
55595
+ await saveRunTasksAsync(manifest, updatedTasks);
55593
55596
  } catch {
55594
55597
  }
55595
55598
  }, 15e3);
@@ -55639,7 +55642,7 @@ async function runCoalescedTaskGroup(input) {
55639
55642
  resultArtifact
55640
55643
  };
55641
55644
  });
55642
- saveRunTasks(manifest, updatedTasks);
55645
+ await saveRunTasksAsync(manifest, updatedTasks);
55643
55646
  let updatedManifest = {
55644
55647
  ...manifest,
55645
55648
  artifacts: mergeArtifacts([...manifest.artifacts, ...newArtifacts])
@@ -85061,6 +85064,45 @@ Subagent may need manual intervention.`
85061
85064
  return loaded.tasks.some((t2) => t2.status === "running" || t2.status === "queued");
85062
85065
  };
85063
85066
  });
85067
+ const cleanupSessionResourcesOnly = () => {
85068
+ if (cleanedUp) return;
85069
+ cleanedUp = true;
85070
+ if (preloadTimer) {
85071
+ clearTimeout(preloadTimer);
85072
+ preloadTimer = void 0;
85073
+ }
85074
+ crewRunWatchers?.closeAll();
85075
+ crewRunWatchers = void 0;
85076
+ userCrewWatchers?.closeAll();
85077
+ userCrewWatchers = void 0;
85078
+ stopSessionBoundSubagents();
85079
+ crewScheduler?.stop();
85080
+ stopAsyncRunNotifier(notifierState);
85081
+ purgeStaleActiveRunIndexSyncIfLoaded();
85082
+ stopCrewWidget(currentCtx, widgetState, currentCtx ? loadConfig(currentCtx.cwd).config.ui : void 0);
85083
+ clearPiCrewPowerbar(pi.events);
85084
+ disposePowerbarCoalescer();
85085
+ disposeObservability(observabilityState, cleanedUp);
85086
+ lifecycleState.deliveryCoordinator?.dispose();
85087
+ clearHooksScoped();
85088
+ uninstallCrewGlobalRegistry();
85089
+ lifecycleState.overflowTracker?.dispose();
85090
+ lifecycleState.deliveryCoordinator = void 0;
85091
+ lifecycleState.overflowTracker = void 0;
85092
+ manifestCache2.dispose();
85093
+ runSnapshotCache.dispose?.();
85094
+ clearProjectRootCache();
85095
+ renderScheduler?.dispose();
85096
+ renderScheduler = void 0;
85097
+ autoRecoveryLast.clear();
85098
+ disposeNotifications(lifecycleState);
85099
+ rpcHandle?.unsubscribe();
85100
+ rpcHandle = void 0;
85101
+ disposeI18n();
85102
+ sessionGeneration += 1;
85103
+ currentCtx = void 0;
85104
+ if (globalStore[runtimeCleanupStoreKey] === cleanupSessionResourcesOnly) delete globalStore[runtimeCleanupStoreKey];
85105
+ };
85064
85106
  const cleanupRuntime = () => {
85065
85107
  if (cleanedUp) return;
85066
85108
  cleanedUp = true;
@@ -85103,6 +85145,14 @@ Subagent may need manual intervention.`
85103
85145
  if (globalStore[runtimeCleanupStoreKey] === cleanupRuntime) delete globalStore[runtimeCleanupStoreKey];
85104
85146
  };
85105
85147
  globalStore[runtimeCleanupStoreKey] = cleanupRuntime;
85148
+ pi.on("session_shutdown", (event) => {
85149
+ const reason = typeof event === "object" && event !== null && "reason" in event ? event.reason : void 0;
85150
+ if (reason === "quit" || reason === "reload") {
85151
+ cleanupRuntime();
85152
+ } else {
85153
+ cleanupSessionResourcesOnly();
85154
+ }
85155
+ });
85106
85156
  pi.on("session_start", (_event, ctx) => {
85107
85157
  runArtifactCleanup(ctx.cwd);
85108
85158
  try {
@@ -85583,7 +85633,6 @@ Subagent may need manual intervention.`
85583
85633
  stopAsyncRunNotifier(notifierState);
85584
85634
  stopSessionBoundSubagents();
85585
85635
  });
85586
- pi.on("session_shutdown", () => cleanupRuntime());
85587
85636
  try {
85588
85637
  pi.on("resources_discover", () => {
85589
85638
  const sessionCwd = currentCtx?.cwd ?? process.cwd();