pi-crew 0.9.40 → 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,39 @@
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
+
3
37
  ## [0.9.40] — stale-reconciler fix + async dispatch + gitignore cleanup (2026-07-16)
4
38
 
5
39
  ### Fixes
@@ -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",
@@ -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",
@@ -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",
@@ -18425,7 +18425,7 @@
18425
18425
  "imports": [],
18426
18426
  "exports": [],
18427
18427
  "inputs": {},
18428
- "bytes": 6709812
18428
+ "bytes": 6712404
18429
18429
  },
18430
18430
  "dist/index.mjs": {
18431
18431
  "imports": [
@@ -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
@@ -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
@@ -22135,7 +22135,7 @@
22135
22135
  "bytesInOutput": 81
22136
22136
  }
22137
22137
  },
22138
- "bytes": 3177730
22138
+ "bytes": 3178836
22139
22139
  }
22140
22140
  }
22141
22141
  }
package/dist/index.mjs CHANGED
@@ -18699,34 +18699,56 @@ function appendTranscript(input, line4) {
18699
18699
  }
18700
18700
  trackTranscriptWrite(safePath, line4);
18701
18701
  }
18702
- 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) {
18703
18735
  const content = `${redactJsonLine(line4)}
18704
18736
  `;
18705
- try {
18706
- const fd = await fs28.promises.open(
18707
- safePath,
18708
- fs28.constants.O_WRONLY | fs28.constants.O_NOFOLLOW | fs28.constants.O_CREAT | fs28.constants.O_APPEND,
18709
- 384
18710
- );
18711
- try {
18712
- await fd.write(content, void 0, "utf-8");
18713
- } finally {
18714
- await fd.close();
18715
- }
18716
- } catch (error) {
18717
- 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);
18718
18741
  }
18719
- }
18720
- function trackTranscriptWrite(safePath, line4) {
18721
- const p = appendTranscriptAsync(safePath, line4).finally(() => {
18722
- pendingTranscriptWrites.delete(p);
18723
- });
18724
- pendingTranscriptWrites.add(p);
18742
+ batch.push(content);
18743
+ scheduleTranscriptFlush();
18725
18744
  }
18726
18745
  async function flushPendingTranscriptWrites() {
18727
- while (pendingTranscriptWrites.size > 0) {
18728
- const drained = [...pendingTranscriptWrites];
18729
- await Promise.allSettled(drained);
18746
+ if (transcriptFlushTimer) {
18747
+ clearTimeout(transcriptFlushTimer);
18748
+ transcriptFlushTimer = void 0;
18749
+ }
18750
+ while (transcriptBatches.size > 0) {
18751
+ await flushTranscriptBatches();
18730
18752
  }
18731
18753
  }
18732
18754
  function compactString(value, maxChars = MAX_COMPACT_CONTENT_CHARS, opts = {}) {
@@ -18832,19 +18854,27 @@ function displayTextFromCompactEvent(event) {
18832
18854
  }).join("\n").trim();
18833
18855
  return text || (typeof record.text === "string" ? record.text : void 0);
18834
18856
  }
18835
- function compactChildPiLine(line4) {
18836
- try {
18837
- const parsed = JSON.parse(line4);
18838
- const compact = compactChildPiEvent(parsed);
18839
- return {
18840
- json: true,
18841
- event: compact,
18842
- persistedLine: compact ? JSON.stringify(compact) : "",
18843
- displayLine: displayTextFromCompactEvent(compact)
18844
- };
18845
- } catch {
18846
- 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
+ }
18847
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
+ };
18848
18878
  }
