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 +34 -0
- package/dist/build-meta.json +10 -10
- package/dist/index.mjs +85 -49
- package/dist/index.mjs.map +3 -3
- package/docs/optimization-plan-2026-07.md +366 -0
- package/docs/phase4-triage.md +58 -0
- package/package.json +1 -1
- package/src/runtime/adaptive-plan.ts +7 -7
- package/src/runtime/child-pi.ts +135 -69
- package/src/runtime/heartbeat-watcher.ts +5 -0
- package/src/runtime/live-agent-manager.ts +0 -27
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
|
package/dist/build-meta.json
CHANGED
|
@@ -6751,7 +6751,7 @@
|
|
|
6751
6751
|
"format": "esm"
|
|
6752
6752
|
},
|
|
6753
6753
|
"src/runtime/live-agent-manager.ts": {
|
|
6754
|
-
"bytes":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
18706
|
-
|
|
18707
|
-
|
|
18708
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18728
|
-
|
|
18729
|
-
|
|
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
|
|
18836
|
-
|
|
18837
|
-
|
|
18838
|
-
|
|
18839
|
-
|
|
18840
|
-
|
|
18841
|
-
|
|
18842
|
-
|
|
18843
|
-
|
|
18844
|
-
|
|
18845
|
-
|
|
18846
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
19758
|
-
|
|
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
|
|
42549
|
+
void appendTranscriptAsync(filePath, event);
|
|
42515
42550
|
}
|
|
42516
|
-
async function
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}`;
|