pi-pier 0.2.6 → 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 +20 -7
- package/package.json +7 -2
- package/schemas/efficiency-config.schema.json +77 -0
- package/skills/pi-herdr-workflow/SKILL.md +13 -1
- package/src/ansi-text.ts +83 -0
- package/src/ask-multi.ts +260 -0
- package/src/ask-user.ts +458 -0
- package/src/bootstrap.ts +5 -2
- package/src/compact-coordinator.ts +454 -0
- package/src/compact-economics-core.ts +396 -0
- package/src/config-catalog-core.ts +452 -0
- package/src/config-command.ts +181 -0
- package/src/config-guide.ts +361 -0
- package/src/core/grid-shape.ts +4 -2
- package/src/core/heat-plan.ts +160 -0
- package/src/core/observation.ts +555 -0
- package/src/core/subagent.ts +187 -393
- package/src/core/terminal.ts +270 -47
- package/src/core/todo.ts +49 -14
- package/src/dashboard-command.ts +89 -0
- package/src/efficiency-config-core.ts +661 -0
- package/src/efficiency-config-io.ts +53 -0
- package/src/efficiency-store.ts +312 -0
- package/src/focus-poller.ts +244 -0
- package/src/gc-core.ts +85 -0
- package/src/git-adapter.ts +12 -3
- package/src/herdr-client.ts +292 -36
- package/src/index-locks.ts +27 -9
- package/src/index-master.ts +5 -0
- package/src/index-notices.ts +13 -2
- package/src/index-pipe.ts +5 -0
- package/src/index-worker.ts +5 -0
- package/src/index.ts +360 -60
- package/src/jev-client.ts +146 -0
- package/src/jev-core.ts +381 -0
- package/src/lock-core.ts +105 -16
- package/src/observation-core.ts +248 -0
- package/src/pier-options.ts +96 -0
- package/src/pipe-channel.ts +52 -7
- package/src/reducer-core.ts +358 -0
- package/src/reducer-invoker.ts +392 -0
- package/src/renderers.ts +198 -0
- package/src/role-loader.ts +2 -1
- package/src/role-manifest.ts +0 -4
- package/src/runtime-policy.ts +5 -8
- package/src/session-tail.ts +60 -5
- package/src/settle-wake-core.ts +3 -1
- package/src/slim-frame.ts +60 -3
- package/src/storage-layout.ts +23 -0
- package/src/subagent-core.ts +3 -1
- package/src/subagent-execute.ts +328 -0
- package/src/subagent-gc.ts +33 -13
- package/src/subagent-launch.ts +4 -0
- package/src/subagent-list-action.ts +69 -0
- package/src/subagent-output-action.ts +104 -0
- package/src/subagent-output-adapter.ts +27 -0
- package/src/subagent-output-core.ts +263 -0
- package/src/subagent-poll-loop.ts +59 -39
- package/src/subagent-poller.ts +46 -0
- package/src/subagent-ready.ts +89 -0
- package/src/subagent-registry.ts +70 -0
- package/src/subagent-resolution.ts +41 -0
- package/src/subagent-scope.ts +3 -1
- package/src/subagent-session-io.ts +115 -32
- package/src/subagent-spawn.ts +153 -8
- package/src/swallow.ts +75 -0
- package/src/terminal-core.ts +161 -10
- package/src/todo-core.ts +5 -4
- package/src/todo-reminder-core.ts +8 -1
- package/src/todos-service.ts +18 -6
- package/src/tool-error.ts +26 -0
- package/src/vocab.ts +7 -4
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D100 Online Context Compact Coordinator.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates:
|
|
5
|
+
* - Tracking todo completion boundaries (filtered to source === 'tool').
|
|
6
|
+
* - Evaluating KV-cache economics & window protection on turn_end.
|
|
7
|
+
* - Guarding against ejecting queued messages (hasPendingMessages check).
|
|
8
|
+
* - Aborting turn and executing Pi native compaction with custom instructions.
|
|
9
|
+
* - Feeding remaining tasks forward and injecting continuation turn.
|
|
10
|
+
* - Persisting state to session entries across branches and restores.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
14
|
+
import {
|
|
15
|
+
decideCompaction,
|
|
16
|
+
DEFAULT_COMPACTION_ECONOMICS,
|
|
17
|
+
estimateRemainingRequests,
|
|
18
|
+
MAX_BOUNDARY_SAMPLES,
|
|
19
|
+
nativeCompactionFeasible,
|
|
20
|
+
resolveCacheRatioFromCost,
|
|
21
|
+
type CompactionDecision,
|
|
22
|
+
} from './compact-economics-core.ts';
|
|
23
|
+
import type { TodoItem } from './todo-core.ts';
|
|
24
|
+
import type { TodoCompletionSource } from './todos-service.ts';
|
|
25
|
+
import type { OnlineContextCompactConfig } from './efficiency-config-core.ts';
|
|
26
|
+
import {
|
|
27
|
+
appendEfficiencyLog,
|
|
28
|
+
efficiencyLogPath,
|
|
29
|
+
resolveSessionRoot,
|
|
30
|
+
} from './efficiency-store.ts';
|
|
31
|
+
|
|
32
|
+
export const COMPACT_STATE_CUSTOM_TYPE = 'pi-herdr.efficiency-state';
|
|
33
|
+
export const COMPACTION_CONTINUE_TYPE = 'pi-herdr.compaction-continue';
|
|
34
|
+
export const DEFAULT_KEEP_RECENT_TOKENS = 20_000;
|
|
35
|
+
export const DEFAULT_MEMO_TOKENS = 1_000;
|
|
36
|
+
|
|
37
|
+
export interface CoordinatorState {
|
|
38
|
+
version: 1;
|
|
39
|
+
epoch: number;
|
|
40
|
+
completedBoundaryRequestCounts: number[];
|
|
41
|
+
carriedDebtTokens: number;
|
|
42
|
+
cacheDebtRepaymentTokens: number;
|
|
43
|
+
priorCompactionCount: number;
|
|
44
|
+
positiveContextDeltaTotal: number;
|
|
45
|
+
positiveContextDeltaCount: number;
|
|
46
|
+
lastContextTokens: number | null;
|
|
47
|
+
currentBoundaryRequestCount: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function initialCoordinatorState(): CoordinatorState {
|
|
51
|
+
return {
|
|
52
|
+
version: 1,
|
|
53
|
+
epoch: 0,
|
|
54
|
+
completedBoundaryRequestCounts: [],
|
|
55
|
+
carriedDebtTokens: 0,
|
|
56
|
+
cacheDebtRepaymentTokens: 0,
|
|
57
|
+
priorCompactionCount: 0,
|
|
58
|
+
positiveContextDeltaTotal: 0,
|
|
59
|
+
positiveContextDeltaCount: 0,
|
|
60
|
+
lastContextTokens: null,
|
|
61
|
+
currentBoundaryRequestCount: 0,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function restoreCoordinatorState(entries: readonly unknown[]): CoordinatorState {
|
|
66
|
+
if (!Array.isArray(entries)) return initialCoordinatorState();
|
|
67
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
68
|
+
const entry = entries[i] as { type?: string; customType?: string; data?: unknown };
|
|
69
|
+
if (entry && entry.type === 'custom' && entry.customType === COMPACT_STATE_CUSTOM_TYPE) {
|
|
70
|
+
const d = entry.data as Partial<CoordinatorState>;
|
|
71
|
+
if (d && d.version === 1 && Array.isArray(d.completedBoundaryRequestCounts)) {
|
|
72
|
+
return {
|
|
73
|
+
version: 1,
|
|
74
|
+
epoch: typeof d.epoch === 'number' ? d.epoch : 0,
|
|
75
|
+
completedBoundaryRequestCounts: [...d.completedBoundaryRequestCounts],
|
|
76
|
+
carriedDebtTokens: typeof d.carriedDebtTokens === 'number' ? d.carriedDebtTokens : 0,
|
|
77
|
+
cacheDebtRepaymentTokens: typeof d.cacheDebtRepaymentTokens === 'number' ? d.cacheDebtRepaymentTokens : 0,
|
|
78
|
+
priorCompactionCount: typeof d.priorCompactionCount === 'number' ? d.priorCompactionCount : 0,
|
|
79
|
+
positiveContextDeltaTotal: typeof d.positiveContextDeltaTotal === 'number' ? d.positiveContextDeltaTotal : 0,
|
|
80
|
+
positiveContextDeltaCount: typeof d.positiveContextDeltaCount === 'number' ? d.positiveContextDeltaCount : 0,
|
|
81
|
+
lastContextTokens: typeof d.lastContextTokens === 'number' ? d.lastContextTokens : null,
|
|
82
|
+
currentBoundaryRequestCount: typeof d.currentBoundaryRequestCount === 'number' ? d.currentBoundaryRequestCount : 0,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return initialCoordinatorState();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class CompactCoordinator {
|
|
91
|
+
state: CoordinatorState = initialCoordinatorState();
|
|
92
|
+
compactionInFlight = false;
|
|
93
|
+
intentionalAbort = false;
|
|
94
|
+
selectedCompaction: CompactionDecision | null = null;
|
|
95
|
+
pendingBoundaryCompleted = false;
|
|
96
|
+
|
|
97
|
+
recordBoundaryCompleted(count: number, source?: TodoCompletionSource): void {
|
|
98
|
+
// Only count completed transitions originating from model tool calls
|
|
99
|
+
if (source !== 'tool' || count <= 0) return;
|
|
100
|
+
this.pendingBoundaryCompleted = true;
|
|
101
|
+
if (this.state.currentBoundaryRequestCount > 0) {
|
|
102
|
+
this.state.completedBoundaryRequestCounts.push(this.state.currentBoundaryRequestCount);
|
|
103
|
+
if (this.state.completedBoundaryRequestCounts.length > MAX_BOUNDARY_SAMPLES) {
|
|
104
|
+
this.state.completedBoundaryRequestCounts.splice(
|
|
105
|
+
0,
|
|
106
|
+
this.state.completedBoundaryRequestCounts.length - MAX_BOUNDARY_SAMPLES,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
this.state.currentBoundaryRequestCount = 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
onBeforeProviderRequest(contextTokens: number): void {
|
|
114
|
+
this.state.currentBoundaryRequestCount++;
|
|
115
|
+
if (this.state.carriedDebtTokens > 0 && this.state.cacheDebtRepaymentTokens > 0) {
|
|
116
|
+
this.state.carriedDebtTokens = Math.max(
|
|
117
|
+
0,
|
|
118
|
+
this.state.carriedDebtTokens - this.state.cacheDebtRepaymentTokens,
|
|
119
|
+
);
|
|
120
|
+
if (this.state.carriedDebtTokens === 0) {
|
|
121
|
+
this.state.cacheDebtRepaymentTokens = 0;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (this.state.lastContextTokens !== null && contextTokens > this.state.lastContextTokens) {
|
|
125
|
+
this.state.positiveContextDeltaTotal += contextTokens - this.state.lastContextTokens;
|
|
126
|
+
this.state.positiveContextDeltaCount++;
|
|
127
|
+
}
|
|
128
|
+
this.state.lastContextTokens = contextTokens;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
onInput(event: { text?: string; source?: string; streamingBehavior?: string }): void {
|
|
132
|
+
// Ignore internal messages dispatched by extensions (D96 reminders, pipe injections, followUps)
|
|
133
|
+
if (event.source === 'extension' || event.text?.startsWith('CORRECTION:')) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const fromHuman =
|
|
138
|
+
event.source === 'interactive' ||
|
|
139
|
+
event.source === 'rpc' ||
|
|
140
|
+
event.source === undefined;
|
|
141
|
+
|
|
142
|
+
// If human typed a new prompt or steer directive, reset expectations (correction)
|
|
143
|
+
if (fromHuman) {
|
|
144
|
+
this.selectedCompaction = null;
|
|
145
|
+
this.intentionalAbort = false;
|
|
146
|
+
this.pendingBoundaryCompleted = false;
|
|
147
|
+
// completedBoundaryRequestCounts deliberately survives: it measures
|
|
148
|
+
// requests-per-boundary pacing, which holds across human turns. Clearing it
|
|
149
|
+
// zeroed the mean and pinned expectedRemainingRequests at 1 after every
|
|
150
|
+
// prompt, so OCC never saw a horizon worth compacting under (2026-09-17
|
|
151
|
+
// review: compact.jsonl epoch kept climbing 1→9 with zero compactions).
|
|
152
|
+
this.state.carriedDebtTokens = 0;
|
|
153
|
+
this.state.epoch++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
onTurnEnd(opts: {
|
|
158
|
+
ctx: ExtensionContext;
|
|
159
|
+
todos: readonly TodoItem[];
|
|
160
|
+
config: OnlineContextCompactConfig;
|
|
161
|
+
cancelReminder?: () => void;
|
|
162
|
+
}): void {
|
|
163
|
+
const boundary = this.pendingBoundaryCompleted;
|
|
164
|
+
this.pendingBoundaryCompleted = false;
|
|
165
|
+
|
|
166
|
+
if (!boundary || this.selectedCompaction) return;
|
|
167
|
+
if (!opts.config.enabled) return;
|
|
168
|
+
|
|
169
|
+
// Safety guard: if user typed something that is queued, don't abort!
|
|
170
|
+
if (opts.ctx.hasPendingMessages?.()) return;
|
|
171
|
+
|
|
172
|
+
const usage = opts.ctx.getContextUsage?.();
|
|
173
|
+
const contextTokens =
|
|
174
|
+
typeof usage?.tokens === 'number' && usage.tokens > 0
|
|
175
|
+
? usage.tokens
|
|
176
|
+
: this.state.lastContextTokens ?? 0;
|
|
177
|
+
const contextWindowTokens = usage?.contextWindow ?? opts.ctx.model?.contextWindow ?? null;
|
|
178
|
+
const fixedTokens = Math.ceil(Buffer.byteLength(opts.ctx.getSystemPrompt?.() ?? '') / 4);
|
|
179
|
+
const keepRecentTokens = opts.config.keepRecentTokens ?? DEFAULT_KEEP_RECENT_TOKENS;
|
|
180
|
+
const archiveTokens = Math.max(0, contextTokens - fixedTokens - keepRecentTokens);
|
|
181
|
+
const remainingBoundaries = opts.todos.filter(
|
|
182
|
+
(it) => it.status !== 'completed' && it.status !== 'abandoned',
|
|
183
|
+
).length;
|
|
184
|
+
|
|
185
|
+
const averageContextTokenIncrement =
|
|
186
|
+
this.state.positiveContextDeltaCount > 0
|
|
187
|
+
? this.state.positiveContextDeltaTotal / this.state.positiveContextDeltaCount
|
|
188
|
+
: null;
|
|
189
|
+
const cacheRatio = resolveCacheRatioFromCost(
|
|
190
|
+
opts.config.cacheWriteReadRatio,
|
|
191
|
+
opts.ctx.model?.cost,
|
|
192
|
+
{ provider: opts.ctx.model?.provider, modelId: opts.ctx.model?.id },
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const decision = decideCompaction({
|
|
196
|
+
writeTokens: contextTokens,
|
|
197
|
+
archiveTokens,
|
|
198
|
+
memoTokens: DEFAULT_MEMO_TOKENS,
|
|
199
|
+
contextTokens,
|
|
200
|
+
completedBoundaryRequestCounts: this.state.completedBoundaryRequestCounts,
|
|
201
|
+
remainingBoundaries,
|
|
202
|
+
averageContextTokenIncrement,
|
|
203
|
+
contextWindowTokens,
|
|
204
|
+
priorCompactionCount: this.state.priorCompactionCount,
|
|
205
|
+
carriedDebtTokens: this.state.carriedDebtTokens,
|
|
206
|
+
cacheDebtRepaymentTokens: this.state.cacheDebtRepaymentTokens,
|
|
207
|
+
cacheWriteReadRatio: cacheRatio,
|
|
208
|
+
economics: {
|
|
209
|
+
...DEFAULT_COMPACTION_ECONOMICS,
|
|
210
|
+
firstCompactionRequestScale: opts.config.firstCompactionRequestScale,
|
|
211
|
+
subsequentCompactionMargin: opts.config.subsequentCompactionMargin,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (decision.compact) {
|
|
216
|
+
const branch = opts.ctx.sessionManager?.getBranch?.() ?? [];
|
|
217
|
+
const keepRecentTokens = opts.config.keepRecentTokens ?? DEFAULT_KEEP_RECENT_TOKENS;
|
|
218
|
+
const feasible = nativeCompactionFeasible(branch, keepRecentTokens);
|
|
219
|
+
if (!feasible) {
|
|
220
|
+
if (opts.config.logEnabled) {
|
|
221
|
+
this.logDecision(opts.ctx, { ...decision, compact: false, reason: 'native_not_compactable' });
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.selectedCompaction = decision;
|
|
227
|
+
this.intentionalAbort = true;
|
|
228
|
+
opts.cancelReminder?.();
|
|
229
|
+
opts.ctx.abort();
|
|
230
|
+
} else if (opts.config.logEnabled) {
|
|
231
|
+
this.logDecision(opts.ctx, decision);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async onAgentSettled(opts: {
|
|
236
|
+
ctx: ExtensionContext;
|
|
237
|
+
todos: readonly TodoItem[];
|
|
238
|
+
pi: ExtensionAPI;
|
|
239
|
+
config: OnlineContextCompactConfig;
|
|
240
|
+
cancelReminder?: () => void;
|
|
241
|
+
onBeforeCompact?: (sessionRoot: string, ctx: ExtensionContext) => Promise<void>;
|
|
242
|
+
}): Promise<void> {
|
|
243
|
+
if (!this.selectedCompaction || !opts.ctx.isIdle()) return;
|
|
244
|
+
const decision = this.selectedCompaction;
|
|
245
|
+
this.selectedCompaction = null;
|
|
246
|
+
|
|
247
|
+
opts.cancelReminder?.();
|
|
248
|
+
this.compactionInFlight = true;
|
|
249
|
+
|
|
250
|
+
const sessionDir = opts.ctx.sessionManager?.getSessionDir?.();
|
|
251
|
+
const sessionId = opts.ctx.sessionManager?.getSessionId?.();
|
|
252
|
+
const sessionRoot = resolveSessionRoot(sessionDir, sessionId);
|
|
253
|
+
|
|
254
|
+
if (sessionRoot && opts.onBeforeCompact) {
|
|
255
|
+
try {
|
|
256
|
+
await opts.onBeforeCompact(sessionRoot, opts.ctx);
|
|
257
|
+
} catch {
|
|
258
|
+
/* ignore batch packing errors before compaction */
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const remaining = opts.todos.filter(
|
|
263
|
+
(it) => it.status !== 'completed' && it.status !== 'abandoned',
|
|
264
|
+
);
|
|
265
|
+
const taskLines = remaining
|
|
266
|
+
.map((it) => `- [${it.status}] ${it.content}${it.blocker ? ` (waiting on: ${it.blocker})` : ''}`)
|
|
267
|
+
.join('\n');
|
|
268
|
+
|
|
269
|
+
const customInstructions = [
|
|
270
|
+
'Preserve completed work, verification results, important decisions, and remaining work.',
|
|
271
|
+
remaining.length > 0 ? `Active/remaining tasks to preserve:\n${taskLines}` : 'All listed tasks are completed.',
|
|
272
|
+
].join('\n\n');
|
|
273
|
+
|
|
274
|
+
const startMs = Date.now();
|
|
275
|
+
await new Promise<void>((resolve) => {
|
|
276
|
+
let settled = false;
|
|
277
|
+
const finish = () => {
|
|
278
|
+
if (!settled) {
|
|
279
|
+
settled = true;
|
|
280
|
+
resolve();
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
opts.ctx.compact({
|
|
285
|
+
customInstructions,
|
|
286
|
+
onComplete: (compaction) => {
|
|
287
|
+
try {
|
|
288
|
+
this.state.priorCompactionCount++;
|
|
289
|
+
const incrementalRatio = decision.incrementalCacheCostRatio ?? 0;
|
|
290
|
+
this.state.carriedDebtTokens = decision.writeTokens * incrementalRatio;
|
|
291
|
+
this.state.cacheDebtRepaymentTokens = Math.max(0, decision.archiveTokens - decision.memoTokens);
|
|
292
|
+
this.state.epoch++;
|
|
293
|
+
|
|
294
|
+
opts.pi.appendEntry(COMPACT_STATE_CUSTOM_TYPE, this.state);
|
|
295
|
+
|
|
296
|
+
if (opts.config.logEnabled) {
|
|
297
|
+
const sessionDir = opts.ctx.sessionManager?.getSessionDir?.();
|
|
298
|
+
const sessionId = opts.ctx.sessionManager?.getSessionId?.();
|
|
299
|
+
const root = resolveSessionRoot(sessionDir, sessionId);
|
|
300
|
+
if (root) {
|
|
301
|
+
const logPath = efficiencyLogPath(root, 'compact');
|
|
302
|
+
void appendEfficiencyLog(logPath, {
|
|
303
|
+
schema: 'pier-efficiency/1',
|
|
304
|
+
mechanism: 'onlineContextCompact',
|
|
305
|
+
ts: new Date().toISOString(),
|
|
306
|
+
sessionId: sessionId ?? 'unknown',
|
|
307
|
+
epoch: this.state.epoch,
|
|
308
|
+
decision: decision.reason,
|
|
309
|
+
writeTokens: decision.writeTokens,
|
|
310
|
+
archiveTokens: decision.archiveTokens,
|
|
311
|
+
savedTokens: Math.max(0, decision.archiveTokens - decision.memoTokens),
|
|
312
|
+
breakevenRequests: decision.breakevenRequests,
|
|
313
|
+
expectedRemainingRequests: decision.expectedRemainingRequests,
|
|
314
|
+
carriedDebtTokens: this.state.carriedDebtTokens,
|
|
315
|
+
summaryTokens: typeof compaction?.summary === 'string' ? Math.ceil(compaction.summary.length / 4) : 0,
|
|
316
|
+
priorCompactionCount: this.state.priorCompactionCount,
|
|
317
|
+
durationMs: Date.now() - startMs,
|
|
318
|
+
}).catch(() => {});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
opts.pi.sendMessage(
|
|
324
|
+
{
|
|
325
|
+
customType: COMPACTION_CONTINUE_TYPE,
|
|
326
|
+
content: 'Online context compaction finished. Active tasks preserved. Continue working on remaining tasks.',
|
|
327
|
+
display: false,
|
|
328
|
+
},
|
|
329
|
+
{ triggerTurn: true },
|
|
330
|
+
);
|
|
331
|
+
} catch {
|
|
332
|
+
/* ignore continuation failure */
|
|
333
|
+
}
|
|
334
|
+
} finally {
|
|
335
|
+
this.compactionInFlight = false;
|
|
336
|
+
this.intentionalAbort = false;
|
|
337
|
+
finish();
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
onError: (error) => {
|
|
341
|
+
this.compactionInFlight = false;
|
|
342
|
+
this.intentionalAbort = false;
|
|
343
|
+
|
|
344
|
+
// `Compaction cancelled` / AbortError mean the user (or pi) cancelled on purpose.
|
|
345
|
+
// Telemetry must keep those apart from real failures, and a cancelled compaction
|
|
346
|
+
// must NOT be answered with a turn-triggering continuation.
|
|
347
|
+
const cancelled =
|
|
348
|
+
error instanceof Error &&
|
|
349
|
+
(error.name === 'AbortError' || error.message === 'Compaction cancelled');
|
|
350
|
+
|
|
351
|
+
if (opts.config.logEnabled) {
|
|
352
|
+
const sessionDir = opts.ctx.sessionManager?.getSessionDir?.();
|
|
353
|
+
const sessionId = opts.ctx.sessionManager?.getSessionId?.();
|
|
354
|
+
const root = resolveSessionRoot(sessionDir, sessionId);
|
|
355
|
+
if (root) {
|
|
356
|
+
const logPath = efficiencyLogPath(root, 'compact');
|
|
357
|
+
void appendEfficiencyLog(logPath, {
|
|
358
|
+
schema: 'pier-efficiency/1',
|
|
359
|
+
mechanism: 'onlineContextCompact',
|
|
360
|
+
event: 'compaction-failed',
|
|
361
|
+
ts: new Date().toISOString(),
|
|
362
|
+
sessionId: sessionId ?? 'unknown',
|
|
363
|
+
epoch: this.state.epoch,
|
|
364
|
+
decision: decision.reason,
|
|
365
|
+
cancelled,
|
|
366
|
+
error: error instanceof Error ? error.message : String(error),
|
|
367
|
+
durationMs: Date.now() - startMs,
|
|
368
|
+
}).catch(() => {});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (cancelled) {
|
|
373
|
+
finish();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Resume the task: OCC aborted the turn on purpose, so a failed compaction would
|
|
378
|
+
// otherwise leave the session parked on an aborted assistant message. Pi's own
|
|
379
|
+
// threshold compaction stays available as the safety net.
|
|
380
|
+
try {
|
|
381
|
+
opts.pi.sendMessage(
|
|
382
|
+
{
|
|
383
|
+
customType: COMPACTION_CONTINUE_TYPE,
|
|
384
|
+
content: 'Context compaction failed. Continuing with the current context.',
|
|
385
|
+
display: false,
|
|
386
|
+
},
|
|
387
|
+
{ triggerTurn: true },
|
|
388
|
+
);
|
|
389
|
+
} catch {
|
|
390
|
+
/* ignore continuation failure */
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
finish();
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
rebuildFromBranch(entries: readonly unknown[]): void {
|
|
400
|
+
this.state = restoreCoordinatorState(entries);
|
|
401
|
+
this.selectedCompaction = null;
|
|
402
|
+
this.compactionInFlight = false;
|
|
403
|
+
this.intentionalAbort = false;
|
|
404
|
+
this.pendingBoundaryCompleted = false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
getRemainingHorizon(remainingBoundaries = 3, contextWindowTokens: number | null = null): number {
|
|
408
|
+
const counts = this.state.completedBoundaryRequestCounts;
|
|
409
|
+
if (counts.length === 0) return 4;
|
|
410
|
+
|
|
411
|
+
const est = estimateRemainingRequests({
|
|
412
|
+
completedBoundaryRequestCounts: counts,
|
|
413
|
+
remainingBoundaries,
|
|
414
|
+
scale: 1.0,
|
|
415
|
+
standardDeviationK: 0.0,
|
|
416
|
+
contextTokens: this.state.lastContextTokens ?? 0,
|
|
417
|
+
contextWindowTokens,
|
|
418
|
+
averageContextTokenIncrement:
|
|
419
|
+
this.state.positiveContextDeltaCount > 0
|
|
420
|
+
? this.state.positiveContextDeltaTotal / this.state.positiveContextDeltaCount
|
|
421
|
+
: null,
|
|
422
|
+
});
|
|
423
|
+
return Math.max(1, est.expectedRemainingRequests);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private logDecision(ctx: ExtensionContext, decision: CompactionDecision): void {
|
|
427
|
+
const sessionDir = ctx.sessionManager?.getSessionDir?.();
|
|
428
|
+
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
429
|
+
const root = resolveSessionRoot(sessionDir, sessionId);
|
|
430
|
+
if (!root) return;
|
|
431
|
+
const logPath = efficiencyLogPath(root, 'compact');
|
|
432
|
+
void appendEfficiencyLog(logPath, {
|
|
433
|
+
schema: 'pier-efficiency/1',
|
|
434
|
+
mechanism: 'onlineContextCompact',
|
|
435
|
+
event: 'decision',
|
|
436
|
+
ts: new Date().toISOString(),
|
|
437
|
+
sessionId: sessionId ?? 'unknown',
|
|
438
|
+
epoch: this.state.epoch,
|
|
439
|
+
decision: decision.reason,
|
|
440
|
+
compact: decision.compact,
|
|
441
|
+
writeTokens: decision.writeTokens,
|
|
442
|
+
archiveTokens: decision.archiveTokens,
|
|
443
|
+
breakevenRequests: decision.breakevenRequests,
|
|
444
|
+
expectedRemainingRequests: decision.expectedRemainingRequests,
|
|
445
|
+
// 2026-09-17: without resolvedRatio/model the 61x cache_ratio_unavailable
|
|
446
|
+
// run was un-diagnosable from logs alone (which price table? which family?).
|
|
447
|
+
resolvedRatio: decision.cacheWriteReadRatio,
|
|
448
|
+
incrementalCacheCostRatio: decision.incrementalCacheCostRatio,
|
|
449
|
+
remainingBoundaries: decision.remainingBoundaries,
|
|
450
|
+
model: ctx.model?.id ?? undefined,
|
|
451
|
+
provider: ctx.model?.provider ?? undefined,
|
|
452
|
+
}).catch(() => {});
|
|
453
|
+
}
|
|
454
|
+
}
|