pi-plans 0.2.0 → 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 +74 -21
- package/index.ts +115 -9
- package/package.json +7 -1
- package/references/pi-planning-workflow.md +18 -3
- package/references/state-and-config.md +34 -2
- package/scripts/validate.ts +4 -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 +1125 -371
- package/src/config-command.ts +326 -0
- package/src/exec.ts +356 -686
- package/src/refine-prompts.ts +50 -0
- package/src/refine-ui-helpers.ts +71 -18
- package/src/refine-ui-state.ts +87 -21
- package/src/refine-ui.ts +210 -102
- package/src/state.ts +19 -6
- package/src/subagent.ts +163 -61
- package/tests/ask-choice.test.ts +263 -0
- package/tests/autocomplete.test.ts +6 -1
- 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 +371 -57
- package/tests/config-command.test.ts +255 -0
- package/tests/exec.test.ts +665 -241
- 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/refine-prompts.test.ts +67 -2
- package/tests/refine-ui.test.ts +337 -72
- package/tests/subagent.test.ts +26 -20
- package/tools/ask-choice.ts +158 -11
- package/tools/code-graph.ts +254 -0
- package/tools/graph-aware-file-tools.ts +392 -0
- package/tools/plans.ts +84 -1
- package/tools/refine.ts +61 -15
package/src/exec.ts
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* progress is reported through the bottom status bar until every item passes.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { randomUUID } from "node:crypto";
|
|
11
10
|
import * as fs from "node:fs";
|
|
12
11
|
import type {
|
|
13
12
|
CompactionResult,
|
|
@@ -18,20 +17,25 @@ import type {
|
|
|
18
17
|
SessionCompactEvent,
|
|
19
18
|
SessionCompactFailedEvent,
|
|
20
19
|
} from "@earendil-works/pi-coding-agent";
|
|
21
|
-
import {
|
|
20
|
+
import { VERSION } from "@earendil-works/pi-coding-agent";
|
|
22
21
|
import {
|
|
23
|
-
|
|
22
|
+
buildPiPlansVccCompaction,
|
|
24
23
|
compactionCurrentI,
|
|
25
|
-
currentIExceedsTrigger,
|
|
26
24
|
entryCurrentIMarkers,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
type CompactionDetailsLike,
|
|
25
|
+
formatVccCompactionStats,
|
|
26
|
+
loadVccSettings,
|
|
27
|
+
scaffoldVccSettings,
|
|
28
|
+
shouldScheduleAutoContinue,
|
|
32
29
|
type CompactionEntryLike,
|
|
30
|
+
type PiPlansCompactionPhase,
|
|
31
|
+
type PiPlansVccPhaseContext,
|
|
32
|
+
type PiPlansVccSettings,
|
|
33
|
+
type VccCompactionBuildResult,
|
|
34
|
+
type VccCompactionStats,
|
|
33
35
|
} from "./compaction.ts";
|
|
34
|
-
import { getRun, readActive, setRunStatus, utcNow } from "./state.ts";
|
|
36
|
+
import { getRun, readActive, resolveStateRootOrNull, setRunStatus, utcNow } from "./state.ts";
|
|
37
|
+
import { graphBlockForExecutor } from "./code-graph/prompts.ts";
|
|
38
|
+
import { resolveGraphMode } from "./code-graph/mode.ts";
|
|
35
39
|
import {
|
|
36
40
|
extractCoverage,
|
|
37
41
|
latestPlanVersion,
|
|
@@ -86,9 +90,6 @@ export function getExecution(): ExecState | null {
|
|
|
86
90
|
return execution;
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
const EXECUTION_COMPACTION_TRIGGER_PERCENT = 20;
|
|
90
|
-
const EXECUTION_COMPACTION_REARM_PERCENT = 80;
|
|
91
|
-
const EXECUTION_COMPACTION_REARM_HIGH_PERCENT = 95;
|
|
92
93
|
const EXECUTION_COMPACTION_RESUME_MESSAGE = "Continue execution.";
|
|
93
94
|
|
|
94
95
|
interface ExecutionCompactionState {
|
|
@@ -99,6 +100,11 @@ interface ExecutionCompactionState {
|
|
|
99
100
|
lastSuccessfulUsagePercent: number | null;
|
|
100
101
|
lastSuccessfulAt: string | null;
|
|
101
102
|
rearmPending: boolean;
|
|
103
|
+
/** Terminal failure metadata is retained for diagnostics, not proactive retry. */
|
|
104
|
+
terminalBackoffTokens: number | null;
|
|
105
|
+
pendingStats: VccCompactionStats | null;
|
|
106
|
+
pendingFollowUpPrompt: string | null;
|
|
107
|
+
pendingContinueAfterThresholdCompact: boolean;
|
|
102
108
|
}
|
|
103
109
|
|
|
104
110
|
type ExecutionCompactionSession = { __executionCompaction?: ExecutionCompactionState };
|
|
@@ -126,6 +132,10 @@ function ensureExecutionCompactionState(ctx: ExtensionContext): ExecutionCompact
|
|
|
126
132
|
lastSuccessfulUsagePercent: null,
|
|
127
133
|
lastSuccessfulAt: null,
|
|
128
134
|
rearmPending: false,
|
|
135
|
+
terminalBackoffTokens: null,
|
|
136
|
+
pendingStats: null,
|
|
137
|
+
pendingFollowUpPrompt: null,
|
|
138
|
+
pendingContinueAfterThresholdCompact: false,
|
|
129
139
|
});
|
|
130
140
|
}
|
|
131
141
|
|
|
@@ -142,79 +152,12 @@ function consumeExecutionCompactionResumeGuard(ctx: ExtensionContext): boolean {
|
|
|
142
152
|
return true;
|
|
143
153
|
}
|
|
144
154
|
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
if (!state) return;
|
|
148
|
-
const percent = ctx.getContextUsage()?.percent ?? null;
|
|
149
|
-
if (percent !== null && percent < EXECUTION_COMPACTION_REARM_PERCENT && state.cooldownActive) {
|
|
150
|
-
state.cooldownActive = false;
|
|
151
|
-
state.rearmPending = true;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function executionCurrentIUsage(ctx: ExtensionContext): { tokens: number; contextWindow: number; eligible?: boolean } | null {
|
|
156
|
-
const usage = ctx.getContextUsage();
|
|
157
|
-
if (!usage || typeof usage.contextWindow !== "number" || usage.contextWindow <= 0) return null;
|
|
158
|
-
const manager = ctx.sessionManager as unknown as { getBranch?: () => CompactionEntryLike[] };
|
|
159
|
-
if (execution?.implItems?.length && typeof manager.getBranch === "function") {
|
|
160
|
-
try {
|
|
161
|
-
const entries = manager.getBranch();
|
|
162
|
-
if (entries.length) {
|
|
163
|
-
const plan = planIAwareCompaction({
|
|
164
|
-
entries,
|
|
165
|
-
currentI: execution.currentI,
|
|
166
|
-
knownIIds: execution.implItems.map((item) => item.id),
|
|
167
|
-
contextWindow: usage.contextWindow,
|
|
168
|
-
tokensBefore: usage.tokens ?? undefined,
|
|
169
|
-
});
|
|
170
|
-
if (plan.currentI || execution.currentI) {
|
|
171
|
-
return {
|
|
172
|
-
tokens: plan.currentITokens,
|
|
173
|
-
contextWindow: usage.contextWindow,
|
|
174
|
-
eligible: plan.firstKeptEntryIndex !== null && plan.firstKeptEntryIndex > 0,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
} catch {
|
|
179
|
-
// A read-only session projection is optional in test and startup contexts.
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (typeof usage.tokens !== "number") return null;
|
|
183
|
-
return { tokens: usage.tokens, contextWindow: usage.contextWindow };
|
|
184
|
-
}
|
|
185
|
-
export function shouldTriggerExecutionCompaction(ctx: ExtensionContext): boolean {
|
|
186
|
-
const currentUsage = executionCurrentIUsage(ctx);
|
|
187
|
-
if (!currentUsage || currentUsage.eligible === false || !currentIExceedsTrigger(currentUsage.tokens, currentUsage.contextWindow)) return false;
|
|
188
|
-
const percent = (currentUsage.tokens / currentUsage.contextWindow) * 100;
|
|
189
|
-
const state = executionCompactionState(ctx);
|
|
190
|
-
if (!state) return true;
|
|
191
|
-
if (state.inFlight || state.resumeGuard || state.cooldownActive) return false;
|
|
192
|
-
if (state.rearmPending) {
|
|
193
|
-
if (percent < EXECUTION_COMPACTION_REARM_HIGH_PERCENT) return false;
|
|
194
|
-
state.rearmPending = false;
|
|
195
|
-
}
|
|
196
|
-
return true;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function requestExecutionCompaction(ctx: ExtensionContext): void {
|
|
200
|
-
const state = ensureExecutionCompactionState(ctx);
|
|
201
|
-
if (state.inFlight || state.resumeGuard) return;
|
|
202
|
-
state.inFlight = true;
|
|
203
|
-
state.lastAttemptReason = "threshold";
|
|
204
|
-
try {
|
|
205
|
-
ctx.compact({ customInstructions: "pi-plans execution auto compact" });
|
|
206
|
-
} catch (error) {
|
|
207
|
-
state.inFlight = false;
|
|
208
|
-
ctx.ui.notify(`pi-plans: could not request execution compaction (${String(error)}).`, "warning");
|
|
209
|
-
}
|
|
155
|
+
export function shouldTriggerExecutionCompaction(_ctx: ExtensionContext): boolean {
|
|
156
|
+
return false;
|
|
210
157
|
}
|
|
211
158
|
|
|
212
159
|
export function handleExecutionTurnCompaction(ctx: ExtensionContext): void {
|
|
213
|
-
|
|
214
|
-
if (consumeExecutionCompactionResumeGuard(ctx)) return;
|
|
215
|
-
if (shouldTriggerExecutionCompaction(ctx)) {
|
|
216
|
-
requestExecutionCompaction(ctx);
|
|
217
|
-
}
|
|
160
|
+
consumeExecutionCompactionResumeGuard(ctx);
|
|
218
161
|
}
|
|
219
162
|
|
|
220
163
|
export function computeExecutionProgress(execution: ExecState): { done: number; total: number } {
|
|
@@ -240,11 +183,6 @@ export function computeExecutionProgress(execution: ExecState): { done: number;
|
|
|
240
183
|
};
|
|
241
184
|
}
|
|
242
185
|
|
|
243
|
-
export function executionProgress(): { done: number; total: number } | null {
|
|
244
|
-
if (!execution) return null;
|
|
245
|
-
return computeExecutionProgress(execution);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
186
|
function formatElapsed(startedAt: string): string {
|
|
249
187
|
const total = Math.max(0, Math.floor((Date.now() - Date.parse(startedAt)) / 1000));
|
|
250
188
|
const h = String(Math.floor(total / 3600)).padStart(2, "0");
|
|
@@ -405,368 +343,170 @@ export function registerExecutionTurnHandlers(
|
|
|
405
343
|
|
|
406
344
|
const EXECUTION_RESUME_CUSTOM_TYPE = "pi-plans-exec-resume";
|
|
407
345
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
return (message.content ?? [])
|
|
420
|
-
.filter((part) => part.type === "text")
|
|
421
|
-
.map((part) => part.text ?? "")
|
|
422
|
-
.join("\n")
|
|
423
|
-
.trim();
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function messageRoleLabel(role?: string): string {
|
|
427
|
-
switch (role) {
|
|
428
|
-
case "assistant": return "Assistant";
|
|
429
|
-
case "user": return "User";
|
|
430
|
-
case "toolResult": return "Tool";
|
|
431
|
-
case "custom": return "Custom";
|
|
432
|
-
default: return role ? role : "Message";
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function isInternalExecutionCustomType(customType?: string): boolean {
|
|
437
|
-
return customType === "pi-plans-exec"
|
|
438
|
-
|| customType === "pi-plans-exec-cleared"
|
|
439
|
-
|| customType === "pi-plans-exec-start"
|
|
440
|
-
|| customType === "pi-plans-exec-context"
|
|
441
|
-
|| customType === EXECUTION_RESUME_CUSTOM_TYPE;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function isSummarizableEntry(entry: CompactBranchEntry): boolean {
|
|
445
|
-
return entry.type === "message" && !!entry.message;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function renderMessageLine(entry: CompactBranchEntry): string {
|
|
449
|
-
if (!isSummarizableEntry(entry)) return "";
|
|
450
|
-
const text = messageText(entry.message);
|
|
451
|
-
if (!text) return "";
|
|
452
|
-
return `- [${messageRoleLabel(entry.message?.role)}] ${compactText(text)}`;
|
|
346
|
+
function activeVccSettings(ctx: ExtensionContext, phase: PiPlansCompactionPhase): { settings: PiPlansVccSettings; runId: string; artifactDir: string } | null {
|
|
347
|
+
const stateRoot = resolveStateRootOrNull(ctx.cwd);
|
|
348
|
+
if (!stateRoot) return null;
|
|
349
|
+
const active = readActive(ctx.cwd);
|
|
350
|
+
if (!active) return null;
|
|
351
|
+
const run = getRun(ctx.cwd, active.run_id);
|
|
352
|
+
if (!run) return null;
|
|
353
|
+
if (phase === "planning" && run.status !== "planning") return null;
|
|
354
|
+
if (phase === "execution" && run.status !== "executing") return null;
|
|
355
|
+
scaffoldVccSettings(stateRoot);
|
|
356
|
+
return { settings: loadVccSettings(stateRoot), runId: run.run_id, artifactDir: run.artifact_dir };
|
|
453
357
|
}
|
|
454
358
|
|
|
455
|
-
function
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
lastCompletionIndex = i;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
if (lastCompletionIndex >= 0) {
|
|
467
|
-
const next = branchEntries.slice(lastCompletionIndex + 1).find((entry) => entry.id && !isInternalExecutionCustomType(entry.customType));
|
|
468
|
-
if (next?.id) return next.id;
|
|
469
|
-
}
|
|
470
|
-
const startIndex = branchEntries.findIndex((entry) => entry.type === "custom" && entry.customType === "pi-plans-exec-start");
|
|
471
|
-
if (startIndex >= 0) {
|
|
472
|
-
const next = branchEntries.slice(startIndex + 1).find((entry) => entry.id && !isInternalExecutionCustomType(entry.customType));
|
|
473
|
-
if (next?.id) return next.id;
|
|
474
|
-
}
|
|
475
|
-
return fallback;
|
|
359
|
+
function executionVccContext(): PiPlansVccPhaseContext {
|
|
360
|
+
return {
|
|
361
|
+
phase: "execution",
|
|
362
|
+
planPath: execution?.planPath ?? null,
|
|
363
|
+
currentI: execution?.currentI ?? null,
|
|
364
|
+
remainingVerifierIds: execution?.items.filter((item) => !item.done).map((item) => item.id) ?? [],
|
|
365
|
+
implementationIds: execution?.implItems?.map((item) => item.id) ?? [],
|
|
366
|
+
};
|
|
476
367
|
}
|
|
477
368
|
|
|
478
|
-
function
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
let
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (line) currentLines.push(line);
|
|
488
|
-
for (const marker of scanDoneMarkers(messageText(entry.message))) {
|
|
489
|
-
const itemIndex = completedItems.findIndex((item, index) => index >= completedIndex && item.id === marker);
|
|
490
|
-
if (itemIndex < 0) continue;
|
|
491
|
-
const item = completedItems[itemIndex];
|
|
492
|
-
sections.push(`### \`${item.id}\` ${item.text.split(";")[0]}
|
|
493
|
-
${currentLines.length ? currentLines.join("\n") : "- (no transcript captured)"}`);
|
|
494
|
-
currentLines = [];
|
|
495
|
-
completedIndex = itemIndex + 1;
|
|
369
|
+
function planningVccContext(branchEntries: CompactionEntryLike[], fallback: { runId?: string; artifactDir?: string }): PiPlansVccPhaseContext {
|
|
370
|
+
let runId: string | null = fallback.runId ?? null;
|
|
371
|
+
let artifactDir: string | null = fallback.artifactDir ?? null;
|
|
372
|
+
let planPath: string | null = null;
|
|
373
|
+
let currentI: string | null = null;
|
|
374
|
+
for (const entry of branchEntries) {
|
|
375
|
+
if (entry.type === "custom" && entry.customType === PLANNING_RUN_START_CUSTOM_TYPE) {
|
|
376
|
+
runId = typeof entry.data?.runId === "string" ? entry.data.runId : runId;
|
|
377
|
+
artifactDir = typeof entry.data?.artifactDir === "string" ? entry.data.artifactDir : artifactDir;
|
|
496
378
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function splitTurnBoundaryIndex(event: SessionBeforeCompactEvent): number | undefined {
|
|
502
|
-
if (!event.preparation.isSplitTurn) return undefined;
|
|
503
|
-
const index = event.branchEntries.findIndex((entry) => entry.id === event.preparation.firstKeptEntryId);
|
|
504
|
-
return index >= 0 ? index : undefined;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function buildExecutionIPlan(event: SessionBeforeCompactEvent, ctx?: ExtensionContext): ReturnType<typeof planIAwareCompaction> {
|
|
508
|
-
if (!execution) throw new Error("execution state is unavailable");
|
|
509
|
-
const usage = eventPreparationUsage(event, ctx);
|
|
510
|
-
return planIAwareCompaction({
|
|
511
|
-
entries: event.branchEntries as unknown as CompactionEntryLike[],
|
|
512
|
-
currentI: execution.currentI,
|
|
513
|
-
knownIIds: execution.implItems?.map((item) => item.id),
|
|
514
|
-
contextWindow: usage.contextWindow,
|
|
515
|
-
tokensBefore: event.preparation.tokensBefore,
|
|
516
|
-
fallbackFirstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
517
|
-
maxFirstKeptEntryIndex: splitTurnBoundaryIndex(event),
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function eventPreparationUsage(event: SessionBeforeCompactEvent, ctx?: ExtensionContext): { contextWindow: number | null } {
|
|
522
|
-
const fromContext = ctx?.getContextUsage()?.contextWindow;
|
|
523
|
-
const contextWindow = (event as SessionBeforeCompactEvent & { contextWindow?: number }).contextWindow ?? fromContext;
|
|
524
|
-
return { contextWindow: typeof contextWindow === "number" && contextWindow > 0 ? contextWindow : null };
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function previousCompactionDetails(entries: CompactionEntryLike[]): CompactionDetailsLike | undefined {
|
|
528
|
-
let merged: CompactionDetailsLike | undefined;
|
|
529
|
-
for (const entry of entries) {
|
|
530
|
-
if (entry.type !== "compaction") continue;
|
|
531
|
-
const raw = entry.details ?? entry.data;
|
|
532
|
-
if (!raw || typeof raw !== "object") continue;
|
|
533
|
-
const details = raw as CompactionDetailsLike;
|
|
534
|
-
if (!details.readRecords && !details.metrics && !details.iSections) continue;
|
|
535
|
-
merged = mergeCompactionDetails(merged, details);
|
|
536
|
-
}
|
|
537
|
-
return merged;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
function buildImplementationSections(plan: ReturnType<typeof planIAwareCompaction>): string[] {
|
|
541
|
-
const sections: string[] = [];
|
|
542
|
-
for (const slice of plan.slices) {
|
|
543
|
-
if (slice.id === null) continue;
|
|
544
|
-
const lines = slice.entries
|
|
545
|
-
.filter((entry) => plan.summaryEntries.includes(entry))
|
|
546
|
-
.map((entry) => renderMessageLine(entry as CompactBranchEntry))
|
|
547
|
-
.filter(Boolean);
|
|
548
|
-
if (lines.length) {
|
|
549
|
-
sections.push(`### ${slice.current ? "Current I" : "Implementation I"} \`${slice.id}\`\n${lines.join("\\n")}`);
|
|
379
|
+
if (entry.type === "custom" && entry.customType === PLANNING_PLAN_WRITTEN_CUSTOM_TYPE) {
|
|
380
|
+
planPath = typeof entry.data?.planPath === "string" ? entry.data.planPath : planPath;
|
|
550
381
|
}
|
|
382
|
+
for (const id of entryCurrentIMarkers(entry)) currentI = id;
|
|
383
|
+
currentI = compactionCurrentI(entry) ?? currentI;
|
|
551
384
|
}
|
|
552
|
-
return
|
|
385
|
+
return { phase: "planning", runId, artifactDir, planPath, currentI };
|
|
553
386
|
}
|
|
554
387
|
|
|
555
|
-
function
|
|
556
|
-
|
|
557
|
-
ctx
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const active = readActive(ctx.cwd);
|
|
564
|
-
const run = active ? getRun(ctx.cwd, active.run_id) : null;
|
|
565
|
-
const branchEntries = event.branchEntries as unknown as CompactionEntryLike[];
|
|
566
|
-
const summaryEntries = boundaryIndex >= 0
|
|
567
|
-
? (branchEntries.slice(0, boundaryIndex) as CompactBranchEntry[])
|
|
568
|
-
: (branchEntries as CompactBranchEntry[]);
|
|
569
|
-
const sections = buildFinishedItemSections(summaryEntries);
|
|
570
|
-
const readRecords = plan?.readRecords ?? extractReadRecords(summaryEntries as unknown as CompactionEntryLike[]);
|
|
571
|
-
const priorDetails = previousCompactionDetails(branchEntries);
|
|
572
|
-
const metrics = plan?.metrics ?? {
|
|
573
|
-
contextWindow: ctx.getContextUsage()?.contextWindow ?? null,
|
|
574
|
-
tokensBefore: event.preparation.tokensBefore,
|
|
575
|
-
currentITokens: 0,
|
|
576
|
-
summaryTokens: 0,
|
|
577
|
-
keptSuffixTokens: 0,
|
|
578
|
-
estimatedAfterTokens: null,
|
|
579
|
-
targetRatio: 0.1,
|
|
580
|
-
currentI: execution.currentI ?? null,
|
|
581
|
-
firstKeptEntryId,
|
|
582
|
-
targetMet: false,
|
|
583
|
-
hardFloorReason: "I-aware budget unavailable for this legacy execution snapshot",
|
|
584
|
-
};
|
|
585
|
-
const details = mergeCompactionDetails(priorDetails, {
|
|
586
|
-
kind: "pi-plans-execution-compaction",
|
|
587
|
-
version: 1,
|
|
588
|
-
currentI: execution.currentI ?? plan?.currentI ?? null,
|
|
589
|
-
iSections: plan?.slices.map((slice) => ({ id: slice.id, entryIds: slice.entries.map((entry) => entry.id).filter((id): id is string => !!id) })) ?? [],
|
|
590
|
-
readRecords,
|
|
591
|
-
metrics: { ...metrics, firstKeptEntryId },
|
|
388
|
+
function buildExecutionVccResult(event: SessionBeforeCompactEvent, ctx: ExtensionContext): VccCompactionBuildResult | null {
|
|
389
|
+
if (!execution) return null;
|
|
390
|
+
const active = activeVccSettings(ctx, "execution");
|
|
391
|
+
if (!active) return null;
|
|
392
|
+
return buildPiPlansVccCompaction({
|
|
393
|
+
branchEntries: event.branchEntries as unknown as CompactionEntryLike[],
|
|
394
|
+
preparation: event.preparation,
|
|
395
|
+
customInstructions: event.customInstructions,
|
|
592
396
|
reason: event.reason,
|
|
593
397
|
willRetry: event.willRetry,
|
|
594
|
-
|
|
398
|
+
settings: active.settings,
|
|
399
|
+
phaseContext: executionVccContext(),
|
|
595
400
|
});
|
|
596
|
-
const parts: string[] = [];
|
|
597
|
-
if (event.customInstructions?.trim()) {
|
|
598
|
-
parts.push(`## Compact Instructions\n${compactText(event.customInstructions, 1000)}`);
|
|
599
|
-
}
|
|
600
|
-
parts.push(`## Plan Before This Run\n- Request: ${compactText(run?.request_text ?? execution.planPath, 280)}\n- Plan file: \`${execution.planPath}\``);
|
|
601
|
-
if (event.preparation.previousSummary?.trim()) {
|
|
602
|
-
parts.push(`## Previous Compact Summary\n${event.preparation.previousSummary.trim()}`);
|
|
603
|
-
}
|
|
604
|
-
parts.push(`## Implementation Items\n${plan?.slices.length ? (buildImplementationSections(plan).join("\\n\\n") || "- (no I transcript captured)") : "- Legacy snapshot: current I is inferred from the execution frontier."}`);
|
|
605
|
-
parts.push(`## Finished VC Items\n${sections.length ? sections.join("\\n\\n") : "- (none yet)"}`);
|
|
606
|
-
parts.push(`## Current I\n- \`${execution.currentI ?? plan?.currentI ?? "unknown"}\`\n- Recent legal suffix begins at \`${firstKeptEntryId}\`.`);
|
|
607
|
-
parts.push(`## Read Records\n${readRecords.length ? readRecords.map((record) => formatReadRecord(record)).join("\\n") : "- (none)"}`);
|
|
608
|
-
parts.push(`## Compaction Boundary\n- firstKeptEntryId: \`${firstKeptEntryId}\`\n- currentI: \`${execution.currentI ?? plan?.currentI ?? "unknown"}\`\n- tokensBefore: ${metrics.tokensBefore}\n- estimatedAfterTokens: ${metrics.estimatedAfterTokens ?? "unknown"}\n- targetRatio: ${metrics.targetRatio}\n- targetMet: ${metrics.targetMet}\n- hardFloorReason: ${metrics.hardFloorReason ?? "none"}`);
|
|
609
|
-
parts.push(`## Current Work\n- Raw tail preserved from \`${firstKeptEntryId}\` onward.${event.preparation.isSplitTurn ? "\\n- Split-turn prefix remains in the kept tail." : ""}`);
|
|
610
|
-
return { parts, details };
|
|
611
401
|
}
|
|
612
402
|
|
|
613
403
|
export function buildExecutionCompactionResult(event: SessionBeforeCompactEvent, ctx: ExtensionContext): CompactionResult | null {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
const hasIState = (execution.implItems?.length ?? 0) > 0;
|
|
617
|
-
const plan = hasIState ? buildExecutionIPlan(event, ctx) : null;
|
|
618
|
-
const firstKeptEntryId = plan?.firstKeptEntryId
|
|
619
|
-
?? findExecutionCompactionCutEntryId(event.branchEntries as CompactBranchEntry[], event.preparation.firstKeptEntryId);
|
|
620
|
-
const boundaryIndex = event.branchEntries.findIndex((entry) => entry.id === firstKeptEntryId);
|
|
621
|
-
const { parts, details } = executionSummaryParts(event, ctx, firstKeptEntryId, boundaryIndex, plan);
|
|
622
|
-
return {
|
|
623
|
-
summary: parts.join("\\n\\n"),
|
|
624
|
-
firstKeptEntryId,
|
|
625
|
-
tokensBefore: event.preparation.tokensBefore,
|
|
626
|
-
estimatedTokensAfter: plan?.metrics.estimatedAfterTokens ?? undefined,
|
|
627
|
-
details,
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
function assistantResponseText(response: { content?: Array<{ type?: string; text?: string }> }): string {
|
|
632
|
-
return (response.content ?? [])
|
|
633
|
-
.filter((part) => part.type === "text")
|
|
634
|
-
.map((part) => part.text ?? "")
|
|
635
|
-
.join("\\n")
|
|
636
|
-
.trim();
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function isUsableCompactionResponse(response: { content?: Array<{ type?: string; text?: string }>; stopReason?: string }, summary: string): boolean {
|
|
640
|
-
if (!summary) return false;
|
|
641
|
-
if (["length", "toolUse", "error", "aborted", "deferred"].includes(response.stopReason ?? "")) return false;
|
|
642
|
-
return ["## Implementation Items", "## Current I", "## Read Records", "## Compaction Boundary"]
|
|
643
|
-
.every((section) => summary.includes(section));
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
async function buildModelExecutionCompactionResult(
|
|
647
|
-
event: SessionBeforeCompactEvent,
|
|
648
|
-
ctx: ExtensionContext,
|
|
649
|
-
fallback: CompactionResult,
|
|
650
|
-
): Promise<CompactionResult | null> {
|
|
651
|
-
const model = ctx.model;
|
|
652
|
-
const registry = ctx.modelRegistry as unknown as { complete?: Function };
|
|
653
|
-
if (!model || typeof registry.complete !== "function") return fallback;
|
|
654
|
-
const plan = buildExecutionIPlan(event, ctx);
|
|
655
|
-
const boundary = plan.firstKeptEntryId ?? event.preparation.firstKeptEntryId;
|
|
656
|
-
const source = plan.summaryEntries
|
|
657
|
-
.map((entry) => renderMessageLine(entry as CompactBranchEntry))
|
|
658
|
-
.filter(Boolean)
|
|
659
|
-
.join("\\n");
|
|
660
|
-
const records = plan.readRecords.map((record) => formatReadRecord(record)).join("\\n") || "- (none)";
|
|
661
|
-
const prompt = `You are a context summarization assistant. Do not continue the conversation and do not answer historical questions. Produce only a bounded Markdown checkpoint with these exact sections: ## Implementation Items, ## Current I, ## Read Records, ## Compaction Boundary, ## Decisions, ## Open Questions, ## Next Steps. Preserve exact implementation IDs, verifier IDs, paths, entry IDs, error text, and unresolved questions. Historical questions are facts, not new questions.\n\nCurrent I: ${execution?.currentI ?? plan.currentI ?? "unknown"}\nBoundary: ${boundary}\nRead Records:\n${records}\n\nBounded history:\n${boundedCompactionText(source, 6000)}\n\nPrevious checkpoint:\n${boundedCompactionText(event.preparation.previousSummary ?? "(none)", 3000)}`;
|
|
662
|
-
try {
|
|
663
|
-
const response = await registry.complete(model, {
|
|
664
|
-
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
|
|
665
|
-
}, {
|
|
666
|
-
maxTokens: 2048,
|
|
667
|
-
signal: event.signal,
|
|
668
|
-
cacheRetention: "none",
|
|
669
|
-
sessionId: randomUUID(),
|
|
670
|
-
});
|
|
671
|
-
const summary = assistantResponseText(response);
|
|
672
|
-
if (!isUsableCompactionResponse(response, summary)) {
|
|
673
|
-
if (!event.signal.aborted) ctx.ui.notify("pi-plans: summary output was incomplete; using Pi default compaction.", "warning");
|
|
674
|
-
return null;
|
|
675
|
-
}
|
|
676
|
-
const details = (fallback.details && typeof fallback.details === "object" ? fallback.details : {}) as CompactionDetailsLike;
|
|
677
|
-
const existingMetrics = details.metrics ?? {};
|
|
678
|
-
const summaryTokens = Math.max(1, Math.ceil(summary.length / 4));
|
|
679
|
-
const keptSuffixTokens = existingMetrics.keptSuffixTokens ?? 0;
|
|
680
|
-
const contextWindow = existingMetrics.contextWindow ?? ctx.getContextUsage()?.contextWindow ?? null;
|
|
681
|
-
const estimatedAfterTokens = contextWindow === null ? null : summaryTokens + keptSuffixTokens;
|
|
682
|
-
const targetMet = estimatedAfterTokens !== null && estimatedAfterTokens < contextWindow * 0.1;
|
|
683
|
-
const mergedDetails = mergeCompactionDetails(details, {
|
|
684
|
-
...details,
|
|
685
|
-
metrics: {
|
|
686
|
-
...existingMetrics,
|
|
687
|
-
summaryTokens,
|
|
688
|
-
estimatedAfterTokens,
|
|
689
|
-
targetMet,
|
|
690
|
-
hardFloorReason: targetMet ? null : existingMetrics.hardFloorReason ?? "summary or retained context exceeds the 10% target",
|
|
691
|
-
},
|
|
692
|
-
});
|
|
693
|
-
return { ...fallback, summary, estimatedTokensAfter: estimatedAfterTokens ?? undefined, usage: response.usage, details: mergedDetails };
|
|
694
|
-
} catch (error) {
|
|
695
|
-
if (!event.signal.aborted) ctx.ui.notify(`pi-plans: summary model failed; using Pi default compaction (${String(error)}).`, "warning");
|
|
696
|
-
return null;
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
function notifyHardFloor(ctx: ExtensionContext, compaction: CompactionResult): void {
|
|
701
|
-
const metrics = (compaction.details as CompactionDetailsLike | undefined)?.metrics;
|
|
702
|
-
if (!metrics || metrics.targetMet !== false) return;
|
|
703
|
-
ctx.ui.notify(
|
|
704
|
-
`pi-plans: compaction target <10% is unreachable; retaining the legal floor (${metrics.hardFloorReason ?? "unknown reason"}).`,
|
|
705
|
-
"warning",
|
|
706
|
-
);
|
|
404
|
+
const built = buildExecutionVccResult(event, ctx);
|
|
405
|
+
return built?.kind === "compaction" ? built.compaction : null;
|
|
707
406
|
}
|
|
708
407
|
|
|
709
408
|
export function handleExecutionBeforeCompact(
|
|
710
409
|
pi: ExtensionAPI,
|
|
711
410
|
ctx: ExtensionContext,
|
|
712
411
|
event: SessionBeforeCompactEvent,
|
|
713
|
-
): SessionBeforeCompactResult |
|
|
412
|
+
): SessionBeforeCompactResult | undefined {
|
|
714
413
|
if (!execution) return undefined;
|
|
715
|
-
|
|
414
|
+
const state = ensureExecutionCompactionState(ctx);
|
|
415
|
+
state.inFlight = true;
|
|
416
|
+
state.lastAttemptReason = event.reason;
|
|
417
|
+
state.pendingStats = null;
|
|
418
|
+
state.pendingFollowUpPrompt = null;
|
|
419
|
+
state.pendingContinueAfterThresholdCompact = false;
|
|
420
|
+
let built: VccCompactionBuildResult | null;
|
|
716
421
|
try {
|
|
717
|
-
|
|
422
|
+
built = buildExecutionVccResult(event, ctx);
|
|
718
423
|
} catch (error) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
ctx.ui.notify(`pi-plans: compaction preparation failed; using Pi default compaction (${String(error)}).`, "warning");
|
|
424
|
+
state.inFlight = false;
|
|
425
|
+
ctx.ui.notify(`pi-plans: VCC compaction preparation failed; using Pi default compaction (${String(error)}).`, "warning");
|
|
722
426
|
return undefined;
|
|
723
427
|
}
|
|
724
|
-
if (!fallback)
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
if (!ctx.model || typeof registry.complete !== "function") {
|
|
728
|
-
requestExecutionFlush(pi, ctx);
|
|
729
|
-
return { compaction: fallback };
|
|
428
|
+
if (!built || built.kind === "fallback") {
|
|
429
|
+
state.inFlight = false;
|
|
430
|
+
return undefined;
|
|
730
431
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
return {
|
|
735
|
-
}
|
|
432
|
+
if (built.kind === "cancel") {
|
|
433
|
+
state.inFlight = false;
|
|
434
|
+
ctx.ui.notify(built.message, "warning");
|
|
435
|
+
return { cancel: true };
|
|
436
|
+
}
|
|
437
|
+
state.pendingStats = built.stats;
|
|
438
|
+
state.pendingFollowUpPrompt = built.followUpPrompt;
|
|
439
|
+
state.pendingContinueAfterThresholdCompact = built.settings.continueAfterThresholdCompact;
|
|
440
|
+
requestExecutionFlush(pi, ctx);
|
|
441
|
+
return { compaction: built.compaction };
|
|
736
442
|
}
|
|
737
443
|
|
|
738
|
-
|
|
444
|
+
function runtimePiVersion(ctx: ExtensionContext): unknown {
|
|
445
|
+
return (ctx as ExtensionContext & { piVersion?: unknown }).piVersion ?? VERSION;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export async function handleExecutionCompact(pi: ExtensionAPI, ctx: ExtensionContext, event: SessionCompactEvent): Promise<void> {
|
|
739
449
|
if (!execution) return;
|
|
740
450
|
const state = ensureExecutionCompactionState(ctx);
|
|
451
|
+
const stats = state.pendingStats;
|
|
452
|
+
const followUpPrompt = state.pendingFollowUpPrompt;
|
|
453
|
+
const continueAfterThresholdCompact = state.pendingContinueAfterThresholdCompact;
|
|
454
|
+
state.pendingStats = null;
|
|
455
|
+
state.pendingFollowUpPrompt = null;
|
|
456
|
+
state.pendingContinueAfterThresholdCompact = false;
|
|
741
457
|
state.inFlight = false;
|
|
742
458
|
state.lastAttemptReason = event.reason;
|
|
743
459
|
state.cooldownActive = true;
|
|
744
460
|
state.rearmPending = false;
|
|
461
|
+
state.terminalBackoffTokens = null;
|
|
745
462
|
state.lastSuccessfulAt = utcNow();
|
|
746
463
|
state.lastSuccessfulUsagePercent = ctx.getContextUsage()?.percent ?? state.lastSuccessfulUsagePercent;
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
464
|
+
state.resumeGuard = false;
|
|
465
|
+
if (!event.willRetry && stats) {
|
|
466
|
+
ctx.ui.notify(formatVccCompactionStats(stats), "info");
|
|
467
|
+
if (followUpPrompt) {
|
|
468
|
+
await pi.sendUserMessage?.(followUpPrompt);
|
|
469
|
+
} else if ((event.reason === "threshold" || event.reason === "overflow") && shouldScheduleAutoContinue(continueAfterThresholdCompact, runtimePiVersion(ctx))) {
|
|
470
|
+
state.resumeGuard = true;
|
|
471
|
+
pi.sendMessage(
|
|
472
|
+
{
|
|
473
|
+
customType: EXECUTION_RESUME_CUSTOM_TYPE,
|
|
474
|
+
content: EXECUTION_COMPACTION_RESUME_MESSAGE,
|
|
475
|
+
display: false,
|
|
476
|
+
},
|
|
477
|
+
{ triggerTurn: true },
|
|
478
|
+
);
|
|
479
|
+
}
|
|
759
480
|
}
|
|
760
481
|
requestExecutionFlush(pi, ctx);
|
|
761
482
|
updateStatusWidget(ctx);
|
|
762
483
|
}
|
|
763
484
|
|
|
485
|
+
|
|
764
486
|
export function handleExecutionCompactFailed(pi: ExtensionAPI, ctx: ExtensionContext, event: SessionCompactFailedEvent): void {
|
|
765
487
|
if (!execution) return;
|
|
766
488
|
const state = executionCompactionState(ctx);
|
|
767
|
-
const
|
|
768
|
-
if (
|
|
769
|
-
|
|
489
|
+
const terminal = isTerminalCompactionFailure(event);
|
|
490
|
+
if (terminal) {
|
|
491
|
+
// Pi refused or aborted the compaction. Hold the cooldown and re-arm
|
|
492
|
+
// only after real growth or high-watermark pressure so the loop stops.
|
|
493
|
+
if (state) {
|
|
494
|
+
state.inFlight = false;
|
|
495
|
+
state.resumeGuard = false;
|
|
496
|
+
state.cooldownActive = true;
|
|
497
|
+
state.rearmPending = false;
|
|
498
|
+
state.lastAttemptReason = event.reason;
|
|
499
|
+
const tokens = ctx.getContextUsage()?.tokens;
|
|
500
|
+
state.terminalBackoffTokens = typeof tokens === "number" ? tokens : Number.POSITIVE_INFINITY;
|
|
501
|
+
state.pendingStats = null;
|
|
502
|
+
state.pendingFollowUpPrompt = null;
|
|
503
|
+
state.pendingContinueAfterThresholdCompact = false;
|
|
504
|
+
}
|
|
505
|
+
const message = terminal.kind === "content"
|
|
506
|
+
? "pi-plans: compaction found nothing to summarize; backing off until the session grows past the keep-recent window."
|
|
507
|
+
: "pi-plans: compaction was aborted (provider interruption, user cancel, or a competing manual compact); backing off until the session grows or usage nears the window.";
|
|
508
|
+
ctx.ui.notify(message, "info");
|
|
509
|
+
requestExecutionFlush(pi, ctx);
|
|
770
510
|
return;
|
|
771
511
|
}
|
|
772
512
|
if (state) {
|
|
@@ -775,6 +515,9 @@ export function handleExecutionCompactFailed(pi: ExtensionAPI, ctx: ExtensionCon
|
|
|
775
515
|
state.cooldownActive = false;
|
|
776
516
|
state.rearmPending = false;
|
|
777
517
|
state.lastAttemptReason = event.reason;
|
|
518
|
+
state.pendingStats = null;
|
|
519
|
+
state.pendingFollowUpPrompt = null;
|
|
520
|
+
state.pendingContinueAfterThresholdCompact = false;
|
|
778
521
|
}
|
|
779
522
|
ctx.ui.notify(
|
|
780
523
|
`pi-plans: compaction failed (${event.reason}); execution remains active and will wait for the next eligible turn.`,
|
|
@@ -788,15 +531,14 @@ export function filterExecutionResumeMessages<T extends { customType?: string }>
|
|
|
788
531
|
}
|
|
789
532
|
|
|
790
533
|
// ---------------------------------------------------------------------------
|
|
791
|
-
// Planning-phase
|
|
792
|
-
//
|
|
793
|
-
//
|
|
794
|
-
//
|
|
534
|
+
// Planning-phase compaction: Pi core owns scheduling; this hook customizes
|
|
535
|
+
// active planning compact events with the same VCC builder used by execution.
|
|
536
|
+
// The two state machines are kept independent (different memory slot and
|
|
537
|
+
// snapshot key) so execution never bleeds into planning.
|
|
795
538
|
// ---------------------------------------------------------------------------
|
|
796
539
|
|
|
797
540
|
export const PLANNING_RUN_START_CUSTOM_TYPE = "pi-plans-run-start";
|
|
798
541
|
export const PLANNING_PLAN_WRITTEN_CUSTOM_TYPE = "pi-plans-plan-written";
|
|
799
|
-
const PLANNING_QA_SECTION_HEADER = "## Q&A During Planning";
|
|
800
542
|
const PLANNING_RESUME_CUSTOM_TYPE = "pi-plans-plan-resume";
|
|
801
543
|
|
|
802
544
|
interface PlanningCompactionState {
|
|
@@ -806,149 +548,114 @@ interface PlanningCompactionState {
|
|
|
806
548
|
lastAttemptReason: "manual" | "threshold" | "overflow" | null;
|
|
807
549
|
lastSuccessfulUsagePercent: number | null;
|
|
808
550
|
lastSuccessfulAt: string | null;
|
|
551
|
+
/** Terminal "nothing to compact" backoff: tokens observed when Pi refused. */
|
|
552
|
+
terminalBackoffTokens: number | null;
|
|
553
|
+
pendingStats: VccCompactionStats | null;
|
|
554
|
+
pendingFollowUpPrompt: string | null;
|
|
555
|
+
pendingContinueAfterThresholdCompact: boolean;
|
|
809
556
|
}
|
|
810
557
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|| customType === EXECUTION_RESUME_CUSTOM_TYPE
|
|
826
|
-
|| customType === PLANNING_RUN_START_CUSTOM_TYPE
|
|
827
|
-
|| customType === PLANNING_PLAN_WRITTEN_CUSTOM_TYPE
|
|
828
|
-
|| customType === PLANNING_RESUME_CUSTOM_TYPE
|
|
829
|
-
|| customType === AUTOCOMPLETE_ENTRY
|
|
830
|
-
);
|
|
558
|
+
function ensurePlanningCompactionState(ctx: ExtensionContext): PlanningCompactionState {
|
|
559
|
+
const session = ctx.sessionManager as unknown as { __planningCompaction?: PlanningCompactionState };
|
|
560
|
+
return (session.__planningCompaction ??= {
|
|
561
|
+
inFlight: false,
|
|
562
|
+
resumeGuard: false,
|
|
563
|
+
cooldownActive: false,
|
|
564
|
+
lastAttemptReason: null,
|
|
565
|
+
lastSuccessfulUsagePercent: null,
|
|
566
|
+
lastSuccessfulAt: null,
|
|
567
|
+
terminalBackoffTokens: null,
|
|
568
|
+
pendingStats: null,
|
|
569
|
+
pendingFollowUpPrompt: null,
|
|
570
|
+
pendingContinueAfterThresholdCompact: false,
|
|
571
|
+
});
|
|
831
572
|
}
|
|
832
573
|
|
|
833
|
-
function
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
574
|
+
function isTerminalCompactionFailure(event: { errorMessage?: string; aborted?: boolean }): { kind: "content" | "abort-stream" } | null {
|
|
575
|
+
const message = (event.errorMessage ?? "").toLowerCase();
|
|
576
|
+
if (message.includes("nothing to compact") || message.includes("already compacted") || message.includes("session too small")) {
|
|
577
|
+
return { kind: "content" };
|
|
578
|
+
}
|
|
579
|
+
// abort/stream class: explicit event names only, so that provider blips
|
|
580
|
+
// (network down, etc.) stay retryable.
|
|
581
|
+
const abortPatterns = [
|
|
582
|
+
"this operation was aborted",
|
|
583
|
+
"aborted",
|
|
584
|
+
"stream ended before a terminal response event",
|
|
585
|
+
"turn prefix summarization failed",
|
|
586
|
+
"auto-compaction failed",
|
|
587
|
+
"context overflow recovery failed",
|
|
588
|
+
];
|
|
589
|
+
if (abortPatterns.some((pattern) => message.includes(pattern))) {
|
|
590
|
+
return { kind: "abort-stream" };
|
|
591
|
+
}
|
|
592
|
+
// Aborted with no recognized message: still an abort-class terminal so the
|
|
593
|
+
// next eligible turn does not immediately retry the same operation.
|
|
594
|
+
if (event.aborted === true) {
|
|
595
|
+
return { kind: "abort-stream" };
|
|
596
|
+
}
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** Session-scoped phase-local "compaction in flight" guard.
|
|
601
|
+
* - Set on `session_before_compact` for the phase attributed by the custom
|
|
602
|
+
* instructions hint; auto-compaction (no hint) marks both phases defensively.
|
|
603
|
+
* - Cleared on `session_compact` and `session_compact_failed`.
|
|
604
|
+
* - Retained so lifecycle events expose the same phase-local state to tests
|
|
605
|
+
* and future Pi core schema additions. */
|
|
606
|
+
type CompactionPhase = "planning" | "execution";
|
|
607
|
+
|
|
608
|
+
function compactionLifecycleStore(ctx: ExtensionContext): {
|
|
609
|
+
planning: boolean;
|
|
610
|
+
execution: boolean;
|
|
611
|
+
} {
|
|
612
|
+
const carrier = ctx.sessionManager as unknown as {
|
|
613
|
+
__piPlansCompactionInFlight?: { planning: boolean; execution: boolean };
|
|
614
|
+
};
|
|
615
|
+
carrier.__piPlansCompactionInFlight ??= { planning: false, execution: false };
|
|
616
|
+
return carrier.__piPlansCompactionInFlight;
|
|
839
617
|
}
|
|
840
618
|
|
|
841
|
-
function
|
|
842
|
-
return
|
|
619
|
+
function isPlanningCustomInstructions(hint: unknown): boolean {
|
|
620
|
+
return typeof hint === "string" && hint.startsWith("pi-plans planning");
|
|
843
621
|
}
|
|
844
622
|
|
|
845
|
-
function
|
|
846
|
-
|
|
847
|
-
const text = (entry.message.content ?? [])
|
|
848
|
-
.filter((part) => part.type === "text")
|
|
849
|
-
.map((part) => part.text ?? "")
|
|
850
|
-
.join("\n")
|
|
851
|
-
.trim();
|
|
852
|
-
if (!text) return null;
|
|
853
|
-
const role = entry.message.role ?? "message";
|
|
854
|
-
const normalized = text.replace(/\s+/g, " ").trim();
|
|
855
|
-
const limit = 180;
|
|
856
|
-
const clipped = normalized.length > limit ? `${normalized.slice(0, limit - 1)}…` : normalized;
|
|
857
|
-
return `- [${role}] ${clipped}`;
|
|
623
|
+
function isExecutionCustomInstructions(hint: unknown): boolean {
|
|
624
|
+
return typeof hint === "string" && hint.startsWith("pi-plans execution");
|
|
858
625
|
}
|
|
859
626
|
|
|
860
|
-
function
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
} else if (entry.type === "custom" && entry.customType === PLANNING_RUN_START_CUSTOM_TYPE) {
|
|
871
|
-
runStartIndexes.push(i);
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
const planIndex = planWrittenIndexes[planWrittenIndexes.length - 1] ?? -1;
|
|
875
|
-
const startIndex = runStartIndexes[runStartIndexes.length - 1] ?? -1;
|
|
876
|
-
const anchorIndex = planIndex >= 0 ? planIndex : startIndex;
|
|
877
|
-
if (anchorIndex < 0) {
|
|
878
|
-
return { id: fallback, qaWindowEntries: [], hasMarker: false };
|
|
879
|
-
}
|
|
880
|
-
const next = branchEntries
|
|
881
|
-
.slice(anchorIndex + 1)
|
|
882
|
-
.find((entry) => entry.id && !isPlanningInternalCustomType(entry.customType));
|
|
883
|
-
if (!next?.id) {
|
|
884
|
-
return { id: fallback, qaWindowEntries: [], hasMarker: true };
|
|
627
|
+
export function noteCompactionStarted(ctx: ExtensionContext, customInstructions: unknown): void {
|
|
628
|
+
const store = compactionLifecycleStore(ctx);
|
|
629
|
+
if (isPlanningCustomInstructions(customInstructions)) {
|
|
630
|
+
store.planning = true;
|
|
631
|
+
} else if (isExecutionCustomInstructions(customInstructions)) {
|
|
632
|
+
store.execution = true;
|
|
633
|
+
} else {
|
|
634
|
+
// Auto-compaction (threshold/overflow/manual without our hint) marks both.
|
|
635
|
+
store.planning = true;
|
|
636
|
+
store.execution = true;
|
|
885
637
|
}
|
|
886
|
-
const qaWindowEntries = branchEntries
|
|
887
|
-
.slice(startIndex >= 0 ? startIndex + 1 : 0, anchorIndex)
|
|
888
|
-
.filter((entry) => entry.type === "message" && entry.message);
|
|
889
|
-
return { id: next.id, qaWindowEntries, hasMarker: true };
|
|
890
638
|
}
|
|
891
639
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
640
|
+
/** Pi core's `SessionCompactEvent` / `SessionCompactFailedEvent` do not carry
|
|
641
|
+
* `customInstructions` in any emission site, so the END side has no way to
|
|
642
|
+
* know which phase the compaction belonged to. Clearing both phases is the
|
|
643
|
+
* safe default — the per-phase start side (above) already encodes the hint
|
|
644
|
+
* attribution. The hint parameter is retained for API symmetry and future
|
|
645
|
+
* Pi core schema additions. */
|
|
646
|
+
export function noteCompactionEnded(ctx: ExtensionContext, _customInstructions: unknown): void {
|
|
647
|
+
const store = compactionLifecycleStore(ctx);
|
|
648
|
+
store.planning = false;
|
|
649
|
+
store.execution = false;
|
|
902
650
|
}
|
|
903
651
|
|
|
904
|
-
function
|
|
905
|
-
const
|
|
906
|
-
|
|
907
|
-
const run = getRun(workdir, active.run_id);
|
|
908
|
-
if (!run || run.status !== "planning") return null;
|
|
909
|
-
return { runId: run.run_id, artifactDir: run.artifact_dir };
|
|
652
|
+
export function compactionInFlight(ctx: ExtensionContext, phase: CompactionPhase): boolean {
|
|
653
|
+
const store = compactionLifecycleStore(ctx);
|
|
654
|
+
return store[phase];
|
|
910
655
|
}
|
|
911
656
|
|
|
912
|
-
function
|
|
913
|
-
|
|
914
|
-
if (!usage || typeof usage.contextWindow !== "number" || usage.contextWindow <= 0) return null;
|
|
915
|
-
const manager = ctx.sessionManager as unknown as { getBranch?: () => CompactionEntryLike[] };
|
|
916
|
-
if (typeof manager.getBranch === "function") {
|
|
917
|
-
try {
|
|
918
|
-
const entries = manager.getBranch();
|
|
919
|
-
const currentI = entries.flatMap((entry) => entryCurrentIMarkers(entry)).at(-1)
|
|
920
|
-
?? entries.map((entry) => compactionCurrentI(entry)).filter((id): id is string => !!id).at(-1);
|
|
921
|
-
if (currentI) {
|
|
922
|
-
const plan = planIAwareCompaction({
|
|
923
|
-
entries,
|
|
924
|
-
currentI,
|
|
925
|
-
contextWindow: usage.contextWindow,
|
|
926
|
-
tokensBefore: usage.tokens ?? undefined,
|
|
927
|
-
});
|
|
928
|
-
return {
|
|
929
|
-
tokens: plan.currentITokens,
|
|
930
|
-
contextWindow: usage.contextWindow,
|
|
931
|
-
eligible: plan.firstKeptEntryIndex !== null && plan.firstKeptEntryIndex > 0,
|
|
932
|
-
};
|
|
933
|
-
}
|
|
934
|
-
} catch {
|
|
935
|
-
// Session projection is unavailable during startup in some hosts.
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
if (typeof usage.tokens !== "number") return null;
|
|
939
|
-
return { tokens: usage.tokens, contextWindow: usage.contextWindow };
|
|
940
|
-
}
|
|
941
|
-
export function shouldTriggerPlanningCompaction(ctx: ExtensionContext): boolean {
|
|
942
|
-
if (getExecution()) return false;
|
|
943
|
-
const ctxWorkdir = ctx.cwd;
|
|
944
|
-
if (!resolvePlanningCompactionContext(ctxWorkdir)) return false;
|
|
945
|
-
const currentUsage = planningCurrentIUsage(ctx);
|
|
946
|
-
if (!currentUsage || currentUsage.eligible === false || !currentIExceedsTrigger(currentUsage.tokens, currentUsage.contextWindow)) return false;
|
|
947
|
-
const session = ctx.sessionManager as unknown as { __planningCompaction?: PlanningCompactionState };
|
|
948
|
-
const state = session.__planningCompaction;
|
|
949
|
-
if (!state) return true;
|
|
950
|
-
if (state.inFlight || state.resumeGuard || state.cooldownActive) return false;
|
|
951
|
-
return true;
|
|
657
|
+
export function shouldTriggerPlanningCompaction(_ctx: ExtensionContext): boolean {
|
|
658
|
+
return false;
|
|
952
659
|
}
|
|
953
660
|
|
|
954
661
|
export function consumePlanningCompactionResumeGuard(ctx: ExtensionContext): boolean {
|
|
@@ -958,119 +665,34 @@ export function consumePlanningCompactionResumeGuard(ctx: ExtensionContext): boo
|
|
|
958
665
|
return true;
|
|
959
666
|
}
|
|
960
667
|
|
|
961
|
-
export function refreshPlanningCompactionCooldown(
|
|
962
|
-
|
|
963
|
-
const state = session.__planningCompaction;
|
|
964
|
-
if (!state) return;
|
|
965
|
-
const percent = ctx.getContextUsage()?.percent ?? null;
|
|
966
|
-
if (percent !== null && percent < 85 && state.cooldownActive) {
|
|
967
|
-
state.cooldownActive = false;
|
|
968
|
-
}
|
|
668
|
+
export function refreshPlanningCompactionCooldown(_ctx: ExtensionContext): void {
|
|
669
|
+
// Pi core owns scheduling; retained for lifecycle compatibility only.
|
|
969
670
|
}
|
|
970
671
|
|
|
971
|
-
export function requestPlanningCompaction(
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
inFlight: false,
|
|
975
|
-
resumeGuard: false,
|
|
976
|
-
cooldownActive: false,
|
|
977
|
-
lastAttemptReason: null,
|
|
978
|
-
lastSuccessfulUsagePercent: null,
|
|
979
|
-
lastSuccessfulAt: null,
|
|
980
|
-
} satisfies PlanningCompactionState);
|
|
981
|
-
if (state.inFlight || state.resumeGuard) return;
|
|
982
|
-
state.inFlight = true;
|
|
983
|
-
state.lastAttemptReason = "threshold";
|
|
984
|
-
try {
|
|
985
|
-
ctx.compact({ customInstructions: "pi-plans planning auto compact" });
|
|
986
|
-
} catch (error) {
|
|
987
|
-
state.inFlight = false;
|
|
988
|
-
ctx.ui.notify(`pi-plans: could not request planning compaction (${String(error)}).`, "warning");
|
|
989
|
-
}
|
|
672
|
+
export function requestPlanningCompaction(_ctx: ExtensionContext): void {
|
|
673
|
+
// Proactive pi-plans compaction is intentionally disabled. Manual,
|
|
674
|
+
// threshold, and overflow compactions are handled by session_before_compact.
|
|
990
675
|
}
|
|
991
676
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
const
|
|
995
|
-
if (!
|
|
996
|
-
const branchEntries = event.branchEntries as unknown as
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
?? null;
|
|
1002
|
-
const iPlan = currentI
|
|
1003
|
-
? planIAwareCompaction({
|
|
1004
|
-
entries: branchEntries as unknown as CompactionEntryLike[],
|
|
1005
|
-
currentI,
|
|
1006
|
-
contextWindow: eventPreparationUsage(event, ctx).contextWindow,
|
|
1007
|
-
tokensBefore: event.preparation.tokensBefore,
|
|
1008
|
-
fallbackFirstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
1009
|
-
maxFirstKeptEntryIndex: splitTurnBoundaryIndex(event),
|
|
1010
|
-
})
|
|
1011
|
-
: null;
|
|
1012
|
-
const firstKeptEntryId = iPlan?.firstKeptEntryId ?? legacyCut.id;
|
|
1013
|
-
const qaSection = legacyCut.hasMarker ? buildPlanningQASection(legacyCut.qaWindowEntries) : null;
|
|
1014
|
-
const boundaryIndex = event.branchEntries.findIndex((entry) => entry.id === firstKeptEntryId);
|
|
1015
|
-
const summaryEntries = boundaryIndex >= 0 ? branchEntries.slice(0, boundaryIndex) : branchEntries;
|
|
1016
|
-
const readRecords = iPlan?.readRecords ?? extractReadRecords(summaryEntries as unknown as CompactionEntryLike[]);
|
|
1017
|
-
const parts: string[] = [];
|
|
1018
|
-
if (event.customInstructions?.trim()) {
|
|
1019
|
-
parts.push(`## Compact Instructions
|
|
1020
|
-
${compactText(event.customInstructions, 1000)}`);
|
|
1021
|
-
}
|
|
1022
|
-
if (qaSection) {
|
|
1023
|
-
parts.push(qaSection);
|
|
1024
|
-
}
|
|
1025
|
-
const previousSummary = event.preparation.previousSummary?.trim();
|
|
1026
|
-
parts.push(`## Goal
|
|
1027
|
-
${compactText(planningCtx.runId, 80)} — keep current planning progress.`);
|
|
1028
|
-
parts.push(`## Constraints & Preferences
|
|
1029
|
-
- Stay in the active planning run (\`${planningCtx.runId}\`).
|
|
1030
|
-
- Plan files live under \`${planningCtx.artifactDir}\`.`);
|
|
1031
|
-
parts.push(`## Progress
|
|
1032
|
-
### Done
|
|
1033
|
-
- Pre-plan history compressed below.
|
|
1034
|
-
|
|
1035
|
-
### In Progress
|
|
1036
|
-
- Current planning question or open decision.
|
|
1037
|
-
|
|
1038
|
-
### Blocked
|
|
1039
|
-
- ${legacyCut.hasMarker ? "None" : "Planning cut-point marker missing; falling back to default."}`);
|
|
1040
|
-
if (iPlan) {
|
|
1041
|
-
const iSections = iPlan.slices
|
|
1042
|
-
.map((slice) => {
|
|
1043
|
-
const lines = slice.entries.filter((entry) => iPlan.summaryEntries.includes(entry)).map((entry) => summarizePlanningEntryLine(entry)).filter(Boolean);
|
|
1044
|
-
return slice.id && lines.length ? `### ${slice.current ? "Current I" : "Implementation I"} \`${slice.id}\`\n${lines.join("\\n")}` : "";
|
|
1045
|
-
})
|
|
1046
|
-
.filter(Boolean);
|
|
1047
|
-
parts.push(`## Current I\n- \`${currentI}\`\n${iSections.join("\\n\\n") || "- Current I transcript is in the retained suffix."}`);
|
|
1048
|
-
parts.push(`## Read Records\n${readRecords.length ? readRecords.map((record) => formatReadRecord(record)).join("\\n") : "- (none)"}`);
|
|
1049
|
-
parts.push(`## Compaction Boundary\n- firstKeptEntryId: \`${firstKeptEntryId}\`\n- currentI: \`${currentI}\`\n- targetMet: ${iPlan.metrics.targetMet}\n- hardFloorReason: ${iPlan.metrics.hardFloorReason ?? "none"}`);
|
|
1050
|
-
}
|
|
1051
|
-
if (previousSummary) {
|
|
1052
|
-
parts.push(`## Previous Compact Summary\n${previousSummary}`);
|
|
1053
|
-
}
|
|
1054
|
-
parts.push(`## Next Steps
|
|
1055
|
-
- Resume the active planning turn from the raw tail.`);
|
|
1056
|
-
const priorDetails = previousCompactionDetails(branchEntries as unknown as CompactionEntryLike[]);
|
|
1057
|
-
const details = mergeCompactionDetails(priorDetails, {
|
|
1058
|
-
kind: "pi-plans-planning-compaction",
|
|
1059
|
-
version: 1,
|
|
1060
|
-
currentI,
|
|
1061
|
-
iSections: iPlan?.slices.map((slice) => ({ id: slice.id, entryIds: slice.entries.map((entry) => entry.id).filter((id): id is string => !!id) })),
|
|
1062
|
-
readRecords,
|
|
1063
|
-
metrics: iPlan?.metrics,
|
|
677
|
+
function buildPlanningVccResult(event: SessionBeforeCompactEvent, ctx: ExtensionContext): VccCompactionBuildResult | null {
|
|
678
|
+
if (getExecution()) return null;
|
|
679
|
+
const active = activeVccSettings(ctx, "planning");
|
|
680
|
+
if (!active) return null;
|
|
681
|
+
const branchEntries = event.branchEntries as unknown as CompactionEntryLike[];
|
|
682
|
+
return buildPiPlansVccCompaction({
|
|
683
|
+
branchEntries,
|
|
684
|
+
preparation: event.preparation,
|
|
685
|
+
customInstructions: event.customInstructions,
|
|
1064
686
|
reason: event.reason,
|
|
1065
|
-
|
|
687
|
+
willRetry: event.willRetry,
|
|
688
|
+
settings: active.settings,
|
|
689
|
+
phaseContext: planningVccContext(branchEntries, active),
|
|
1066
690
|
});
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
details,
|
|
1073
|
-
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export function buildPlanningCompactionResult(event: SessionBeforeCompactEvent, ctx: ExtensionContext): CompactionResult | null {
|
|
694
|
+
const built = buildPlanningVccResult(event, ctx);
|
|
695
|
+
return built?.kind === "compaction" ? built.compaction : null;
|
|
1074
696
|
}
|
|
1075
697
|
|
|
1076
698
|
export function handlePlanningBeforeCompact(
|
|
@@ -1079,82 +701,103 @@ export function handlePlanningBeforeCompact(
|
|
|
1079
701
|
event: SessionBeforeCompactEvent,
|
|
1080
702
|
): SessionBeforeCompactResult | undefined {
|
|
1081
703
|
if (getExecution()) return undefined;
|
|
1082
|
-
|
|
1083
|
-
const session = ctx.sessionManager as unknown as { __planningCompaction?: PlanningCompactionState };
|
|
1084
|
-
const state = (session.__planningCompaction ??= {
|
|
1085
|
-
inFlight: false,
|
|
1086
|
-
resumeGuard: false,
|
|
1087
|
-
cooldownActive: false,
|
|
1088
|
-
lastAttemptReason: null,
|
|
1089
|
-
lastSuccessfulUsagePercent: null,
|
|
1090
|
-
lastSuccessfulAt: null,
|
|
1091
|
-
} satisfies PlanningCompactionState);
|
|
1092
|
-
const percent = ctx.getContextUsage()?.percent ?? null;
|
|
1093
|
-
if (event.reason === "threshold" && (percent === null || percent < 100)) {
|
|
1094
|
-
state.inFlight = false;
|
|
1095
|
-
state.lastAttemptReason = event.reason;
|
|
1096
|
-
return { cancel: true };
|
|
1097
|
-
}
|
|
704
|
+
const state = ensurePlanningCompactionState(ctx);
|
|
1098
705
|
state.inFlight = true;
|
|
1099
706
|
state.lastAttemptReason = event.reason;
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
let
|
|
707
|
+
state.pendingStats = null;
|
|
708
|
+
state.pendingFollowUpPrompt = null;
|
|
709
|
+
state.pendingContinueAfterThresholdCompact = false;
|
|
710
|
+
let built: VccCompactionBuildResult | null;
|
|
1104
711
|
try {
|
|
1105
|
-
|
|
712
|
+
built = buildPlanningVccResult(event, ctx);
|
|
1106
713
|
} catch (error) {
|
|
1107
714
|
state.inFlight = false;
|
|
1108
|
-
ctx.ui.notify(`pi-plans: planning compaction preparation failed; using Pi default compaction (${String(error)}).`, "warning");
|
|
715
|
+
ctx.ui.notify(`pi-plans: VCC planning compaction preparation failed; using Pi default compaction (${String(error)}).`, "warning");
|
|
1109
716
|
return undefined;
|
|
1110
717
|
}
|
|
1111
|
-
if (!
|
|
718
|
+
if (!built || built.kind === "fallback") {
|
|
1112
719
|
state.inFlight = false;
|
|
1113
720
|
return undefined;
|
|
1114
721
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
722
|
+
if (built.kind === "cancel") {
|
|
723
|
+
state.inFlight = false;
|
|
724
|
+
ctx.ui.notify(built.message, "warning");
|
|
725
|
+
return { cancel: true };
|
|
726
|
+
}
|
|
727
|
+
state.pendingStats = built.stats;
|
|
728
|
+
state.pendingFollowUpPrompt = built.followUpPrompt;
|
|
729
|
+
state.pendingContinueAfterThresholdCompact = built.settings.continueAfterThresholdCompact;
|
|
730
|
+
return { compaction: built.compaction };
|
|
1117
731
|
}
|
|
1118
732
|
|
|
1119
|
-
export function handlePlanningCompact(pi: ExtensionAPI, ctx: ExtensionContext, event: SessionCompactEvent): void {
|
|
733
|
+
export async function handlePlanningCompact(pi: ExtensionAPI, ctx: ExtensionContext, event: SessionCompactEvent): Promise<void> {
|
|
1120
734
|
if (getExecution()) return;
|
|
1121
735
|
const session = ctx.sessionManager as unknown as { __planningCompaction?: PlanningCompactionState };
|
|
1122
736
|
const state = session.__planningCompaction;
|
|
1123
737
|
if (!state) return;
|
|
738
|
+
const stats = state.pendingStats;
|
|
739
|
+
const followUpPrompt = state.pendingFollowUpPrompt;
|
|
740
|
+
const continueAfterThresholdCompact = state.pendingContinueAfterThresholdCompact;
|
|
741
|
+
state.pendingStats = null;
|
|
742
|
+
state.pendingFollowUpPrompt = null;
|
|
743
|
+
state.pendingContinueAfterThresholdCompact = false;
|
|
1124
744
|
state.inFlight = false;
|
|
1125
745
|
state.lastAttemptReason = event.reason;
|
|
746
|
+
state.terminalBackoffTokens = null;
|
|
1126
747
|
state.cooldownActive = true;
|
|
1127
748
|
state.lastSuccessfulAt = utcNow();
|
|
1128
749
|
state.lastSuccessfulUsagePercent = ctx.getContextUsage()?.percent ?? state.lastSuccessfulUsagePercent;
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
750
|
+
state.resumeGuard = false;
|
|
751
|
+
if (!event.willRetry && stats) {
|
|
752
|
+
ctx.ui.notify(formatVccCompactionStats(stats), "info");
|
|
753
|
+
if (followUpPrompt) {
|
|
754
|
+
await pi.sendUserMessage?.(followUpPrompt);
|
|
755
|
+
} else if ((event.reason === "threshold" || event.reason === "overflow") && shouldScheduleAutoContinue(continueAfterThresholdCompact, runtimePiVersion(ctx))) {
|
|
756
|
+
state.resumeGuard = true;
|
|
757
|
+
pi.sendMessage(
|
|
758
|
+
{
|
|
759
|
+
customType: PLANNING_RESUME_CUSTOM_TYPE,
|
|
760
|
+
content: "Continue planning.",
|
|
761
|
+
display: false,
|
|
762
|
+
},
|
|
763
|
+
{ triggerTurn: true },
|
|
764
|
+
);
|
|
765
|
+
}
|
|
1141
766
|
}
|
|
1142
767
|
}
|
|
1143
768
|
|
|
769
|
+
|
|
1144
770
|
export function handlePlanningCompactFailed(pi: ExtensionAPI, ctx: ExtensionContext, event: SessionCompactFailedEvent): void {
|
|
1145
771
|
if (getExecution()) return;
|
|
1146
772
|
const session = ctx.sessionManager as unknown as { __planningCompaction?: PlanningCompactionState };
|
|
1147
773
|
const state = session.__planningCompaction;
|
|
1148
774
|
if (!state) return;
|
|
1149
|
-
const
|
|
1150
|
-
if (
|
|
775
|
+
const terminal = isTerminalCompactionFailure(event);
|
|
776
|
+
if (terminal) {
|
|
777
|
+
// Pi refused or aborted the compaction. Hold the cooldown and re-arm
|
|
778
|
+
// only after real growth or high-watermark pressure so the loop stops.
|
|
779
|
+
state.inFlight = false;
|
|
780
|
+
state.resumeGuard = false;
|
|
781
|
+
state.cooldownActive = true;
|
|
1151
782
|
state.lastAttemptReason = event.reason;
|
|
783
|
+
const tokens = ctx.getContextUsage()?.tokens;
|
|
784
|
+
state.terminalBackoffTokens = typeof tokens === "number" ? tokens : Number.POSITIVE_INFINITY;
|
|
785
|
+
state.pendingStats = null;
|
|
786
|
+
state.pendingFollowUpPrompt = null;
|
|
787
|
+
state.pendingContinueAfterThresholdCompact = false;
|
|
788
|
+
const message = terminal.kind === "content"
|
|
789
|
+
? "pi-plans: compaction found nothing to summarize; backing off until the session grows past the keep-recent window."
|
|
790
|
+
: "pi-plans: compaction was aborted (provider interruption, user cancel, or a competing manual compact); backing off until the session grows or usage nears the window.";
|
|
791
|
+
ctx.ui.notify(message, "info");
|
|
1152
792
|
return;
|
|
1153
793
|
}
|
|
1154
794
|
state.inFlight = false;
|
|
1155
795
|
state.resumeGuard = false;
|
|
1156
796
|
state.cooldownActive = false;
|
|
1157
797
|
state.lastAttemptReason = event.reason;
|
|
798
|
+
state.pendingStats = null;
|
|
799
|
+
state.pendingFollowUpPrompt = null;
|
|
800
|
+
state.pendingContinueAfterThresholdCompact = false;
|
|
1158
801
|
ctx.ui.notify(
|
|
1159
802
|
`pi-plans: planning compaction failed (${event.reason}); will try again on the next eligible turn.`,
|
|
1160
803
|
"warning",
|
|
@@ -1249,14 +892,26 @@ export async function completeExecution(pi: ExtensionAPI, ctx: ExtensionContext)
|
|
|
1249
892
|
const planPath = execution.planPath;
|
|
1250
893
|
execution = null;
|
|
1251
894
|
pi.appendEntry("pi-plans-exec-cleared", { reason: "complete" });
|
|
895
|
+
// Post-execution goal-running continuation: in interactive sessions, attach
|
|
896
|
+
// the continuation block and trigger a new turn so the agent immediately
|
|
897
|
+
// enters the implementation-review loop. Headless sessions keep the silent
|
|
898
|
+
// completion behavior. Both completeExecution call sites (turn_end and the
|
|
899
|
+
// restoreFromSession recovery path) share this behavior.
|
|
900
|
+
const interactive = ctx.hasUI === true;
|
|
901
|
+
const content = interactive
|
|
902
|
+
? `**Plan complete!** ✅ \`${planPath}\`\n\n${summary}\n\n${AMELIORATION_PROMPT_TEXT}`
|
|
903
|
+
: `**Plan complete!** ✅ \`${planPath}\`\n\n${summary}`;
|
|
1252
904
|
pi.sendMessage(
|
|
1253
905
|
{
|
|
1254
906
|
customType: "pi-plans-complete",
|
|
1255
|
-
content
|
|
907
|
+
content,
|
|
1256
908
|
display: true,
|
|
1257
909
|
},
|
|
1258
|
-
{ triggerTurn:
|
|
910
|
+
{ triggerTurn: interactive },
|
|
1259
911
|
);
|
|
912
|
+
if (interactive) {
|
|
913
|
+
pi.appendEntry("pi-plans-ameliorate", { planPath, phase: "goal-started", rounds: null, currentRound: 0 });
|
|
914
|
+
}
|
|
1260
915
|
const active = readActive(ctx.cwd);
|
|
1261
916
|
if (active) {
|
|
1262
917
|
try {
|
|
@@ -1268,12 +923,25 @@ export async function completeExecution(pi: ExtensionAPI, ctx: ExtensionContext)
|
|
|
1268
923
|
updateStatusWidget(ctx);
|
|
1269
924
|
}
|
|
1270
925
|
|
|
926
|
+
/** Instructions appended to the post-execution completion message in
|
|
927
|
+
* interactive sessions, telling the agent to enter the goal-running
|
|
928
|
+
* implementation-review loop. */
|
|
929
|
+
export const AMELIORATION_PROMPT_TEXT = `---
|
|
930
|
+
Goal-running continuation: immediately ask the user now via ask_choice (autoComplete: false, in the session language) for the termination condition of the implementation-review loop: until no high-severity finding (hard cap 5 rounds, recommended) / 1 round / 2 rounds / 3 rounds. Then keep running the loop without asking whether to continue: each round calls refine (role: "reviewer", target: "implementation"), accepts findings on evidence, applies fixes, re-runs relevant tests, and repeats until the chosen termination condition or the 5-round cap.`;
|
|
931
|
+
|
|
1271
932
|
/** Injection text for before_agent_start while executing. */
|
|
1272
|
-
export function executionContextMessage(): string | null {
|
|
933
|
+
export function executionContextMessage(ctx: ExtensionContext): string | null {
|
|
1273
934
|
if (!execution) return null;
|
|
1274
935
|
const remaining = execution.items.filter((item) => !item.done);
|
|
1275
936
|
const list =
|
|
1276
937
|
remaining.map((item) => `- \`${item.id}\` ${item.text}`).join("\n") || "(none — report completion now)";
|
|
938
|
+
// Live read: the injected guidance and the tool wrappers share the same
|
|
939
|
+
// tri-state, so they can never contradict each other mid-run.
|
|
940
|
+
const mode = resolveGraphMode(ctx?.cwd ?? process.cwd());
|
|
941
|
+
const graphLine =
|
|
942
|
+
mode === "config-unavailable"
|
|
943
|
+
? `${graphBlockForExecutor(false)}\n[pi-plans: config unreadable this turn; graph features are off until .git/pi_plans/config.json is repaired]`
|
|
944
|
+
: graphBlockForExecutor(mode === "enabled");
|
|
1277
945
|
const implementationItems = execution.implItems?.length
|
|
1278
946
|
? `\nImplementation items: ${execution.implItems.map((item) => item.id).join(", ")}${execution.currentI ? `\nCurrent implementation item: \`${execution.currentI}\`` : ""}\nWhen beginning an implementation item, emit its current anchor exactly once as \`[I-###:current]\`; then use \`[I-###:implemented]\` or \`[I-###:validating]\` for progress.`
|
|
1279
947
|
: "";
|
|
@@ -1283,6 +951,8 @@ Implement the accepted plan at ${execution.planPath} (${execution.items.length -
|
|
|
1283
951
|
Remaining verifier items:
|
|
1284
952
|
${list}${implementationItems}
|
|
1285
953
|
|
|
954
|
+
${graphLine}
|
|
955
|
+
|
|
1286
956
|
Execution rules:
|
|
1287
957
|
- Implement implementation items in dependency order; grow the change in layers — smallest end-to-end slice first, then stack each new capability on top of what already works.
|
|
1288
958
|
- Report implementation-item progress with lightweight markers in your reply: write \`[I-001:implemented]\` when an item's code is done, \`[I-001:validating]\` when you start verifying it. The execution status bar tracks these states.
|