@lll9p/pi-better-compaction 0.2.1 → 0.5.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 +103 -132
- package/README.zh-CN.md +162 -0
- package/package.json +7 -2
- package/src/compact-client-v2.ts +428 -0
- package/src/compact-client.ts +1 -63
- package/src/config.ts +38 -0
- package/src/extension-runtime.ts +247 -17
- package/src/midrun.ts +229 -0
- package/src/native-fallback.ts +21 -3
- package/src/retained-messages.ts +98 -0
- package/src/runtime.ts +41 -2
- package/src/shared-headers.ts +103 -0
- package/src/types.ts +30 -4
package/src/extension-runtime.ts
CHANGED
|
@@ -6,15 +6,18 @@ import type {
|
|
|
6
6
|
SessionBeforeCompactEvent,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { executeNativeCompaction } from "./compact-client";
|
|
9
|
+
import { executeV2Compaction } from "./compact-client-v2";
|
|
9
10
|
import { loadExtensionConfig } from "./config";
|
|
10
11
|
import { writeDebugArtifact } from "./debug";
|
|
11
12
|
import { resolveLatestNativeCompactionEntry } from "./details-store";
|
|
13
|
+
import { registerMidRunGuard } from "./midrun";
|
|
12
14
|
import { runNativeFallbackCompaction } from "./native-fallback";
|
|
13
15
|
import {
|
|
14
16
|
rewriteResponsesPayloadWithNativeReplay,
|
|
15
17
|
serializeLiveTailToResponsesInput,
|
|
16
18
|
} from "./payload-rewrite";
|
|
17
19
|
import { getCompactionRequestExtras, rememberRequestContext } from "./request-context-cache";
|
|
20
|
+
import { buildRetainedMessages } from "./retained-messages";
|
|
18
21
|
import {
|
|
19
22
|
isResponsesCompatiblePayload,
|
|
20
23
|
resolveNativeCompactionEnvironment,
|
|
@@ -26,6 +29,8 @@ import {
|
|
|
26
29
|
createNativeCompactionResult,
|
|
27
30
|
EXTENSION_ID,
|
|
28
31
|
isNativeCompactionDetails,
|
|
32
|
+
NATIVE_COMPACTION_STRATEGY,
|
|
33
|
+
NATIVE_COMPACTION_STRATEGY_V2,
|
|
29
34
|
type ExtensionConfig,
|
|
30
35
|
type NativeCompactionDetails,
|
|
31
36
|
type NativeCompactionRequestMeta,
|
|
@@ -36,6 +41,20 @@ type ResponsesCompactOutcome =
|
|
|
36
41
|
| { outcome: "aborted" }
|
|
37
42
|
| { outcome: "failed" };
|
|
38
43
|
|
|
44
|
+
export type ExtensionRuntimeDependencies = {
|
|
45
|
+
loadExtensionConfig: typeof loadExtensionConfig;
|
|
46
|
+
executeNativeCompaction: typeof executeNativeCompaction;
|
|
47
|
+
executeV2Compaction: typeof executeV2Compaction;
|
|
48
|
+
runNativeFallbackCompaction: typeof runNativeFallbackCompaction;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const DEFAULT_DEPENDENCIES: ExtensionRuntimeDependencies = {
|
|
52
|
+
loadExtensionConfig,
|
|
53
|
+
executeNativeCompaction,
|
|
54
|
+
executeV2Compaction,
|
|
55
|
+
runNativeFallbackCompaction,
|
|
56
|
+
};
|
|
57
|
+
|
|
39
58
|
function buildCompactionRequestMeta(event: SessionBeforeCompactEvent): NativeCompactionRequestMeta {
|
|
40
59
|
return {
|
|
41
60
|
tokensBefore: event.preparation.tokensBefore,
|
|
@@ -87,14 +106,15 @@ function buildCompactionInstructions(systemPrompt: string, customInstructions?:
|
|
|
87
106
|
return systemPrompt;
|
|
88
107
|
}
|
|
89
108
|
|
|
90
|
-
return `${systemPrompt}\n\nAdditional
|
|
109
|
+
return `${systemPrompt}\n\nAdditional compaction guidance:\n${guidance}`;
|
|
91
110
|
}
|
|
92
111
|
|
|
93
|
-
async function
|
|
112
|
+
async function runResponsesV1Compact(
|
|
94
113
|
event: SessionBeforeCompactEvent,
|
|
95
114
|
ctx: ExtensionContext,
|
|
96
115
|
config: ExtensionConfig,
|
|
97
116
|
runtime: NativeCompactionRuntime,
|
|
117
|
+
dependencies: ExtensionRuntimeDependencies,
|
|
98
118
|
): Promise<ResponsesCompactOutcome> {
|
|
99
119
|
const instructions = buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions);
|
|
100
120
|
const branchEntries = ctx.sessionManager.getBranch();
|
|
@@ -135,7 +155,7 @@ async function runResponsesNativeCompact(
|
|
|
135
155
|
writeDebugArtifact(
|
|
136
156
|
"compaction-event",
|
|
137
157
|
{
|
|
138
|
-
event: "session_before_compact.
|
|
158
|
+
event: "session_before_compact.v1-compact-skip",
|
|
139
159
|
reason: latestNativeCompaction.reason,
|
|
140
160
|
provider: runtime.provider,
|
|
141
161
|
api: runtime.api,
|
|
@@ -157,7 +177,7 @@ async function runResponsesNativeCompact(
|
|
|
157
177
|
request = { ...request, ...extras };
|
|
158
178
|
}
|
|
159
179
|
|
|
160
|
-
const compactResult = await executeNativeCompaction({
|
|
180
|
+
const compactResult = await dependencies.executeNativeCompaction({
|
|
161
181
|
runtime,
|
|
162
182
|
request,
|
|
163
183
|
signal: event.signal,
|
|
@@ -169,7 +189,7 @@ async function runResponsesNativeCompact(
|
|
|
169
189
|
writeDebugArtifact(
|
|
170
190
|
"compaction-event",
|
|
171
191
|
{
|
|
172
|
-
event: "session_before_compact.
|
|
192
|
+
event: "session_before_compact.v1-compact-failure",
|
|
173
193
|
reason: compactResult.reason,
|
|
174
194
|
status: compactResult.status,
|
|
175
195
|
errorMessage: compactResult.errorMessage,
|
|
@@ -196,7 +216,7 @@ async function runResponsesNativeCompact(
|
|
|
196
216
|
writeDebugArtifact(
|
|
197
217
|
"compaction-event",
|
|
198
218
|
{
|
|
199
|
-
event: "session_before_compact.invalid-native-details",
|
|
219
|
+
event: "session_before_compact.v1-invalid-native-details",
|
|
200
220
|
reason: error instanceof Error ? error.message : String(error),
|
|
201
221
|
provider: runtime.provider,
|
|
202
222
|
api: runtime.api,
|
|
@@ -219,7 +239,7 @@ async function runResponsesNativeCompact(
|
|
|
219
239
|
writeDebugArtifact(
|
|
220
240
|
"compaction-event",
|
|
221
241
|
{
|
|
222
|
-
event: "session_before_compact.
|
|
242
|
+
event: "session_before_compact.v1-compact-success",
|
|
223
243
|
provider: runtime.provider,
|
|
224
244
|
api: runtime.api,
|
|
225
245
|
model: runtime.model,
|
|
@@ -238,8 +258,171 @@ async function runResponsesNativeCompact(
|
|
|
238
258
|
return { outcome: "success", compaction };
|
|
239
259
|
}
|
|
240
260
|
|
|
241
|
-
|
|
242
|
-
|
|
261
|
+
/**
|
|
262
|
+
* V2 compaction: stream a Responses request with compaction_trigger appended.
|
|
263
|
+
* On success, returns retained messages + encrypted compaction blob.
|
|
264
|
+
*/
|
|
265
|
+
async function runResponsesV2Compact(
|
|
266
|
+
event: SessionBeforeCompactEvent,
|
|
267
|
+
ctx: ExtensionContext,
|
|
268
|
+
config: ExtensionConfig,
|
|
269
|
+
runtime: NativeCompactionRuntime,
|
|
270
|
+
dependencies: ExtensionRuntimeDependencies,
|
|
271
|
+
): Promise<ResponsesCompactOutcome> {
|
|
272
|
+
const instructions = buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions);
|
|
273
|
+
const branchEntries = ctx.sessionManager.getBranch();
|
|
274
|
+
const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
|
|
275
|
+
provider: runtime.provider,
|
|
276
|
+
api: runtime.api,
|
|
277
|
+
model: runtime.model,
|
|
278
|
+
baseUrl: runtime.baseUrl,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
let requestSource: "session-context" | "non-native-session-context" | "latest-native-replay";
|
|
282
|
+
let request: NativeCompactionRequestBody;
|
|
283
|
+
if (latestNativeCompaction.ok) {
|
|
284
|
+
const liveTailEntries = branchEntries.slice(latestNativeCompaction.index + 1);
|
|
285
|
+
requestSource = "latest-native-replay";
|
|
286
|
+
const input: ResponsesInputItem[] = [
|
|
287
|
+
...(cloneOpaqueWindow(latestNativeCompaction.entry.details.compactedWindow) as ResponsesInputItem[]),
|
|
288
|
+
...serializeLiveTailToResponsesInput({ model: runtime.currentModel, entries: liveTailEntries }),
|
|
289
|
+
];
|
|
290
|
+
request = {
|
|
291
|
+
model: runtime.currentModel.id,
|
|
292
|
+
input,
|
|
293
|
+
instructions,
|
|
294
|
+
};
|
|
295
|
+
} else if (
|
|
296
|
+
latestNativeCompaction.reason === "no-compaction" ||
|
|
297
|
+
(latestNativeCompaction.reason === "latest-compaction-not-native" &&
|
|
298
|
+
config.allowCompactionContinuityBreak)
|
|
299
|
+
) {
|
|
300
|
+
requestSource =
|
|
301
|
+
latestNativeCompaction.reason === "no-compaction" ? "session-context" : "non-native-session-context";
|
|
302
|
+
request = serializeMessagesToCompactRequest({
|
|
303
|
+
model: runtime.currentModel,
|
|
304
|
+
messages: ctx.sessionManager.buildSessionContext().messages,
|
|
305
|
+
instructions,
|
|
306
|
+
});
|
|
307
|
+
} else {
|
|
308
|
+
writeDebugArtifact(
|
|
309
|
+
"compaction-event",
|
|
310
|
+
{
|
|
311
|
+
event: "session_before_compact.v2-compact-skip",
|
|
312
|
+
reason: latestNativeCompaction.reason,
|
|
313
|
+
provider: runtime.provider,
|
|
314
|
+
api: runtime.api,
|
|
315
|
+
model: runtime.model,
|
|
316
|
+
baseUrl: runtime.baseUrl,
|
|
317
|
+
latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
|
|
318
|
+
latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
|
|
319
|
+
},
|
|
320
|
+
config,
|
|
321
|
+
ctx,
|
|
322
|
+
);
|
|
323
|
+
return { outcome: "failed" };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const extras = getCompactionRequestExtras(runtime.model, getSessionId(ctx));
|
|
327
|
+
if (extras) {
|
|
328
|
+
request = { ...request, ...extras };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const v2Result = await dependencies.executeV2Compaction({
|
|
332
|
+
runtime,
|
|
333
|
+
request,
|
|
334
|
+
signal: event.signal,
|
|
335
|
+
settings: config,
|
|
336
|
+
context: ctx,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
if (!v2Result.ok) {
|
|
340
|
+
writeDebugArtifact(
|
|
341
|
+
"compaction-event",
|
|
342
|
+
{
|
|
343
|
+
event: "session_before_compact.v2-compact-failure",
|
|
344
|
+
reason: v2Result.reason,
|
|
345
|
+
status: v2Result.status,
|
|
346
|
+
errorMessage: v2Result.errorMessage,
|
|
347
|
+
},
|
|
348
|
+
config,
|
|
349
|
+
ctx,
|
|
350
|
+
);
|
|
351
|
+
return v2Result.reason === "aborted" ? { outcome: "aborted" } : { outcome: "failed" };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Build compacted window: retained messages + compaction blob.
|
|
355
|
+
const retainedMessages = buildRetainedMessages(request.input);
|
|
356
|
+
const compactedWindow = [...retainedMessages, v2Result.compactionItem];
|
|
357
|
+
|
|
358
|
+
let details: NativeCompactionDetails;
|
|
359
|
+
try {
|
|
360
|
+
details = createNativeCompactionDetails(
|
|
361
|
+
{
|
|
362
|
+
provider: runtime.provider,
|
|
363
|
+
api: runtime.api,
|
|
364
|
+
model: runtime.model,
|
|
365
|
+
baseUrl: runtime.baseUrl,
|
|
366
|
+
compactedWindow,
|
|
367
|
+
compactResponseId: v2Result.responseId,
|
|
368
|
+
createdAt: v2Result.createdAt,
|
|
369
|
+
requestMeta: buildCompactionRequestMeta(event),
|
|
370
|
+
},
|
|
371
|
+
NATIVE_COMPACTION_STRATEGY_V2,
|
|
372
|
+
);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
writeDebugArtifact(
|
|
375
|
+
"compaction-event",
|
|
376
|
+
{
|
|
377
|
+
event: "session_before_compact.v2-invalid-native-details",
|
|
378
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
379
|
+
provider: runtime.provider,
|
|
380
|
+
api: runtime.api,
|
|
381
|
+
model: runtime.model,
|
|
382
|
+
baseUrl: runtime.baseUrl,
|
|
383
|
+
},
|
|
384
|
+
config,
|
|
385
|
+
ctx,
|
|
386
|
+
);
|
|
387
|
+
return { outcome: "failed" };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// V2 blob is encrypted; no summary text can be extracted.
|
|
391
|
+
const compaction = createNativeCompactionResult({
|
|
392
|
+
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
393
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
394
|
+
details,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
writeDebugArtifact(
|
|
398
|
+
"compaction-event",
|
|
399
|
+
{
|
|
400
|
+
event: "session_before_compact.v2-compact-success",
|
|
401
|
+
provider: runtime.provider,
|
|
402
|
+
api: runtime.api,
|
|
403
|
+
model: runtime.model,
|
|
404
|
+
requestSource,
|
|
405
|
+
requestInputItems: request.input.length,
|
|
406
|
+
requestExtras: extras ? Object.keys(extras) : [],
|
|
407
|
+
compactResponseId: v2Result.responseId,
|
|
408
|
+
retainedMessageCount: retainedMessages.length,
|
|
409
|
+
compactedItems: compactedWindow.length,
|
|
410
|
+
usage: v2Result.usage,
|
|
411
|
+
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
412
|
+
},
|
|
413
|
+
config,
|
|
414
|
+
ctx,
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
return { outcome: "success", compaction };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function handleSessionBeforeCompact(
|
|
421
|
+
event: SessionBeforeCompactEvent,
|
|
422
|
+
ctx: ExtensionContext,
|
|
423
|
+
dependencies: ExtensionRuntimeDependencies,
|
|
424
|
+
) {
|
|
425
|
+
const { config } = dependencies.loadExtensionConfig();
|
|
243
426
|
if (!config.enabled) {
|
|
244
427
|
return undefined;
|
|
245
428
|
}
|
|
@@ -271,7 +454,14 @@ async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx:
|
|
|
271
454
|
responsesCompactApis: config.responsesCompactApis,
|
|
272
455
|
});
|
|
273
456
|
if (resolution.ok) {
|
|
274
|
-
|
|
457
|
+
let responsesOutcome: ResponsesCompactOutcome;
|
|
458
|
+
|
|
459
|
+
if (config.compactionVersion === "v2") {
|
|
460
|
+
responsesOutcome = await runResponsesV2Compact(event, ctx, config, resolution.runtime, dependencies);
|
|
461
|
+
} else {
|
|
462
|
+
responsesOutcome = await runResponsesV1Compact(event, ctx, config, resolution.runtime, dependencies);
|
|
463
|
+
}
|
|
464
|
+
|
|
275
465
|
if (responsesOutcome.outcome === "success") {
|
|
276
466
|
return { compaction: responsesOutcome.compaction };
|
|
277
467
|
}
|
|
@@ -296,7 +486,12 @@ async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx:
|
|
|
296
486
|
}
|
|
297
487
|
|
|
298
488
|
// Branch 2: run pi's native compaction method with the configured model.
|
|
299
|
-
const fallback = await runNativeFallbackCompaction({
|
|
489
|
+
const fallback = await dependencies.runNativeFallbackCompaction({
|
|
490
|
+
ctx,
|
|
491
|
+
event,
|
|
492
|
+
config,
|
|
493
|
+
sessionId: getSessionId(ctx),
|
|
494
|
+
});
|
|
300
495
|
if (fallback.ok) {
|
|
301
496
|
if (ctx.hasUI) {
|
|
302
497
|
ctx.ui.notify(
|
|
@@ -309,6 +504,7 @@ async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx:
|
|
|
309
504
|
{
|
|
310
505
|
event: "session_before_compact.fallback-success",
|
|
311
506
|
model: fallback.model,
|
|
507
|
+
usage: fallback.usage,
|
|
312
508
|
},
|
|
313
509
|
config,
|
|
314
510
|
ctx,
|
|
@@ -344,8 +540,12 @@ async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx:
|
|
|
344
540
|
return undefined;
|
|
345
541
|
}
|
|
346
542
|
|
|
347
|
-
async function handleBeforeProviderRequest(
|
|
348
|
-
|
|
543
|
+
async function handleBeforeProviderRequest(
|
|
544
|
+
event: BeforeProviderRequestEvent,
|
|
545
|
+
ctx: ExtensionContext,
|
|
546
|
+
dependencies: ExtensionRuntimeDependencies,
|
|
547
|
+
) {
|
|
548
|
+
const { config } = dependencies.loadExtensionConfig();
|
|
349
549
|
if (!config.enabled) {
|
|
350
550
|
return undefined;
|
|
351
551
|
}
|
|
@@ -468,9 +668,14 @@ async function handleBeforeProviderRequest(event: BeforeProviderRequestEvent, ct
|
|
|
468
668
|
return rewrite.rewrittenPayload;
|
|
469
669
|
}
|
|
470
670
|
|
|
471
|
-
export
|
|
671
|
+
export function registerExtensionRuntime(
|
|
672
|
+
pi: ExtensionAPI,
|
|
673
|
+
dependencies: ExtensionRuntimeDependencies = DEFAULT_DEPENDENCIES,
|
|
674
|
+
): void {
|
|
675
|
+
registerMidRunGuard(pi, dependencies.loadExtensionConfig);
|
|
676
|
+
|
|
472
677
|
pi.on("session_start", (_event, ctx) => {
|
|
473
|
-
const { config, source, warnings } = loadExtensionConfig();
|
|
678
|
+
const { config, source, warnings } = dependencies.loadExtensionConfig();
|
|
474
679
|
if (!config.enabled) return;
|
|
475
680
|
|
|
476
681
|
if (warnings.length > 0 && ctx.hasUI && config.debug) {
|
|
@@ -499,6 +704,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
499
704
|
}
|
|
500
705
|
});
|
|
501
706
|
|
|
502
|
-
pi.on("session_before_compact",
|
|
503
|
-
|
|
707
|
+
pi.on("session_before_compact", (event, ctx) =>
|
|
708
|
+
handleSessionBeforeCompact(event, ctx, dependencies),
|
|
709
|
+
);
|
|
710
|
+
pi.on("before_provider_request", (event, ctx) =>
|
|
711
|
+
handleBeforeProviderRequest(event, ctx, dependencies),
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
pi.on("session_compact_failed", (event, ctx) => {
|
|
715
|
+
const { config } = dependencies.loadExtensionConfig();
|
|
716
|
+
if (!config.enabled) return;
|
|
717
|
+
|
|
718
|
+
writeDebugArtifact(
|
|
719
|
+
"compaction-event",
|
|
720
|
+
{
|
|
721
|
+
event: "session_compact_failed",
|
|
722
|
+
reason: event.reason,
|
|
723
|
+
errorMessage: event.errorMessage,
|
|
724
|
+
aborted: event.aborted,
|
|
725
|
+
willRetry: event.willRetry,
|
|
726
|
+
fromExtension: event.fromExtension,
|
|
727
|
+
},
|
|
728
|
+
config,
|
|
729
|
+
ctx,
|
|
730
|
+
);
|
|
731
|
+
});
|
|
504
732
|
}
|
|
733
|
+
|
|
734
|
+
export default registerExtensionRuntime;
|
package/src/midrun.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { loadExtensionConfig } from "./config";
|
|
3
|
+
import { writeDebugArtifact } from "./debug";
|
|
4
|
+
import { EXTENSION_ID, type LoadedExtensionConfig } from "./types";
|
|
5
|
+
|
|
6
|
+
type MidRunPhase = "idle" | "abort-pending" | "compacting" | "resume-pending" | "failed";
|
|
7
|
+
|
|
8
|
+
type MidRunState = {
|
|
9
|
+
phase: MidRunPhase;
|
|
10
|
+
generation: number;
|
|
11
|
+
sessionId?: string;
|
|
12
|
+
baselineCompactionId?: string;
|
|
13
|
+
triggerTokens?: number;
|
|
14
|
+
triggerPercent?: number;
|
|
15
|
+
triggerContextWindow?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type ConfigLoader = () => LoadedExtensionConfig;
|
|
19
|
+
|
|
20
|
+
const RESUME_CUSTOM_TYPE = "pi-better-compaction-midrun-resume";
|
|
21
|
+
const RESUME_PROMPT = `[pi-better-compaction/midrun]
|
|
22
|
+
Context compaction completed. Continue the interrupted task from the exact point where execution stopped.`;
|
|
23
|
+
const MIDRUN_COMPACTION_INSTRUCTIONS =
|
|
24
|
+
"Preserve the active task, completed work, decisions, changed files, failures, current tool-loop state, and exact next steps so execution can resume immediately after compaction.";
|
|
25
|
+
|
|
26
|
+
function getSessionId(ctx: ExtensionContext): string | undefined {
|
|
27
|
+
try {
|
|
28
|
+
return ctx.sessionManager.getSessionId();
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getLatestCompactionId(ctx: ExtensionContext): string | undefined {
|
|
35
|
+
const branch = ctx.sessionManager.getBranch();
|
|
36
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
37
|
+
const entry = branch[index];
|
|
38
|
+
if (entry?.type === "compaction") return entry.id;
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resetState(state: MidRunState): void {
|
|
44
|
+
state.phase = "idle";
|
|
45
|
+
state.generation += 1;
|
|
46
|
+
state.sessionId = undefined;
|
|
47
|
+
state.baselineCompactionId = undefined;
|
|
48
|
+
state.triggerTokens = undefined;
|
|
49
|
+
state.triggerPercent = undefined;
|
|
50
|
+
state.triggerContextWindow = undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sameSession(state: MidRunState, ctx: ExtensionContext): boolean {
|
|
54
|
+
return state.sessionId !== undefined && state.sessionId === getSessionId(ctx);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function notifyFailure(ctx: ExtensionContext, message: string): void {
|
|
58
|
+
if (ctx.hasUI) {
|
|
59
|
+
ctx.ui.notify(`${EXTENSION_ID}: ${message}`, "error");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function scheduleResume(
|
|
64
|
+
pi: ExtensionAPI,
|
|
65
|
+
ctx: ExtensionContext,
|
|
66
|
+
state: MidRunState,
|
|
67
|
+
generation: number,
|
|
68
|
+
): void {
|
|
69
|
+
state.phase = "resume-pending";
|
|
70
|
+
|
|
71
|
+
setImmediate(() => {
|
|
72
|
+
if (
|
|
73
|
+
state.generation !== generation ||
|
|
74
|
+
state.phase !== "resume-pending" ||
|
|
75
|
+
!sameSession(state, ctx)
|
|
76
|
+
) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) {
|
|
81
|
+
resetState(state);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
resetState(state);
|
|
86
|
+
pi.sendMessage(
|
|
87
|
+
{
|
|
88
|
+
customType: RESUME_CUSTOM_TYPE,
|
|
89
|
+
content: RESUME_PROMPT,
|
|
90
|
+
display: false,
|
|
91
|
+
details: { source: "midrun-compaction" },
|
|
92
|
+
},
|
|
93
|
+
{ triggerTurn: true },
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function registerMidRunGuard(
|
|
99
|
+
pi: ExtensionAPI,
|
|
100
|
+
loadConfig: ConfigLoader = loadExtensionConfig,
|
|
101
|
+
): void {
|
|
102
|
+
const state: MidRunState = { phase: "idle", generation: 0 };
|
|
103
|
+
|
|
104
|
+
pi.on("turn_end", (event, ctx) => {
|
|
105
|
+
const { config } = loadConfig();
|
|
106
|
+
if (!config.enabled || !config.midRun.enabled || state.phase !== "idle") return;
|
|
107
|
+
if (event.toolResults.length === 0 || ctx.hasPendingMessages()) return;
|
|
108
|
+
|
|
109
|
+
const usage = ctx.getContextUsage();
|
|
110
|
+
if (!usage || usage.tokens == null || usage.percent == null) return;
|
|
111
|
+
if (usage.percent < config.midRun.thresholdPercent) return;
|
|
112
|
+
|
|
113
|
+
const sessionId = getSessionId(ctx);
|
|
114
|
+
if (!sessionId) return;
|
|
115
|
+
|
|
116
|
+
state.phase = "abort-pending";
|
|
117
|
+
state.generation += 1;
|
|
118
|
+
state.sessionId = sessionId;
|
|
119
|
+
state.baselineCompactionId = getLatestCompactionId(ctx);
|
|
120
|
+
state.triggerTokens = usage.tokens;
|
|
121
|
+
state.triggerPercent = usage.percent;
|
|
122
|
+
state.triggerContextWindow = usage.contextWindow;
|
|
123
|
+
|
|
124
|
+
writeDebugArtifact(
|
|
125
|
+
"lifecycle",
|
|
126
|
+
{
|
|
127
|
+
event: "midrun.threshold",
|
|
128
|
+
turnIndex: event.turnIndex,
|
|
129
|
+
tokens: usage.tokens,
|
|
130
|
+
contextWindow: usage.contextWindow,
|
|
131
|
+
percent: usage.percent,
|
|
132
|
+
thresholdPercent: config.midRun.thresholdPercent,
|
|
133
|
+
baselineCompactionId: state.baselineCompactionId,
|
|
134
|
+
},
|
|
135
|
+
config,
|
|
136
|
+
ctx,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Never compact while the agent run is active; let Pi settle first.
|
|
140
|
+
ctx.abort();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
144
|
+
if (state.phase !== "abort-pending") return;
|
|
145
|
+
if (!sameSession(state, ctx)) {
|
|
146
|
+
resetState(state);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// An earlier agent_settled handler may already have started or queued another run.
|
|
151
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) {
|
|
152
|
+
resetState(state);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const generation = state.generation;
|
|
157
|
+
const latestCompactionId = getLatestCompactionId(ctx);
|
|
158
|
+
if (latestCompactionId !== state.baselineCompactionId) {
|
|
159
|
+
const { config } = loadConfig();
|
|
160
|
+
writeDebugArtifact(
|
|
161
|
+
"lifecycle",
|
|
162
|
+
{
|
|
163
|
+
event: "midrun.coalesced",
|
|
164
|
+
reason: "compaction-already-occurred-during-abort",
|
|
165
|
+
baselineCompactionId: state.baselineCompactionId,
|
|
166
|
+
latestCompactionId,
|
|
167
|
+
},
|
|
168
|
+
config,
|
|
169
|
+
ctx,
|
|
170
|
+
);
|
|
171
|
+
scheduleResume(pi, ctx, state, generation);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const { config } = loadConfig();
|
|
176
|
+
if (!config.enabled || !config.midRun.enabled) {
|
|
177
|
+
scheduleResume(pi, ctx, state, generation);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const usage = ctx.getContextUsage();
|
|
182
|
+
if (usage?.percent != null && usage.percent < config.midRun.thresholdPercent) {
|
|
183
|
+
scheduleResume(pi, ctx, state, generation);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
state.phase = "compacting";
|
|
188
|
+
ctx.compact({
|
|
189
|
+
customInstructions: MIDRUN_COMPACTION_INSTRUCTIONS,
|
|
190
|
+
onComplete: () => {
|
|
191
|
+
if (
|
|
192
|
+
state.generation !== generation ||
|
|
193
|
+
state.phase !== "compacting" ||
|
|
194
|
+
!sameSession(state, ctx)
|
|
195
|
+
) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
scheduleResume(pi, ctx, state, generation);
|
|
199
|
+
},
|
|
200
|
+
onError: (error) => {
|
|
201
|
+
if (state.generation !== generation || !sameSession(state, ctx)) return;
|
|
202
|
+
|
|
203
|
+
const latest = getLatestCompactionId(ctx);
|
|
204
|
+
if (/Already compacted/i.test(error.message) && latest !== state.baselineCompactionId) {
|
|
205
|
+
scheduleResume(pi, ctx, state, generation);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
state.phase = "failed";
|
|
210
|
+
writeDebugArtifact(
|
|
211
|
+
"lifecycle",
|
|
212
|
+
{
|
|
213
|
+
event: "midrun.failed",
|
|
214
|
+
errorMessage: error.message,
|
|
215
|
+
triggerTokens: state.triggerTokens,
|
|
216
|
+
triggerPercent: state.triggerPercent,
|
|
217
|
+
triggerContextWindow: state.triggerContextWindow,
|
|
218
|
+
},
|
|
219
|
+
config,
|
|
220
|
+
ctx,
|
|
221
|
+
);
|
|
222
|
+
notifyFailure(ctx, `mid-run compaction failed: ${error.message}`);
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
pi.on("session_start", () => resetState(state));
|
|
228
|
+
pi.on("session_shutdown", () => resetState(state));
|
|
229
|
+
}
|
package/src/native-fallback.ts
CHANGED
|
@@ -26,6 +26,7 @@ export type NativeFallbackResult =
|
|
|
26
26
|
ok: true;
|
|
27
27
|
result: CompactionResult;
|
|
28
28
|
model: { provider: string; id: string };
|
|
29
|
+
usage?: CompactionResult["usage"];
|
|
29
30
|
}
|
|
30
31
|
| {
|
|
31
32
|
ok: false;
|
|
@@ -38,9 +39,21 @@ export type NativeFallbackResult =
|
|
|
38
39
|
export type NativeCompactFn = typeof compact;
|
|
39
40
|
|
|
40
41
|
type ResolvedAuth =
|
|
41
|
-
| { ok: true; apiKey?: string; headers?: Record<string, string>; env?: Record<string, string> }
|
|
42
|
+
| { ok: true; apiKey?: string; headers?: Record<string, string | null>; env?: Record<string, string> }
|
|
42
43
|
| { ok: false; error: string };
|
|
43
44
|
|
|
45
|
+
/** Strip null-valued entries so downstream consumers receive a clean Record<string, string>. */
|
|
46
|
+
function filterNullHeaders(headers: Record<string, string | null> | undefined): Record<string, string> | undefined {
|
|
47
|
+
if (!headers) return undefined;
|
|
48
|
+
const filtered: Record<string, string> = {};
|
|
49
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
50
|
+
if (value !== null) {
|
|
51
|
+
filtered[key] = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return Object.keys(filtered).length > 0 ? filtered : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
44
57
|
/** Parse "provider/model-id" (model ids may themselves contain slashes). */
|
|
45
58
|
export function parseModelSpec(spec: string): ParsedModelSpec | undefined {
|
|
46
59
|
const trimmed = spec.trim();
|
|
@@ -82,6 +95,7 @@ export async function runNativeFallbackCompaction(args: {
|
|
|
82
95
|
event: SessionBeforeCompactEvent;
|
|
83
96
|
config: ExtensionConfig;
|
|
84
97
|
compactFn?: NativeCompactFn;
|
|
98
|
+
sessionId?: string;
|
|
85
99
|
}): Promise<NativeFallbackResult> {
|
|
86
100
|
const { ctx, event, config } = args;
|
|
87
101
|
const compactFn = args.compactFn ?? compact;
|
|
@@ -120,12 +134,15 @@ export async function runNativeFallbackCompaction(args: {
|
|
|
120
134
|
event.preparation,
|
|
121
135
|
model,
|
|
122
136
|
auth.apiKey,
|
|
123
|
-
auth.headers,
|
|
137
|
+
filterNullHeaders(auth.headers),
|
|
124
138
|
event.customInstructions,
|
|
125
139
|
event.signal,
|
|
126
140
|
config.compactionThinkingLevel,
|
|
127
|
-
undefined,
|
|
141
|
+
undefined, // streamFn
|
|
128
142
|
auth.env,
|
|
143
|
+
undefined, // retry (use pi defaults)
|
|
144
|
+
undefined, // callbacks
|
|
145
|
+
args.sessionId,
|
|
129
146
|
);
|
|
130
147
|
|
|
131
148
|
if (event.signal.aborted) {
|
|
@@ -139,6 +156,7 @@ export async function runNativeFallbackCompaction(args: {
|
|
|
139
156
|
ok: true,
|
|
140
157
|
result,
|
|
141
158
|
model: { provider: model.provider, id: model.id },
|
|
159
|
+
usage: result.usage,
|
|
142
160
|
};
|
|
143
161
|
} catch (error) {
|
|
144
162
|
if (event.signal.aborted || isAbortError(error)) {
|