pi-crew 0.9.39 → 0.9.41

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,53 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.41] — communication-layer optimization (2026-07-16)
4
+
5
+ ### Performance
6
+
7
+ - **Eliminated double `JSON.parse` in `child-pi.ts` `emitLine` hot path** — `src/runtime/child-pi.ts`. The observer previously parsed each stdout line twice (once for raw assistant-text extraction, once inside `compactChildPiLine`). Now parses once and passes the result to both paths via a `preParsed` parameter. Benchmark: **2.0 → 1.0 JSON.parse/line** (~25% faster at 1000 events). Extracted shared `nonJsonLineResult` helper for the non-JSON fallback.
8
+ - **Batched transcript writes** — `src/runtime/child-pi.ts`. Replaced per-line `open/write/close` (3 syscalls × N lines) with a module-scoped batch buffer that flushes all accumulated lines per path in a single `open/write/close` every 50ms or on lifecycle boundaries. Added `resetTranscriptBatchState()` for test isolation.
9
+ - **Migrated non-terminal event writes to fire-and-forget** — `src/runtime/adaptive-plan.ts`. 4 diagnostic `adaptive.plan_*` events migrated to `appendEventFireAndForget`; 1 audit-critical event (`plan_injected`) upgraded to `await appendEventAsync` to prevent ordering inversion with subsequent task events.
10
+
11
+ ### Cleanup
12
+
13
+ - **Removed dead code in `live-agent-manager.ts`** — deleted `drainIrcMessages`, `removeLiveAgentHandle`, and `listActiveLiveAgentsByWorkspace`, three internal functions with zero callers (verified via repo-wide grep; `removeLiveAgentHandle` logic was already inlined into `terminateLiveAgent`). Updated stale test comments.
14
+ - **Stale-comment cleanup in `child-pi.ts`** — 3 comments still referenced the removed `pendingTranscriptWrites` Set after the Phase 3 batching change; updated to describe the new batch-buffer mechanism.
15
+
16
+ ### Documentation
17
+
18
+ - **Added `docs/optimization-plan-2026-07.md`** — detailed 8-phase communication-layer optimization plan with verification gates, risk assessments, and rollback procedures.
19
+ - **Added `docs/phase4-triage.md`** — documents the sync/async event-log migration triage, including the ordering risk between sync (filesystem lock) and async (in-process promise chain) write paths.
20
+ - **Added `bench/child-pi-parse.bench.ts`** + baseline/final/session-verify measurement files — reproducible benchmark for the parse-count optimization.
21
+
22
+ ### Notes
23
+
24
+ - Phase 5 (`pollRunToTerminal` → fs.watch) was attempted but reverted before review rounds. The async watcher coordination is too risky for a small win. Documented as a follow-up in `docs/optimization-plan-2026-07.md`.
25
+ - Phases 6 (steer+control poll coalesce) and 7 (redaction trust-boundary) were skipped per the plan — Phase 6 is optional, Phase 7 was explicitly DEFER until a benchmark proves redaction is a bottleneck.
26
+
27
+ ### Tests
28
+
29
+ - 6072 unit tests pass (was 6038 before)
30
+ - 15+ integration tests pass
31
+ - Lint + format:check clean (CI green on ubuntu + windows + macos)
32
+ - New regression tests:
33
+ - `test/unit/child-pi-single-parse.test.ts` — 6 tests verifying single-parse invariant and preParsed path equivalence
34
+ - `test/unit/transcript-batch.test.ts` — 5 tests verifying batched write correctness, ordering, and timer-path flush
35
+ - Updated `test/unit/live-agent-manager-cov.test.ts` — stale comments removed
36
+
37
+ ## [0.9.40] — stale-reconciler fix + async dispatch + gitignore cleanup (2026-07-16)
38
+
39
+ ### Fixes
40
+
41
+ - **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.
42
+
43
+ ### Performance
44
+
45
+ - **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.
46
+
47
+ ### Cleanup
48
+
49
+ - **`src/**/*.js` gitignored** — `.gitignore`. 290 strip-types companion files in `src/` were untracked noise in `git status`. Added `src/**/*.js` rule. No runtime impact.
50
+
3
51
  ## [0.9.39] — foreground abort fix + security hardening (2026-07-15)
4
52
 
5
53
  ### Fixes
@@ -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",
@@ -6751,7 +6751,7 @@
6751
6751
  "format": "esm"
6752
6752
  },
