pi-plans 0.1.2 → 0.3.0
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/README.md +98 -19
- package/index.ts +147 -66
- package/package.json +7 -1
- package/references/pi-planning-workflow.md +21 -3
- package/references/state-and-config.md +35 -3
- package/scripts/validate.ts +4 -0
- package/src/autocomplete.ts +163 -0
- package/src/code-graph/commands.ts +437 -0
- package/src/code-graph/discovery.ts +118 -0
- package/src/code-graph/git.ts +108 -0
- package/src/code-graph/identity.ts +59 -0
- package/src/code-graph/indexer.ts +281 -0
- package/src/code-graph/materialize.ts +166 -0
- package/src/code-graph/mode.ts +28 -0
- package/src/code-graph/mutations.ts +160 -0
- package/src/code-graph/parser.ts +51 -0
- package/src/code-graph/parsers/javascript.ts +35 -0
- package/src/code-graph/parsers/python.ts +160 -0
- package/src/code-graph/parsers/tree-sitter.ts +316 -0
- package/src/code-graph/paths.ts +85 -0
- package/src/code-graph/prompts.ts +18 -0
- package/src/code-graph/resolver.ts +69 -0
- package/src/code-graph/runtime.ts +158 -0
- package/src/code-graph/schema.ts +135 -0
- package/src/code-graph/screening.ts +82 -0
- package/src/code-graph/store.ts +278 -0
- package/src/code-graph/summary.ts +435 -0
- package/src/code-graph/types.ts +163 -0
- package/src/compaction.ts +1256 -0
- package/src/config-command.ts +326 -0
- package/src/exec.ts +519 -625
- package/src/plan.ts +37 -0
- package/src/query-hook.ts +82 -0
- package/src/refine-prompts.ts +50 -0
- package/src/refine-ui-helpers.ts +142 -0
- package/src/refine-ui-state.ts +144 -0
- package/src/refine-ui.ts +430 -0
- package/src/state.ts +24 -29
- package/src/subagent.ts +299 -70
- package/tests/ask-choice.test.ts +263 -0
- package/tests/autocomplete.test.ts +147 -0
- package/tests/code-graph-apply.test.ts +185 -0
- package/tests/code-graph-commands.test.ts +211 -0
- package/tests/code-graph-db.test.ts +166 -0
- package/tests/code-graph-discovery.test.ts +38 -0
- package/tests/code-graph-git.test.ts +94 -0
- package/tests/code-graph-index.test.ts +175 -0
- package/tests/code-graph-loop.e2e.test.ts +159 -0
- package/tests/code-graph-mutations.test.ts +117 -0
- package/tests/code-graph-parser.test.ts +85 -0
- package/tests/code-graph-rollback.test.ts +100 -0
- package/tests/code-graph-summary-batching.test.ts +518 -0
- package/tests/code-graph-summary.test.ts +148 -0
- package/tests/compaction.test.ts +388 -0
- package/tests/config-command.test.ts +255 -0
- package/tests/exec.test.ts +751 -422
- package/tests/execute-plan.test.ts +65 -0
- package/tests/fixtures/code-graph/sample.js +36 -0
- package/tests/fixtures/code-graph/sample.py +20 -0
- package/tests/fixtures/code-graph/sample.ts +15 -0
- package/tests/graph-aware-file-tools.test.ts +411 -0
- package/tests/plan.test.ts +11 -1
- package/tests/plans.test.ts +6 -5
- package/tests/query-hook.test.ts +82 -0
- package/tests/refine-prompts.test.ts +67 -2
- package/tests/refine-ui.test.ts +392 -0
- package/tests/state.test.ts +12 -15
- package/tests/subagent.test.ts +120 -0
- package/tools/ask-choice.ts +180 -11
- package/tools/code-graph.ts +254 -0
- package/tools/execute-plan.ts +7 -39
- package/tools/graph-aware-file-tools.ts +392 -0
- package/tools/plans.ts +84 -18
- package/tools/refine.ts +180 -80
- package/src/execution-panel.ts +0 -633
- package/tests/execution-panel.test.ts +0 -234
package/tests/exec.test.ts
CHANGED
|
@@ -8,12 +8,12 @@ import { after, before, describe, it } from "node:test";
|
|
|
8
8
|
import {
|
|
9
9
|
applyDoneMarkers,
|
|
10
10
|
applyImplMarkers,
|
|
11
|
+
applyCurrentIMarker,
|
|
11
12
|
buildExecutionCompactionResult,
|
|
12
13
|
buildPlanningCompactionResult,
|
|
13
14
|
completeExecution,
|
|
14
15
|
consumePendingExecutionFlush,
|
|
15
16
|
consumePlanningCompactionResumeGuard,
|
|
16
|
-
consumePendingPanelSync,
|
|
17
17
|
drainExecutionFlush,
|
|
18
18
|
executionContextMessage,
|
|
19
19
|
filterExecutionResumeMessages,
|
|
@@ -21,7 +21,11 @@ import {
|
|
|
21
21
|
getExecution,
|
|
22
22
|
handleExecutionBeforeCompact,
|
|
23
23
|
handleExecutionCompact,
|
|
24
|
+
handleExecutionTurnCompaction,
|
|
24
25
|
handleExecutionCompactFailed,
|
|
26
|
+
compactionInFlight,
|
|
27
|
+
noteCompactionStarted,
|
|
28
|
+
noteCompactionEnded,
|
|
25
29
|
handlePlanningBeforeCompact,
|
|
26
30
|
handlePlanningCompact,
|
|
27
31
|
handlePlanningCompactFailed,
|
|
@@ -32,17 +36,14 @@ import {
|
|
|
32
36
|
requestPlanningCompaction,
|
|
33
37
|
restoreFromSession,
|
|
34
38
|
shouldTriggerPlanningCompaction,
|
|
35
|
-
ensureExecutionModelActive,
|
|
36
39
|
startExecution,
|
|
37
40
|
recordExecutionTurn,
|
|
38
41
|
registerExecutionTurnHandlers,
|
|
39
42
|
stopExecution,
|
|
40
|
-
chooseExecutionModelSelection,
|
|
41
|
-
toggleExecutionPanelView,
|
|
42
43
|
updateStatusWidget,
|
|
43
44
|
} from "../src/exec.ts";
|
|
44
45
|
import type { CheckItem } from "../src/plan.ts";
|
|
45
|
-
import { initState,
|
|
46
|
+
import { initState, setGraphEnabled, setRunStatus, startRun } from "../src/state.ts";
|
|
46
47
|
|
|
47
48
|
interface Recorded {
|
|
48
49
|
entries: { type: string; customType?: string; data?: unknown }[];
|
|
@@ -50,8 +51,6 @@ interface Recorded {
|
|
|
50
51
|
status: string | undefined;
|
|
51
52
|
statusCalls: number;
|
|
52
53
|
colors: string[];
|
|
53
|
-
widget?: { key: string; options?: unknown; factory: any };
|
|
54
|
-
widgetCalls: number;
|
|
55
54
|
models: { provider: string; id: string }[];
|
|
56
55
|
thinkingLevels: (string | null)[];
|
|
57
56
|
notifies: { message: string; severity: string }[];
|
|
@@ -59,6 +58,7 @@ interface Recorded {
|
|
|
59
58
|
selectAnswer?: string;
|
|
60
59
|
current: { provider: string; id: string } | null;
|
|
61
60
|
thinking: string | null;
|
|
61
|
+
userMessages: string[];
|
|
62
62
|
compacts?: { customInstructions?: string }[];
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -76,13 +76,13 @@ function makeHarness(workdir: string): Harness {
|
|
|
76
76
|
status: undefined,
|
|
77
77
|
statusCalls: 0,
|
|
78
78
|
colors: [],
|
|
79
|
-
widgetCalls: 0,
|
|
80
79
|
models: [],
|
|
81
80
|
thinkingLevels: [],
|
|
82
81
|
notifies: [],
|
|
83
82
|
selects: [],
|
|
84
83
|
current: { provider: "p", id: "m" },
|
|
85
84
|
thinking: "high",
|
|
85
|
+
userMessages: [],
|
|
86
86
|
};
|
|
87
87
|
let contextPercent: number | null = 0;
|
|
88
88
|
const registryModels = [
|
|
@@ -113,7 +113,9 @@ function makeHarness(workdir: string): Harness {
|
|
|
113
113
|
sendMessage: (message: { customType: string; content: string }, options?: { triggerTurn?: boolean }) => {
|
|
114
114
|
recorded.messages.push({ ...message, options });
|
|
115
115
|
},
|
|
116
|
-
sendUserMessage: async () => {
|
|
116
|
+
sendUserMessage: async (content: string) => {
|
|
117
|
+
recorded.userMessages.push(content);
|
|
118
|
+
},
|
|
117
119
|
setModel: async (model: { provider: string; id: string }) => {
|
|
118
120
|
recorded.models.push({ provider: model.provider, id: model.id });
|
|
119
121
|
recorded.current = { provider: model.provider, id: model.id };
|
|
@@ -129,14 +131,6 @@ function makeHarness(workdir: string): Harness {
|
|
|
129
131
|
recorded.statusCalls += 1;
|
|
130
132
|
recorded.status = value;
|
|
131
133
|
},
|
|
132
|
-
setWidget: (key: string, factory: any, options?: unknown) => {
|
|
133
|
-
recorded.widgetCalls += 1;
|
|
134
|
-
if (factory === undefined) {
|
|
135
|
-
recorded.widget = undefined;
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
recorded.widget = { key, options, factory };
|
|
139
|
-
},
|
|
140
134
|
notify: (message: string, severity: string) => {
|
|
141
135
|
recorded.notifies.push({ message, severity });
|
|
142
136
|
},
|
|
@@ -190,6 +184,22 @@ function items(...ids: string[]): CheckItem[] {
|
|
|
190
184
|
return ids.map((id) => ({ id, text: `\`${id}\` demo item`, done: false }));
|
|
191
185
|
}
|
|
192
186
|
|
|
187
|
+
function compactableBranchEntries(): any[] {
|
|
188
|
+
return [
|
|
189
|
+
{ id: "u-1", type: "message", message: { role: "user", content: [{ type: "text", text: "start the work" }] } },
|
|
190
|
+
{ id: "a-1", type: "message", message: { role: "assistant", content: [{ type: "text", text: "made progress" }] } },
|
|
191
|
+
{ id: "u-2", type: "message", message: { role: "user", content: [{ type: "text", text: "continue from here" }] } },
|
|
192
|
+
{ id: "a-2", type: "message", message: { role: "assistant", content: [{ type: "text", text: "tail work" }] } },
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function startActiveRun(workdir: string, status: "planning" | "executing" = "planning") {
|
|
197
|
+
initState(workdir);
|
|
198
|
+
const { run } = startRun(workdir, { topic: "compact", skill: "plan-normal", requestText: "x" });
|
|
199
|
+
if (status !== "planning") setRunStatus(workdir, run.run_id, status);
|
|
200
|
+
return run;
|
|
201
|
+
}
|
|
202
|
+
|
|
193
203
|
describe("execution loop", () => {
|
|
194
204
|
let tmpRoot: string;
|
|
195
205
|
let counter = 0;
|
|
@@ -214,36 +224,20 @@ describe("execution loop", () => {
|
|
|
214
224
|
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
215
225
|
await startExecution(pi, ctx, path.join(workdir, "PLAN_v1.md"), items("VC-001", "VC-002"));
|
|
216
226
|
|
|
217
|
-
// Collapsed by default: progress lives in the bottom status bar (same
|
|
218
|
-
// layer as the ⛔/⌛ indicators); no panel widget is registered yet.
|
|
219
|
-
assert.equal(recorded.widget, undefined);
|
|
220
227
|
assert.match(recorded.status ?? "", /⌛ plans 0\/2: spent \d{2}:\d{2}:\d{2}/);
|
|
221
228
|
assert.match(recorded.status ?? "", /in-toks/);
|
|
222
229
|
assert.match(recorded.status ?? "", /out-toks/);
|
|
223
230
|
|
|
224
|
-
toggleExecutionPanelView(pi, ctx);
|
|
225
|
-
assert.ok(recorded.widget);
|
|
226
|
-
assert.equal(recorded.widget?.key, "pi-plans-execution");
|
|
227
|
-
assert.deepEqual(recorded.widget?.options, { placement: "belowEditor" });
|
|
228
|
-
// Expanded: footer ⌛ line is cleared — the panel renders its own header.
|
|
229
|
-
assert.equal(recorded.status, undefined);
|
|
230
|
-
const expandedWidget = recorded.widget?.factory(
|
|
231
|
-
{} as any,
|
|
232
|
-
{ fg: (_color: string, text: string) => text, strikethrough: (text: string) => `~~${text}~~` },
|
|
233
|
-
);
|
|
234
|
-
assert.ok(expandedWidget);
|
|
235
|
-
const expandedLines = expandedWidget.render(80);
|
|
236
|
-
// Header line first, then the legacy item list.
|
|
237
|
-
assert.match(expandedLines[0] ?? "", /⌛ plans 0\/2: spent/);
|
|
238
|
-
assert.match(expandedLines.join("\n"), /☐/);
|
|
239
|
-
// Detail view never repeats the keyboard hint.
|
|
240
|
-
assert.doesNotMatch(expandedLines.join("\n"), /alt\+o/);
|
|
241
|
-
|
|
242
231
|
assert.ok(getExecution());
|
|
243
|
-
const rules = executionContextMessage()!;
|
|
232
|
+
const rules = executionContextMessage(ctx)!;
|
|
244
233
|
assert.match(rules, /PI-PLANS EXECUTION/);
|
|
245
234
|
assert.match(rules, /VC-001/);
|
|
246
|
-
|
|
235
|
+
assert.match(rules, /subprocess-backed verification/);
|
|
236
|
+
assert.match(rules, /waiting for/);
|
|
237
|
+
assert.match(rules, /5s\s*->\s*10s\s*->\s*20s\s*->\s*40s\s*->\s*80s/);
|
|
238
|
+
assert.match(rules, /keep polling at 80s/);
|
|
239
|
+
assert.match(rules, /restart at 5s for each new subprocess/);
|
|
240
|
+
// Representative anchors for the core execution rules (PLAN_v2 D-003/D-004).
|
|
247
241
|
assert.match(rules, /for the long term/);
|
|
248
242
|
assert.match(rules, /Simplest implementation/);
|
|
249
243
|
assert.match(rules, /grow the change in layers/);
|
|
@@ -263,24 +257,93 @@ describe("execution loop", () => {
|
|
|
263
257
|
assert.ok(recorded.messages.some((message) => message.customType === "pi-plans-complete"));
|
|
264
258
|
});
|
|
265
259
|
|
|
266
|
-
it("
|
|
260
|
+
it("completion enters goal-running continuation and triggers a turn in interactive sessions", async () => {
|
|
267
261
|
const workdir = freshWorkdir();
|
|
268
262
|
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
263
|
+
const planPath = path.join(workdir, "PLAN_v1.md");
|
|
264
|
+
await startExecution(pi, ctx, planPath, items("VC-001", "VC-002"));
|
|
265
|
+
assert.deepEqual(applyDoneMarkers("[DONE:VC-001] [DONE:VC-002]"), ["VC-001", "VC-002"]);
|
|
266
|
+
|
|
267
|
+
await completeExecution(pi, ctx);
|
|
268
|
+
|
|
269
|
+
const completeMessage = recorded.messages.find((message) => message.customType === "pi-plans-complete");
|
|
270
|
+
assert.ok(completeMessage);
|
|
271
|
+
assert.match(completeMessage.content, /Goal-running continuation/);
|
|
272
|
+
assert.match(completeMessage.content, /termination condition of the implementation-review loop/);
|
|
273
|
+
assert.doesNotMatch(completeMessage.content, /Run a post-execution amelioration round/);
|
|
274
|
+
assert.equal(completeMessage.options?.triggerTurn, true);
|
|
275
|
+
const ameliorateEntry = recorded.entries.find((entry) => entry.customType === "pi-plans-ameliorate");
|
|
276
|
+
assert.ok(ameliorateEntry);
|
|
277
|
+
const data = ameliorateEntry.data as Record<string, unknown>;
|
|
278
|
+
assert.equal(data.phase, "goal-started");
|
|
279
|
+
assert.equal(data.rounds, null);
|
|
280
|
+
assert.equal(data.currentRound, 0);
|
|
281
|
+
assert.equal(data.planPath, planPath);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("completion stays silent in headless sessions", async () => {
|
|
285
|
+
const workdir = freshWorkdir();
|
|
286
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
287
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v1.md"), items("VC-001"));
|
|
288
|
+
assert.deepEqual(applyDoneMarkers("[DONE:VC-001]"), ["VC-001"]);
|
|
289
|
+
(ctx as any).hasUI = false;
|
|
290
|
+
|
|
291
|
+
await completeExecution(pi, ctx);
|
|
292
|
+
|
|
293
|
+
const completeMessage = recorded.messages.find((message) => message.customType === "pi-plans-complete");
|
|
294
|
+
assert.ok(completeMessage);
|
|
295
|
+
assert.doesNotMatch(completeMessage.content, /Goal-running continuation/);
|
|
296
|
+
assert.equal(completeMessage.options?.triggerTurn, false);
|
|
297
|
+
assert.equal(
|
|
298
|
+
recorded.entries.some((entry) => entry.customType === "pi-plans-ameliorate"),
|
|
299
|
+
false,
|
|
300
|
+
"headless completion must not append the ameliorate entry",
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("restoreFromSession completion triggers the same goal-running continuation in interactive sessions", async () => {
|
|
305
|
+
const workdir = freshWorkdir();
|
|
306
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
307
|
+
const planPath = path.join(workdir, "PLAN_v1.md");
|
|
308
|
+
fs.writeFileSync(planPath, "# plan");
|
|
269
309
|
const snapshot = {
|
|
270
|
-
planPath
|
|
310
|
+
planPath,
|
|
271
311
|
items: items("VC-001", "VC-002"),
|
|
272
312
|
startedAt: "2026-08-25T00:00:00Z",
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
313
|
+
usage: { inToks: 0, outToks: 0 },
|
|
314
|
+
implItems: [],
|
|
315
|
+
implStatus: {},
|
|
316
|
+
compaction: {
|
|
317
|
+
inFlight: true,
|
|
318
|
+
resumeGuard: true,
|
|
319
|
+
cooldownActive: true,
|
|
320
|
+
lastAttemptReason: "threshold",
|
|
321
|
+
lastSuccessfulUsagePercent: 100,
|
|
322
|
+
lastSuccessfulAt: "2026-08-25T00:01:00Z",
|
|
282
323
|
},
|
|
324
|
+
};
|
|
325
|
+
const entries = [
|
|
326
|
+
{ type: "custom", customType: "pi-plans-exec", data: snapshot },
|
|
327
|
+
{
|
|
328
|
+
type: "message",
|
|
329
|
+
message: { role: "assistant", content: [{ type: "text", text: "did [DONE:VC-001] [DONE:VC-002]" }] },
|
|
283
330
|
},
|
|
331
|
+
];
|
|
332
|
+
await restoreFromSession(pi, ctx, entries as any);
|
|
333
|
+
const completeMessage = recorded.messages.find((message) => message.customType === "pi-plans-complete");
|
|
334
|
+
assert.ok(completeMessage, "restore path must fire completeExecution");
|
|
335
|
+
assert.match(completeMessage.content, /Goal-running continuation/);
|
|
336
|
+
assert.equal(completeMessage.options?.triggerTurn, true);
|
|
337
|
+
assert.ok(recorded.entries.some((entry) => entry.customType === "pi-plans-ameliorate"));
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("restores progress from session entries and rescans messages", async () => {
|
|
341
|
+
const workdir = freshWorkdir();
|
|
342
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
343
|
+
const snapshot = {
|
|
344
|
+
planPath: path.join(workdir, "PLAN_v1.md"),
|
|
345
|
+
items: items("VC-001", "VC-002"),
|
|
346
|
+
startedAt: "2026-08-25T00:00:00Z",
|
|
284
347
|
compaction: {
|
|
285
348
|
inFlight: true,
|
|
286
349
|
resumeGuard: true,
|
|
@@ -302,15 +365,6 @@ describe("execution loop", () => {
|
|
|
302
365
|
const execution = getExecution();
|
|
303
366
|
assert.ok(execution);
|
|
304
367
|
assert.equal("compaction" in execution!, false, "legacy scheduler state must not reactivate on restore");
|
|
305
|
-
assert.ok(recorded.widget);
|
|
306
|
-
const widget = recorded.widget?.factory(
|
|
307
|
-
{} as any,
|
|
308
|
-
{ fg: (_color: string, text: string) => text, strikethrough: (text: string) => `~~${text}~~` },
|
|
309
|
-
);
|
|
310
|
-
assert.ok(widget);
|
|
311
|
-
const rendered = widget.render(80);
|
|
312
|
-
assert.match(rendered.join("\n"), /☑/);
|
|
313
|
-
assert.match(rendered.join("\n"), /\+1/);
|
|
314
368
|
|
|
315
369
|
const clearedEntries = [...entries, { type: "custom", customType: "pi-plans-exec-cleared", data: {} }];
|
|
316
370
|
await restoreFromSession(pi, ctx, clearedEntries as any);
|
|
@@ -343,99 +397,21 @@ describe("execution loop", () => {
|
|
|
343
397
|
assert.equal(getExecution(), null);
|
|
344
398
|
});
|
|
345
399
|
|
|
346
|
-
it("
|
|
400
|
+
it("starts execution without switching models", async () => {
|
|
347
401
|
const workdir = freshWorkdir();
|
|
348
402
|
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
349
|
-
await startExecution(pi, ctx, path.join(workdir, "PLAN_v6.md"), items("VC-001")
|
|
350
|
-
planningSelector: "p/m:high",
|
|
351
|
-
executionSelector: "prov/other:xhigh",
|
|
352
|
-
});
|
|
403
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v6.md"), items("VC-001"));
|
|
353
404
|
|
|
354
|
-
assert.deepEqual(recorded.models, [
|
|
355
|
-
assert.equal(recorded.thinking, "
|
|
405
|
+
assert.deepEqual(recorded.models, []);
|
|
406
|
+
assert.equal(recorded.thinking, "high");
|
|
356
407
|
assert.match(recorded.status ?? "", /⌛ plans 0\/1/);
|
|
357
408
|
|
|
358
409
|
await stopExecution(pi, ctx, "restore-check");
|
|
359
|
-
assert.equal(recorded.models.at(-1)?.provider, "p");
|
|
360
|
-
assert.equal(recorded.thinking, "high");
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it("defers execution model restore until the first agent turn and restores planning model on completion", async () => {
|
|
364
|
-
const workdir = freshWorkdir();
|
|
365
|
-
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
366
|
-
const snapshot = {
|
|
367
|
-
planPath: path.join(workdir, "PLAN_v7.md"),
|
|
368
|
-
items: items("VC-001"),
|
|
369
|
-
startedAt: "2026-08-25T00:00:00Z",
|
|
370
|
-
modelState: { planningSelector: "p/m:high", executionSelector: "prov/other:xhigh" },
|
|
371
|
-
};
|
|
372
|
-
fs.writeFileSync(snapshot.planPath, "# plan");
|
|
373
|
-
const entries = [{ type: "custom", customType: "pi-plans-exec", data: snapshot }];
|
|
374
|
-
|
|
375
|
-
await restoreFromSession(pi, ctx, entries as any);
|
|
376
|
-
assert.ok(getExecution());
|
|
377
410
|
assert.deepEqual(recorded.models, []);
|
|
378
|
-
|
|
379
|
-
await ensureExecutionModelActive(pi, ctx);
|
|
380
|
-
assert.deepEqual(recorded.models, [{ provider: "prov", id: "other" }]);
|
|
381
|
-
assert.equal(recorded.thinking, "xhigh");
|
|
382
|
-
|
|
383
|
-
const doneEntries = [
|
|
384
|
-
...entries,
|
|
385
|
-
{ type: "message", message: { role: "assistant", content: [{ type: "text", text: "did [DONE:VC-001]" }] } },
|
|
386
|
-
];
|
|
387
|
-
await restoreFromSession(pi, ctx, doneEntries as any);
|
|
388
|
-
assert.equal(getExecution(), null);
|
|
389
|
-
assert.equal(recorded.models.at(-1)?.provider, "p");
|
|
390
411
|
assert.equal(recorded.thinking, "high");
|
|
391
412
|
});
|
|
392
413
|
|
|
393
|
-
it("
|
|
394
|
-
const workdir = freshWorkdir();
|
|
395
|
-
const { ctx, recorded } = makeHarness(workdir);
|
|
396
|
-
initState(workdir);
|
|
397
|
-
recorded.selectAnswer = "Inherit current session model";
|
|
398
|
-
const selection = await chooseExecutionModelSelection(ctx as any, "Execution model", {
|
|
399
|
-
model_selector: null,
|
|
400
|
-
source: "unset",
|
|
401
|
-
updated_at: null,
|
|
402
|
-
}, async (selector) => {
|
|
403
|
-
setExecutionModel(workdir, { modelSelector: selector, source: "user" });
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
assert.equal(recorded.selects.length, 1);
|
|
407
|
-
assert.equal(recorded.selects[0]?.options[0], "Inherit current session model");
|
|
408
|
-
assert.equal(selection?.selector, "p/m:high");
|
|
409
|
-
assert.equal(showConfig(workdir).execution.source, "user");
|
|
410
|
-
assert.equal(showConfig(workdir).execution.model_selector, null);
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
it("does not prompt when the execution model is already confirmed as inherit", async () => {
|
|
414
|
-
const workdir = freshWorkdir();
|
|
415
|
-
const { ctx, recorded } = makeHarness(workdir);
|
|
416
|
-
initState(workdir);
|
|
417
|
-
setExecutionModel(workdir, { modelSelector: "inherit", source: "user" });
|
|
418
|
-
const selection = await chooseExecutionModelSelection(ctx as any, "Execution model", showConfig(workdir).execution);
|
|
419
|
-
|
|
420
|
-
assert.equal(recorded.selects.length, 0);
|
|
421
|
-
assert.equal(selection?.selector, "p/m:high");
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it("continues on the current model with a warning when the selector is unavailable", async () => {
|
|
425
|
-
const workdir = freshWorkdir();
|
|
426
|
-
const { ctx, recorded } = makeHarness(workdir);
|
|
427
|
-
(ctx as any).hasUI = false;
|
|
428
|
-
const selection = await chooseExecutionModelSelection(ctx as any, "Execution model", {
|
|
429
|
-
model_selector: "prov/missing:xhigh",
|
|
430
|
-
source: "user",
|
|
431
|
-
updated_at: null,
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
assert.equal(selection?.provider, "p");
|
|
435
|
-
assert.ok(recorded.notifies.some((n) => n.severity === "warning" && n.message.includes("prov/missing")));
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
it("keeps the status bar count-free while executing and points at the panel", async () => {
|
|
414
|
+
it("keeps the execution status bar current while executing", async () => {
|
|
439
415
|
const workdir = freshWorkdir();
|
|
440
416
|
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
441
417
|
await startExecution(pi, ctx, path.join(workdir, "PLAN_v2.md"), items("VC-001", "VC-002"));
|
|
@@ -519,62 +495,51 @@ describe("execution loop", () => {
|
|
|
519
495
|
assert.equal(getExecution(), null);
|
|
520
496
|
});
|
|
521
497
|
|
|
522
|
-
it("
|
|
498
|
+
it("returns control to Pi core without active execution or planning state", async () => {
|
|
523
499
|
const workdir = freshWorkdir();
|
|
524
|
-
const { pi, ctx
|
|
525
|
-
await startExecution(pi, ctx, path.join(workdir, "PLAN_v3.md"), items("VC-001", "VC-002"));
|
|
526
|
-
|
|
527
|
-
const entriesBefore = recorded.entries.length;
|
|
528
|
-
const factoriesBefore = recorded.widgetCalls;
|
|
529
|
-
|
|
530
|
-
// Mid-turn: flip must be pure memory — no session writes, no re-register.
|
|
531
|
-
(ctx as any).isIdle = () => false;
|
|
532
|
-
assert.equal(toggleExecutionPanelView(pi, ctx), true);
|
|
533
|
-
assert.equal(recorded.entries.length, entriesBefore, "busy toggle appended a session entry");
|
|
534
|
-
assert.equal(recorded.widgetCalls, factoriesBefore, "busy toggle re-registered the widget");
|
|
535
|
-
assert.equal(consumePendingPanelSync(), true, "expected a pending panel sync marker");
|
|
536
|
-
assert.equal(consumePendingPanelSync(), false, "marker should be consumed exactly once");
|
|
537
|
-
|
|
538
|
-
// Back to idle: the next toggle persists and syncs without touching the
|
|
539
|
-
// widget slot because the panel is collapsed again.
|
|
540
|
-
ctx.isIdle = () => true;
|
|
541
|
-
assert.equal(toggleExecutionPanelView(pi, ctx), false);
|
|
542
|
-
assert.ok(recorded.entries.length > entriesBefore, "idle toggle did not persist");
|
|
543
|
-
assert.equal(recorded.widgetCalls, factoriesBefore, "collapsed toggle churned the widget registration");
|
|
544
|
-
|
|
545
|
-
await stopExecution(pi, ctx, "test-done");
|
|
546
|
-
});
|
|
500
|
+
const { pi, ctx } = makeHarness(workdir);
|
|
547
501
|
|
|
548
|
-
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
502
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v0.md"), items("VC-001"));
|
|
503
|
+
const executionResult = handleExecutionBeforeCompact(pi, ctx, {
|
|
504
|
+
type: "session_before_compact",
|
|
505
|
+
preparation: makePreparation("threshold", null),
|
|
506
|
+
branchEntries: compactableBranchEntries(),
|
|
507
|
+
reason: "threshold",
|
|
508
|
+
willRetry: false,
|
|
509
|
+
signal: new AbortController().signal,
|
|
510
|
+
});
|
|
511
|
+
assert.equal(executionResult, undefined);
|
|
512
|
+
await stopExecution(pi, ctx, "no-active-run");
|
|
552
513
|
|
|
553
|
-
|
|
554
|
-
const below = handleExecutionBeforeCompact(pi, ctx, {
|
|
514
|
+
const planningResult = handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
555
515
|
type: "session_before_compact",
|
|
556
516
|
preparation: makePreparation("threshold", null),
|
|
557
|
-
branchEntries:
|
|
517
|
+
branchEntries: compactableBranchEntries(),
|
|
558
518
|
reason: "threshold",
|
|
559
519
|
willRetry: false,
|
|
560
520
|
signal: new AbortController().signal,
|
|
561
521
|
});
|
|
562
|
-
assert.equal(
|
|
563
|
-
|
|
522
|
+
assert.equal(planningResult, undefined);
|
|
523
|
+
});
|
|
564
524
|
|
|
565
|
-
|
|
566
|
-
const
|
|
525
|
+
it("lets Pi core own execution compaction scheduling and customizes safe reasons", async () => {
|
|
526
|
+
const workdir = freshWorkdir();
|
|
527
|
+
startActiveRun(workdir);
|
|
528
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
529
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v8.md"), items("VC-001", "VC-002"));
|
|
530
|
+
|
|
531
|
+
const threshold = handleExecutionBeforeCompact(pi, ctx, {
|
|
567
532
|
type: "session_before_compact",
|
|
568
533
|
preparation: makePreparation("threshold", null),
|
|
569
|
-
branchEntries:
|
|
534
|
+
branchEntries: compactableBranchEntries(),
|
|
570
535
|
reason: "threshold",
|
|
571
536
|
willRetry: false,
|
|
572
537
|
signal: new AbortController().signal,
|
|
573
538
|
});
|
|
574
|
-
assert.equal(
|
|
575
|
-
assert.ok(
|
|
539
|
+
assert.equal(threshold?.cancel, undefined);
|
|
540
|
+
assert.ok(threshold?.compaction, "threshold compaction should use the VCC summary when a legal cut exists");
|
|
576
541
|
|
|
577
|
-
const
|
|
542
|
+
const overflowRetry = handleExecutionBeforeCompact(pi, ctx, {
|
|
578
543
|
type: "session_before_compact",
|
|
579
544
|
preparation: makePreparation("overflow", null),
|
|
580
545
|
branchEntries: [],
|
|
@@ -582,8 +547,7 @@ describe("execution loop", () => {
|
|
|
582
547
|
willRetry: true,
|
|
583
548
|
signal: new AbortController().signal,
|
|
584
549
|
});
|
|
585
|
-
assert.equal(
|
|
586
|
-
assert.ok(overflow?.compaction);
|
|
550
|
+
assert.equal(overflowRetry, undefined, "overflow retry falls back to Pi core when VCC has no safe cut");
|
|
587
551
|
|
|
588
552
|
const manual = handleExecutionBeforeCompact(pi, ctx, {
|
|
589
553
|
type: "session_before_compact",
|
|
@@ -593,53 +557,116 @@ describe("execution loop", () => {
|
|
|
593
557
|
willRetry: false,
|
|
594
558
|
signal: new AbortController().signal,
|
|
595
559
|
});
|
|
596
|
-
assert.equal(manual?.cancel,
|
|
597
|
-
assert.ok(
|
|
560
|
+
assert.equal(manual?.cancel, true, "manual compaction cancels instead of discarding unsafe context");
|
|
561
|
+
assert.ok(recorded.notifies.some((note) => note.message.includes("Nothing to compact")));
|
|
562
|
+
|
|
563
|
+
await stopExecution(pi, ctx, "test-done");
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it("does not proactively request execution compaction", async () => {
|
|
567
|
+
const workdir = freshWorkdir();
|
|
568
|
+
startActiveRun(workdir);
|
|
569
|
+
const { pi, ctx, recorded, setUsagePercent } = makeHarness(workdir);
|
|
570
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v9.md"), items("VC-001", "VC-002"));
|
|
571
|
+
|
|
572
|
+
setUsagePercent(99);
|
|
573
|
+
handleExecutionTurnCompaction(ctx);
|
|
574
|
+
handleExecutionTurnCompaction(ctx);
|
|
575
|
+
assert.equal(recorded.compacts?.length ?? 0, 0, "execution scheduling is owned by Pi core");
|
|
598
576
|
|
|
599
577
|
await stopExecution(pi, ctx, "test-done");
|
|
600
578
|
});
|
|
601
579
|
|
|
602
|
-
it("
|
|
580
|
+
it("ignores current-I growth for proactive execution compaction", async () => {
|
|
581
|
+
const workdir = freshWorkdir();
|
|
582
|
+
startActiveRun(workdir);
|
|
583
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
584
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v22.md"), items("VC-001"), [
|
|
585
|
+
{ id: "I-001", text: "First item." },
|
|
586
|
+
]);
|
|
587
|
+
(ctx.sessionManager as any).getBranch = () => [
|
|
588
|
+
{ id: "only", type: "message", tokens: 30000, message: { role: "assistant", content: [{ type: "text", text: "[I-001:current] one oversized turn" }] } },
|
|
589
|
+
];
|
|
590
|
+
handleExecutionTurnCompaction(ctx);
|
|
591
|
+
assert.equal(recorded.compacts?.length ?? 0, 0);
|
|
592
|
+
await stopExecution(pi, ctx, "no-prefix");
|
|
593
|
+
});
|
|
594
|
+
it("wires execution compaction without restoring proactive requests or model helpers", () => {
|
|
603
595
|
const indexSource = fs.readFileSync(path.join(process.cwd(), "index.ts"), "utf8");
|
|
604
596
|
const execSource = fs.readFileSync(path.join(process.cwd(), "src/exec.ts"), "utf8");
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
assert.
|
|
608
|
-
assert.
|
|
609
|
-
|
|
610
|
-
|
|
597
|
+
assert.match(indexSource, /handleExecutionTurnCompaction/);
|
|
598
|
+
assert.match(execSource, /shouldTriggerExecutionCompaction/);
|
|
599
|
+
assert.doesNotMatch(execSource, /ctx\.compact\(/);
|
|
600
|
+
assert.doesNotMatch(
|
|
601
|
+
indexSource,
|
|
602
|
+
/setExecutionModel|chooseExecutionModelSelection|snapshotCurrentModelSelector|ensureExecutionModelActive|restorePlanningModel/,
|
|
603
|
+
);
|
|
604
|
+
assert.doesNotMatch(
|
|
605
|
+
execSource,
|
|
606
|
+
/setExecutionModel|chooseExecutionModelSelection|snapshotCurrentModelSelector|ensureExecutionModelActive|restorePlanningModel|buildModelExecutionCompactionResult|modelRegistry\.complete/,
|
|
607
|
+
);
|
|
611
608
|
});
|
|
612
609
|
|
|
613
|
-
it("
|
|
610
|
+
it("auto-continues execution only for threshold/overflow and honors manual follow-up prompts", async () => {
|
|
614
611
|
const workdir = freshWorkdir();
|
|
612
|
+
startActiveRun(workdir);
|
|
615
613
|
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
614
|
+
(ctx as any).piVersion = "0.84.3";
|
|
616
615
|
await startExecution(pi, ctx, path.join(workdir, "PLAN_v9.md"), items("VC-001"));
|
|
617
616
|
|
|
618
|
-
const
|
|
619
|
-
|
|
617
|
+
const threshold = handleExecutionBeforeCompact(pi, ctx, {
|
|
618
|
+
type: "session_before_compact",
|
|
619
|
+
preparation: makePreparation("threshold", null),
|
|
620
|
+
branchEntries: compactableBranchEntries(),
|
|
621
|
+
reason: "threshold",
|
|
622
|
+
willRetry: false,
|
|
623
|
+
signal: new AbortController().signal,
|
|
624
|
+
});
|
|
625
|
+
assert.ok(threshold?.compaction);
|
|
626
|
+
await handleExecutionCompact(pi, ctx, {
|
|
620
627
|
type: "session_compact",
|
|
621
628
|
compactionEntry: { type: "compaction" } as never,
|
|
622
|
-
fromExtension:
|
|
629
|
+
fromExtension: false,
|
|
623
630
|
reason: "threshold",
|
|
624
631
|
willRetry: false,
|
|
625
632
|
});
|
|
626
|
-
|
|
627
|
-
assert.
|
|
628
|
-
assert.equal(resumes[0]?.options?.triggerTurn, true);
|
|
629
|
-
assert.equal(recorded.compacts?.length ?? 0, beforeRequests, "compaction hook must not invoke ctx.compact");
|
|
633
|
+
assert.equal(recorded.messages.filter((message) => message.customType === "pi-plans-exec-resume").length, 1);
|
|
634
|
+
assert.ok(recorded.notifies.some((note) => note.message.startsWith("pi-vcc: kept")));
|
|
630
635
|
|
|
631
|
-
|
|
636
|
+
handleExecutionBeforeCompact(pi, ctx, {
|
|
637
|
+
type: "session_before_compact",
|
|
638
|
+
preparation: makePreparation("manual", null),
|
|
639
|
+
branchEntries: compactableBranchEntries(),
|
|
640
|
+
reason: "manual",
|
|
641
|
+
willRetry: false,
|
|
642
|
+
signal: new AbortController().signal,
|
|
643
|
+
});
|
|
644
|
+
await handleExecutionCompact(pi, ctx, {
|
|
632
645
|
type: "session_compact",
|
|
633
646
|
compactionEntry: { type: "compaction" } as never,
|
|
634
|
-
fromExtension:
|
|
635
|
-
reason: "
|
|
636
|
-
willRetry:
|
|
647
|
+
fromExtension: false,
|
|
648
|
+
reason: "manual",
|
|
649
|
+
willRetry: false,
|
|
637
650
|
});
|
|
638
|
-
assert.equal(
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
651
|
+
assert.equal(recorded.messages.filter((message) => message.customType === "pi-plans-exec-resume").length, 1);
|
|
652
|
+
|
|
653
|
+
handleExecutionBeforeCompact(pi, ctx, {
|
|
654
|
+
type: "session_before_compact",
|
|
655
|
+
preparation: makePreparation("manual", null),
|
|
656
|
+
branchEntries: compactableBranchEntries(),
|
|
657
|
+
customInstructions: "Run focused tests keep:1",
|
|
658
|
+
reason: "manual",
|
|
659
|
+
willRetry: false,
|
|
660
|
+
signal: new AbortController().signal,
|
|
661
|
+
});
|
|
662
|
+
await handleExecutionCompact(pi, ctx, {
|
|
663
|
+
type: "session_compact",
|
|
664
|
+
compactionEntry: { type: "compaction" } as never,
|
|
665
|
+
fromExtension: false,
|
|
666
|
+
reason: "manual",
|
|
667
|
+
willRetry: false,
|
|
668
|
+
});
|
|
669
|
+
assert.deepEqual(recorded.userMessages, ["Run focused tests"]);
|
|
643
670
|
|
|
644
671
|
handleExecutionCompactFailed(pi, ctx, {
|
|
645
672
|
type: "session_compact_failed",
|
|
@@ -655,29 +682,30 @@ describe("execution loop", () => {
|
|
|
655
682
|
await stopExecution(pi, ctx, "test-done");
|
|
656
683
|
});
|
|
657
684
|
|
|
658
|
-
it("builds a
|
|
685
|
+
it("builds a VCC execution summary with phase context and previous summary", async () => {
|
|
659
686
|
const workdir = freshWorkdir();
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
687
|
+
startActiveRun(workdir);
|
|
688
|
+
const { pi, ctx } = makeHarness(workdir);
|
|
689
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v10.md"), items("VC-001", "VC-002"), [
|
|
690
|
+
{ id: "I-001", text: "First item." },
|
|
691
|
+
{ id: "I-002", text: "Second item." },
|
|
692
|
+
]);
|
|
664
693
|
|
|
665
|
-
const previousSummary = "##
|
|
694
|
+
const previousSummary = "## Legacy Summary\nDeliver auto-compact in execution phase.";
|
|
695
|
+
applyCurrentIMarker("[I-002:current]");
|
|
666
696
|
const preparation = makePreparation("threshold", previousSummary);
|
|
667
697
|
const branchEntries: any[] = [
|
|
668
|
-
{ id: "exec-start", type: "custom", customType: "pi-plans-exec-start" },
|
|
669
698
|
{ id: "u-1", type: "message", message: { role: "user", content: [{ type: "text", text: "implement VC-001" }] } },
|
|
670
|
-
{ id: "a-1", type: "message", message: { role: "assistant", content: [{ type: "text", text: "wrote helper [
|
|
699
|
+
{ id: "a-1", type: "message", message: { role: "assistant", content: [{ type: "text", text: "wrote helper [I-002:current]" }] } },
|
|
671
700
|
{ id: "u-2", type: "message", message: { role: "user", content: [{ type: "text", text: "implement VC-002" }] } },
|
|
672
701
|
{ id: "a-2", type: "message", message: { role: "assistant", content: [{ type: "text", text: "almost done" }] } },
|
|
673
|
-
{ id: "exec-ctx", type: "custom", customType: "pi-plans-exec-context" },
|
|
674
702
|
];
|
|
675
703
|
const result = buildExecutionCompactionResult(
|
|
676
704
|
{
|
|
677
705
|
type: "session_before_compact",
|
|
678
706
|
preparation,
|
|
679
707
|
branchEntries,
|
|
680
|
-
customInstructions: "keep
|
|
708
|
+
customInstructions: "keep:1",
|
|
681
709
|
reason: "threshold",
|
|
682
710
|
willRetry: false,
|
|
683
711
|
signal: new AbortController().signal,
|
|
@@ -685,30 +713,51 @@ describe("execution loop", () => {
|
|
|
685
713
|
ctx,
|
|
686
714
|
);
|
|
687
715
|
assert.ok(result);
|
|
688
|
-
assert.
|
|
689
|
-
assert.match(result!.summary,
|
|
690
|
-
assert.match(result!.summary,
|
|
691
|
-
assert.match(result!.summary,
|
|
692
|
-
assert.match(result!.summary,
|
|
693
|
-
assert.match(result!.summary,
|
|
694
|
-
assert.match(result!.summary,
|
|
695
|
-
assert.
|
|
696
|
-
assert.
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
716
|
+
assert.equal(result!.firstKeptEntryId, "u-2");
|
|
717
|
+
assert.match(result!.summary, /\[Session Goal\]/);
|
|
718
|
+
assert.match(result!.summary, /Execute accepted plan/);
|
|
719
|
+
assert.match(result!.summary, /\[Outstanding Context\]/);
|
|
720
|
+
assert.match(result!.summary, /Current implementation item: I-002/);
|
|
721
|
+
assert.match(result!.summary, /Remaining verifier items: VC-001, VC-002/);
|
|
722
|
+
assert.match(result!.summary, /Previous compact summary: Legacy Summary Deliver auto-compact/);
|
|
723
|
+
assert.equal((result!.details as any).compactor, "pi-vcc");
|
|
724
|
+
assert.equal((result!.details as any).phase, "execution");
|
|
725
|
+
|
|
726
|
+
await stopExecution(pi, ctx, "test-done");
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it("does not call model helpers and respects overrideDefaultCompaction=false", async () => {
|
|
730
|
+
const workdir = freshWorkdir();
|
|
731
|
+
startActiveRun(workdir);
|
|
732
|
+
const { pi, ctx } = makeHarness(workdir);
|
|
733
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v21.md"), items("VC-001"), [
|
|
734
|
+
{ id: "I-001", text: "First item." },
|
|
735
|
+
]);
|
|
736
|
+
let completeCalled = false;
|
|
737
|
+
(ctx.modelRegistry as any).complete = async () => {
|
|
738
|
+
completeCalled = true;
|
|
739
|
+
return {};
|
|
740
|
+
};
|
|
741
|
+
const event: any = {
|
|
700
742
|
type: "session_before_compact",
|
|
701
|
-
preparation,
|
|
702
|
-
branchEntries,
|
|
743
|
+
preparation: makePreparation("threshold", null),
|
|
744
|
+
branchEntries: compactableBranchEntries(),
|
|
703
745
|
reason: "threshold",
|
|
704
746
|
willRetry: false,
|
|
705
747
|
signal: new AbortController().signal,
|
|
706
|
-
}
|
|
748
|
+
};
|
|
749
|
+
const valid = handleExecutionBeforeCompact(pi, ctx, event);
|
|
750
|
+
assert.ok(valid?.compaction);
|
|
751
|
+
assert.equal(completeCalled, false);
|
|
707
752
|
|
|
708
|
-
|
|
709
|
-
|
|
753
|
+
const configPath = path.join(workdir, ".git", "pi_plans", "pi-vcc-config.json");
|
|
754
|
+
fs.writeFileSync(configPath, JSON.stringify({ overrideDefaultCompaction: false }), "utf8");
|
|
755
|
+
const fallback = handleExecutionBeforeCompact(pi, ctx, { ...event, signal: new AbortController().signal });
|
|
756
|
+
assert.equal(fallback, undefined, "override-disabled should return control to Pi default compaction");
|
|
710
757
|
|
|
711
|
-
|
|
758
|
+
await stopExecution(pi, ctx, "model-test");
|
|
759
|
+
});
|
|
760
|
+
it("filters the hidden resume message out of the model context payload", () => {
|
|
712
761
|
const messages = [
|
|
713
762
|
{ customType: "user", content: "real prompt" },
|
|
714
763
|
{ customType: "pi-plans-exec-resume", content: "Continue execution." },
|
|
@@ -719,37 +768,27 @@ describe("execution loop", () => {
|
|
|
719
768
|
assert.equal(filterPlanningResumeMessages(messages).length, 3);
|
|
720
769
|
});
|
|
721
770
|
|
|
722
|
-
it("
|
|
771
|
+
it("builds a VCC planning summary from active-run session context", () => {
|
|
723
772
|
const workdir = freshWorkdir();
|
|
724
773
|
const { ctx } = makeHarness(workdir);
|
|
725
774
|
initState(workdir);
|
|
726
775
|
const { run } = startRun(workdir, { topic: "planning compact", skill: "plan-normal", requestText: "demo" });
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
const makePlanningPreparation = (previousSummary: string | null) => ({
|
|
730
|
-
firstKeptEntryId: "fallback",
|
|
731
|
-
messagesToSummarize: [],
|
|
732
|
-
turnPrefixMessages: [],
|
|
733
|
-
isSplitTurn: false,
|
|
734
|
-
tokensBefore: 50000,
|
|
735
|
-
previousSummary,
|
|
736
|
-
fileOps: { read: [], written: [], edited: [] },
|
|
737
|
-
settings: { enabled: true, reserveTokens: 16384, keepRecentTokens: 20000 },
|
|
738
|
-
});
|
|
776
|
+
const planPath = path.join(run.artifact_dir, "PLAN_v1.md");
|
|
777
|
+
fs.writeFileSync(planPath, "# plan");
|
|
739
778
|
|
|
740
|
-
|
|
741
|
-
const branchEntriesWithPlan = [
|
|
779
|
+
const branchEntries = [
|
|
742
780
|
{ id: "rs", type: "custom", customType: PLANNING_RUN_START_CUSTOM_TYPE, data: { runId: run.run_id, artifactDir: run.artifact_dir } },
|
|
743
781
|
{ id: "u-1", type: "message", message: { role: "user", content: [{ type: "text", text: "background question?" }] } },
|
|
744
|
-
{ id: "a-1", type: "message", message: { role: "assistant", content: [{ type: "text", text: "some context" }] } },
|
|
745
|
-
{ id: "pw", type: "custom", customType: PLANNING_PLAN_WRITTEN_CUSTOM_TYPE, data: { runId: run.run_id, planPath
|
|
782
|
+
{ id: "a-1", type: "message", message: { role: "assistant", content: [{ type: "text", text: "some context [I-004:current]" }] } },
|
|
783
|
+
{ id: "pw", type: "custom", customType: PLANNING_PLAN_WRITTEN_CUSTOM_TYPE, data: { runId: run.run_id, planPath } },
|
|
746
784
|
{ id: "u-2", type: "message", message: { role: "user", content: [{ type: "text", text: "review please" }] } },
|
|
747
785
|
] as any;
|
|
748
786
|
const withPlan = buildPlanningCompactionResult(
|
|
749
787
|
{
|
|
750
788
|
type: "session_before_compact",
|
|
751
|
-
preparation:
|
|
752
|
-
branchEntries
|
|
789
|
+
preparation: makePreparation("threshold", "## Previous\nEarlier summary."),
|
|
790
|
+
branchEntries,
|
|
791
|
+
customInstructions: "keep:1",
|
|
753
792
|
reason: "threshold",
|
|
754
793
|
willRetry: false,
|
|
755
794
|
signal: new AbortController().signal,
|
|
@@ -758,95 +797,57 @@ describe("execution loop", () => {
|
|
|
758
797
|
);
|
|
759
798
|
assert.ok(withPlan);
|
|
760
799
|
assert.deepEqual(withPlan!.firstKeptEntryId, "u-2");
|
|
761
|
-
assert.match(withPlan!.summary,
|
|
762
|
-
assert.match(withPlan!.summary,
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const withoutPlan = buildPlanningCompactionResult(
|
|
771
|
-
{
|
|
772
|
-
type: "session_before_compact",
|
|
773
|
-
preparation: makePlanningPreparation(null),
|
|
774
|
-
branchEntries: branchEntriesWithoutPlan,
|
|
775
|
-
reason: "manual",
|
|
776
|
-
willRetry: false,
|
|
777
|
-
signal: new AbortController().signal,
|
|
778
|
-
},
|
|
779
|
-
ctx,
|
|
780
|
-
);
|
|
781
|
-
assert.ok(withoutPlan);
|
|
782
|
-
assert.deepEqual(withoutPlan!.firstKeptEntryId, "u-1");
|
|
783
|
-
assert.doesNotMatch(withoutPlan!.summary, /## Q&A During Planning/);
|
|
784
|
-
|
|
785
|
-
// Case C: no markers → fallback to preparation.firstKeptEntryId and no QA section.
|
|
786
|
-
const fallback = buildPlanningCompactionResult(
|
|
787
|
-
{
|
|
788
|
-
type: "session_before_compact",
|
|
789
|
-
preparation: makePlanningPreparation("## Previous\nEarlier summary."),
|
|
790
|
-
branchEntries: [],
|
|
791
|
-
reason: "threshold",
|
|
792
|
-
willRetry: false,
|
|
793
|
-
signal: new AbortController().signal,
|
|
794
|
-
},
|
|
795
|
-
ctx,
|
|
796
|
-
);
|
|
797
|
-
assert.ok(fallback);
|
|
798
|
-
assert.deepEqual(fallback!.firstKeptEntryId, "fallback");
|
|
799
|
-
assert.doesNotMatch(fallback!.summary, /## Q&A During Planning/);
|
|
800
|
-
assert.match(fallback!.summary, /## Previous Compact Summary/);
|
|
800
|
+
assert.match(withPlan!.summary, /\[Session Goal\]/);
|
|
801
|
+
assert.match(withPlan!.summary, new RegExp(run.run_id));
|
|
802
|
+
assert.match(withPlan!.summary, /\[Outstanding Context\]/);
|
|
803
|
+
assert.match(withPlan!.summary, /Latest plan path from session/);
|
|
804
|
+
assert.match(withPlan!.summary, /Planning artifact directory from session/);
|
|
805
|
+
assert.match(withPlan!.summary, /Current implementation marker observed during planning: I-004/);
|
|
806
|
+
assert.match(withPlan!.summary, /Previous compact summary: Previous Earlier summary\./);
|
|
807
|
+
assert.equal((withPlan!.details as any).compactor, "pi-vcc");
|
|
808
|
+
assert.equal((withPlan!.details as any).phase, "planning");
|
|
801
809
|
});
|
|
802
810
|
|
|
803
|
-
it("planning hook is gated by run.status=planning and defers
|
|
811
|
+
it("planning hook is gated by run.status=planning and defers while execution is running", async () => {
|
|
804
812
|
const workdir = freshWorkdir();
|
|
805
|
-
const {
|
|
813
|
+
const { pi, ctx } = makeHarness(workdir);
|
|
806
814
|
initState(workdir);
|
|
807
815
|
const { run } = startRun(workdir, { topic: "planning gate", skill: "plan-small", requestText: "x" });
|
|
808
816
|
|
|
809
|
-
|
|
810
|
-
const resultPlanning = handlePlanningBeforeCompact({} as any, ctx as any, {
|
|
817
|
+
const resultPlanning = handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
811
818
|
type: "session_before_compact",
|
|
812
|
-
preparation:
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
turnPrefixMessages: [],
|
|
816
|
-
isSplitTurn: false,
|
|
817
|
-
tokensBefore: 1,
|
|
818
|
-
previousSummary: null,
|
|
819
|
-
fileOps: { read: [], written: [], edited: [] },
|
|
820
|
-
settings: { enabled: true, reserveTokens: 16384, keepRecentTokens: 20000 },
|
|
821
|
-
},
|
|
822
|
-
branchEntries: [],
|
|
819
|
+
preparation: makePreparation("threshold", null),
|
|
820
|
+
branchEntries: compactableBranchEntries(),
|
|
821
|
+
customInstructions: "keep:1",
|
|
823
822
|
reason: "threshold",
|
|
824
823
|
willRetry: false,
|
|
825
824
|
signal: new AbortController().signal,
|
|
826
825
|
});
|
|
827
|
-
assert.ok(resultPlanning?.compaction
|
|
826
|
+
assert.ok(resultPlanning?.compaction);
|
|
828
827
|
|
|
829
|
-
// Flip status to done; planning hook should refuse.
|
|
830
828
|
setRunStatus(workdir, run.run_id, "done");
|
|
831
|
-
|
|
832
|
-
const resultDone = handlePlanningBeforeCompact({} as any, ctx as any, {
|
|
829
|
+
const resultDone = handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
833
830
|
type: "session_before_compact",
|
|
834
|
-
preparation:
|
|
835
|
-
|
|
836
|
-
messagesToSummarize: [],
|
|
837
|
-
turnPrefixMessages: [],
|
|
838
|
-
isSplitTurn: false,
|
|
839
|
-
tokensBefore: 1,
|
|
840
|
-
previousSummary: null,
|
|
841
|
-
fileOps: { read: [], written: [], edited: [] },
|
|
842
|
-
settings: { enabled: true, reserveTokens: 16384, keepRecentTokens: 20000 },
|
|
843
|
-
},
|
|
844
|
-
branchEntries: [],
|
|
831
|
+
preparation: makePreparation("threshold", null),
|
|
832
|
+
branchEntries: compactableBranchEntries(),
|
|
845
833
|
reason: "threshold",
|
|
846
834
|
willRetry: false,
|
|
847
835
|
signal: new AbortController().signal,
|
|
848
836
|
});
|
|
849
837
|
assert.equal(resultDone, undefined);
|
|
838
|
+
|
|
839
|
+
setRunStatus(workdir, run.run_id, "planning");
|
|
840
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v14.md"), items("VC-001"));
|
|
841
|
+
const duringExecution = handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
842
|
+
type: "session_before_compact",
|
|
843
|
+
preparation: makePreparation("threshold", null),
|
|
844
|
+
branchEntries: compactableBranchEntries(),
|
|
845
|
+
reason: "threshold",
|
|
846
|
+
willRetry: false,
|
|
847
|
+
signal: new AbortController().signal,
|
|
848
|
+
});
|
|
849
|
+
assert.equal(duringExecution, undefined);
|
|
850
|
+
await stopExecution(pi, ctx, "planning-gate");
|
|
850
851
|
});
|
|
851
852
|
|
|
852
853
|
it("turn_end writes are unconditionally deferred even when isIdle reads true", async () => {
|
|
@@ -910,63 +911,10 @@ describe("execution loop", () => {
|
|
|
910
911
|
assert.equal(lastComplete.items?.[0]?.done, true, "completion snapshot missing final done state");
|
|
911
912
|
});
|
|
912
913
|
|
|
913
|
-
it("plans-list groups by implementation item with status tags and strikethrough", async () => {
|
|
914
|
-
const workdir = freshWorkdir();
|
|
915
|
-
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
916
|
-
const implItems = [
|
|
917
|
-
{ id: "I-001", text: "Add state helpers; evaluate percent." },
|
|
918
|
-
{ id: "I-002", text: "Wire the turn_end trigger and status bar." },
|
|
919
|
-
{ id: "I-003", text: "Zero coverage orphan item." },
|
|
920
|
-
];
|
|
921
|
-
const vcItems: CheckItem[] = [
|
|
922
|
-
{ id: "VC-001", text: "`VC-001` covers `I-001`; pass condition: tests", done: false },
|
|
923
|
-
{ id: "VC-002", text: "`VC-002` covers `I-001` and `I-002`; pass condition: lint", done: false },
|
|
924
|
-
];
|
|
925
|
-
await startExecution(pi, ctx, path.join(workdir, "PLAN_v14.md"), vcItems, undefined, implItems);
|
|
926
|
-
|
|
927
|
-
// Status bar counts by I, excluding the zero-coverage orphan.
|
|
928
|
-
updateStatusWidget(ctx);
|
|
929
|
-
assert.match(recorded.status ?? "", /⌛ plans 0\/2: spent/);
|
|
930
|
-
|
|
931
|
-
toggleExecutionPanelView(pi, ctx);
|
|
932
|
-
assert.ok(recorded.widget);
|
|
933
|
-
// Expanded: footer ⌛ line cleared — the panel renders its own header.
|
|
934
|
-
assert.equal(recorded.status, undefined);
|
|
935
|
-
const widget = recorded.widget!.factory(
|
|
936
|
-
{} as any,
|
|
937
|
-
{ fg: (_c: string, t: string) => t, strikethrough: (t: string) => `~~${t}~~` },
|
|
938
|
-
);
|
|
939
|
-
const lines = widget.render(120).join("\n");
|
|
940
|
-
assert.match(lines, /⌛ plans 0\/2: spent/);
|
|
941
|
-
assert.match(lines, /\[Implementing\] I-001: Add state helpers/);
|
|
942
|
-
assert.match(lines, /\[Pending\] I-002: Wire the turn_end trigger/);
|
|
943
|
-
assert.match(lines, /\[Pending\] I-003: Zero coverage orphan item/);
|
|
944
|
-
// Only I status rows — no VC sub-rows in grouped mode.
|
|
945
|
-
assert.doesNotMatch(lines, /☐/);
|
|
946
|
-
assert.doesNotMatch(lines, /`VC-001` covers/);
|
|
947
|
-
|
|
948
|
-
// Markers move the states; fan-out advances both covered I items.
|
|
949
|
-
applyImplMarkers("[I-001:implemented]");
|
|
950
|
-
applyDoneMarkers("[DONE:VC-001]");
|
|
951
|
-
applyDoneMarkers("[DONE:VC-002]");
|
|
952
|
-
widget.invalidate();
|
|
953
|
-
const rendered = widget.render(120).join("\n");
|
|
954
|
-
assert.match(rendered, /~~\[VC passed\] I-001: Add state helpers~~/);
|
|
955
|
-
assert.match(rendered, /~~\[VC passed\] I-002: Wire the turn_end trigger and status bar\.~~/);
|
|
956
|
-
|
|
957
|
-
// Status bar now counts both I items as passed.
|
|
958
|
-
updateStatusWidget(ctx);
|
|
959
|
-
assert.equal(recorded.status, undefined, "expanded panel keeps the footer ⌛ line cleared");
|
|
960
|
-
|
|
961
|
-
await stopExecution(pi, ctx, "done");
|
|
962
|
-
// Panel cleared and execution over: the ⌛ line must not linger.
|
|
963
|
-
assert.doesNotMatch(recorded.status ?? "", /⌛ plans/);
|
|
964
|
-
});
|
|
965
|
-
|
|
966
914
|
it("updates the status bar in real time on every turn without session writes", async () => {
|
|
967
915
|
const workdir = freshWorkdir();
|
|
968
916
|
const { pi, ctx, recorded, setUsagePercent } = makeHarness(workdir);
|
|
969
|
-
await startExecution(pi, ctx, path.join(workdir, "PLAN_v15.md"), items("VC-001", "VC-002"),
|
|
917
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v15.md"), items("VC-001", "VC-002"), [
|
|
970
918
|
{ id: "I-001", text: "First item." },
|
|
971
919
|
{ id: "I-002", text: "Second item." },
|
|
972
920
|
]);
|
|
@@ -1003,19 +951,6 @@ describe("execution loop", () => {
|
|
|
1003
951
|
});
|
|
1004
952
|
assert.match(recorded.status ?? "", /⌛ plans 1\/3: spent/);
|
|
1005
953
|
assert.match(recorded.status ?? "", /12 in-toks/);
|
|
1006
|
-
assert.equal(recorded.widgetCalls, 0, "collapsed progress must not touch the widget slot");
|
|
1007
|
-
|
|
1008
|
-
// Expanded progress is updated through the same event handler without
|
|
1009
|
-
// re-registering the live widget.
|
|
1010
|
-
toggleExecutionPanelView(pi, ctx);
|
|
1011
|
-
const renderRequests = { count: 0 };
|
|
1012
|
-
const widget = recorded.widget?.factory(
|
|
1013
|
-
{ requestRender: () => { renderRequests.count += 1; } },
|
|
1014
|
-
{ fg: (_color: string, text: string) => text, strikethrough: (text: string) => text },
|
|
1015
|
-
);
|
|
1016
|
-
assert.ok(widget);
|
|
1017
|
-
assert.match(widget.render(100)[0] ?? "", /⌛ plans 1\/3: spent/);
|
|
1018
|
-
const widgetCalls = recorded.widgetCalls;
|
|
1019
954
|
|
|
1020
955
|
await emit("message_end", {
|
|
1021
956
|
message: { role: "assistant", usage: { input: 8, output: 3 } },
|
|
@@ -1023,17 +958,15 @@ describe("execution loop", () => {
|
|
|
1023
958
|
await emit("turn_end", {
|
|
1024
959
|
message: { role: "assistant", content: [{ type: "text", text: "verified [DONE:VC-002]" }] },
|
|
1025
960
|
});
|
|
1026
|
-
assert.
|
|
1027
|
-
assert.
|
|
1028
|
-
assert.equal(renderRequests.count, 1, "expanded progress must request a TUI render");
|
|
1029
|
-
assert.match(widget.render(100)[0] ?? "", /⌛ plans 2\/3: spent/);
|
|
961
|
+
assert.match(recorded.status ?? "", /⌛ plans 2\/3: spent/);
|
|
962
|
+
assert.match(recorded.status ?? "", /20 in-toks/);
|
|
1030
963
|
|
|
1031
964
|
await stopExecution(pi, ctx, "event-chain-test");
|
|
1032
965
|
});
|
|
1033
966
|
it("applies impl markers with silent unknown ids and later-overwrite semantics", async () => {
|
|
1034
967
|
const workdir = freshWorkdir();
|
|
1035
968
|
const { pi, ctx } = makeHarness(workdir);
|
|
1036
|
-
await startExecution(pi, ctx, path.join(workdir, "PLAN_v16.md"), items("VC-001"),
|
|
969
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v16.md"), items("VC-001"), [
|
|
1037
970
|
{ id: "I-001", text: "First item." },
|
|
1038
971
|
]);
|
|
1039
972
|
|
|
@@ -1045,6 +978,27 @@ describe("execution loop", () => {
|
|
|
1045
978
|
await stopExecution(pi, ctx, "done");
|
|
1046
979
|
});
|
|
1047
980
|
|
|
981
|
+
it("applies current-I markers to the live progress bar and snapshot", async () => {
|
|
982
|
+
const workdir = freshWorkdir();
|
|
983
|
+
const { pi, ctx, recorded, emit } = makeHarness(workdir);
|
|
984
|
+
registerExecutionTurnHandlers(pi);
|
|
985
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v20.md"), items("VC-001", "VC-002", "VC-003"), [
|
|
986
|
+
{ id: "I-001", text: "First item." },
|
|
987
|
+
{ id: "I-002", text: "Second item." },
|
|
988
|
+
{ id: "I-003", text: "Third item." },
|
|
989
|
+
]);
|
|
990
|
+
|
|
991
|
+
await emit("turn_end", {
|
|
992
|
+
message: { role: "assistant", content: [{ type: "text", text: "starting [I-003:current]" }] },
|
|
993
|
+
});
|
|
994
|
+
assert.match(recorded.status ?? "", /⌛ plans 2\/3: spent/);
|
|
995
|
+
assert.equal(getExecution()?.currentI, "I-003");
|
|
996
|
+
drainExecutionFlush(pi, ctx);
|
|
997
|
+
const snapshot = recorded.entries.filter((entry) => entry.customType === "pi-plans-exec").at(-1)?.data as { currentI?: string };
|
|
998
|
+
assert.equal(snapshot.currentI, "I-003");
|
|
999
|
+
assert.equal(applyCurrentIMarker("[I-999:current]"), false);
|
|
1000
|
+
await stopExecution(pi, ctx, "current-I-test");
|
|
1001
|
+
});
|
|
1048
1002
|
it("replays impl markers from post-snapshot messages on restore", async () => {
|
|
1049
1003
|
const workdir = freshWorkdir();
|
|
1050
1004
|
const { pi, ctx } = makeHarness(workdir);
|
|
@@ -1064,54 +1018,429 @@ describe("execution loop", () => {
|
|
|
1064
1018
|
assert.equal(getExecution()?.implStatus?.["I-001"], "implemented");
|
|
1065
1019
|
});
|
|
1066
1020
|
|
|
1021
|
+
|
|
1067
1022
|
it("keeps the injection rules teaching the impl markers", async () => {
|
|
1068
1023
|
const workdir = freshWorkdir();
|
|
1069
1024
|
const { pi, ctx } = makeHarness(workdir);
|
|
1070
|
-
await startExecution(pi, ctx, path.join(workdir, "PLAN_v18.md"), items("VC-001"),
|
|
1025
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v18.md"), items("VC-001"), [
|
|
1071
1026
|
{ id: "I-001", text: "First item." },
|
|
1072
1027
|
]);
|
|
1073
|
-
const rules = executionContextMessage()!;
|
|
1028
|
+
const rules = executionContextMessage(ctx)!;
|
|
1074
1029
|
assert.match(rules, /\[I-001:implemented\]/);
|
|
1075
1030
|
assert.match(rules, /\[I-001:validating\]/);
|
|
1031
|
+
assert.match(rules, /subprocess-backed verification/);
|
|
1032
|
+
assert.match(rules, /waiting for/);
|
|
1033
|
+
assert.match(rules, /5s\s*->\s*10s\s*->\s*20s\s*->\s*40s\s*->\s*80s/);
|
|
1034
|
+
assert.match(rules, /keep polling at 80s/);
|
|
1035
|
+
assert.match(rules, /restart at 5s for each new subprocess/);
|
|
1076
1036
|
await stopExecution(pi, ctx, "done");
|
|
1077
1037
|
});
|
|
1078
1038
|
|
|
1079
|
-
it("
|
|
1039
|
+
it("does not proactively request planning compaction", () => {
|
|
1080
1040
|
const workdir = freshWorkdir();
|
|
1081
|
-
const {
|
|
1041
|
+
const { ctx, setUsagePercent, recorded } = makeHarness(workdir);
|
|
1082
1042
|
initState(workdir);
|
|
1083
|
-
startRun(workdir, { topic: "planning
|
|
1043
|
+
startRun(workdir, { topic: "planning no-op", skill: "plan-normal", requestText: "x" });
|
|
1084
1044
|
|
|
1085
1045
|
setUsagePercent(120);
|
|
1086
|
-
assert.equal(shouldTriggerPlanningCompaction(ctx as any), true);
|
|
1087
|
-
requestPlanningCompaction(ctx as any);
|
|
1088
|
-
// In flight, second trigger ignored.
|
|
1089
|
-
requestPlanningCompaction(ctx as any);
|
|
1090
1046
|
assert.equal(shouldTriggerPlanningCompaction(ctx as any), false);
|
|
1047
|
+
requestPlanningCompaction(ctx as any);
|
|
1048
|
+
refreshPlanningCompactionCooldown(ctx as any);
|
|
1049
|
+
assert.equal(recorded.compacts?.length ?? 0, 0, "planning scheduling is owned by Pi core");
|
|
1050
|
+
});
|
|
1091
1051
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1052
|
+
it("auto-continues planning only for threshold/overflow and honors manual follow-up prompts", async () => {
|
|
1053
|
+
const workdir = freshWorkdir();
|
|
1054
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
1055
|
+
(ctx as any).piVersion = "0.84.3";
|
|
1056
|
+
initState(workdir);
|
|
1057
|
+
startRun(workdir, { topic: "planning success", skill: "plan-normal", requestText: "x" });
|
|
1058
|
+
|
|
1059
|
+
const threshold = handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
1060
|
+
type: "session_before_compact",
|
|
1061
|
+
preparation: makePreparation("threshold", null),
|
|
1062
|
+
branchEntries: compactableBranchEntries(),
|
|
1063
|
+
customInstructions: "keep:1",
|
|
1064
|
+
reason: "threshold",
|
|
1065
|
+
willRetry: false,
|
|
1066
|
+
signal: new AbortController().signal,
|
|
1067
|
+
});
|
|
1068
|
+
assert.ok(threshold?.compaction);
|
|
1069
|
+
await handlePlanningCompact(pi as any, ctx as any, {
|
|
1094
1070
|
type: "session_compact",
|
|
1095
1071
|
compactionEntry: { type: "compaction" } as never,
|
|
1096
|
-
fromExtension:
|
|
1097
|
-
reason: "
|
|
1072
|
+
fromExtension: false,
|
|
1073
|
+
reason: "threshold",
|
|
1098
1074
|
willRetry: false,
|
|
1099
1075
|
});
|
|
1100
|
-
|
|
1101
|
-
assert.ok(resume);
|
|
1076
|
+
assert.equal(recorded.messages.filter((message) => message.customType === "pi-plans-plan-resume").length, 1);
|
|
1102
1077
|
assert.equal(consumePlanningCompactionResumeGuard(ctx as any), true);
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1078
|
+
|
|
1079
|
+
handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
1080
|
+
type: "session_before_compact",
|
|
1081
|
+
preparation: makePreparation("manual", null),
|
|
1082
|
+
branchEntries: compactableBranchEntries(),
|
|
1083
|
+
customInstructions: "Ask the next question keep:1",
|
|
1084
|
+
reason: "manual",
|
|
1085
|
+
willRetry: false,
|
|
1086
|
+
signal: new AbortController().signal,
|
|
1087
|
+
});
|
|
1088
|
+
await handlePlanningCompact(pi as any, ctx as any, {
|
|
1089
|
+
type: "session_compact",
|
|
1090
|
+
compactionEntry: { type: "compaction" } as never,
|
|
1091
|
+
fromExtension: false,
|
|
1092
|
+
reason: "manual",
|
|
1093
|
+
willRetry: false,
|
|
1094
|
+
});
|
|
1095
|
+
assert.deepEqual(recorded.userMessages, ["Ask the next question"]);
|
|
1096
|
+
assert.equal(recorded.messages.filter((message) => message.customType === "pi-plans-plan-resume").length, 1);
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
it("planning request helper remains a no-op regardless of idleness", () => {
|
|
1100
|
+
const workdir = freshWorkdir();
|
|
1101
|
+
const { ctx, recorded, setUsagePercent } = makeHarness(workdir);
|
|
1102
|
+
initState(workdir);
|
|
1103
|
+
startRun(workdir, { topic: "idle no-op", skill: "plan-normal", requestText: "x" });
|
|
1108
1104
|
setUsagePercent(120);
|
|
1109
|
-
|
|
1105
|
+
|
|
1106
|
+
(ctx as any).isIdle = () => false;
|
|
1107
|
+
requestPlanningCompaction(ctx as any);
|
|
1108
|
+
(ctx as any).isIdle = () => true;
|
|
1109
|
+
(ctx as any).hasPendingMessages = () => false;
|
|
1110
|
+
requestPlanningCompaction(ctx as any);
|
|
1111
|
+
assert.equal(recorded.compacts?.length ?? 0, 0);
|
|
1112
|
+
assert.equal((ctx as any).sessionManager.__planningCompaction, undefined);
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
it("planning compact failures notify but do not re-request proactively", () => {
|
|
1116
|
+
const workdir = freshWorkdir();
|
|
1117
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
1118
|
+
initState(workdir);
|
|
1119
|
+
startRun(workdir, { topic: "failure notify", skill: "plan-normal", requestText: "x" });
|
|
1120
|
+
handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
1121
|
+
type: "session_before_compact",
|
|
1122
|
+
preparation: makePreparation("threshold", null),
|
|
1123
|
+
branchEntries: compactableBranchEntries(),
|
|
1124
|
+
customInstructions: "keep:1",
|
|
1125
|
+
reason: "threshold",
|
|
1126
|
+
willRetry: false,
|
|
1127
|
+
signal: new AbortController().signal,
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
handlePlanningCompactFailed(pi as any, ctx as any, {
|
|
1131
|
+
type: "session_compact_failed",
|
|
1132
|
+
reason: "manual",
|
|
1133
|
+
errorMessage: "Compaction failed: Nothing to compact (session too small)",
|
|
1134
|
+
aborted: false,
|
|
1135
|
+
willRetry: false,
|
|
1136
|
+
fromExtension: false,
|
|
1137
|
+
});
|
|
1138
|
+
assert.ok(recorded.notifies.some((note) => note.message.includes("nothing to summarize")));
|
|
1139
|
+
requestPlanningCompaction(ctx as any);
|
|
1140
|
+
assert.equal(recorded.compacts?.length ?? 0, 0);
|
|
1141
|
+
|
|
1142
|
+
handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
1143
|
+
type: "session_before_compact",
|
|
1144
|
+
preparation: makePreparation("threshold", null),
|
|
1145
|
+
branchEntries: compactableBranchEntries(),
|
|
1146
|
+
customInstructions: "keep:1",
|
|
1147
|
+
reason: "threshold",
|
|
1148
|
+
willRetry: false,
|
|
1149
|
+
signal: new AbortController().signal,
|
|
1150
|
+
});
|
|
1151
|
+
handlePlanningCompactFailed(pi as any, ctx as any, {
|
|
1152
|
+
type: "session_compact_failed",
|
|
1153
|
+
reason: "manual",
|
|
1154
|
+
errorMessage: "network down",
|
|
1155
|
+
aborted: false,
|
|
1156
|
+
willRetry: false,
|
|
1157
|
+
fromExtension: false,
|
|
1158
|
+
});
|
|
1159
|
+
assert.ok(recorded.notifies.some((note) => note.message.includes("will try again")));
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
it("execution turn compaction helper remains a no-op and failures keep execution active", async () => {
|
|
1163
|
+
const workdir = freshWorkdir();
|
|
1164
|
+
startActiveRun(workdir);
|
|
1165
|
+
const { pi, ctx, recorded, setUsagePercent } = makeHarness(workdir);
|
|
1166
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v23.md"), items("VC-001"), [
|
|
1167
|
+
{ id: "I-001", text: "First item." },
|
|
1168
|
+
]);
|
|
1169
|
+
|
|
1170
|
+
setUsagePercent(96);
|
|
1171
|
+
handleExecutionTurnCompaction(ctx);
|
|
1172
|
+
assert.equal(recorded.compacts?.length ?? 0, 0, "execution scheduling is owned by Pi core");
|
|
1173
|
+
|
|
1174
|
+
handleExecutionBeforeCompact(pi, ctx, {
|
|
1175
|
+
type: "session_before_compact",
|
|
1176
|
+
preparation: makePreparation("threshold", null),
|
|
1177
|
+
branchEntries: compactableBranchEntries(),
|
|
1178
|
+
customInstructions: "keep:1",
|
|
1179
|
+
reason: "threshold",
|
|
1180
|
+
willRetry: false,
|
|
1181
|
+
signal: new AbortController().signal,
|
|
1182
|
+
});
|
|
1183
|
+
handleExecutionCompactFailed(pi, ctx, {
|
|
1184
|
+
type: "session_compact_failed",
|
|
1185
|
+
reason: "manual",
|
|
1186
|
+
errorMessage: "Compaction failed: Already compacted",
|
|
1187
|
+
aborted: false,
|
|
1188
|
+
willRetry: false,
|
|
1189
|
+
fromExtension: false,
|
|
1190
|
+
});
|
|
1191
|
+
assert.ok(getExecution());
|
|
1192
|
+
assert.ok(recorded.notifies.some((note) => note.message.includes("nothing to summarize")));
|
|
1193
|
+
|
|
1194
|
+
await stopExecution(pi, ctx, "test-done");
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
it("treats planning abort/stream compact failures as terminal and notifies explicitly", () => {
|
|
1198
|
+
const workdir = freshWorkdir();
|
|
1199
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
1200
|
+
initState(workdir);
|
|
1201
|
+
startRun(workdir, { topic: "abort-stream classification", skill: "plan-normal", requestText: "x" });
|
|
1202
|
+
|
|
1203
|
+
const abortMessages = [
|
|
1204
|
+
"Auto-compaction failed: Turn prefix summarization failed: This operation was aborted",
|
|
1205
|
+
"Error: OpenAI Responses stream ended before a terminal response event",
|
|
1206
|
+
"Auto-compaction failed: context overflow recovery failed",
|
|
1207
|
+
"this operation was aborted",
|
|
1208
|
+
"aborted",
|
|
1209
|
+
];
|
|
1210
|
+
for (const errorMessage of abortMessages) {
|
|
1211
|
+
recorded.notifies.length = 0;
|
|
1212
|
+
handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
1213
|
+
type: "session_before_compact",
|
|
1214
|
+
preparation: makePreparation("threshold", null),
|
|
1215
|
+
branchEntries: compactableBranchEntries(),
|
|
1216
|
+
customInstructions: "keep:1",
|
|
1217
|
+
reason: "threshold",
|
|
1218
|
+
willRetry: false,
|
|
1219
|
+
signal: new AbortController().signal,
|
|
1220
|
+
});
|
|
1221
|
+
handlePlanningCompactFailed(pi as any, ctx as any, {
|
|
1222
|
+
type: "session_compact_failed",
|
|
1223
|
+
reason: "threshold",
|
|
1224
|
+
errorMessage,
|
|
1225
|
+
aborted: errorMessage.includes("aborted"),
|
|
1226
|
+
willRetry: false,
|
|
1227
|
+
fromExtension: false,
|
|
1228
|
+
});
|
|
1229
|
+
const backoffNote = recorded.notifies.find((n) => n.message.includes("was aborted"));
|
|
1230
|
+
assert.ok(backoffNote, `expected abort-class notify for: ${errorMessage}`);
|
|
1231
|
+
assert.match(backoffNote!.message, /provider interruption|competing manual/);
|
|
1232
|
+
const before = recorded.compacts?.length ?? 0;
|
|
1233
|
+
requestPlanningCompaction(ctx as any);
|
|
1234
|
+
assert.equal((recorded.compacts?.length ?? 0) - before, 0);
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
it("event.aborted=true with empty errorMessage still enters abort-class handling", () => {
|
|
1239
|
+
const workdir = freshWorkdir();
|
|
1240
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
1241
|
+
initState(workdir);
|
|
1242
|
+
startRun(workdir, { topic: "aborted-no-message", skill: "plan-normal", requestText: "x" });
|
|
1243
|
+
handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
1244
|
+
type: "session_before_compact",
|
|
1245
|
+
preparation: makePreparation("threshold", null),
|
|
1246
|
+
branchEntries: compactableBranchEntries(),
|
|
1247
|
+
customInstructions: "keep:1",
|
|
1248
|
+
reason: "threshold",
|
|
1249
|
+
willRetry: false,
|
|
1250
|
+
signal: new AbortController().signal,
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
handlePlanningCompactFailed(pi as any, ctx as any, {
|
|
1254
|
+
type: "session_compact_failed",
|
|
1255
|
+
reason: "threshold",
|
|
1256
|
+
errorMessage: undefined,
|
|
1257
|
+
aborted: true,
|
|
1258
|
+
willRetry: false,
|
|
1259
|
+
fromExtension: false,
|
|
1260
|
+
});
|
|
1261
|
+
const backoffNote = recorded.notifies.find((n) => n.message.includes("was aborted"));
|
|
1262
|
+
assert.ok(backoffNote);
|
|
1263
|
+
const before = recorded.compacts?.length ?? 0;
|
|
1264
|
+
requestPlanningCompaction(ctx as any);
|
|
1265
|
+
assert.equal((recorded.compacts?.length ?? 0) - before, 0);
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
it("network-style planning failures stay retryable without proactive requests", () => {
|
|
1269
|
+
const workdir = freshWorkdir();
|
|
1270
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
1271
|
+
initState(workdir);
|
|
1272
|
+
startRun(workdir, { topic: "network-retryable", skill: "plan-normal", requestText: "x" });
|
|
1273
|
+
handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
1274
|
+
type: "session_before_compact",
|
|
1275
|
+
preparation: makePreparation("threshold", null),
|
|
1276
|
+
branchEntries: compactableBranchEntries(),
|
|
1277
|
+
customInstructions: "keep:1",
|
|
1278
|
+
reason: "threshold",
|
|
1279
|
+
willRetry: false,
|
|
1280
|
+
signal: new AbortController().signal,
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
handlePlanningCompactFailed(pi as any, ctx as any, {
|
|
1284
|
+
type: "session_compact_failed",
|
|
1285
|
+
reason: "threshold",
|
|
1286
|
+
errorMessage: "network down",
|
|
1287
|
+
aborted: false,
|
|
1288
|
+
willRetry: false,
|
|
1289
|
+
fromExtension: false,
|
|
1290
|
+
});
|
|
1291
|
+
const abortNote = recorded.notifies.find((n) => n.message.includes("was aborted"));
|
|
1292
|
+
assert.equal(abortNote, undefined, "network down must not be classified as abort-class");
|
|
1293
|
+
const retryNote = recorded.notifies.find((n) => n.message.includes("will try again"));
|
|
1294
|
+
assert.ok(retryNote, "network down must keep the retryable path");
|
|
1295
|
+
const before = recorded.compacts?.length ?? 0;
|
|
1296
|
+
requestPlanningCompaction(ctx as any);
|
|
1297
|
+
assert.equal((recorded.compacts?.length ?? 0) - before, 0);
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
it("lifecycle flags distinguish unhinted and phase-attributed compactions", () => {
|
|
1301
|
+
const workdir = freshWorkdir();
|
|
1302
|
+
const { ctx } = makeHarness(workdir);
|
|
1303
|
+
|
|
1304
|
+
noteCompactionStarted(ctx as any, undefined);
|
|
1305
|
+
assert.equal(compactionInFlight(ctx as any, "planning"), true, "unhinted compaction marks planning inFlight");
|
|
1306
|
+
assert.equal(compactionInFlight(ctx as any, "execution"), true, "unhinted compaction marks execution inFlight");
|
|
1307
|
+
noteCompactionEnded(ctx as any, undefined);
|
|
1308
|
+
assert.equal(compactionInFlight(ctx as any, "planning"), false);
|
|
1309
|
+
assert.equal(compactionInFlight(ctx as any, "execution"), false);
|
|
1310
|
+
|
|
1311
|
+
noteCompactionStarted(ctx as any, "pi-plans planning auto compact");
|
|
1312
|
+
assert.equal(compactionInFlight(ctx as any, "planning"), true);
|
|
1313
|
+
assert.equal(compactionInFlight(ctx as any, "execution"), false);
|
|
1314
|
+
noteCompactionEnded(ctx as any, "pi-plans planning auto compact");
|
|
1315
|
+
assert.equal(compactionInFlight(ctx as any, "planning"), false);
|
|
1316
|
+
|
|
1317
|
+
noteCompactionStarted(ctx as any, "pi-plans execution auto compact");
|
|
1318
|
+
assert.equal(compactionInFlight(ctx as any, "planning"), false);
|
|
1319
|
+
assert.equal(compactionInFlight(ctx as any, "execution"), true);
|
|
1320
|
+
noteCompactionEnded(ctx as any, "pi-plans execution auto compact");
|
|
1321
|
+
assert.equal(compactionInFlight(ctx as any, "execution"), false);
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
it("execution handler classifies abort/stream failures as terminal", async () => {
|
|
1325
|
+
const workdir = freshWorkdir();
|
|
1326
|
+
startActiveRun(workdir);
|
|
1327
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
1328
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v4.md"), items("VC-001"));
|
|
1329
|
+
|
|
1330
|
+
const abortMessages = [
|
|
1331
|
+
"Auto-compaction failed: Turn prefix summarization failed: This operation was aborted",
|
|
1332
|
+
"Error: OpenAI Responses stream ended before a terminal response event",
|
|
1333
|
+
"Auto-compaction failed: context overflow recovery failed",
|
|
1334
|
+
"aborted",
|
|
1335
|
+
];
|
|
1336
|
+
for (const errorMessage of abortMessages) {
|
|
1337
|
+
recorded.notifies.length = 0;
|
|
1338
|
+
handleExecutionBeforeCompact(pi, ctx, {
|
|
1339
|
+
type: "session_before_compact",
|
|
1340
|
+
preparation: makePreparation("threshold", null),
|
|
1341
|
+
branchEntries: compactableBranchEntries(),
|
|
1342
|
+
customInstructions: "keep:1",
|
|
1343
|
+
reason: "threshold",
|
|
1344
|
+
willRetry: false,
|
|
1345
|
+
signal: new AbortController().signal,
|
|
1346
|
+
});
|
|
1347
|
+
handleExecutionCompactFailed(pi, ctx, {
|
|
1348
|
+
type: "session_compact_failed",
|
|
1349
|
+
reason: "threshold",
|
|
1350
|
+
errorMessage,
|
|
1351
|
+
aborted: errorMessage.includes("aborted"),
|
|
1352
|
+
willRetry: false,
|
|
1353
|
+
fromExtension: false,
|
|
1354
|
+
});
|
|
1355
|
+
const note = recorded.notifies.find((n) => n.message.includes("was aborted"));
|
|
1356
|
+
assert.ok(note, `expected abort-class notify for: ${errorMessage}`);
|
|
1357
|
+
assert.match(note!.message, /provider interruption|competing manual/);
|
|
1358
|
+
}
|
|
1359
|
+
handleExecutionBeforeCompact(pi, ctx, {
|
|
1360
|
+
type: "session_before_compact",
|
|
1361
|
+
preparation: makePreparation("threshold", null),
|
|
1362
|
+
branchEntries: compactableBranchEntries(),
|
|
1363
|
+
customInstructions: "keep:1",
|
|
1364
|
+
reason: "threshold",
|
|
1365
|
+
willRetry: false,
|
|
1366
|
+
signal: new AbortController().signal,
|
|
1367
|
+
});
|
|
1368
|
+
handleExecutionCompactFailed(pi, ctx, {
|
|
1369
|
+
type: "session_compact_failed",
|
|
1370
|
+
reason: "threshold",
|
|
1371
|
+
errorMessage: undefined,
|
|
1372
|
+
aborted: true,
|
|
1373
|
+
willRetry: false,
|
|
1374
|
+
fromExtension: false,
|
|
1375
|
+
});
|
|
1376
|
+
const cleanNote = recorded.notifies.find((n) => n.message.includes("was aborted"));
|
|
1377
|
+
assert.ok(cleanNote, "aborted-without-message must enter abort-class handling on execution side too");
|
|
1378
|
+
await stopExecution(pi, ctx, "test-done");
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
it("index.ts wires session_compact/session_compact_failed to clear inFlight flags (F-003/F-004 coverage)", () => {
|
|
1382
|
+
const indexSource = fs.readFileSync(path.join(process.cwd(), "index.ts"), "utf8");
|
|
1383
|
+
assert.match(indexSource, /await handleExecutionCompact\(pi, ctx, event\)/);
|
|
1384
|
+
assert.match(indexSource, /await handlePlanningCompact\(pi, ctx, event\)/);
|
|
1385
|
+
|
|
1386
|
+
const workdir = freshWorkdir();
|
|
1387
|
+
// Use a minimal harness that exposes sessionManager so we can inspect the
|
|
1388
|
+
// in-flight store after dispatching the registered event handlers.
|
|
1389
|
+
const ctx: any = {
|
|
1390
|
+
cwd: workdir,
|
|
1391
|
+
hasUI: false,
|
|
1392
|
+
sessionManager: {},
|
|
1393
|
+
};
|
|
1394
|
+
// Simulate the lifecycle that index.ts wires in production:
|
|
1395
|
+
noteCompactionStarted(ctx, undefined);
|
|
1396
|
+
assert.equal(compactionInFlight(ctx, "planning"), true);
|
|
1397
|
+
assert.equal(compactionInFlight(ctx, "execution"), true);
|
|
1398
|
+
noteCompactionEnded(ctx, undefined);
|
|
1399
|
+
assert.equal(compactionInFlight(ctx, "planning"), false);
|
|
1400
|
+
assert.equal(compactionInFlight(ctx, "execution"), false);
|
|
1401
|
+
// And a planning-attributed start + end still clears both (end has no hint).
|
|
1402
|
+
noteCompactionStarted(ctx, "pi-plans planning auto compact");
|
|
1403
|
+
assert.equal(compactionInFlight(ctx, "planning"), true);
|
|
1404
|
+
noteCompactionEnded(ctx, "pi-plans planning auto compact");
|
|
1405
|
+
assert.equal(compactionInFlight(ctx, "planning"), false);
|
|
1406
|
+
});
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
describe("execution injection reads graph config live", () => {
|
|
1410
|
+
it("reflects graph_enabled flips on the next assembly and surfaces config failures", async (t) => {
|
|
1411
|
+
const workdir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-exec-graph-"));
|
|
1412
|
+
t.after(() => {
|
|
1413
|
+
try {
|
|
1414
|
+
fs.rmSync(workdir, { recursive: true, force: true });
|
|
1415
|
+
} catch {
|
|
1416
|
+
/* ignore */
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
const { pi, ctx } = makeHarness(workdir);
|
|
1420
|
+
setGraphEnabled(workdir, true);
|
|
1421
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v1.md"), items("VC-001"));
|
|
1422
|
+
|
|
1423
|
+
const enabledRules = executionContextMessage(ctx)!;
|
|
1424
|
+
assert.match(enabledRules, /Code graph loop: indexed code files read as a function digest/);
|
|
1425
|
+
assert.doesNotMatch(enabledRules, /Code graph disabled/);
|
|
1426
|
+
|
|
1427
|
+
setGraphEnabled(workdir, false);
|
|
1428
|
+
assert.match(executionContextMessage(ctx)!, /Code graph disabled/);
|
|
1429
|
+
|
|
1430
|
+
fs.writeFileSync(path.join(workdir, ".git", "pi_plans", "config.json"), "{broken");
|
|
1431
|
+
const brokenRules = executionContextMessage(ctx)!;
|
|
1432
|
+
assert.match(brokenRules, /Code graph disabled/);
|
|
1433
|
+
assert.match(brokenRules, /config unreadable this turn/);
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
it("keeps no graphEnabled snapshot in the execution state source", () => {
|
|
1437
|
+
const source = fs.readFileSync(path.join(process.cwd(), "src", "exec.ts"), "utf8");
|
|
1438
|
+
assert.doesNotMatch(source, /graphEnabled/);
|
|
1439
|
+
assert.match(source, /resolveGraphMode/);
|
|
1110
1440
|
});
|
|
1111
1441
|
});
|
|
1112
1442
|
|
|
1113
|
-
function makePreparation(reason: "manual" | "threshold" | "overflow", previousSummary: string | null): any {
|
|
1114
|
-
return {
|
|
1443
|
+
function makePreparation(reason: "manual" | "threshold" | "overflow", previousSummary: string | null): any { return {
|
|
1115
1444
|
firstKeptEntryId: "a-2",
|
|
1116
1445
|
messagesToSummarize: [],
|
|
1117
1446
|
turnPrefixMessages: [],
|