pi-plans 0.1.2 → 0.2.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 +43 -17
- package/index.ts +34 -59
- package/package.json +1 -1
- package/references/pi-planning-workflow.md +6 -3
- package/references/state-and-config.md +1 -1
- package/src/autocomplete.ts +163 -0
- package/src/compaction.ts +502 -0
- package/src/exec.ts +557 -333
- package/src/plan.ts +37 -0
- package/src/query-hook.ts +82 -0
- package/src/refine-ui-helpers.ts +89 -0
- package/src/refine-ui-state.ts +78 -0
- package/src/refine-ui.ts +322 -0
- package/src/state.ts +9 -27
- package/src/subagent.ts +196 -69
- package/tests/autocomplete.test.ts +142 -0
- package/tests/compaction.test.ts +74 -0
- package/tests/exec.test.ts +153 -248
- package/tests/execute-plan.test.ts +65 -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-ui.test.ts +127 -0
- package/tests/state.test.ts +12 -15
- package/tests/subagent.test.ts +114 -0
- package/tools/ask-choice.ts +22 -0
- package/tools/execute-plan.ts +7 -39
- package/tools/plans.ts +1 -18
- package/tools/refine.ts +125 -71
- package/src/execution-panel.ts +0 -633
- package/tests/execution-panel.test.ts +0 -234
package/src/exec.ts
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
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
|
+
import { randomUUID } from "node:crypto";
|
|
10
11
|
import * as fs from "node:fs";
|
|
11
12
|
import type {
|
|
12
13
|
CompactionResult,
|
|
@@ -17,56 +18,48 @@ import type {
|
|
|
17
18
|
SessionCompactEvent,
|
|
18
19
|
SessionCompactFailedEvent,
|
|
19
20
|
} from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { AUTOCOMPLETE_ENTRY } from "./autocomplete.ts";
|
|
20
22
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
type ItemDiffSummary,
|
|
34
|
-
} from "./execution-panel.ts";
|
|
35
|
-
import { getRun, readActive, setRunStatus, utcNow, type ExecutionConfig } from "./state.ts";
|
|
23
|
+
compactText as boundedCompactionText,
|
|
24
|
+
compactionCurrentI,
|
|
25
|
+
currentIExceedsTrigger,
|
|
26
|
+
entryCurrentIMarkers,
|
|
27
|
+
extractReadRecords,
|
|
28
|
+
formatReadRecord,
|
|
29
|
+
mergeCompactionDetails,
|
|
30
|
+
planIAwareCompaction,
|
|
31
|
+
type CompactionDetailsLike,
|
|
32
|
+
type CompactionEntryLike,
|
|
33
|
+
} from "./compaction.ts";
|
|
34
|
+
import { getRun, readActive, setRunStatus, utcNow } from "./state.ts";
|
|
36
35
|
import {
|
|
36
|
+
extractCoverage,
|
|
37
37
|
latestPlanVersion,
|
|
38
|
+
resolveImplStatuses,
|
|
38
39
|
scanDoneMarkers,
|
|
39
40
|
scanImplMarkers,
|
|
41
|
+
scanCurrentIMarkers,
|
|
42
|
+
resolveCurrentI,
|
|
43
|
+
inferCurrentI,
|
|
40
44
|
type CheckItem,
|
|
41
45
|
type ImplItem,
|
|
42
46
|
type ImplMarkerState,
|
|
43
47
|
} from "./plan.ts";
|
|
44
48
|
|
|
45
|
-
export interface ExecState
|
|
49
|
+
export interface ExecState {
|
|
50
|
+
planPath: string;
|
|
51
|
+
items: CheckItem[];
|
|
46
52
|
startedAt: string;
|
|
47
|
-
panel?: ExecutionPanelState;
|
|
48
53
|
usage: { inToks: number; outToks: number };
|
|
49
|
-
modelState?: ExecutionModelState;
|
|
50
54
|
implItems?: ImplItem[];
|
|
51
55
|
implStatus?: Record<string, ImplMarkerState>;
|
|
56
|
+
currentI?: string;
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
let execution: ExecState | null = null;
|
|
55
60
|
|
|
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).
|
|
61
|
+
// Execution-loop persistence is deferred until the agent settles so turn_end
|
|
62
|
+
// never causes session writes during a streaming run.
|
|
70
63
|
let pendingExecutionFlush = false;
|
|
71
64
|
|
|
72
65
|
export function consumePendingExecutionFlush(): boolean {
|
|
@@ -75,25 +68,17 @@ export function consumePendingExecutionFlush(): boolean {
|
|
|
75
68
|
return pending;
|
|
76
69
|
}
|
|
77
70
|
|
|
78
|
-
function requestExecutionFlush(
|
|
71
|
+
function requestExecutionFlush(_pi: ExtensionAPI, _ctx: ExtensionContext): void {
|
|
79
72
|
// 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.
|
|
73
|
+
// operations where isIdle() reads true; persistence happens only at the
|
|
74
|
+
// drain points: agent_settled, the next before_agent_start, and stop/complete.
|
|
84
75
|
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
76
|
}
|
|
90
77
|
|
|
91
78
|
export function drainExecutionFlush(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
92
|
-
if (!execution) return;
|
|
93
|
-
if (!pendingExecutionFlush && !consumePendingPanelSync()) return;
|
|
79
|
+
if (!execution || !pendingExecutionFlush) return;
|
|
94
80
|
pendingExecutionFlush = false;
|
|
95
81
|
persist(pi);
|
|
96
|
-
syncExecutionPanel(ctx);
|
|
97
82
|
updateStatusWidget(ctx);
|
|
98
83
|
}
|
|
99
84
|
|
|
@@ -101,173 +86,158 @@ export function getExecution(): ExecState | null {
|
|
|
101
86
|
return execution;
|
|
102
87
|
}
|
|
103
88
|
|
|
104
|
-
const
|
|
105
|
-
const
|
|
89
|
+
const EXECUTION_COMPACTION_TRIGGER_PERCENT = 20;
|
|
90
|
+
const EXECUTION_COMPACTION_REARM_PERCENT = 80;
|
|
91
|
+
const EXECUTION_COMPACTION_REARM_HIGH_PERCENT = 95;
|
|
92
|
+
const EXECUTION_COMPACTION_RESUME_MESSAGE = "Continue execution.";
|
|
106
93
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
94
|
+
interface ExecutionCompactionState {
|
|
95
|
+
inFlight: boolean;
|
|
96
|
+
resumeGuard: boolean;
|
|
97
|
+
cooldownActive: boolean;
|
|
98
|
+
lastAttemptReason: string | null;
|
|
99
|
+
lastSuccessfulUsagePercent: number | null;
|
|
100
|
+
lastSuccessfulAt: string | null;
|
|
101
|
+
rearmPending: boolean;
|
|
110
102
|
}
|
|
111
103
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
104
|
+
type ExecutionCompactionSession = { __executionCompaction?: ExecutionCompactionState };
|
|
105
|
+
type ExecutionCompactionContext = ExtensionContext & { sessionManager?: ExecutionCompactionSession };
|
|
106
|
+
|
|
107
|
+
function getExecutionCompactionSession(ctx: ExtensionContext, create = false): ExecutionCompactionSession | undefined {
|
|
108
|
+
const carrier = ctx as ExecutionCompactionContext;
|
|
109
|
+
if (carrier.sessionManager) return carrier.sessionManager;
|
|
110
|
+
if (!create) return undefined;
|
|
111
|
+
carrier.sessionManager = {};
|
|
112
|
+
return carrier.sessionManager;
|
|
118
113
|
}
|
|
119
114
|
|
|
120
|
-
function
|
|
121
|
-
return
|
|
115
|
+
function executionCompactionState(ctx: ExtensionContext): ExecutionCompactionState | undefined {
|
|
116
|
+
return getExecutionCompactionSession(ctx)?.__executionCompaction;
|
|
122
117
|
}
|
|
123
118
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
119
|
+
function ensureExecutionCompactionState(ctx: ExtensionContext): ExecutionCompactionState {
|
|
120
|
+
const session = getExecutionCompactionSession(ctx, true)!;
|
|
121
|
+
return (session.__executionCompaction ??= {
|
|
122
|
+
inFlight: false,
|
|
123
|
+
resumeGuard: false,
|
|
124
|
+
cooldownActive: false,
|
|
125
|
+
lastAttemptReason: null,
|
|
126
|
+
lastSuccessfulUsagePercent: null,
|
|
127
|
+
lastSuccessfulAt: null,
|
|
128
|
+
rearmPending: false,
|
|
129
|
+
});
|
|
133
130
|
}
|
|
134
131
|
|
|
135
|
-
|
|
136
|
-
|
|
132
|
+
function resetExecutionCompactionState(ctx: ExtensionContext): void {
|
|
133
|
+
const session = getExecutionCompactionSession(ctx);
|
|
134
|
+
if (!session) return;
|
|
135
|
+
delete session.__executionCompaction;
|
|
137
136
|
}
|
|
138
137
|
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const model = ctx.modelRegistry.find(provider, modelId);
|
|
154
|
-
if (model) {
|
|
155
|
-
return { provider, modelId, thinkingLevel, selector, model };
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function listAvailableModelSelections(ctx: ExtensionContext): ModelSelection[] {
|
|
163
|
-
const rawSelections = ctx.scopedModels.length > 0
|
|
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);
|
|
138
|
+
function consumeExecutionCompactionResumeGuard(ctx: ExtensionContext): boolean {
|
|
139
|
+
const state = executionCompactionState(ctx);
|
|
140
|
+
if (!state?.resumeGuard) return false;
|
|
141
|
+
state.resumeGuard = false;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function refreshExecutionCompactionCooldown(ctx: ExtensionContext): void {
|
|
146
|
+
const state = executionCompactionState(ctx);
|
|
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;
|
|
184
152
|
}
|
|
185
|
-
return unique;
|
|
186
153
|
}
|
|
187
154
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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.
|
|
192
180
|
}
|
|
193
|
-
return true;
|
|
194
181
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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;
|
|
199
195
|
}
|
|
200
196
|
return true;
|
|
201
197
|
}
|
|
202
198
|
|
|
203
|
-
|
|
204
|
-
ctx
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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,
|
|
230
|
-
);
|
|
231
|
-
if (!choice) return current;
|
|
232
|
-
if (choice === CURRENT_SESSION_MODEL_CHOICE) {
|
|
233
|
-
if (onChosenSelector) {
|
|
234
|
-
await onChosenSelector("inherit");
|
|
235
|
-
}
|
|
236
|
-
return current;
|
|
237
|
-
}
|
|
238
|
-
if (onChosenSelector) {
|
|
239
|
-
await onChosenSelector(choice);
|
|
240
|
-
}
|
|
241
|
-
return resolveModelSelection(ctx, choice) ?? current;
|
|
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");
|
|
242
209
|
}
|
|
210
|
+
}
|
|
243
211
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
212
|
+
export function handleExecutionTurnCompaction(ctx: ExtensionContext): void {
|
|
213
|
+
refreshExecutionCompactionCooldown(ctx);
|
|
214
|
+
if (consumeExecutionCompactionResumeGuard(ctx)) return;
|
|
215
|
+
if (shouldTriggerExecutionCompaction(ctx)) {
|
|
216
|
+
requestExecutionCompaction(ctx);
|
|
249
217
|
}
|
|
218
|
+
}
|
|
250
219
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (onChosenSelector) {
|
|
268
|
-
await onChosenSelector(choice);
|
|
220
|
+
export function computeExecutionProgress(execution: ExecState): { done: number; total: number } {
|
|
221
|
+
const implItems = execution.implItems ?? [];
|
|
222
|
+
if (implItems.length) {
|
|
223
|
+
const statuses = resolveImplStatuses(implItems, execution.items, execution.implStatus);
|
|
224
|
+
const counted = implItems.filter((impl) =>
|
|
225
|
+
execution.items.some((item) => extractCoverage(item.text).includes(impl.id)),
|
|
226
|
+
);
|
|
227
|
+
const total = counted.length > 0 ? counted.length : implItems.length;
|
|
228
|
+
const vcDone = counted.filter((impl) => statuses[impl.id] === "vc-passed").length;
|
|
229
|
+
const currentIndex = execution.currentI
|
|
230
|
+
? implItems.findIndex((impl) => impl.id === execution.currentI)
|
|
231
|
+
: -1;
|
|
232
|
+
return {
|
|
233
|
+
done: Math.min(total, Math.max(vcDone, currentIndex < 0 ? 0 : currentIndex)),
|
|
234
|
+
total,
|
|
235
|
+
};
|
|
269
236
|
}
|
|
270
|
-
return
|
|
237
|
+
return {
|
|
238
|
+
done: execution.items.filter((item) => item.done).length,
|
|
239
|
+
total: execution.items.length,
|
|
240
|
+
};
|
|
271
241
|
}
|
|
272
242
|
|
|
273
243
|
export function executionProgress(): { done: number; total: number } | null {
|
|
@@ -275,13 +245,26 @@ export function executionProgress(): { done: number; total: number } | null {
|
|
|
275
245
|
return computeExecutionProgress(execution);
|
|
276
246
|
}
|
|
277
247
|
|
|
248
|
+
function formatElapsed(startedAt: string): string {
|
|
249
|
+
const total = Math.max(0, Math.floor((Date.now() - Date.parse(startedAt)) / 1000));
|
|
250
|
+
const h = String(Math.floor(total / 3600)).padStart(2, "0");
|
|
251
|
+
const m = String(Math.floor((total % 3600) / 60)).padStart(2, "0");
|
|
252
|
+
const sec = String(total % 60).padStart(2, "0");
|
|
253
|
+
return `${h}:${m}:${sec}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function formatToks(tokens: number): string {
|
|
257
|
+
const n = Math.max(0, Math.round(tokens));
|
|
258
|
+
return n < 1000 ? String(n) : `${(n / 1000).toFixed(1)}k`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function formatExecutionStatusLine(execution: ExecState): string {
|
|
262
|
+
const progress = computeExecutionProgress(execution);
|
|
263
|
+
return `⌛ plans ${progress.done}/${progress.total}: spent ${formatElapsed(execution.startedAt)} · ${formatToks(execution.usage.inToks)} in-toks · ${formatToks(execution.usage.outToks)} out-toks`;
|
|
264
|
+
}
|
|
265
|
+
|
|
278
266
|
export function updateStatusWidget(ctx: ExtensionContext): void {
|
|
279
267
|
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
268
|
const line = formatExecutionStatusLine(execution);
|
|
286
269
|
ctx.ui.setStatus("pi-plans", ctx.ui.theme.fg("accent", line));
|
|
287
270
|
return;
|
|
@@ -326,35 +309,23 @@ function persist(pi: ExtensionAPI): void {
|
|
|
326
309
|
items: execution.items,
|
|
327
310
|
startedAt: execution.startedAt,
|
|
328
311
|
usage: execution.usage,
|
|
329
|
-
panel: snapshotPanelState(execution),
|
|
330
312
|
implItems: execution.implItems,
|
|
313
|
+
implStatus: execution.implStatus,
|
|
314
|
+
currentI: execution.currentI,
|
|
331
315
|
});
|
|
332
316
|
}
|
|
333
317
|
|
|
334
|
-
export function syncExecutionPanel(ctx: ExtensionContext): void {
|
|
335
|
-
if (!execution) {
|
|
336
|
-
clearExecutionPanel(ctx);
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
refreshExecutionPanel(ctx, execution);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
318
|
export async function startExecution(
|
|
343
319
|
pi: ExtensionAPI,
|
|
344
320
|
ctx: ExtensionContext,
|
|
345
321
|
planPath: string,
|
|
346
322
|
items: CheckItem[],
|
|
347
|
-
modelState?: ExecutionModelState,
|
|
348
323
|
implItems?: ImplItem[],
|
|
349
324
|
): 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
|
|
325
|
+
execution = { planPath, items, startedAt: utcNow(), usage: { inToks: 0, outToks: 0 }, implItems: implItems ?? [], implStatus: {} };
|
|
353
326
|
pendingExecutionFlush = false; // fresh run: no inherited flush debt
|
|
327
|
+
resetExecutionCompactionState(ctx);
|
|
354
328
|
persist(pi);
|
|
355
|
-
if (modelState) {
|
|
356
|
-
await ensureExecutionModelActive(pi, ctx);
|
|
357
|
-
}
|
|
358
329
|
const active = readActive(ctx.cwd);
|
|
359
330
|
if (active) {
|
|
360
331
|
try {
|
|
@@ -372,54 +343,22 @@ export async function startExecution(
|
|
|
372
343
|
{ triggerTurn: false },
|
|
373
344
|
);
|
|
374
345
|
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
346
|
}
|
|
407
347
|
|
|
408
348
|
/** Record one assistant turn: accumulate usage and mark any completed items. */
|
|
409
349
|
export function recordExecutionTurn(
|
|
410
350
|
pi: ExtensionAPI,
|
|
411
|
-
|
|
412
|
-
|
|
351
|
+
_ctx: ExtensionContext,
|
|
352
|
+
_completedIds: string[],
|
|
413
353
|
usage?: { input: number; output: number },
|
|
414
|
-
):
|
|
415
|
-
if (!execution) return
|
|
354
|
+
): void {
|
|
355
|
+
if (!execution) return;
|
|
416
356
|
if (usage) {
|
|
417
357
|
execution.usage.inToks += usage.input;
|
|
418
358
|
execution.usage.outToks += usage.output;
|
|
419
359
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
return summary;
|
|
360
|
+
requestExecutionFlush(pi, _ctx);
|
|
361
|
+
updateStatusWidget(_ctx);
|
|
423
362
|
}
|
|
424
363
|
|
|
425
364
|
export function registerExecutionTurnHandlers(
|
|
@@ -448,11 +387,12 @@ export function registerExecutionTurnHandlers(
|
|
|
448
387
|
.join("\n");
|
|
449
388
|
const changedIds = applyDoneMarkers(text);
|
|
450
389
|
const changedImpls = applyImplMarkers(text);
|
|
390
|
+
const changedCurrentI = applyCurrentIMarker(text);
|
|
451
391
|
const projection = (event.message as { usage?: { input?: number; output?: number } }).usage;
|
|
452
392
|
const raw = projection ?? lastAssistantUsage;
|
|
453
393
|
lastAssistantUsage = null; // consumed: never re-attribute a stale turn
|
|
454
394
|
const usage = raw ? { input: raw.input ?? 0, output: raw.output ?? 0 } : undefined;
|
|
455
|
-
if (usage || changedIds.length > 0 || changedImpls.length > 0) {
|
|
395
|
+
if (usage || changedIds.length > 0 || changedImpls.length > 0 || changedCurrentI) {
|
|
456
396
|
// Attribute this turn's usage now; `[DONE]` markers still only mark completion.
|
|
457
397
|
recordExecutionTurn(pi, ctx, changedIds, usage);
|
|
458
398
|
}
|
|
@@ -558,64 +498,264 @@ ${currentLines.length ? currentLines.join("\n") : "- (no transcript captured)"}`
|
|
|
558
498
|
return sections;
|
|
559
499
|
}
|
|
560
500
|
|
|
561
|
-
|
|
562
|
-
if (!
|
|
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")}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return sections;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function executionSummaryParts(
|
|
556
|
+
event: SessionBeforeCompactEvent,
|
|
557
|
+
ctx: ExtensionContext,
|
|
558
|
+
firstKeptEntryId: string,
|
|
559
|
+
boundaryIndex: number,
|
|
560
|
+
plan: ReturnType<typeof planIAwareCompaction> | null,
|
|
561
|
+
): { parts: string[]; details: CompactionDetailsLike } {
|
|
562
|
+
if (!execution) return { parts: [], details: {} };
|
|
563
563
|
const active = readActive(ctx.cwd);
|
|
564
564
|
const run = active ? getRun(ctx.cwd, active.run_id) : null;
|
|
565
|
-
const
|
|
566
|
-
const boundaryIndex = event.branchEntries.findIndex((entry) => entry.id === firstKeptEntryId);
|
|
565
|
+
const branchEntries = event.branchEntries as unknown as CompactionEntryLike[];
|
|
567
566
|
const summaryEntries = boundaryIndex >= 0
|
|
568
|
-
? (
|
|
569
|
-
: (
|
|
567
|
+
? (branchEntries.slice(0, boundaryIndex) as CompactBranchEntry[])
|
|
568
|
+
: (branchEntries as CompactBranchEntry[]);
|
|
570
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 },
|
|
592
|
+
reason: event.reason,
|
|
593
|
+
willRetry: event.willRetry,
|
|
594
|
+
finishedItems: execution.items.filter((item) => item.done).map((item) => item.id),
|
|
595
|
+
});
|
|
571
596
|
const parts: string[] = [];
|
|
572
597
|
if (event.customInstructions?.trim()) {
|
|
573
|
-
parts.push(`## Compact Instructions
|
|
574
|
-
${compactText(event.customInstructions, 1000)}`);
|
|
598
|
+
parts.push(`## Compact Instructions\n${compactText(event.customInstructions, 1000)}`);
|
|
575
599
|
}
|
|
576
|
-
parts.push(`## Plan Before This Run
|
|
577
|
-
- Request: ${compactText(run?.request_text ?? execution.planPath, 280)}
|
|
578
|
-
- Plan file: \`${execution.planPath}\``);
|
|
600
|
+
parts.push(`## Plan Before This Run\n- Request: ${compactText(run?.request_text ?? execution.planPath, 280)}\n- Plan file: \`${execution.planPath}\``);
|
|
579
601
|
if (event.preparation.previousSummary?.trim()) {
|
|
580
|
-
parts.push(`## Previous Compact Summary
|
|
581
|
-
${event.preparation.previousSummary.trim()}`);
|
|
602
|
+
parts.push(`## Previous Compact Summary\n${event.preparation.previousSummary.trim()}`);
|
|
582
603
|
}
|
|
583
|
-
parts.push(`##
|
|
584
|
-
${sections.length ? sections.join("
|
|
585
|
-
parts.push(`## Current
|
|
586
|
-
|
|
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
|
+
}
|
|
612
|
+
|
|
613
|
+
export function buildExecutionCompactionResult(event: SessionBeforeCompactEvent, ctx: ExtensionContext): CompactionResult | null {
|
|
614
|
+
if (!execution) return null;
|
|
615
|
+
const branchEntries = event.branchEntries as unknown as CompactionEntryLike[];
|
|
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);
|
|
587
622
|
return {
|
|
588
|
-
summary: parts.join("
|
|
623
|
+
summary: parts.join("\\n\\n"),
|
|
589
624
|
firstKeptEntryId,
|
|
590
625
|
tokensBefore: event.preparation.tokensBefore,
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
reason: event.reason,
|
|
594
|
-
willRetry: event.willRetry,
|
|
595
|
-
finishedItems: execution.items.filter((item) => item.done).map((item) => item.id),
|
|
596
|
-
},
|
|
626
|
+
estimatedTokensAfter: plan?.metrics.estimatedAfterTokens ?? undefined,
|
|
627
|
+
details,
|
|
597
628
|
};
|
|
598
629
|
}
|
|
599
630
|
|
|
600
|
-
|
|
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
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
export function handleExecutionBeforeCompact(
|
|
710
|
+
pi: ExtensionAPI,
|
|
711
|
+
ctx: ExtensionContext,
|
|
712
|
+
event: SessionBeforeCompactEvent,
|
|
713
|
+
): SessionBeforeCompactResult | Promise<SessionBeforeCompactResult | undefined> | undefined {
|
|
601
714
|
if (!execution) return undefined;
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
715
|
+
let fallback: CompactionResult | null;
|
|
716
|
+
try {
|
|
717
|
+
fallback = buildExecutionCompactionResult(event, ctx);
|
|
718
|
+
} catch (error) {
|
|
719
|
+
const state = executionCompactionState(ctx);
|
|
720
|
+
if (state) state.inFlight = false;
|
|
721
|
+
ctx.ui.notify(`pi-plans: compaction preparation failed; using Pi default compaction (${String(error)}).`, "warning");
|
|
722
|
+
return undefined;
|
|
723
|
+
}
|
|
724
|
+
if (!fallback) return undefined;
|
|
725
|
+
notifyHardFloor(ctx, fallback);
|
|
726
|
+
const registry = ctx.modelRegistry as unknown as { complete?: Function };
|
|
727
|
+
if (!ctx.model || typeof registry.complete !== "function") {
|
|
728
|
+
requestExecutionFlush(pi, ctx);
|
|
729
|
+
return { compaction: fallback };
|
|
730
|
+
}
|
|
731
|
+
return buildModelExecutionCompactionResult(event, ctx, fallback).then((compaction) => {
|
|
732
|
+
if (!compaction) return undefined;
|
|
733
|
+
requestExecutionFlush(pi, ctx);
|
|
734
|
+
return { compaction };
|
|
735
|
+
});
|
|
606
736
|
}
|
|
607
737
|
|
|
608
738
|
export function handleExecutionCompact(pi: ExtensionAPI, ctx: ExtensionContext, event: SessionCompactEvent): void {
|
|
609
739
|
if (!execution) return;
|
|
740
|
+
const state = ensureExecutionCompactionState(ctx);
|
|
741
|
+
state.inFlight = false;
|
|
742
|
+
state.lastAttemptReason = event.reason;
|
|
743
|
+
state.cooldownActive = true;
|
|
744
|
+
state.rearmPending = false;
|
|
745
|
+
state.lastSuccessfulAt = utcNow();
|
|
746
|
+
state.lastSuccessfulUsagePercent = ctx.getContextUsage()?.percent ?? state.lastSuccessfulUsagePercent;
|
|
610
747
|
if (!event.willRetry) {
|
|
748
|
+
state.resumeGuard = true;
|
|
611
749
|
pi.sendMessage(
|
|
612
750
|
{
|
|
613
751
|
customType: EXECUTION_RESUME_CUSTOM_TYPE,
|
|
614
|
-
content:
|
|
752
|
+
content: EXECUTION_COMPACTION_RESUME_MESSAGE,
|
|
615
753
|
display: false,
|
|
616
754
|
},
|
|
617
755
|
{ triggerTurn: true },
|
|
618
756
|
);
|
|
757
|
+
} else {
|
|
758
|
+
state.resumeGuard = false;
|
|
619
759
|
}
|
|
620
760
|
requestExecutionFlush(pi, ctx);
|
|
621
761
|
updateStatusWidget(ctx);
|
|
@@ -623,6 +763,19 @@ export function handleExecutionCompact(pi: ExtensionAPI, ctx: ExtensionContext,
|
|
|
623
763
|
|
|
624
764
|
export function handleExecutionCompactFailed(pi: ExtensionAPI, ctx: ExtensionContext, event: SessionCompactFailedEvent): void {
|
|
625
765
|
if (!execution) return;
|
|
766
|
+
const state = executionCompactionState(ctx);
|
|
767
|
+
const expectedThresholdCancel = event.reason === "threshold" && event.aborted && !state?.inFlight;
|
|
768
|
+
if (expectedThresholdCancel) {
|
|
769
|
+
if (state) state.lastAttemptReason = event.reason;
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (state) {
|
|
773
|
+
state.inFlight = false;
|
|
774
|
+
state.resumeGuard = false;
|
|
775
|
+
state.cooldownActive = false;
|
|
776
|
+
state.rearmPending = false;
|
|
777
|
+
state.lastAttemptReason = event.reason;
|
|
778
|
+
}
|
|
626
779
|
ctx.ui.notify(
|
|
627
780
|
`pi-plans: compaction failed (${event.reason}); execution remains active and will wait for the next eligible turn.`,
|
|
628
781
|
"warning",
|
|
@@ -673,9 +826,22 @@ function isPlanningInternalCustomType(customType?: string): boolean {
|
|
|
673
826
|
|| customType === PLANNING_RUN_START_CUSTOM_TYPE
|
|
674
827
|
|| customType === PLANNING_PLAN_WRITTEN_CUSTOM_TYPE
|
|
675
828
|
|| customType === PLANNING_RESUME_CUSTOM_TYPE
|
|
829
|
+
|| customType === AUTOCOMPLETE_ENTRY
|
|
676
830
|
);
|
|
677
831
|
}
|
|
678
832
|
|
|
833
|
+
function summarizePlanningEntryText(entry: PlanningBranchEntry): string {
|
|
834
|
+
return (entry.message?.content ?? [])
|
|
835
|
+
.filter((part) => part.type === "text")
|
|
836
|
+
.map((part) => part.text ?? "")
|
|
837
|
+
.join("\n")
|
|
838
|
+
.trim();
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function summarizePlanningEntryLine(entry: PlanningBranchEntry): string | null {
|
|
842
|
+
return summarizePlanningMessageLine(entry);
|
|
843
|
+
}
|
|
844
|
+
|
|
679
845
|
function summarizePlanningMessageLine(entry: PlanningBranchEntry): string | null {
|
|
680
846
|
if (!entry.message) return null;
|
|
681
847
|
const text = (entry.message.content ?? [])
|
|
@@ -743,12 +909,41 @@ function resolvePlanningCompactionContext(workdir: string): { runId: string; art
|
|
|
743
909
|
return { runId: run.run_id, artifactDir: run.artifact_dir };
|
|
744
910
|
}
|
|
745
911
|
|
|
912
|
+
function planningCurrentIUsage(ctx: ExtensionContext): { tokens: number; contextWindow: number; eligible?: boolean } | null {
|
|
913
|
+
const usage = ctx.getContextUsage();
|
|
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
|
+
}
|
|
746
941
|
export function shouldTriggerPlanningCompaction(ctx: ExtensionContext): boolean {
|
|
747
942
|
if (getExecution()) return false;
|
|
748
943
|
const ctxWorkdir = ctx.cwd;
|
|
749
944
|
if (!resolvePlanningCompactionContext(ctxWorkdir)) return false;
|
|
750
|
-
const
|
|
751
|
-
if (
|
|
945
|
+
const currentUsage = planningCurrentIUsage(ctx);
|
|
946
|
+
if (!currentUsage || currentUsage.eligible === false || !currentIExceedsTrigger(currentUsage.tokens, currentUsage.contextWindow)) return false;
|
|
752
947
|
const session = ctx.sessionManager as unknown as { __planningCompaction?: PlanningCompactionState };
|
|
753
948
|
const state = session.__planningCompaction;
|
|
754
949
|
if (!state) return true;
|
|
@@ -786,7 +981,12 @@ export function requestPlanningCompaction(ctx: ExtensionContext): void {
|
|
|
786
981
|
if (state.inFlight || state.resumeGuard) return;
|
|
787
982
|
state.inFlight = true;
|
|
788
983
|
state.lastAttemptReason = "threshold";
|
|
789
|
-
|
|
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
|
+
}
|
|
790
990
|
}
|
|
791
991
|
|
|
792
992
|
export function buildPlanningCompactionResult(event: SessionBeforeCompactEvent, ctx: ExtensionContext): CompactionResult | null {
|
|
@@ -794,8 +994,26 @@ export function buildPlanningCompactionResult(event: SessionBeforeCompactEvent,
|
|
|
794
994
|
const planningCtx = resolvePlanningCompactionContext(ctxWorkdir);
|
|
795
995
|
if (!planningCtx) return null;
|
|
796
996
|
const branchEntries = event.branchEntries as unknown as PlanningBranchEntry[];
|
|
797
|
-
const
|
|
798
|
-
const
|
|
997
|
+
const legacyCut = findPlanningCutEntryId(branchEntries, event.preparation.firstKeptEntryId);
|
|
998
|
+
const markerIds = branchEntries.flatMap((entry) => scanCurrentIMarkers(summarizePlanningEntryText(entry)));
|
|
999
|
+
const currentI = markerIds.at(-1)?.id
|
|
1000
|
+
?? branchEntries.map((entry) => compactionCurrentI(entry)).filter((id): id is string => !!id).at(-1)
|
|
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[]);
|
|
799
1017
|
const parts: string[] = [];
|
|
800
1018
|
if (event.customInstructions?.trim()) {
|
|
801
1019
|
parts.push(`## Compact Instructions
|
|
@@ -818,22 +1036,40 @@ ${compactText(planningCtx.runId, 80)} — keep current planning progress.`);
|
|
|
818
1036
|
- Current planning question or open decision.
|
|
819
1037
|
|
|
820
1038
|
### Blocked
|
|
821
|
-
- ${hasMarker ? "None" : "Planning cut-point marker missing; falling back to default."}`);
|
|
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
|
+
}
|
|
822
1051
|
if (previousSummary) {
|
|
823
|
-
parts.push(`## Previous Compact Summary
|
|
824
|
-
${previousSummary}`);
|
|
1052
|
+
parts.push(`## Previous Compact Summary\n${previousSummary}`);
|
|
825
1053
|
}
|
|
826
1054
|
parts.push(`## Next Steps
|
|
827
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,
|
|
1064
|
+
reason: event.reason,
|
|
1065
|
+
hasMarker: legacyCut.hasMarker,
|
|
1066
|
+
});
|
|
828
1067
|
return {
|
|
829
1068
|
summary: parts.join("\n\n"),
|
|
830
1069
|
firstKeptEntryId,
|
|
831
1070
|
tokensBefore: event.preparation.tokensBefore,
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
reason: event.reason,
|
|
835
|
-
hasMarker,
|
|
836
|
-
},
|
|
1071
|
+
estimatedTokensAfter: iPlan?.metrics.estimatedAfterTokens ?? undefined,
|
|
1072
|
+
details,
|
|
837
1073
|
};
|
|
838
1074
|
}
|
|
839
1075
|
|
|
@@ -864,11 +1100,19 @@ export function handlePlanningBeforeCompact(
|
|
|
864
1100
|
if (percent !== null && percent < 85) {
|
|
865
1101
|
state.cooldownActive = false;
|
|
866
1102
|
}
|
|
867
|
-
|
|
1103
|
+
let compaction: CompactionResult | null;
|
|
1104
|
+
try {
|
|
1105
|
+
compaction = buildPlanningCompactionResult(event, ctx);
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
state.inFlight = false;
|
|
1108
|
+
ctx.ui.notify(`pi-plans: planning compaction preparation failed; using Pi default compaction (${String(error)}).`, "warning");
|
|
1109
|
+
return undefined;
|
|
1110
|
+
}
|
|
868
1111
|
if (!compaction) {
|
|
869
1112
|
state.inFlight = false;
|
|
870
1113
|
return undefined;
|
|
871
1114
|
}
|
|
1115
|
+
notifyHardFloor(ctx, compaction);
|
|
872
1116
|
return { compaction };
|
|
873
1117
|
}
|
|
874
1118
|
|
|
@@ -921,41 +1165,12 @@ export function filterPlanningResumeMessages<T extends { customType?: string }>(
|
|
|
921
1165
|
return messages.filter((message) => message.customType !== PLANNING_RESUME_CUSTOM_TYPE);
|
|
922
1166
|
}
|
|
923
1167
|
|
|
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
1168
|
export async function stopExecution(pi: ExtensionAPI, ctx: ExtensionContext, reason: string): Promise<void> {
|
|
953
1169
|
if (!execution) return;
|
|
954
|
-
|
|
1170
|
+
resetExecutionCompactionState(ctx);
|
|
955
1171
|
// Final synchronous write: drain any deferred flush and land the last snapshot.
|
|
956
1172
|
pendingExecutionFlush = false;
|
|
957
1173
|
persist(pi);
|
|
958
|
-
clearExecutionPanel(ctx);
|
|
959
1174
|
execution = null;
|
|
960
1175
|
pi.appendEntry("pi-plans-exec-cleared", { reason });
|
|
961
1176
|
pi.sendMessage(
|
|
@@ -1010,19 +1225,28 @@ export function applyImplMarkers(text: string): string[] {
|
|
|
1010
1225
|
return changed;
|
|
1011
1226
|
}
|
|
1012
1227
|
|
|
1228
|
+
export function applyCurrentIMarker(text: string): boolean {
|
|
1229
|
+
if (!execution?.implItems?.length) return false;
|
|
1230
|
+
const markers = scanCurrentIMarkers(text);
|
|
1231
|
+
const resolved = resolveCurrentI(execution.implItems, markers, execution.currentI);
|
|
1232
|
+
if (!resolved || resolved === execution.currentI) return false;
|
|
1233
|
+
execution.currentI = resolved;
|
|
1234
|
+
return true;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1013
1237
|
export function isExecutionComplete(): boolean {
|
|
1014
1238
|
return execution !== null && execution.items.length > 0 && execution.items.every((item) => item.done);
|
|
1015
1239
|
}
|
|
1016
1240
|
|
|
1017
1241
|
export async function completeExecution(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
1018
1242
|
if (!execution) return;
|
|
1019
|
-
|
|
1243
|
+
resetExecutionCompactionState(ctx);
|
|
1020
1244
|
// Final synchronous write: drain any deferred flush and land the last snapshot.
|
|
1021
1245
|
pendingExecutionFlush = false;
|
|
1022
1246
|
persist(pi);
|
|
1247
|
+
|
|
1023
1248
|
const summary = execution.items.map((item) => `- ✅ \`${item.id}\` ${item.text.split(";")[0]}`).join("\n");
|
|
1024
1249
|
const planPath = execution.planPath;
|
|
1025
|
-
clearExecutionPanel(ctx);
|
|
1026
1250
|
execution = null;
|
|
1027
1251
|
pi.appendEntry("pi-plans-exec-cleared", { reason: "complete" });
|
|
1028
1252
|
pi.sendMessage(
|
|
@@ -1050,15 +1274,19 @@ export function executionContextMessage(): string | null {
|
|
|
1050
1274
|
const remaining = execution.items.filter((item) => !item.done);
|
|
1051
1275
|
const list =
|
|
1052
1276
|
remaining.map((item) => `- \`${item.id}\` ${item.text}`).join("\n") || "(none — report completion now)";
|
|
1277
|
+
const implementationItems = execution.implItems?.length
|
|
1278
|
+
? `\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
|
+
: "";
|
|
1053
1280
|
return `[PI-PLANS EXECUTION — write access enabled]
|
|
1054
1281
|
Implement the accepted plan at ${execution.planPath} (${execution.items.length - remaining.length}/${execution.items.length} verifier items done).
|
|
1055
1282
|
|
|
1056
1283
|
Remaining verifier items:
|
|
1057
|
-
${list}
|
|
1284
|
+
${list}${implementationItems}
|
|
1058
1285
|
|
|
1059
1286
|
Execution rules:
|
|
1060
1287
|
- 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
|
|
1288
|
+
- 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.
|
|
1289
|
+
- 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
1290
|
- Simplest implementation that fully meets the item: no speculative abstractions, configuration, or indirection; keep components modular with clearly separated concerns.
|
|
1063
1291
|
- 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
1292
|
- 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 +1309,7 @@ interface SessionEntry {
|
|
|
1081
1309
|
*/
|
|
1082
1310
|
export async function restoreFromSession(pi: ExtensionAPI, ctx: ExtensionContext, entries: SessionEntry[]): Promise<void> {
|
|
1083
1311
|
pendingExecutionFlush = false; // no flush debt survives a restart
|
|
1312
|
+
resetExecutionCompactionState(ctx);
|
|
1084
1313
|
let snapshotIndex = -1;
|
|
1085
1314
|
let snapshot: ExecState | null = null;
|
|
1086
1315
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
@@ -1093,21 +1322,18 @@ export async function restoreFromSession(pi: ExtensionAPI, ctx: ExtensionContext
|
|
|
1093
1322
|
if (entry.type === "custom" && entry.customType === "pi-plans-exec-cleared") {
|
|
1094
1323
|
// Execution was explicitly stopped or completed after the last snapshot.
|
|
1095
1324
|
execution = null;
|
|
1096
|
-
syncExecutionPanel(ctx);
|
|
1097
1325
|
updateStatusWidget(ctx);
|
|
1098
1326
|
return;
|
|
1099
1327
|
}
|
|
1100
1328
|
}
|
|
1101
1329
|
if (!snapshot) {
|
|
1102
1330
|
execution = null;
|
|
1103
|
-
syncExecutionPanel(ctx);
|
|
1104
1331
|
updateStatusWidget(ctx);
|
|
1105
1332
|
return;
|
|
1106
1333
|
}
|
|
1107
1334
|
// Ignore stale plans whose file vanished.
|
|
1108
1335
|
if (!fs.existsSync(snapshot.planPath)) {
|
|
1109
1336
|
execution = null;
|
|
1110
|
-
syncExecutionPanel(ctx);
|
|
1111
1337
|
updateStatusWidget(ctx);
|
|
1112
1338
|
return;
|
|
1113
1339
|
}
|
|
@@ -1116,16 +1342,14 @@ export async function restoreFromSession(pi: ExtensionAPI, ctx: ExtensionContext
|
|
|
1116
1342
|
items: snapshot.items.map((item) => ({ ...item })),
|
|
1117
1343
|
startedAt: snapshot.startedAt,
|
|
1118
1344
|
usage: snapshot.usage ?? { inToks: 0, outToks: 0 },
|
|
1119
|
-
panel: executionPanelFromEntryData(snapshot.panel) ?? createExecutionPanelState(),
|
|
1120
|
-
modelState: snapshot.modelState,
|
|
1121
1345
|
implItems: snapshot.implItems ?? [],
|
|
1122
1346
|
implStatus: { ...(snapshot.implStatus ?? {}) },
|
|
1347
|
+
currentI: snapshot.currentI ?? inferCurrentI(snapshot.implItems, snapshot.items, snapshot.implStatus),
|
|
1123
1348
|
};
|
|
1124
1349
|
for (let i = snapshotIndex + 1; i < entries.length; i++) {
|
|
1125
1350
|
const entry = entries[i];
|
|
1126
1351
|
if (entry.type === "custom" && entry.customType === "pi-plans-exec-cleared") {
|
|
1127
1352
|
execution = null;
|
|
1128
|
-
syncExecutionPanel(ctx);
|
|
1129
1353
|
break;
|
|
1130
1354
|
}
|
|
1131
1355
|
const message = entry.message;
|
|
@@ -1136,6 +1360,7 @@ export async function restoreFromSession(pi: ExtensionAPI, ctx: ExtensionContext
|
|
|
1136
1360
|
.join("\n");
|
|
1137
1361
|
applyDoneMarkers(text);
|
|
1138
1362
|
applyImplMarkers(text);
|
|
1363
|
+
applyCurrentIMarker(text);
|
|
1139
1364
|
}
|
|
1140
1365
|
}
|
|
1141
1366
|
if (execution) {
|
|
@@ -1145,6 +1370,5 @@ export async function restoreFromSession(pi: ExtensionAPI, ctx: ExtensionContext
|
|
|
1145
1370
|
await completeExecution(pi, ctx);
|
|
1146
1371
|
}
|
|
1147
1372
|
}
|
|
1148
|
-
syncExecutionPanel(ctx);
|
|
1149
1373
|
updateStatusWidget(ctx);
|
|
1150
1374
|
}
|