18849
18879
  async function observeStdoutChunk(input, text) {
18850
18880
  const observer = new ChildPiLineObserver(input);
@@ -19611,7 +19641,7 @@ ${JSON.stringify({ type: "message_end", usage: { input: 10, output: 5, cost: 1e-
19611
19641
  }
19612
19642
  }
19613
19643
  }
19614
- 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;
19615
19645
  var init_child_pi = __esm({
19616
19646
  "src/runtime/child-pi.ts"() {
19617
19647
  "use strict";
@@ -19693,7 +19723,8 @@ var init_child_pi = __esm({
19693
19723
  "PI_CREW_MAX_OUTPUT",
19694
19724
  "PI_CREW_STEERING_FILE"
19695
19725
  ];
19696
- pendingTranscriptWrites = /* @__PURE__ */ new Set();
19726
+ transcriptBatches = /* @__PURE__ */ new Map();
19727
+ TRANSCRIPT_FLUSH_MS = 50;
19697
19728
  ChildPiLineObserver = class _ChildPiLineObserver {
19698
19729
  buffer = "";
19699
19730
  input;
@@ -19753,9 +19784,14 @@ var init_child_pi = __esm({
19753
19784
  }
19754
19785
  emitLine(line4) {
19755
19786
  if (!line4.trim()) return;
19787
+ let parsed;
19756
19788
  try {
19757
- const rawParsed = JSON.parse(line4);
19758
- 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);
19759
19795
  if (rawTexts.length > 0) {
19760
19796
  this.rawTextEvents.push(...rawTexts);
19761
19797
  const rawOverflow = this.rawTextEvents.length - _ChildPiLineObserver.MAX_RAW_TEXT_EVENTS;
@@ -19767,9 +19803,8 @@ var init_child_pi = __esm({
19767
19803
  if (findingsOverflow > 0) this.intermediateFindings.splice(0, findingsOverflow);
19768
19804
  }
19769
19805
  }
19770
- } catch {
19771
19806
  }
19772
- const compact = compactChildPiLine(line4);
19807
+ const compact = parsed !== void 0 ? compactChildPiLine(line4, parsed) : nonJsonLineResult(line4);
19773
19808
  if (compact.event !== void 0) {
19774
19809
  try {
19775
19810
  this.input.onJsonEvent?.(compact.event);
@@ -42511,9 +42546,9 @@ function loadLiveSessionModule() {
42511
42546
  }
42512
42547
  function appendTranscript2(filePath, event) {
42513
42548
  if (!filePath) return;
42514
- void appendTranscriptAsync2(filePath, event);
42549
+ void appendTranscriptAsync(filePath, event);
42515
42550
  }
42516
- async function appendTranscriptAsync2(filePath, event) {
42551
+ async function appendTranscriptAsync(filePath, event) {
42517
42552
  try {
42518
42553
  await fs49.promises.mkdir(path42.dirname(filePath), { recursive: true });
42519
42554
  const content = `${JSON.stringify(redactSecrets(event))}
@@ -60054,7 +60089,7 @@ async function injectAdaptivePlanIfReady(input) {
60054
60089
  missingPlan: false
60055
60090
  };
60056
60091
  if (!completedAssess.resultArtifact?.path) {
60057
- appendEvent(input.manifest.eventsPath, {
60092
+ appendEventFireAndForget(input.manifest.eventsPath, {
60058
60093
  type: "adaptive.plan_missing",
60059
60094
  runId: input.manifest.runId,
60060
60095
  taskId: completedAssess.id,
@@ -60073,7 +60108,7 @@ async function injectAdaptivePlanIfReady(input) {
60073
60108
  try {
60074
60109
  text = fs85.readFileSync(resultPath, "utf-8");
60075
60110
  } catch {
60076
- appendEvent(input.manifest.eventsPath, {
60111
+ appendEventFireAndForget(input.manifest.eventsPath, {
60077
60112
  type: "adaptive.plan_missing",
60078
60113
  runId: input.manifest.runId,
60079
60114
  taskId: assessTask.id,
@@ -60104,7 +60139,7 @@ async function injectAdaptivePlanIfReady(input) {
60104
60139
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
60105
60140
  artifacts: [...input.manifest.artifacts, repairArtifact]
60106
60141
  });
60107
- appendEvent(input.manifest.eventsPath, {
60142
+ appendEventFireAndForget(input.manifest.eventsPath, {
60108
60143
  type: "adaptive.plan_repaired",
60109
60144
  runId: input.manifest.runId,
60110
60145
  taskId: assessTask.id,
@@ -60112,14 +60147,14 @@ async function injectAdaptivePlanIfReady(input) {
60112
60147
  data: { reason: repair.reason }
60113
60148
  });
60114
60149
  } else {
60115
- appendEvent(input.manifest.eventsPath, {
60150
+ appendEventFireAndForget(input.manifest.eventsPath, {
60116
60151
  type: "adaptive.plan_repair_failed",
60117
60152
  runId: input.manifest.runId,
60118
60153
  taskId: assessTask.id,
60119
60154
  message: "Adaptive planner output could not be repaired.",
60120
60155
  data: { reason: repair.reason }
60121
60156
  });
60122
- appendEvent(input.manifest.eventsPath, {
60157
+ appendEventFireAndForget(input.manifest.eventsPath, {
60123
60158
  type: "adaptive.plan_missing",
60124
60159
  runId: input.manifest.runId,
60125
60160
  taskId: assessTask.id,
@@ -60186,7 +60221,7 @@ async function injectAdaptivePlanIfReady(input) {
60186
60221
  } : task.graph
60187
60222
  }));
60188
60223
  const allTasks = refreshTaskGraphQueues([...input.tasks, ...withGraph]);
60189
- appendEvent(input.manifest.eventsPath, {
60224
+ await appendEventAsync(input.manifest.eventsPath, {
60190
60225
  type: "adaptive.plan_injected",
60191
60226
  runId: input.manifest.runId,
60192
60227
  taskId: assessTask.id,
@@ -79032,6 +79067,7 @@ var init_heartbeat_watcher = __esm({
79032
79067
  if (!fs99.existsSync(run.stateRoot)) continue;
79033
79068
  const loaded = loadRunManifestById(this.opts.cwd, run.runId);
79034
79069
  if (!loaded) continue;
79070
+ if (loaded.manifest.status !== "running") continue;
79035
79071
  for (const task of loaded.tasks) {
79036
79072
  if (task.status !== "running") continue;
79037
79073
  const key = `${run.runId}:${task.id}`;