6753
6753
  "src/runtime/live-agent-manager.ts": {
6754
- "bytes": 24685,
6754
+ "bytes": 23837,
6755
6755
  "imports": [
6756
6756
  {
6757
6757
  "path": "src/utils/internal-error.ts",
@@ -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",
@@ -7387,7 +7387,7 @@
7387
7387
  "format": "esm"
7388
7388
  },
7389
7389
  "src/runtime/child-pi.ts": {
7390
- "bytes": 71886,
7390
+ "bytes": 73980,
7391
7391
  "imports": [
7392
7392
  {
7393
7393
  "path": "node:child_process",
@@ -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",
@@ -15133,7 +15133,7 @@
15133
15133
  "format": "esm"
15134
15134
  },
15135
15135
  "src/runtime/adaptive-plan.ts": {
15136
- "bytes": 18288,
15136
+ "bytes": 18395,
15137
15137
  "imports": [
15138
15138
  {
15139
15139
  "path": "node:fs",
@@ -17643,7 +17643,7 @@
17643
17643
  "format": "esm"
17644
17644
  },
17645
17645
  "src/runtime/heartbeat-watcher.ts": {
17646
- "bytes": 8071,
17646
+ "bytes": 8429,
17647
17647
  "imports": [
17648
17648
  {
17649
17649
  "path": "node:fs",
@@ -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": 6712404
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
@@ -20815,7 +20815,7 @@
20815
20815
  "bytesInOutput": 1801
20816
20816
  },
20817
20817
  "src/runtime/child-pi.ts": {
20818
- "bytesInOutput": 47895
20818
+ "bytesInOutput": 48865
20819
20819
  },
20820
20820
  "src/runtime/heartbeat-gradient.ts": {
20821
20821
  "bytesInOutput": 953
@@ -21472,7 +21472,7 @@
21472
21472
  "bytesInOutput": 4743
21473
21473
  },
21474
21474
  "src/runtime/live-session-runtime.ts": {
21475
- "bytesInOutput": 30989
21475
+ "bytesInOutput": 30987
21476
21476
  },
21477
21477
  "src/subagents/live/session-runtime.ts": {
21478
21478
  "bytesInOutput": 140
@@ -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
@@ -21820,7 +21820,7 @@
21820
21820
  "bytesInOutput": 3799
21821
21821
  },
21822
21822
  "src/runtime/adaptive-plan.ts": {
21823
- "bytesInOutput": 15447
21823
+ "bytesInOutput": 15523
21824
21824
  },
21825
21825
  "src/workflows/topology-analyzer.ts": {
21826
21826
  "bytesInOutput": 4262
@@ -22009,7 +22009,7 @@
22009
22009
  "bytesInOutput": 2463
22010
22010
  },
22011
22011
  "src/runtime/heartbeat-watcher.ts": {
22012
- "bytesInOutput": 5931
22012
+ "bytesInOutput": 5993
22013
22013
  },
22014
22014
  "src/extension/registration/observability.ts": {
22015
22015
  "bytesInOutput": 7512
@@ -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": 3178836
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);
@@ -18696,34 +18699,56 @@ function appendTranscript(input, line4) {
18696
18699
  }
18697
18700
  trackTranscriptWrite(safePath, line4);
18698
18701
  }
18699
- async function appendTranscriptAsync(safePath, line4) {
18702
+ function scheduleTranscriptFlush() {
18703
+ if (transcriptFlushTimer) return;
18704
+ transcriptFlushTimer = setTimeout(() => {
18705
+ transcriptFlushTimer = void 0;
18706
+ void flushTranscriptBatches();
18707
+ }, TRANSCRIPT_FLUSH_MS);
18708
+ transcriptFlushTimer.unref?.();
18709
+ }
18710
+ async function flushTranscriptBatches() {
18711
+ const entries = [...transcriptBatches.entries()];
18712
+ transcriptBatches.clear();
18713
+ await Promise.allSettled(
18714
+ entries.map(async ([safePath, lines]) => {
18715
+ if (lines.length === 0) return;
18716
+ const content = lines.join("");
18717
+ try {
18718
+ const fd = await fs28.promises.open(
18719
+ safePath,
18720
+ fs28.constants.O_WRONLY | fs28.constants.O_NOFOLLOW | fs28.constants.O_CREAT | fs28.constants.O_APPEND,
18721
+ 384
18722
+ );
18723
+ try {
18724
+ await fd.write(content, void 0, "utf-8");
18725
+ } finally {
18726
+ await fd.close();
18727
+ }
18728
+ } catch (error) {
18729
+ logInternalError("child-pi.transcript-write-failed", error, `path=${safePath}`);
18730
+ }
18731
+ })
18732
+ );
18733
+ }
18734
+ function trackTranscriptWrite(safePath, line4) {
18700
18735
  const content = `${redactJsonLine(line4)}
18701
18736
  `;
18702
- try {
18703
- const fd = await fs28.promises.open(
18704
- safePath,
18705
- fs28.constants.O_WRONLY | fs28.constants.O_NOFOLLOW | fs28.constants.O_CREAT | fs28.constants.O_APPEND,
18706
- 384
18707
- );
18708
- try {
18709
- await fd.write(content, void 0, "utf-8");
18710
- } finally {
18711
- await fd.close();
18712
- }
18713
- } catch (error) {
18714
- logInternalError("child-pi.transcript-write-failed", error, `path=${safePath}`);
18737
+ let batch = transcriptBatches.get(safePath);
18738
+ if (!batch) {
18739
+ batch = [];
18740
+ transcriptBatches.set(safePath, batch);
18715
18741
  }
18716
- }
18717
- function trackTranscriptWrite(safePath, line4) {
18718
- const p = appendTranscriptAsync(safePath, line4).finally(() => {
18719
- pendingTranscriptWrites.delete(p);
18720
- });
18721
- pendingTranscriptWrites.add(p);
18742
+ batch.push(content);
18743
+ scheduleTranscriptFlush();
18722
18744
  }
18723
18745
  async function flushPendingTranscriptWrites() {
18724
- while (pendingTranscriptWrites.size > 0) {
18725
- const drained = [...pendingTranscriptWrites];
18726
- await Promise.allSettled(drained);
18746
+ if (transcriptFlushTimer) {
18747
+ clearTimeout(transcriptFlushTimer);
18748
+ transcriptFlushTimer = void 0;
18749
+ }
18750
+ while (transcriptBatches.size > 0) {
18751
+ await flushTranscriptBatches();
18727
18752
  }
18728
18753
  }
18729
18754
  function compactString(value, maxChars = MAX_COMPACT_CONTENT_CHARS, opts = {}) {
@@ -18829,19 +18854,27 @@ function displayTextFromCompactEvent(event) {
18829
18854
  }).join("\n").trim();
18830
18855
  return text || (typeof record.text === "string" ? record.text : void 0);
18831
18856
  }
18832
- function compactChildPiLine(line4) {
18833
- try {
18834
- const parsed = JSON.parse(line4);
18835
- const compact = compactChildPiEvent(parsed);
18836
- return {
18837
- json: true,
18838
- event: compact,
18839
- persistedLine: compact ? JSON.stringify(compact) : "",
18840
- displayLine: displayTextFromCompactEvent(compact)
18841
- };
18842
- } catch {
18843
- return { json: false, persistedLine: line4, displayLine: line4 };
18857
+ function nonJsonLineResult(line4) {
18858
+ return { json: false, persistedLine: line4, displayLine: line4 };
18859
+ }
18860
+ function compactChildPiLine(line4, preParsed) {
18861
+ let parsed;
18862
+ if (preParsed !== void 0) {
18863
+ parsed = preParsed;
18864
+ } else {
18865
+ try {
18866
+ parsed = JSON.parse(line4);
18867
+ } catch {
18868
+ return nonJsonLineResult(line4);
18869
+ }
18844
18870
  }
18871
+ const compact = compactChildPiEvent(parsed);
18872
+ return {
18873
+ json: true,
18874
+ event: compact,
18875
+ persistedLine: compact ? JSON.stringify(compact) : "",
18876
+ displayLine: displayTextFromCompactEvent(compact)
18877
+ };
18845
18878
  }
18846
18879
  async function observeStdoutChunk(input, text) {
18847
18880
  const observer = new ChildPiLineObserver(input);
@@ -19608,7 +19641,7 @@ ${JSON.stringify({ type: "message_end", usage: { input: 10, output: 5, cost: 1e-
19608
19641
  }
19609
19642
  }
19610
19643
  }
19611
- var POST_EXIT_STDIO_GUARD_MS, FINAL_DRAIN_MS, HARD_KILL_MS, RESPONSE_TIMEOUT_MS, MAX_CAPTURE_BYTES, MAX_ASSISTANT_TEXT_CHARS, MAX_TOOL_RESULT_CHARS, MAX_TOOL_INPUT_CHARS, MAX_COMPACT_CONTENT_CHARS, activeChildProcesses, childHardKillTimers, BASE_ALLOWLIST, pendingTranscriptWrites, ChildPiLineObserver;
19644
+ var POST_EXIT_STDIO_GUARD_MS, FINAL_DRAIN_MS, HARD_KILL_MS, RESPONSE_TIMEOUT_MS, MAX_CAPTURE_BYTES, MAX_ASSISTANT_TEXT_CHARS, MAX_TOOL_RESULT_CHARS, MAX_TOOL_INPUT_CHARS, MAX_COMPACT_CONTENT_CHARS, activeChildProcesses, childHardKillTimers, BASE_ALLOWLIST, transcriptBatches, transcriptFlushTimer, TRANSCRIPT_FLUSH_MS, ChildPiLineObserver;
19612
19645
  var init_child_pi = __esm({
19613
19646
  "src/runtime/child-pi.ts"() {
19614
19647
  "use strict";
@@ -19690,7 +19723,8 @@ var init_child_pi = __esm({
19690
19723
  "PI_CREW_MAX_OUTPUT",
19691
19724
  "PI_CREW_STEERING_FILE"
19692
19725
  ];
19693
- pendingTranscriptWrites = /* @__PURE__ */ new Set();
19726
+ transcriptBatches = /* @__PURE__ */ new Map();
19727
+ TRANSCRIPT_FLUSH_MS = 50;
19694
19728
  ChildPiLineObserver = class _ChildPiLineObserver {
19695
19729
  buffer = "";
19696
19730
  input;
@@ -19750,9 +19784,14 @@ var init_child_pi = __esm({
19750
19784
  }
19751
19785
  emitLine(line4) {
19752
19786
  if (!line4.trim()) return;
19787
+ let parsed;
19753
19788
  try {
19754
- const rawParsed = JSON.parse(line4);
19755
- const rawTexts = extractText(rawParsed);
19789
+ parsed = JSON.parse(line4);
19790
+ } catch {
19791
+ parsed = void 0;
19792
+ }
19793
+ if (parsed !== void 0) {
19794
+ const rawTexts = extractText(parsed);
19756
19795
  if (rawTexts.length > 0) {
19757
19796
  this.rawTextEvents.push(...rawTexts);
19758
19797
  const rawOverflow = this.rawTextEvents.length - _ChildPiLineObserver.MAX_RAW_TEXT_EVENTS;
@@ -19764,9 +19803,8 @@ var init_child_pi = __esm({
19764
19803
  if (findingsOverflow > 0) this.intermediateFindings.splice(0, findingsOverflow);
19765
19804
  }
19766
19805
  }
19767
- } catch {
19768
19806
  }
19769
- const compact = compactChildPiLine(line4);
19807
+ const compact = parsed !== void 0 ? compactChildPiLine(line4, parsed) : nonJsonLineResult(line4);
19770
19808
  if (compact.event !== void 0) {
19771
19809
  try {
19772
19810
  this.input.onJsonEvent?.(compact.event);
@@ -42508,9 +42546,9 @@ function loadLiveSessionModule() {
42508
42546
  }
42509
42547
  function appendTranscript2(filePath, event) {
42510
42548
  if (!filePath) return;
42511
- void appendTranscriptAsync2(filePath, event);
42549
+ void appendTranscriptAsync(filePath, event);
42512
42550
  }
42513
- async function appendTranscriptAsync2(filePath, event) {
42551
+ async function appendTranscriptAsync(filePath, event) {
42514
42552
  try {
42515
42553
  await fs49.promises.mkdir(path42.dirname(filePath), { recursive: true });
42516
42554
  const content = `${JSON.stringify(redactSecrets(event))}
@@ -55557,7 +55595,7 @@ async function runCoalescedTaskGroup(input) {
55557
55595
  }
55558
55596
  return t2;
55559
55597
  });
55560
- saveRunTasks(manifest, updatedTasks);
55598
+ await saveRunTasksAsync(manifest, updatedTasks);
55561
55599
  await appendEventAsync(manifest.eventsPath, {
55562
55600
  type: "task.coalesced_dispatch_start",
55563
55601
  runId: manifest.runId,
@@ -55572,14 +55610,14 @@ async function runCoalescedTaskGroup(input) {
55572
55610
  heartbeat: t2.heartbeat ?? createWorkerHeartbeat(t2.id)
55573
55611
  };
55574
55612
  });
55575
- saveRunTasks(manifest, updatedTasks);
55613
+ await saveRunTasksAsync(manifest, updatedTasks);
55576
55614
  let rawOutput = "";
55577
55615
  let success = false;
55578
55616
  if (!executeWorkers) {
55579
55617
  rawOutput = buildScaffoldOutput(groupTasks);
55580
55618
  success = true;
55581
55619
  } else {
55582
- const heartbeatTimer = setInterval(() => {
55620
+ const heartbeatTimer = setInterval(async () => {
55583
55621
  const now = (/* @__PURE__ */ new Date()).toISOString();
55584
55622
  updatedTasks = updatedTasks.map((t2) => {
55585
55623
  if (!taskIds.includes(t2.id)) return t2;
@@ -55589,7 +55627,7 @@ async function runCoalescedTaskGroup(input) {
55589
55627
  };
55590
55628
  });
55591
55629
  try {
55592
- saveRunTasks(manifest, updatedTasks);
55630
+ await saveRunTasksAsync(manifest, updatedTasks);
55593
55631
  } catch {
55594
55632
  }
55595
55633
  }, 15e3);
@@ -55639,7 +55677,7 @@ async function runCoalescedTaskGroup(input) {
55639
55677
  resultArtifact
55640
55678
  };
55641
55679
  });
55642
- saveRunTasks(manifest, updatedTasks);
55680
+ await saveRunTasksAsync(manifest, updatedTasks);
55643
55681
  let updatedManifest = {
55644
55682
  ...manifest,
55645
55683
  artifacts: mergeArtifacts([...manifest.artifacts, ...newArtifacts])
@@ -60051,7 +60089,7 @@ async function injectAdaptivePlanIfReady(input) {
60051
60089
  missingPlan: false
60052
60090
  };
60053
60091
  if (!completedAssess.resultArtifact?.path) {
60054
- appendEvent(input.manifest.eventsPath, {
60092
+ appendEventFireAndForget(input.manifest.eventsPath, {
60055
60093
  type: "adaptive.plan_missing",
60056
60094
  runId: input.manifest.runId,
60057
60095
  taskId: completedAssess.id,
@@ -60070,7 +60108,7 @@ async function injectAdaptivePlanIfReady(input) {
60070
60108
  try {
60071
60109
  text = fs85.readFileSync(resultPath, "utf-8");
60072
60110
  } catch {
60073
- appendEvent(input.manifest.eventsPath, {
60111
+ appendEventFireAndForget(input.manifest.eventsPath, {
60074
60112
  type: "adaptive.plan_missing",
60075
60113
  runId: input.manifest.runId,
60076
60114
  taskId: assessTask.id,
@@ -60101,7 +60139,7 @@ async function injectAdaptivePlanIfReady(input) {
60101
60139
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
60102
60140
  artifacts: [...input.manifest.artifacts, repairArtifact]
60103
60141
  });
60104
- appendEvent(input.manifest.eventsPath, {
60142
+ appendEventFireAndForget(input.manifest.eventsPath, {
60105
60143
  type: "adaptive.plan_repaired",
60106
60144
  runId: input.manifest.runId,
60107
60145
  taskId: assessTask.id,
@@ -60109,14 +60147,14 @@ async function injectAdaptivePlanIfReady(input) {
60109
60147
  data: { reason: repair.reason }
60110
60148
  });
60111
60149
  } else {
60112
- appendEvent(input.manifest.eventsPath, {
60150
+ appendEventFireAndForget(input.manifest.eventsPath, {
60113
60151
  type: "adaptive.plan_repair_failed",
60114
60152
  runId: input.manifest.runId,
60115
60153
  taskId: assessTask.id,
60116
60154
  message: "Adaptive planner output could not be repaired.",
60117
60155
  data: { reason: repair.reason }
60118
60156
  });
60119
- appendEvent(input.manifest.eventsPath, {
60157
+ appendEventFireAndForget(input.manifest.eventsPath, {
60120
60158
  type: "adaptive.plan_missing",
60121
60159
  runId: input.manifest.runId,
60122
60160
  taskId: assessTask.id,
@@ -60183,7 +60221,7 @@ async function injectAdaptivePlanIfReady(input) {
60183
60221
  } : task.graph
60184
60222
  }));
60185
60223
  const allTasks = refreshTaskGraphQueues([...input.tasks, ...withGraph]);
60186
- appendEvent(input.manifest.eventsPath, {
60224
+ await appendEventAsync(input.manifest.eventsPath, {
60187
60225
  type: "adaptive.plan_injected",
60188
60226
  runId: input.manifest.runId,
60189
60227
  taskId: assessTask.id,
@@ -79029,6 +79067,7 @@ var init_heartbeat_watcher = __esm({
79029
79067
  if (!fs99.existsSync(run.stateRoot)) continue;
79030
79068
  const loaded = loadRunManifestById(this.opts.cwd, run.runId);
79031
79069
  if (!loaded) continue;
79070
+ if (loaded.manifest.status !== "running") continue;
79032
79071
  for (const task of loaded.tasks) {
79033
79072
  if (task.status !== "running") continue;
79034
79073
  const key = `${run.runId}:${task.id}`;
@@ -85061,6 +85100,45 @@ Subagent may need manual intervention.`
85061
85100
  return loaded.tasks.some((t2) => t2.status === "running" || t2.status === "queued");
85062
85101
  };
85063
85102
  });
85103
+ const cleanupSessionResourcesOnly = () => {
85104
+ if (cleanedUp) return;
85105
+ cleanedUp = true;
85106
+ if (preloadTimer) {
85107
+ clearTimeout(preloadTimer);
85108
+ preloadTimer = void 0;
85109
+ }
85110
+ crewRunWatchers?.closeAll();
85111
+ crewRunWatchers = void 0;
85112
+ userCrewWatchers?.closeAll();
85113
+ userCrewWatchers = void 0;
85114
+ stopSessionBoundSubagents();
85115
+ crewScheduler?.stop();
85116
+ stopAsyncRunNotifier(notifierState);
85117
+ purgeStaleActiveRunIndexSyncIfLoaded();
85118
+ stopCrewWidget(currentCtx, widgetState, currentCtx ? loadConfig(currentCtx.cwd).config.ui : void 0);
85119
+ clearPiCrewPowerbar(pi.events);
85120
+ disposePowerbarCoalescer();
85121
+ disposeObservability(observabilityState, cleanedUp);
85122
+ lifecycleState.deliveryCoordinator?.dispose();
85123
+ clearHooksScoped();
85124
+ uninstallCrewGlobalRegistry();
85125
+ lifecycleState.overflowTracker?.dispose();
85126
+ lifecycleState.deliveryCoordinator = void 0;
85127
+ lifecycleState.overflowTracker = void 0;
85128
+ manifestCache2.dispose();
85129
+ runSnapshotCache.dispose?.();
85130
+ clearProjectRootCache();
85131
+ renderScheduler?.dispose();
85132
+ renderScheduler = void 0;
85133
+ autoRecoveryLast.clear();
85134
+ disposeNotifications(lifecycleState);
85135
+ rpcHandle?.unsubscribe();
85136
+ rpcHandle = void 0;
85137
+ disposeI18n();
85138
+ sessionGeneration += 1;
85139
+ currentCtx = void 0;
85140
+ if (globalStore[runtimeCleanupStoreKey] === cleanupSessionResourcesOnly) delete globalStore[runtimeCleanupStoreKey];
85141
+ };
85064
85142
  const cleanupRuntime = () => {
85065
85143
  if (cleanedUp) return;
85066
85144
  cleanedUp = true;
@@ -85103,6 +85181,14 @@ Subagent may need manual intervention.`
85103
85181
  if (globalStore[runtimeCleanupStoreKey] === cleanupRuntime) delete globalStore[runtimeCleanupStoreKey];
85104
85182
  };
85105
85183
  globalStore[runtimeCleanupStoreKey] = cleanupRuntime;
85184
+ pi.on("session_shutdown", (event) => {
85185
+ const reason = typeof event === "object" && event !== null && "reason" in event ? event.reason : void 0;
85186
+ if (reason === "quit" || reason === "reload") {
85187
+ cleanupRuntime();
85188
+ } else {
85189
+ cleanupSessionResourcesOnly();
85190
+ }
85191
+ });
85106
85192
  pi.on("session_start", (_event, ctx) => {
85107
85193
  runArtifactCleanup(ctx.cwd);
85108
85194
  try {
@@ -85583,7 +85669,6 @@ Subagent may need manual intervention.`
85583
85669
  stopAsyncRunNotifier(notifierState);
85584
85670
  stopSessionBoundSubagents();
85585
85671
  });
85586
- pi.on("session_shutdown", () => cleanupRuntime());
85587
85672
  try {
85588
85673
  pi.on("resources_discover", () => {
85589
85674
  const sessionCwd = currentCtx?.cwd ?? process.cwd();