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/src/exec.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* When the user approves the execution handoff, the extension switches into
|
|
5
5
|
* execution mode: every agent turn is injected with the remaining verifier
|
|
6
|
-
* checklist, assistant messages are scanned for [DONE:VC-xxx] markers, and
|
|
7
|
-
*
|
|
6
|
+
* checklist, assistant messages are scanned for [DONE:VC-xxx] markers, and
|
|
7
|
+
* progress is reported through the bottom status bar until every item passes.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
@@ -17,56 +17,53 @@ import type {
|
|
|
17
17
|
SessionCompactEvent,
|
|
18
18
|
SessionCompactFailedEvent,
|
|
19
19
|
} from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { VERSION } from "@earendil-works/pi-coding-agent";
|
|
20
21
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
type
|
|
32
|
-
type
|
|
33
|
-
type
|
|
34
|
-
} from "./
|
|
35
|
-
import { getRun, readActive, setRunStatus, utcNow
|
|
22
|
+
buildPiPlansVccCompaction,
|
|
23
|
+
compactionCurrentI,
|
|
24
|
+
entryCurrentIMarkers,
|
|
25
|
+
formatVccCompactionStats,
|
|
26
|
+
loadVccSettings,
|
|
27
|
+
scaffoldVccSettings,
|
|
28
|
+
shouldScheduleAutoContinue,
|
|
29
|
+
type CompactionEntryLike,
|
|
30
|
+
type PiPlansCompactionPhase,
|
|
31
|
+
type PiPlansVccPhaseContext,
|
|
32
|
+
type PiPlansVccSettings,
|
|
33
|
+
type VccCompactionBuildResult,
|
|
34
|
+
type VccCompactionStats,
|
|
35
|
+
} from "./compaction.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";
|
|
36
39
|
import {
|
|
40
|
+
extractCoverage,
|
|
37
41
|
latestPlanVersion,
|
|
42
|
+
resolveImplStatuses,
|
|
38
43
|
scanDoneMarkers,
|
|
39
44
|
scanImplMarkers,
|
|
45
|
+
scanCurrentIMarkers,
|
|
46
|
+
resolveCurrentI,
|
|
47
|
+
inferCurrentI,
|
|
40
48
|
type CheckItem,
|
|
41
49
|
type ImplItem,
|
|
42
50
|
type ImplMarkerState,
|
|
43
51
|
} from "./plan.ts";
|
|
44
52
|
|
|
45
|
-
export interface ExecState
|
|
53
|
+
export interface ExecState {
|
|
54
|
+
planPath: string;
|
|
55
|
+
items: CheckItem[];
|
|
46
56
|
startedAt: string;
|
|
47
|
-
panel?: ExecutionPanelState;
|
|
48
57
|
usage: { inToks: number; outToks: number };
|
|
49
|
-
modelState?: ExecutionModelState;
|
|
50
58
|
implItems?: ImplItem[];
|
|
51
59
|
implStatus?: Record<string, ImplMarkerState>;
|
|
60
|
+
currentI?: string;
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
let execution: ExecState | null = null;
|
|
55
64
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
let pendingPanelSync = false;
|
|
59
|
-
|
|
60
|
-
export function consumePendingPanelSync(): boolean {
|
|
61
|
-
const pending = pendingPanelSync;
|
|
62
|
-
pendingPanelSync = false;
|
|
63
|
-
return pending;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Execution-loop UI churn deferral: mid-agent appendEntry forces a TUI
|
|
67
|
-
// relayout (see toggleExecutionPanelView), so while the agent runs we only
|
|
68
|
-
// arm a pending flag and flush once at a settle boundary (agent_settled,
|
|
69
|
-
// next before_agent_start, stop/complete).
|
|
65
|
+
// Execution-loop persistence is deferred until the agent settles so turn_end
|
|
66
|
+
// never causes session writes during a streaming run.
|
|
70
67
|
let pendingExecutionFlush = false;
|
|
71
68
|
|
|
72
69
|
export function consumePendingExecutionFlush(): boolean {
|
|
@@ -75,25 +72,17 @@ export function consumePendingExecutionFlush(): boolean {
|
|
|
75
72
|
return pending;
|
|
76
73
|
}
|
|
77
74
|
|
|
78
|
-
function requestExecutionFlush(
|
|
75
|
+
function requestExecutionFlush(_pi: ExtensionAPI, _ctx: ExtensionContext): void {
|
|
79
76
|
// Unconditional defer. turn_end fires mid-run in a gap between agent
|
|
80
|
-
// operations where isIdle() reads true
|
|
81
|
-
//
|
|
82
|
-
// happens only at the drain points: agent_settled, the next
|
|
83
|
-
// before_agent_start, and stop/complete.
|
|
77
|
+
// operations where isIdle() reads true; persistence happens only at the
|
|
78
|
+
// drain points: agent_settled, the next before_agent_start, and stop/complete.
|
|
84
79
|
pendingExecutionFlush = true;
|
|
85
|
-
// Keep both execution views current without persisting or re-registering the
|
|
86
|
-
// panel widget. Collapsed refresh is idempotent; expanded refresh invalidates
|
|
87
|
-
// the live widget's render cache.
|
|
88
|
-
syncExecutionPanel(ctx);
|
|
89
80
|
}
|
|
90
81
|
|
|
91
82
|
export function drainExecutionFlush(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
92
|
-
if (!execution) return;
|
|
93
|
-
if (!pendingExecutionFlush && !consumePendingPanelSync()) return;
|
|
83
|
+
if (!execution || !pendingExecutionFlush) return;
|
|
94
84
|
pendingExecutionFlush = false;
|
|
95
85
|
persist(pi);
|
|
96
|
-
syncExecutionPanel(ctx);
|
|
97
86
|
updateStatusWidget(ctx);
|
|
98
87
|
}
|
|
99
88
|
|
|
@@ -101,187 +90,119 @@ export function getExecution(): ExecState | null {
|
|
|
101
90
|
return execution;
|
|
102
91
|
}
|
|
103
92
|
|
|
104
|
-
const
|
|
105
|
-
const CURRENT_SESSION_MODEL_CHOICE = "Inherit current session model";
|
|
93
|
+
const EXECUTION_COMPACTION_RESUME_MESSAGE = "Continue execution.";
|
|
106
94
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
95
|
+
interface ExecutionCompactionState {
|
|
96
|
+
inFlight: boolean;
|
|
97
|
+
resumeGuard: boolean;
|
|
98
|
+
cooldownActive: boolean;
|
|
99
|
+
lastAttemptReason: string | null;
|
|
100
|
+
lastSuccessfulUsagePercent: number | null;
|
|
101
|
+
lastSuccessfulAt: string | null;
|
|
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;
|
|
110
108
|
}
|
|
111
109
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
type ExecutionCompactionSession = { __executionCompaction?: ExecutionCompactionState };
|
|
111
|
+
type ExecutionCompactionContext = ExtensionContext & { sessionManager?: ExecutionCompactionSession };
|
|
112
|
+
|
|
113
|
+
function getExecutionCompactionSession(ctx: ExtensionContext, create = false): ExecutionCompactionSession | undefined {
|
|
114
|
+
const carrier = ctx as ExecutionCompactionContext;
|
|
115
|
+
if (carrier.sessionManager) return carrier.sessionManager;
|
|
116
|
+
if (!create) return undefined;
|
|
117
|
+
carrier.sessionManager = {};
|
|
118
|
+
return carrier.sessionManager;
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
function
|
|
121
|
-
return
|
|
121
|
+
function executionCompactionState(ctx: ExtensionContext): ExecutionCompactionState | undefined {
|
|
122
|
+
return getExecutionCompactionSession(ctx)?.__executionCompaction;
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
125
|
+
function ensureExecutionCompactionState(ctx: ExtensionContext): ExecutionCompactionState {
|
|
126
|
+
const session = getExecutionCompactionSession(ctx, true)!;
|
|
127
|
+
return (session.__executionCompaction ??= {
|
|
128
|
+
inFlight: false,
|
|
129
|
+
resumeGuard: false,
|
|
130
|
+
cooldownActive: false,
|
|
131
|
+
lastAttemptReason: null,
|
|
132
|
+
lastSuccessfulUsagePercent: null,
|
|
133
|
+
lastSuccessfulAt: null,
|
|
134
|
+
rearmPending: false,
|
|
135
|
+
terminalBackoffTokens: null,
|
|
136
|
+
pendingStats: null,
|
|
137
|
+
pendingFollowUpPrompt: null,
|
|
138
|
+
pendingContinueAfterThresholdCompact: false,
|
|
139
|
+
});
|
|
133
140
|
}
|
|
134
141
|
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
function resetExecutionCompactionState(ctx: ExtensionContext): void {
|
|
143
|
+
const session = getExecutionCompactionSession(ctx);
|
|
144
|
+
if (!session) return;
|
|
145
|
+
delete session.__executionCompaction;
|
|
137
146
|
}
|
|
138
147
|
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const exact = ctx.modelRegistry.find(provider, rawModelId);
|
|
145
|
-
if (exact) {
|
|
146
|
-
return { provider, modelId: rawModelId, thinkingLevel: null, selector, model: exact };
|
|
147
|
-
}
|
|
148
|
-
const colon = rawModelId.lastIndexOf(":");
|
|
149
|
-
if (colon > 0) {
|
|
150
|
-
const modelId = rawModelId.slice(0, colon);
|
|
151
|
-
const thinkingLevel = rawModelId.slice(colon + 1);
|
|
152
|
-
if (THINKING_LEVELS.has(thinkingLevel)) {
|
|
153
|
-
const model = ctx.modelRegistry.find(provider, modelId);
|
|
154
|
-
if (model) {
|
|
155
|
-
return { provider, modelId, thinkingLevel, selector, model };
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return null;
|
|
148
|
+
function consumeExecutionCompactionResumeGuard(ctx: ExtensionContext): boolean {
|
|
149
|
+
const state = executionCompactionState(ctx);
|
|
150
|
+
if (!state?.resumeGuard) return false;
|
|
151
|
+
state.resumeGuard = false;
|
|
152
|
+
return true;
|
|
160
153
|
}
|
|
161
154
|
|
|
162
|
-
function
|
|
163
|
-
|
|
164
|
-
? ctx.scopedModels.map((entry) => ({
|
|
165
|
-
provider: entry.model.provider,
|
|
166
|
-
modelId: entry.model.id,
|
|
167
|
-
thinkingLevel: entry.thinkingLevel ?? null,
|
|
168
|
-
selector: buildModelSelector(entry.model.provider, entry.model.id, entry.thinkingLevel ?? null),
|
|
169
|
-
model: entry.model,
|
|
170
|
-
}))
|
|
171
|
-
: ctx.modelRegistry.getAvailable().map((model) => ({
|
|
172
|
-
provider: model.provider,
|
|
173
|
-
modelId: model.id,
|
|
174
|
-
thinkingLevel: null,
|
|
175
|
-
selector: buildModelSelector(model.provider, model.id),
|
|
176
|
-
model,
|
|
177
|
-
}));
|
|
178
|
-
const seen = new Set<string>();
|
|
179
|
-
const unique: ModelSelection[] = [];
|
|
180
|
-
for (const selection of rawSelections) {
|
|
181
|
-
if (seen.has(selection.selector)) continue;
|
|
182
|
-
seen.add(selection.selector);
|
|
183
|
-
unique.push(selection);
|
|
184
|
-
}
|
|
185
|
-
return unique;
|
|
155
|
+
export function shouldTriggerExecutionCompaction(_ctx: ExtensionContext): boolean {
|
|
156
|
+
return false;
|
|
186
157
|
}
|
|
187
158
|
|
|
188
|
-
export
|
|
189
|
-
|
|
190
|
-
if (selection.thinkingLevel !== null && ctx.thinkingLevel !== selection.thinkingLevel) {
|
|
191
|
-
pi.setThinkingLevel(selection.thinkingLevel as never);
|
|
192
|
-
}
|
|
193
|
-
return true;
|
|
194
|
-
}
|
|
195
|
-
const success = await pi.setModel(selection.model);
|
|
196
|
-
if (!success) return false;
|
|
197
|
-
if (selection.thinkingLevel !== null) {
|
|
198
|
-
pi.setThinkingLevel(selection.thinkingLevel as never);
|
|
199
|
-
}
|
|
200
|
-
return true;
|
|
159
|
+
export function handleExecutionTurnCompaction(ctx: ExtensionContext): void {
|
|
160
|
+
consumeExecutionCompactionResumeGuard(ctx);
|
|
201
161
|
}
|
|
202
162
|
|
|
203
|
-
export
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
)
|
|
209
|
-
const current = currentModelSelection(ctx);
|
|
210
|
-
const available = listAvailableModelSelections(ctx);
|
|
211
|
-
const configuredSelector = configured?.model_selector ?? null;
|
|
212
|
-
const configuredSource = configured?.source ?? "unset";
|
|
213
|
-
|
|
214
|
-
if (configuredSource === "unset") {
|
|
215
|
-
const choices = current
|
|
216
|
-
? [
|
|
217
|
-
CURRENT_SESSION_MODEL_CHOICE,
|
|
218
|
-
...available
|
|
219
|
-
.filter((selection) => selection.selector !== current.selector)
|
|
220
|
-
.map((selection) => selection.selector),
|
|
221
|
-
]
|
|
222
|
-
: available.map((selection) => selection.selector);
|
|
223
|
-
if (!ctx.hasUI || choices.length === 0) {
|
|
224
|
-
ctx.ui.notify(`${prompt}: execution model is not set; continuing with the current session model.`, "warning");
|
|
225
|
-
return current;
|
|
226
|
-
}
|
|
227
|
-
const choice = await ctx.ui.select(
|
|
228
|
-
`${prompt} — execution model is not set. ${current ? "Inherit the current session model or choose another model:" : "Choose a model:"}`,
|
|
229
|
-
choices,
|
|
163
|
+
export function computeExecutionProgress(execution: ExecState): { done: number; total: number } {
|
|
164
|
+
const implItems = execution.implItems ?? [];
|
|
165
|
+
if (implItems.length) {
|
|
166
|
+
const statuses = resolveImplStatuses(implItems, execution.items, execution.implStatus);
|
|
167
|
+
const counted = implItems.filter((impl) =>
|
|
168
|
+
execution.items.some((item) => extractCoverage(item.text).includes(impl.id)),
|
|
230
169
|
);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
return resolveModelSelection(ctx, choice) ?? current;
|
|
170
|
+
const total = counted.length > 0 ? counted.length : implItems.length;
|
|
171
|
+
const vcDone = counted.filter((impl) => statuses[impl.id] === "vc-passed").length;
|
|
172
|
+
const currentIndex = execution.currentI
|
|
173
|
+
? implItems.findIndex((impl) => impl.id === execution.currentI)
|
|
174
|
+
: -1;
|
|
175
|
+
return {
|
|
176
|
+
done: Math.min(total, Math.max(vcDone, currentIndex < 0 ? 0 : currentIndex)),
|
|
177
|
+
total,
|
|
178
|
+
};
|
|
242
179
|
}
|
|
180
|
+
return {
|
|
181
|
+
done: execution.items.filter((item) => item.done).length,
|
|
182
|
+
total: execution.items.length,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
243
185
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
186
|
+
function formatElapsed(startedAt: string): string {
|
|
187
|
+
const total = Math.max(0, Math.floor((Date.now() - Date.parse(startedAt)) / 1000));
|
|
188
|
+
const h = String(Math.floor(total / 3600)).padStart(2, "0");
|
|
189
|
+
const m = String(Math.floor((total % 3600) / 60)).padStart(2, "0");
|
|
190
|
+
const sec = String(total % 60).padStart(2, "0");
|
|
191
|
+
return `${h}:${m}:${sec}`;
|
|
192
|
+
}
|
|
250
193
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
const choice = await ctx.ui.select(
|
|
256
|
-
`${prompt} — "${configuredSelector}" is unavailable. Pick a model:`,
|
|
257
|
-
[
|
|
258
|
-
CURRENT_SESSION_MODEL_CHOICE,
|
|
259
|
-
...available
|
|
260
|
-
.filter((selection) => selection.selector !== current?.selector)
|
|
261
|
-
.map((selection) => selection.selector),
|
|
262
|
-
],
|
|
263
|
-
);
|
|
264
|
-
if (!choice || choice === CURRENT_SESSION_MODEL_CHOICE) {
|
|
265
|
-
return current;
|
|
266
|
-
}
|
|
267
|
-
if (onChosenSelector) {
|
|
268
|
-
await onChosenSelector(choice);
|
|
269
|
-
}
|
|
270
|
-
return resolveModelSelection(ctx, choice) ?? current;
|
|
194
|
+
function formatToks(tokens: number): string {
|
|
195
|
+
const n = Math.max(0, Math.round(tokens));
|
|
196
|
+
return n < 1000 ? String(n) : `${(n / 1000).toFixed(1)}k`;
|
|
271
197
|
}
|
|
272
198
|
|
|
273
|
-
export function
|
|
274
|
-
|
|
275
|
-
return
|
|
199
|
+
export function formatExecutionStatusLine(execution: ExecState): string {
|
|
200
|
+
const progress = computeExecutionProgress(execution);
|
|
201
|
+
return `⌛ plans ${progress.done}/${progress.total}: spent ${formatElapsed(execution.startedAt)} · ${formatToks(execution.usage.inToks)} in-toks · ${formatToks(execution.usage.outToks)} out-toks`;
|
|
276
202
|
}
|
|
277
203
|
|
|
278
204
|
export function updateStatusWidget(ctx: ExtensionContext): void {
|
|
279
205
|
if (execution) {
|
|
280
|
-
if (execution.panel?.expanded) {
|
|
281
|
-
// The expanded panel renders the ⌛ header itself; the footer copy is
|
|
282
|
-
// cleared by refreshExecutionPanel. Write nothing here.
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
206
|
const line = formatExecutionStatusLine(execution);
|
|
286
207
|
ctx.ui.setStatus("pi-plans", ctx.ui.theme.fg("accent", line));
|
|
287
208
|
return;
|
|
@@ -326,35 +247,23 @@ function persist(pi: ExtensionAPI): void {
|
|
|
326
247
|
items: execution.items,
|
|
327
248
|
startedAt: execution.startedAt,
|
|
328
249
|
usage: execution.usage,
|
|
329
|
-
panel: snapshotPanelState(execution),
|
|
330
250
|
implItems: execution.implItems,
|
|
251
|
+
implStatus: execution.implStatus,
|
|
252
|
+
currentI: execution.currentI,
|
|
331
253
|
});
|
|
332
254
|
}
|
|
333
255
|
|
|
334
|
-
export function syncExecutionPanel(ctx: ExtensionContext): void {
|
|
335
|
-
if (!execution) {
|
|
336
|
-
clearExecutionPanel(ctx);
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
refreshExecutionPanel(ctx, execution);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
256
|
export async function startExecution(
|
|
343
257
|
pi: ExtensionAPI,
|
|
344
258
|
ctx: ExtensionContext,
|
|
345
259
|
planPath: string,
|
|
346
260
|
items: CheckItem[],
|
|
347
|
-
modelState?: ExecutionModelState,
|
|
348
261
|
implItems?: ImplItem[],
|
|
349
262
|
): Promise<void> {
|
|
350
|
-
execution = { planPath, items, startedAt: utcNow(),
|
|
351
|
-
attachPanelBaseline(execution, ctx.cwd);
|
|
352
|
-
consumePendingPanelSync(); // fresh run: drop any stale deferral from a previous one
|
|
263
|
+
execution = { planPath, items, startedAt: utcNow(), usage: { inToks: 0, outToks: 0 }, implItems: implItems ?? [], implStatus: {} };
|
|
353
264
|
pendingExecutionFlush = false; // fresh run: no inherited flush debt
|
|
265
|
+
resetExecutionCompactionState(ctx);
|
|
354
266
|
persist(pi);
|
|
355
|
-
if (modelState) {
|
|
356
|
-
await ensureExecutionModelActive(pi, ctx);
|
|
357
|
-
}
|
|
358
267
|
const active = readActive(ctx.cwd);
|
|
359
268
|
if (active) {
|
|
360
269
|
try {
|
|
@@ -372,54 +281,22 @@ export async function startExecution(
|
|
|
372
281
|
{ triggerTurn: false },
|
|
373
282
|
);
|
|
374
283
|
updateStatusWidget(ctx);
|
|
375
|
-
syncExecutionPanel(ctx);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
export function toggleExecutionPanelView(pi: ExtensionAPI, ctx: ExtensionContext): boolean | null {
|
|
379
|
-
if (!execution) return null;
|
|
380
|
-
const expanded = toggleExpanded(execution);
|
|
381
|
-
// While a turn is streaming keep this zero-side-effect: flipping the flag is
|
|
382
|
-
// pure memory; persisting and re-rendering here would write to the session
|
|
383
|
-
// file and force a TUI relayout under the running agent. The next turn_end
|
|
384
|
-
// consumes the pending marker and brings the view in line.
|
|
385
|
-
const idle = typeof ctx.isIdle !== "function" || ctx.isIdle();
|
|
386
|
-
if (idle) {
|
|
387
|
-
persist(pi);
|
|
388
|
-
syncExecutionPanel(ctx);
|
|
389
|
-
} else {
|
|
390
|
-
pendingPanelSync = true;
|
|
391
|
-
}
|
|
392
|
-
return expanded;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
export function recordTouchedPaths(_workdir: string, paths: string[]): void {
|
|
396
|
-
if (!execution || !paths.length) return;
|
|
397
|
-
const panel = execution.panel ?? createExecutionPanelState();
|
|
398
|
-
execution.panel = panel;
|
|
399
|
-
const merged = new Set(panel.touchedPaths);
|
|
400
|
-
for (const raw of paths) {
|
|
401
|
-
const normalized = raw.trim().replace(/[\u0000]+/g, "");
|
|
402
|
-
if (!normalized) continue;
|
|
403
|
-
merged.add(normalized);
|
|
404
|
-
}
|
|
405
|
-
panel.touchedPaths = [...merged];
|
|
406
284
|
}
|
|
407
285
|
|
|
408
286
|
/** Record one assistant turn: accumulate usage and mark any completed items. */
|
|
409
287
|
export function recordExecutionTurn(
|
|
410
288
|
pi: ExtensionAPI,
|
|
411
|
-
|
|
412
|
-
|
|
289
|
+
_ctx: ExtensionContext,
|
|
290
|
+
_completedIds: string[],
|
|
413
291
|
usage?: { input: number; output: number },
|
|
414
|
-
):
|
|
415
|
-
if (!execution) return
|
|
292
|
+
): void {
|
|
293
|
+
if (!execution) return;
|
|
416
294
|
if (usage) {
|
|
417
295
|
execution.usage.inToks += usage.input;
|
|
418
296
|
execution.usage.outToks += usage.output;
|
|
419
297
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
return summary;
|
|
298
|
+
requestExecutionFlush(pi, _ctx);
|
|
299
|
+
updateStatusWidget(_ctx);
|
|
423
300
|
}
|
|
424
301
|
|
|
425
302
|
export function registerExecutionTurnHandlers(
|
|
@@ -448,11 +325,12 @@ export function registerExecutionTurnHandlers(
|
|
|
448
325
|
.join("\n");
|
|
449
326
|
const changedIds = applyDoneMarkers(text);
|
|
450
327
|
const changedImpls = applyImplMarkers(text);
|
|
328
|
+
const changedCurrentI = applyCurrentIMarker(text);
|
|
451
329
|
const projection = (event.message as { usage?: { input?: number; output?: number } }).usage;
|
|
452
330
|
const raw = projection ?? lastAssistantUsage;
|
|
453
331
|
lastAssistantUsage = null; // consumed: never re-attribute a stale turn
|
|
454
332
|
const usage = raw ? { input: raw.input ?? 0, output: raw.output ?? 0 } : undefined;
|
|
455
|
-
if (usage || changedIds.length > 0 || changedImpls.length > 0) {
|
|
333
|
+
if (usage || changedIds.length > 0 || changedImpls.length > 0 || changedCurrentI) {
|
|
456
334
|
// Attribute this turn's usage now; `[DONE]` markers still only mark completion.
|
|
457
335
|
recordExecutionTurn(pi, ctx, changedIds, usage);
|
|
458
336
|
}
|
|
@@ -465,164 +343,182 @@ export function registerExecutionTurnHandlers(
|
|
|
465
343
|
|
|
466
344
|
const EXECUTION_RESUME_CUSTOM_TYPE = "pi-plans-exec-resume";
|
|
467
345
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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 };
|
|
475
357
|
}
|
|
476
358
|
|
|
477
|
-
function
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
.
|
|
483
|
-
|
|
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
|
+
};
|
|
484
367
|
}
|
|
485
368
|
|
|
486
|
-
function
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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;
|
|
378
|
+
}
|
|
379
|
+
if (entry.type === "custom" && entry.customType === PLANNING_PLAN_WRITTEN_CUSTOM_TYPE) {
|
|
380
|
+
planPath = typeof entry.data?.planPath === "string" ? entry.data.planPath : planPath;
|
|
381
|
+
}
|
|
382
|
+
for (const id of entryCurrentIMarkers(entry)) currentI = id;
|
|
383
|
+
currentI = compactionCurrentI(entry) ?? currentI;
|
|
493
384
|
}
|
|
385
|
+
return { phase: "planning", runId, artifactDir, planPath, currentI };
|
|
494
386
|
}
|
|
495
387
|
|
|
496
|
-
function
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (!isSummarizableEntry(entry)) return "";
|
|
510
|
-
const text = messageText(entry.message);
|
|
511
|
-
if (!text) return "";
|
|
512
|
-
return `- [${messageRoleLabel(entry.message?.role)}] ${compactText(text)}`;
|
|
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,
|
|
396
|
+
reason: event.reason,
|
|
397
|
+
willRetry: event.willRetry,
|
|
398
|
+
settings: active.settings,
|
|
399
|
+
phaseContext: executionVccContext(),
|
|
400
|
+
});
|
|
513
401
|
}
|
|
514
402
|
|
|
515
|
-
function
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
for (let i = 0; i < branchEntries.length; i++) {
|
|
519
|
-
const entry = branchEntries[i];
|
|
520
|
-
if (!isSummarizableEntry(entry)) continue;
|
|
521
|
-
const markers = scanDoneMarkers(messageText(entry.message));
|
|
522
|
-
if (markers.some((marker) => completionIds.has(marker))) {
|
|
523
|
-
lastCompletionIndex = i;
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
if (lastCompletionIndex >= 0) {
|
|
527
|
-
const next = branchEntries.slice(lastCompletionIndex + 1).find((entry) => entry.id && !isInternalExecutionCustomType(entry.customType));
|
|
528
|
-
if (next?.id) return next.id;
|
|
529
|
-
}
|
|
530
|
-
const startIndex = branchEntries.findIndex((entry) => entry.type === "custom" && entry.customType === "pi-plans-exec-start");
|
|
531
|
-
if (startIndex >= 0) {
|
|
532
|
-
const next = branchEntries.slice(startIndex + 1).find((entry) => entry.id && !isInternalExecutionCustomType(entry.customType));
|
|
533
|
-
if (next?.id) return next.id;
|
|
534
|
-
}
|
|
535
|
-
return fallback;
|
|
403
|
+
export function buildExecutionCompactionResult(event: SessionBeforeCompactEvent, ctx: ExtensionContext): CompactionResult | null {
|
|
404
|
+
const built = buildExecutionVccResult(event, ctx);
|
|
405
|
+
return built?.kind === "compaction" ? built.compaction : null;
|
|
536
406
|
}
|
|
537
407
|
|
|
538
|
-
function
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
408
|
+
export function handleExecutionBeforeCompact(
|
|
409
|
+
pi: ExtensionAPI,
|
|
410
|
+
ctx: ExtensionContext,
|
|
411
|
+
event: SessionBeforeCompactEvent,
|
|
412
|
+
): SessionBeforeCompactResult | undefined {
|
|
413
|
+
if (!execution) return undefined;
|
|
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;
|
|
421
|
+
try {
|
|
422
|
+
built = buildExecutionVccResult(event, ctx);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
state.inFlight = false;
|
|
425
|
+
ctx.ui.notify(`pi-plans: VCC compaction preparation failed; using Pi default compaction (${String(error)}).`, "warning");
|
|
426
|
+
return undefined;
|
|
557
427
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
export function buildExecutionCompactionResult(event: SessionBeforeCompactEvent, ctx: ExtensionContext): CompactionResult | null {
|
|
562
|
-
if (!execution) return null;
|
|
563
|
-
const active = readActive(ctx.cwd);
|
|
564
|
-
const run = active ? getRun(ctx.cwd, active.run_id) : null;
|
|
565
|
-
const firstKeptEntryId = findExecutionCompactionCutEntryId(event.branchEntries as CompactBranchEntry[], event.preparation.firstKeptEntryId);
|
|
566
|
-
const boundaryIndex = event.branchEntries.findIndex((entry) => entry.id === firstKeptEntryId);
|
|
567
|
-
const summaryEntries = boundaryIndex >= 0
|
|
568
|
-
? (event.branchEntries.slice(0, boundaryIndex) as CompactBranchEntry[])
|
|
569
|
-
: (event.branchEntries as CompactBranchEntry[]);
|
|
570
|
-
const sections = buildFinishedItemSections(summaryEntries);
|
|
571
|
-
const parts: string[] = [];
|
|
572
|
-
if (event.customInstructions?.trim()) {
|
|
573
|
-
parts.push(`## Compact Instructions
|
|
574
|
-
${compactText(event.customInstructions, 1000)}`);
|
|
428
|
+
if (!built || built.kind === "fallback") {
|
|
429
|
+
state.inFlight = false;
|
|
430
|
+
return undefined;
|
|
575
431
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
parts.push(`## Previous Compact Summary
|
|
581
|
-
${event.preparation.previousSummary.trim()}`);
|
|
432
|
+
if (built.kind === "cancel") {
|
|
433
|
+
state.inFlight = false;
|
|
434
|
+
ctx.ui.notify(built.message, "warning");
|
|
435
|
+
return { cancel: true };
|
|
582
436
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
return {
|
|
588
|
-
summary: parts.join("\n\n"),
|
|
589
|
-
firstKeptEntryId,
|
|
590
|
-
tokensBefore: event.preparation.tokensBefore,
|
|
591
|
-
details: {
|
|
592
|
-
kind: "pi-plans-execution-compaction",
|
|
593
|
-
reason: event.reason,
|
|
594
|
-
willRetry: event.willRetry,
|
|
595
|
-
finishedItems: execution.items.filter((item) => item.done).map((item) => item.id),
|
|
596
|
-
},
|
|
597
|
-
};
|
|
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 };
|
|
598
442
|
}
|
|
599
443
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const compaction = buildExecutionCompactionResult(event, ctx);
|
|
603
|
-
if (!compaction) return undefined;
|
|
604
|
-
requestExecutionFlush(pi, ctx);
|
|
605
|
-
return { compaction };
|
|
444
|
+
function runtimePiVersion(ctx: ExtensionContext): unknown {
|
|
445
|
+
return (ctx as ExtensionContext & { piVersion?: unknown }).piVersion ?? VERSION;
|
|
606
446
|
}
|
|
607
447
|
|
|
608
|
-
export function handleExecutionCompact(pi: ExtensionAPI, ctx: ExtensionContext, event: SessionCompactEvent): void {
|
|
448
|
+
export async function handleExecutionCompact(pi: ExtensionAPI, ctx: ExtensionContext, event: SessionCompactEvent): Promise<void> {
|
|
609
449
|
if (!execution) return;
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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;
|
|
457
|
+
state.inFlight = false;
|
|
458
|
+
state.lastAttemptReason = event.reason;
|
|
459
|
+
state.cooldownActive = true;
|
|
460
|
+
state.rearmPending = false;
|
|
461
|
+
state.terminalBackoffTokens = null;
|
|
462
|
+
state.lastSuccessfulAt = utcNow();
|
|
463
|
+
state.lastSuccessfulUsagePercent = ctx.getContextUsage()?.percent ?? state.lastSuccessfulUsagePercent;
|
|
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
|
+
}
|
|
619
480
|
}
|
|
620
481
|
requestExecutionFlush(pi, ctx);
|
|
621
482
|
updateStatusWidget(ctx);
|
|
622
483
|
}
|
|
623
484
|
|
|
485
|
+
|
|
624
486
|
export function handleExecutionCompactFailed(pi: ExtensionAPI, ctx: ExtensionContext, event: SessionCompactFailedEvent): void {
|
|
625
487
|
if (!execution) return;
|
|
488
|
+
const state = executionCompactionState(ctx);
|
|
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);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (state) {
|
|
513
|
+
state.inFlight = false;
|
|
514
|
+
state.resumeGuard = false;
|
|
515
|
+
state.cooldownActive = false;
|
|
516
|
+
state.rearmPending = false;
|
|
517
|
+
state.lastAttemptReason = event.reason;
|
|
518
|
+
state.pendingStats = null;
|
|
519
|
+
state.pendingFollowUpPrompt = null;
|
|
520
|
+
state.pendingContinueAfterThresholdCompact = false;
|
|
521
|
+
}
|
|
626
522
|
ctx.ui.notify(
|
|
627
523
|
`pi-plans: compaction failed (${event.reason}); execution remains active and will wait for the next eligible turn.`,
|
|
628
524
|
"warning",
|
|
@@ -635,15 +531,14 @@ export function filterExecutionResumeMessages<T extends { customType?: string }>
|
|
|
635
531
|
}
|
|
636
532
|
|
|
637
533
|
// ---------------------------------------------------------------------------
|
|
638
|
-
// Planning-phase
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
//
|
|
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.
|
|
642
538
|
// ---------------------------------------------------------------------------
|
|
643
539
|
|
|
644
540
|
export const PLANNING_RUN_START_CUSTOM_TYPE = "pi-plans-run-start";
|
|
645
541
|
export const PLANNING_PLAN_WRITTEN_CUSTOM_TYPE = "pi-plans-plan-written";
|
|
646
|
-
const PLANNING_QA_SECTION_HEADER = "## Q&A During Planning";
|
|
647
542
|
const PLANNING_RESUME_CUSTOM_TYPE = "pi-plans-plan-resume";
|
|
648
543
|
|
|
649
544
|
interface PlanningCompactionState {
|
|
@@ -653,107 +548,114 @@ interface PlanningCompactionState {
|
|
|
653
548
|
lastAttemptReason: "manual" | "threshold" | "overflow" | null;
|
|
654
549
|
lastSuccessfulUsagePercent: number | null;
|
|
655
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;
|
|
656
556
|
}
|
|
657
557
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
+
});
|
|
664
572
|
}
|
|
665
573
|
|
|
666
|
-
function
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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;
|
|
677
598
|
}
|
|
678
599
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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;
|
|
692
617
|
}
|
|
693
618
|
|
|
694
|
-
function
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
for (let i = 0; i < branchEntries.length; i++) {
|
|
701
|
-
const entry = branchEntries[i];
|
|
702
|
-
if (entry.type === "custom" && entry.customType === PLANNING_PLAN_WRITTEN_CUSTOM_TYPE) {
|
|
703
|
-
planWrittenIndexes.push(i);
|
|
704
|
-
} else if (entry.type === "custom" && entry.customType === PLANNING_RUN_START_CUSTOM_TYPE) {
|
|
705
|
-
runStartIndexes.push(i);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
const planIndex = planWrittenIndexes[planWrittenIndexes.length - 1] ?? -1;
|
|
709
|
-
const startIndex = runStartIndexes[runStartIndexes.length - 1] ?? -1;
|
|
710
|
-
const anchorIndex = planIndex >= 0 ? planIndex : startIndex;
|
|
711
|
-
if (anchorIndex < 0) {
|
|
712
|
-
return { id: fallback, qaWindowEntries: [], hasMarker: false };
|
|
713
|
-
}
|
|
714
|
-
const next = branchEntries
|
|
715
|
-
.slice(anchorIndex + 1)
|
|
716
|
-
.find((entry) => entry.id && !isPlanningInternalCustomType(entry.customType));
|
|
717
|
-
if (!next?.id) {
|
|
718
|
-
return { id: fallback, qaWindowEntries: [], hasMarker: true };
|
|
719
|
-
}
|
|
720
|
-
const qaWindowEntries = branchEntries
|
|
721
|
-
.slice(startIndex >= 0 ? startIndex + 1 : 0, anchorIndex)
|
|
722
|
-
.filter((entry) => entry.type === "message" && entry.message);
|
|
723
|
-
return { id: next.id, qaWindowEntries, hasMarker: true };
|
|
619
|
+
function isPlanningCustomInstructions(hint: unknown): boolean {
|
|
620
|
+
return typeof hint === "string" && hint.startsWith("pi-plans planning");
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function isExecutionCustomInstructions(hint: unknown): boolean {
|
|
624
|
+
return typeof hint === "string" && hint.startsWith("pi-plans execution");
|
|
724
625
|
}
|
|
725
626
|
|
|
726
|
-
function
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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;
|
|
732
637
|
}
|
|
733
|
-
if (!lines.length) return null;
|
|
734
|
-
return `${PLANNING_QA_SECTION_HEADER}
|
|
735
|
-
${lines.join("\n")}`;
|
|
736
638
|
}
|
|
737
639
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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;
|
|
744
650
|
}
|
|
745
651
|
|
|
746
|
-
export function
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
const state = session.__planningCompaction;
|
|
754
|
-
if (!state) return true;
|
|
755
|
-
if (state.inFlight || state.resumeGuard || state.cooldownActive) return false;
|
|
756
|
-
return true;
|
|
652
|
+
export function compactionInFlight(ctx: ExtensionContext, phase: CompactionPhase): boolean {
|
|
653
|
+
const store = compactionLifecycleStore(ctx);
|
|
654
|
+
return store[phase];
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export function shouldTriggerPlanningCompaction(_ctx: ExtensionContext): boolean {
|
|
658
|
+
return false;
|
|
757
659
|
}
|
|
758
660
|
|
|
759
661
|
export function consumePlanningCompactionResumeGuard(ctx: ExtensionContext): boolean {
|
|
@@ -763,78 +665,34 @@ export function consumePlanningCompactionResumeGuard(ctx: ExtensionContext): boo
|
|
|
763
665
|
return true;
|
|
764
666
|
}
|
|
765
667
|
|
|
766
|
-
export function refreshPlanningCompactionCooldown(
|
|
767
|
-
|
|
768
|
-
const state = session.__planningCompaction;
|
|
769
|
-
if (!state) return;
|
|
770
|
-
const percent = ctx.getContextUsage()?.percent ?? null;
|
|
771
|
-
if (percent !== null && percent < 85 && state.cooldownActive) {
|
|
772
|
-
state.cooldownActive = false;
|
|
773
|
-
}
|
|
668
|
+
export function refreshPlanningCompactionCooldown(_ctx: ExtensionContext): void {
|
|
669
|
+
// Pi core owns scheduling; retained for lifecycle compatibility only.
|
|
774
670
|
}
|
|
775
671
|
|
|
776
|
-
export function requestPlanningCompaction(
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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.
|
|
675
|
+
}
|
|
676
|
+
|
|
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,
|
|
686
|
+
reason: event.reason,
|
|
687
|
+
willRetry: event.willRetry,
|
|
688
|
+
settings: active.settings,
|
|
689
|
+
phaseContext: planningVccContext(branchEntries, active),
|
|
690
|
+
});
|
|
790
691
|
}
|
|
791
692
|
|
|
792
693
|
export function buildPlanningCompactionResult(event: SessionBeforeCompactEvent, ctx: ExtensionContext): CompactionResult | null {
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
if (!planningCtx) return null;
|
|
796
|
-
const branchEntries = event.branchEntries as unknown as PlanningBranchEntry[];
|
|
797
|
-
const { id: firstKeptEntryId, qaWindowEntries, hasMarker } = findPlanningCutEntryId(branchEntries, event.preparation.firstKeptEntryId);
|
|
798
|
-
const qaSection = hasMarker ? buildPlanningQASection(qaWindowEntries) : null;
|
|
799
|
-
const parts: string[] = [];
|
|
800
|
-
if (event.customInstructions?.trim()) {
|
|
801
|
-
parts.push(`## Compact Instructions
|
|
802
|
-
${compactText(event.customInstructions, 1000)}`);
|
|
803
|
-
}
|
|
804
|
-
if (qaSection) {
|
|
805
|
-
parts.push(qaSection);
|
|
806
|
-
}
|
|
807
|
-
const previousSummary = event.preparation.previousSummary?.trim();
|
|
808
|
-
parts.push(`## Goal
|
|
809
|
-
${compactText(planningCtx.runId, 80)} — keep current planning progress.`);
|
|
810
|
-
parts.push(`## Constraints & Preferences
|
|
811
|
-
- Stay in the active planning run (\`${planningCtx.runId}\`).
|
|
812
|
-
- Plan files live under \`${planningCtx.artifactDir}\`.`);
|
|
813
|
-
parts.push(`## Progress
|
|
814
|
-
### Done
|
|
815
|
-
- Pre-plan history compressed below.
|
|
816
|
-
|
|
817
|
-
### In Progress
|
|
818
|
-
- Current planning question or open decision.
|
|
819
|
-
|
|
820
|
-
### Blocked
|
|
821
|
-
- ${hasMarker ? "None" : "Planning cut-point marker missing; falling back to default."}`);
|
|
822
|
-
if (previousSummary) {
|
|
823
|
-
parts.push(`## Previous Compact Summary
|
|
824
|
-
${previousSummary}`);
|
|
825
|
-
}
|
|
826
|
-
parts.push(`## Next Steps
|
|
827
|
-
- Resume the active planning turn from the raw tail.`);
|
|
828
|
-
return {
|
|
829
|
-
summary: parts.join("\n\n"),
|
|
830
|
-
firstKeptEntryId,
|
|
831
|
-
tokensBefore: event.preparation.tokensBefore,
|
|
832
|
-
details: {
|
|
833
|
-
kind: "pi-plans-planning-compaction",
|
|
834
|
-
reason: event.reason,
|
|
835
|
-
hasMarker,
|
|
836
|
-
},
|
|
837
|
-
};
|
|
694
|
+
const built = buildPlanningVccResult(event, ctx);
|
|
695
|
+
return built?.kind === "compaction" ? built.compaction : null;
|
|
838
696
|
}
|
|
839
697
|
|
|
840
698
|
export function handlePlanningBeforeCompact(
|
|
@@ -843,74 +701,103 @@ export function handlePlanningBeforeCompact(
|
|
|
843
701
|
event: SessionBeforeCompactEvent,
|
|
844
702
|
): SessionBeforeCompactResult | undefined {
|
|
845
703
|
if (getExecution()) return undefined;
|
|
846
|
-
|
|
847
|
-
const session = ctx.sessionManager as unknown as { __planningCompaction?: PlanningCompactionState };
|
|
848
|
-
const state = (session.__planningCompaction ??= {
|
|
849
|
-
inFlight: false,
|
|
850
|
-
resumeGuard: false,
|
|
851
|
-
cooldownActive: false,
|
|
852
|
-
lastAttemptReason: null,
|
|
853
|
-
lastSuccessfulUsagePercent: null,
|
|
854
|
-
lastSuccessfulAt: null,
|
|
855
|
-
} satisfies PlanningCompactionState);
|
|
856
|
-
const percent = ctx.getContextUsage()?.percent ?? null;
|
|
857
|
-
if (event.reason === "threshold" && (percent === null || percent < 100)) {
|
|
858
|
-
state.inFlight = false;
|
|
859
|
-
state.lastAttemptReason = event.reason;
|
|
860
|
-
return { cancel: true };
|
|
861
|
-
}
|
|
704
|
+
const state = ensurePlanningCompactionState(ctx);
|
|
862
705
|
state.inFlight = true;
|
|
863
706
|
state.lastAttemptReason = event.reason;
|
|
864
|
-
|
|
865
|
-
|
|
707
|
+
state.pendingStats = null;
|
|
708
|
+
state.pendingFollowUpPrompt = null;
|
|
709
|
+
state.pendingContinueAfterThresholdCompact = false;
|
|
710
|
+
let built: VccCompactionBuildResult | null;
|
|
711
|
+
try {
|
|
712
|
+
built = buildPlanningVccResult(event, ctx);
|
|
713
|
+
} catch (error) {
|
|
714
|
+
state.inFlight = false;
|
|
715
|
+
ctx.ui.notify(`pi-plans: VCC planning compaction preparation failed; using Pi default compaction (${String(error)}).`, "warning");
|
|
716
|
+
return undefined;
|
|
866
717
|
}
|
|
867
|
-
|
|
868
|
-
if (!compaction) {
|
|
718
|
+
if (!built || built.kind === "fallback") {
|
|
869
719
|
state.inFlight = false;
|
|
870
720
|
return undefined;
|
|
871
721
|
}
|
|
872
|
-
|
|
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 };
|
|
873
731
|
}
|
|
874
732
|
|
|
875
|
-
export function handlePlanningCompact(pi: ExtensionAPI, ctx: ExtensionContext, event: SessionCompactEvent): void {
|
|
733
|
+
export async function handlePlanningCompact(pi: ExtensionAPI, ctx: ExtensionContext, event: SessionCompactEvent): Promise<void> {
|
|
876
734
|
if (getExecution()) return;
|
|
877
735
|
const session = ctx.sessionManager as unknown as { __planningCompaction?: PlanningCompactionState };
|
|
878
736
|
const state = session.__planningCompaction;
|
|
879
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;
|
|
880
744
|
state.inFlight = false;
|
|
881
745
|
state.lastAttemptReason = event.reason;
|
|
746
|
+
state.terminalBackoffTokens = null;
|
|
882
747
|
state.cooldownActive = true;
|
|
883
748
|
state.lastSuccessfulAt = utcNow();
|
|
884
749
|
state.lastSuccessfulUsagePercent = ctx.getContextUsage()?.percent ?? state.lastSuccessfulUsagePercent;
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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
|
+
}
|
|
897
766
|
}
|
|
898
767
|
}
|
|
899
768
|
|
|
769
|
+
|
|
900
770
|
export function handlePlanningCompactFailed(pi: ExtensionAPI, ctx: ExtensionContext, event: SessionCompactFailedEvent): void {
|
|
901
771
|
if (getExecution()) return;
|
|
902
772
|
const session = ctx.sessionManager as unknown as { __planningCompaction?: PlanningCompactionState };
|
|
903
773
|
const state = session.__planningCompaction;
|
|
904
774
|
if (!state) return;
|
|
905
|
-
const
|
|
906
|
-
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;
|
|
907
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");
|
|
908
792
|
return;
|
|
909
793
|
}
|
|
910
794
|
state.inFlight = false;
|
|
911
795
|
state.resumeGuard = false;
|
|
912
796
|
state.cooldownActive = false;
|
|
913
797
|
state.lastAttemptReason = event.reason;
|
|
798
|
+
state.pendingStats = null;
|
|
799
|
+
state.pendingFollowUpPrompt = null;
|
|
800
|
+
state.pendingContinueAfterThresholdCompact = false;
|
|
914
801
|
ctx.ui.notify(
|
|
915
802
|
`pi-plans: planning compaction failed (${event.reason}); will try again on the next eligible turn.`,
|
|
916
803
|
"warning",
|
|
@@ -921,41 +808,12 @@ export function filterPlanningResumeMessages<T extends { customType?: string }>(
|
|
|
921
808
|
return messages.filter((message) => message.customType !== PLANNING_RESUME_CUSTOM_TYPE);
|
|
922
809
|
}
|
|
923
810
|
|
|
924
|
-
export async function ensureExecutionModelActive(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
925
|
-
const state = execution?.modelState;
|
|
926
|
-
if (!state) return;
|
|
927
|
-
const selection = resolveModelSelection(ctx, state.executionSelector);
|
|
928
|
-
if (!selection) {
|
|
929
|
-
ctx.ui.notify(`pi-plans: execution model ${state.executionSelector} not found; continuing on the current model.`, "warning");
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
|
-
const switched = await applyModelSelection(pi, ctx, selection);
|
|
933
|
-
if (!switched) {
|
|
934
|
-
ctx.ui.notify(`pi-plans: could not switch to execution model ${state.executionSelector} (no API key?); continuing on the current model.`, "warning");
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
async function restorePlanningModel(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
939
|
-
const state = execution?.modelState;
|
|
940
|
-
if (!state?.planningSelector) return;
|
|
941
|
-
const selection = resolveModelSelection(ctx, state.planningSelector);
|
|
942
|
-
if (!selection) {
|
|
943
|
-
ctx.ui.notify(`pi-plans: planning model ${state.planningSelector} not found; keeping the current model.`, "warning");
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
const restored = await applyModelSelection(pi, ctx, selection);
|
|
947
|
-
if (!restored) {
|
|
948
|
-
ctx.ui.notify(`pi-plans: could not restore planning model ${state.planningSelector} (no API key?); keeping the current model.`, "warning");
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
811
|
export async function stopExecution(pi: ExtensionAPI, ctx: ExtensionContext, reason: string): Promise<void> {
|
|
953
812
|
if (!execution) return;
|
|
954
|
-
|
|
813
|
+
resetExecutionCompactionState(ctx);
|
|
955
814
|
// Final synchronous write: drain any deferred flush and land the last snapshot.
|
|
956
815
|
pendingExecutionFlush = false;
|
|
957
816
|
persist(pi);
|
|
958
|
-
clearExecutionPanel(ctx);
|
|
959
817
|
execution = null;
|
|
960
818
|
pi.appendEntry("pi-plans-exec-cleared", { reason });
|
|
961
819
|
pi.sendMessage(
|
|
@@ -1010,29 +868,50 @@ export function applyImplMarkers(text: string): string[] {
|
|
|
1010
868
|
return changed;
|
|
1011
869
|
}
|
|
1012
870
|
|
|
871
|
+
export function applyCurrentIMarker(text: string): boolean {
|
|
872
|
+
if (!execution?.implItems?.length) return false;
|
|
873
|
+
const markers = scanCurrentIMarkers(text);
|
|
874
|
+
const resolved = resolveCurrentI(execution.implItems, markers, execution.currentI);
|
|
875
|
+
if (!resolved || resolved === execution.currentI) return false;
|
|
876
|
+
execution.currentI = resolved;
|
|
877
|
+
return true;
|
|
878
|
+
}
|
|
879
|
+
|
|
1013
880
|
export function isExecutionComplete(): boolean {
|
|
1014
881
|
return execution !== null && execution.items.length > 0 && execution.items.every((item) => item.done);
|
|
1015
882
|
}
|
|
1016
883
|
|
|
1017
884
|
export async function completeExecution(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
1018
885
|
if (!execution) return;
|
|
1019
|
-
|
|
886
|
+
resetExecutionCompactionState(ctx);
|
|
1020
887
|
// Final synchronous write: drain any deferred flush and land the last snapshot.
|
|
1021
888
|
pendingExecutionFlush = false;
|
|
1022
889
|
persist(pi);
|
|
890
|
+
|
|
1023
891
|
const summary = execution.items.map((item) => `- ✅ \`${item.id}\` ${item.text.split(";")[0]}`).join("\n");
|
|
1024
892
|
const planPath = execution.planPath;
|
|
1025
|
-
clearExecutionPanel(ctx);
|
|
1026
893
|
execution = null;
|
|
1027
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}`;
|
|
1028
904
|
pi.sendMessage(
|
|
1029
905
|
{
|
|
1030
906
|
customType: "pi-plans-complete",
|
|
1031
|
-
content
|
|
907
|
+
content,
|
|
1032
908
|
display: true,
|
|
1033
909
|
},
|
|
1034
|
-
{ triggerTurn:
|
|
910
|
+
{ triggerTurn: interactive },
|
|
1035
911
|
);
|
|
912
|
+
if (interactive) {
|
|
913
|
+
pi.appendEntry("pi-plans-ameliorate", { planPath, phase: "goal-started", rounds: null, currentRound: 0 });
|
|
914
|
+
}
|
|
1036
915
|
const active = readActive(ctx.cwd);
|
|
1037
916
|
if (active) {
|
|
1038
917
|
try {
|
|
@@ -1044,21 +923,40 @@ export async function completeExecution(pi: ExtensionAPI, ctx: ExtensionContext)
|
|
|
1044
923
|
updateStatusWidget(ctx);
|
|
1045
924
|
}
|
|
1046
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
|
+
|
|
1047
932
|
/** Injection text for before_agent_start while executing. */
|
|
1048
|
-
export function executionContextMessage(): string | null {
|
|
933
|
+
export function executionContextMessage(ctx: ExtensionContext): string | null {
|
|
1049
934
|
if (!execution) return null;
|
|
1050
935
|
const remaining = execution.items.filter((item) => !item.done);
|
|
1051
936
|
const list =
|
|
1052
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");
|
|
945
|
+
const implementationItems = execution.implItems?.length
|
|
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.`
|
|
947
|
+
: "";
|
|
1053
948
|
return `[PI-PLANS EXECUTION — write access enabled]
|
|
1054
949
|
Implement the accepted plan at ${execution.planPath} (${execution.items.length - remaining.length}/${execution.items.length} verifier items done).
|
|
1055
950
|
|
|
1056
951
|
Remaining verifier items:
|
|
1057
|
-
${list}
|
|
952
|
+
${list}${implementationItems}
|
|
953
|
+
|
|
954
|
+
${graphLine}
|
|
1058
955
|
|
|
1059
956
|
Execution rules:
|
|
1060
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.
|
|
1061
|
-
- 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 status
|
|
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.
|
|
959
|
+
- For subprocess-backed verification, when a step starts a subprocess and needs its result before verifying, use literal \`waiting for\` with backoff \`5s -> 10s -> 20s -> 40s -> 80s\`, then keep polling at 80s; restart at 5s for each new subprocess.
|
|
1062
960
|
- Simplest implementation that fully meets the item: no speculative abstractions, configuration, or indirection; keep components modular with clearly separated concerns.
|
|
1063
961
|
- Architectural decisions are for the long term: no stopgaps. Do not add backward-compatibility layers, fallbacks, or migrations — remove the obsolete paths this change obsoletes.
|
|
1064
962
|
- Prefer established, well-maintained libraries when they reduce complexity or improve reliability; before writing your own implementation or adding a package, check the project's existing dependencies (docs and types) — never reimplement common functionality without a clear reason.
|
|
@@ -1081,6 +979,7 @@ interface SessionEntry {
|
|
|
1081
979
|
*/
|
|
1082
980
|
export async function restoreFromSession(pi: ExtensionAPI, ctx: ExtensionContext, entries: SessionEntry[]): Promise<void> {
|
|
1083
981
|
pendingExecutionFlush = false; // no flush debt survives a restart
|
|
982
|
+
resetExecutionCompactionState(ctx);
|
|
1084
983
|
let snapshotIndex = -1;
|
|
1085
984
|
let snapshot: ExecState | null = null;
|
|
1086
985
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
@@ -1093,21 +992,18 @@ export async function restoreFromSession(pi: ExtensionAPI, ctx: ExtensionContext
|
|
|
1093
992
|
if (entry.type === "custom" && entry.customType === "pi-plans-exec-cleared") {
|
|
1094
993
|
// Execution was explicitly stopped or completed after the last snapshot.
|
|
1095
994
|
execution = null;
|
|
1096
|
-
syncExecutionPanel(ctx);
|
|
1097
995
|
updateStatusWidget(ctx);
|
|
1098
996
|
return;
|
|
1099
997
|
}
|
|
1100
998
|
}
|
|
1101
999
|
if (!snapshot) {
|
|
1102
1000
|
execution = null;
|
|
1103
|
-
syncExecutionPanel(ctx);
|
|
1104
1001
|
updateStatusWidget(ctx);
|
|
1105
1002
|
return;
|
|
1106
1003
|
}
|
|
1107
1004
|
// Ignore stale plans whose file vanished.
|
|
1108
1005
|
if (!fs.existsSync(snapshot.planPath)) {
|
|
1109
1006
|
execution = null;
|
|
1110
|
-
syncExecutionPanel(ctx);
|
|
1111
1007
|
updateStatusWidget(ctx);
|
|
1112
1008
|
return;
|
|
1113
1009
|
}
|
|
@@ -1116,16 +1012,14 @@ export async function restoreFromSession(pi: ExtensionAPI, ctx: ExtensionContext
|
|
|
1116
1012
|
items: snapshot.items.map((item) => ({ ...item })),
|
|
1117
1013
|
startedAt: snapshot.startedAt,
|
|
1118
1014
|
usage: snapshot.usage ?? { inToks: 0, outToks: 0 },
|
|
1119
|
-
panel: executionPanelFromEntryData(snapshot.panel) ?? createExecutionPanelState(),
|
|
1120
|
-
modelState: snapshot.modelState,
|
|
1121
1015
|
implItems: snapshot.implItems ?? [],
|
|
1122
1016
|
implStatus: { ...(snapshot.implStatus ?? {}) },
|
|
1017
|
+
currentI: snapshot.currentI ?? inferCurrentI(snapshot.implItems, snapshot.items, snapshot.implStatus),
|
|
1123
1018
|
};
|
|
1124
1019
|
for (let i = snapshotIndex + 1; i < entries.length; i++) {
|
|
1125
1020
|
const entry = entries[i];
|
|
1126
1021
|
if (entry.type === "custom" && entry.customType === "pi-plans-exec-cleared") {
|
|
1127
1022
|
execution = null;
|
|
1128
|
-
syncExecutionPanel(ctx);
|
|
1129
1023
|
break;
|
|
1130
1024
|
}
|
|
1131
1025
|
const message = entry.message;
|
|
@@ -1136,6 +1030,7 @@ export async function restoreFromSession(pi: ExtensionAPI, ctx: ExtensionContext
|
|
|
1136
1030
|
.join("\n");
|
|
1137
1031
|
applyDoneMarkers(text);
|
|
1138
1032
|
applyImplMarkers(text);
|
|
1033
|
+
applyCurrentIMarker(text);
|
|
1139
1034
|
}
|
|
1140
1035
|
}
|
|
1141
1036
|
if (execution) {
|
|
@@ -1145,6 +1040,5 @@ export async function restoreFromSession(pi: ExtensionAPI, ctx: ExtensionContext
|
|
|
1145
1040
|
await completeExecution(pi, ctx);
|
|
1146
1041
|
}
|
|
1147
1042
|
}
|
|
1148
|
-
syncExecutionPanel(ctx);
|
|
1149
1043
|
updateStatusWidget(ctx);
|
|
1150
1044
|
}
|