pi-openai-toolkit 0.15.1 → 0.17.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 +60 -68
- package/README.zh.md +58 -66
- package/docs/configuration.md +9 -3
- package/docs/internals.md +31 -6
- package/package.json +11 -9
- package/src/auto-mode/classifier.ts +161 -165
- package/src/auto-mode/extension.ts +691 -661
- package/src/auto-mode/model-selection-guard.ts +52 -0
- package/src/auto-mode/reviewer.ts +325 -335
- package/src/auto-mode/transcript.ts +135 -133
- package/src/compaction-projection.ts +35 -31
- package/src/context-management/manual-compact.ts +433 -0
- package/src/context-management/tools.ts +11 -2
- package/src/context-management/window-manager.ts +35 -9
- package/src/extension-runtime.ts +64 -30
- package/src/payload-rewrite.ts +34 -64
- package/src/pi-context-hook.ts +117 -116
- package/src/remote-v2-client.ts +108 -33
package/src/extension-runtime.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type {
|
|
3
3
|
BeforeProviderRequestEvent,
|
|
4
|
-
|
|
4
|
+
ContextWithSystemEvent,
|
|
5
5
|
CompactionResult,
|
|
6
6
|
ExtensionAPI,
|
|
7
7
|
ExtensionContext,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { routeContextNamespaceToolMessage } from "./context-management/namespace-tools";
|
|
20
20
|
import { loadHistoryNotesThreadHint } from "./context-management/history-notes";
|
|
21
21
|
import { CodexContextWindowManager } from "./context-management/window-manager";
|
|
22
|
+
import { ManagedManualCompact } from "./context-management/manual-compact";
|
|
22
23
|
import {
|
|
23
24
|
BULK_CLIFF_RATIO,
|
|
24
25
|
decideBulkCliffAction,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
import { writeDebugArtifact, writeReplayFailureArtifact } from "./debug";
|
|
33
34
|
import {
|
|
34
35
|
COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
|
|
36
|
+
COMPACTION_CHECKPOINT_CONTEXT_EDITED,
|
|
35
37
|
COMPACTION_PROJECTION_UNAVAILABLE,
|
|
36
38
|
COMPACTION_SESSION_CONTEXT_UNAVAILABLE,
|
|
37
39
|
COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
|
|
@@ -49,6 +51,7 @@ import {
|
|
|
49
51
|
import { runNativeFallbackCompaction } from "./native-fallback";
|
|
50
52
|
import {
|
|
51
53
|
rewriteResponsesPayloadWithNativeReplay,
|
|
54
|
+
hasInvalidatedCompactionContext,
|
|
52
55
|
removeNativeCompactionRetainedMessages,
|
|
53
56
|
serializeLiveTailToResponsesInput,
|
|
54
57
|
} from "./payload-rewrite";
|
|
@@ -110,6 +113,7 @@ type CompactionDependencies = {
|
|
|
110
113
|
remoteCompact: typeof executeRemoteV2Compaction;
|
|
111
114
|
nativeFallback: typeof runNativeFallbackCompaction;
|
|
112
115
|
contextWindows: CodexContextWindowManager;
|
|
116
|
+
manualCompact?: ManagedManualCompact;
|
|
113
117
|
/** Test seam for the same ordered projection exposed by the Pi host patch. */
|
|
114
118
|
projectCompactionContext?: CompactionContextProjection;
|
|
115
119
|
piContextHookPatch: PiContextHookPatchResult;
|
|
@@ -258,22 +262,11 @@ function findLatestProjectedCompactionSummaryIndex(
|
|
|
258
262
|
return -1;
|
|
259
263
|
}
|
|
260
264
|
|
|
261
|
-
type SessionManagerWithOptionalContext = ExtensionContext["sessionManager"] & {
|
|
262
|
-
buildSessionContext?: () => { messages?: readonly AgentMessage[] };
|
|
263
|
-
};
|
|
264
|
-
|
|
265
265
|
function readSessionContextMessages(ctx: ExtensionContext): AgentMessage[] | undefined {
|
|
266
|
-
const sessionManager = ctx.sessionManager as SessionManagerWithOptionalContext;
|
|
267
|
-
if (typeof sessionManager.buildSessionContext !== "function") {
|
|
268
|
-
return undefined;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
266
|
try {
|
|
272
|
-
const
|
|
273
|
-
if (!
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
return structuredClone([...sessionContext.messages]);
|
|
267
|
+
const projection = ctx.sessionManager.buildSessionProjection();
|
|
268
|
+
if (!projection || !Array.isArray(projection.messages)) return undefined;
|
|
269
|
+
return structuredClone(projection.messages);
|
|
277
270
|
} catch {
|
|
278
271
|
return undefined;
|
|
279
272
|
}
|
|
@@ -337,6 +330,9 @@ async function runResponsesNativeCompact(
|
|
|
337
330
|
let requestSource: "session-context" | "non-native-session-context" | "latest-native-replay";
|
|
338
331
|
let request: NativeCompactionRequestBody;
|
|
339
332
|
if (latestNativeCompaction.ok) {
|
|
333
|
+
if (hasInvalidatedCompactionContext(branchEntries, latestNativeCompaction.entry.id)) {
|
|
334
|
+
return { outcome: "unprojected-input", reason: COMPACTION_CHECKPOINT_CONTEXT_EDITED };
|
|
335
|
+
}
|
|
340
336
|
const details = latestNativeCompaction.entry.details;
|
|
341
337
|
if (!details) {
|
|
342
338
|
return { outcome: "failed" };
|
|
@@ -599,6 +595,10 @@ async function handleSessionBeforeCompact(
|
|
|
599
595
|
if (await remoteContextActive(ctx, config)) {
|
|
600
596
|
try {
|
|
601
597
|
dependencies.contextWindows.synchronize(ctx);
|
|
598
|
+
if (event.reason === "manual" && dependencies.manualCompact && !dependencies.manualCompact.isMaintenance) {
|
|
599
|
+
await dependencies.manualCompact.request(event, ctx, resolved);
|
|
600
|
+
return { cancel: true };
|
|
601
|
+
}
|
|
602
602
|
return dependencies.contextWindows.prepareCompaction(event);
|
|
603
603
|
} catch {
|
|
604
604
|
notifyRemoteContextFailure(ctx, "malformed-window-state");
|
|
@@ -652,7 +652,9 @@ async function handleSessionBeforeCompact(
|
|
|
652
652
|
return { cancel: true };
|
|
653
653
|
}
|
|
654
654
|
if (responsesOutcome.outcome === "unprojected-input") {
|
|
655
|
-
const message = responsesOutcome.reason ===
|
|
655
|
+
const message = responsesOutcome.reason === COMPACTION_CHECKPOINT_CONTEXT_EDITED
|
|
656
|
+
? "A context edit changed history sealed in the opaque checkpoint. Start a new session or navigate before that checkpoint; its encrypted content cannot be selectively rewritten."
|
|
657
|
+
: responsesOutcome.reason === COMPACTION_PROJECTION_UNAVAILABLE
|
|
656
658
|
? "Pi's ordered context-hook projection is unavailable; Remote V2 was not sent raw session history."
|
|
657
659
|
: responsesOutcome.reason === COMPACTION_SESSION_CONTEXT_UNAVAILABLE
|
|
658
660
|
? "Pi did not provide the current session context required by its context-hook projection."
|
|
@@ -674,7 +676,7 @@ async function handleSessionBeforeCompact(
|
|
|
674
676
|
);
|
|
675
677
|
notifyWarning(
|
|
676
678
|
ctx,
|
|
677
|
-
`Remote V2 compaction cancelled (${responsesOutcome.reason}); ${message}
|
|
679
|
+
`Remote V2 compaction cancelled (${responsesOutcome.reason}); ${message} The existing checkpoint and session history were left unchanged.`,
|
|
678
680
|
);
|
|
679
681
|
return { cancel: true };
|
|
680
682
|
}
|
|
@@ -783,6 +785,7 @@ async function handleWindowBulk(
|
|
|
783
785
|
loadConfig: typeof loadToolkitConfig,
|
|
784
786
|
trigger: "model-switch" | "turn-end",
|
|
785
787
|
resolved = contextOperation(loadConfig, ctx),
|
|
788
|
+
beginMaintenance?: () => () => void,
|
|
786
789
|
): Promise<void> {
|
|
787
790
|
requireContextPolicy(resolved, ctx);
|
|
788
791
|
const compaction = resolved.config.compaction;
|
|
@@ -830,15 +833,20 @@ async function handleWindowBulk(
|
|
|
830
833
|
|
|
831
834
|
notifyWarning(ctx, `${gap} to ${modelKey}, past ${Math.round(BULK_CLIFF_RATIO * 100)}% of its window; compacting the retired windows first.`);
|
|
832
835
|
if (typeof ctx.compact !== "function") return;
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
836
|
+
const done = beginMaintenance?.();
|
|
837
|
+
try {
|
|
838
|
+
ctx.compact({
|
|
839
|
+
onComplete: () => done?.(),
|
|
840
|
+
onError: (error: Error) => {
|
|
841
|
+
done?.();
|
|
842
|
+
notifyWarning(ctx, `automatic boundary compaction failed: ${error.message}`);
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
} catch (error) { done?.(); throw error; }
|
|
838
846
|
}
|
|
839
847
|
|
|
840
848
|
async function handleContextInternal(
|
|
841
|
-
event:
|
|
849
|
+
event: ContextWithSystemEvent,
|
|
842
850
|
ctx: ExtensionContext,
|
|
843
851
|
pi: ExtensionAPI,
|
|
844
852
|
loadConfig: typeof loadToolkitConfig,
|
|
@@ -941,7 +949,7 @@ async function handleContextInternal(
|
|
|
941
949
|
* caught exception for a successful projection.
|
|
942
950
|
*/
|
|
943
951
|
async function handleContext(
|
|
944
|
-
event:
|
|
952
|
+
event: ContextWithSystemEvent,
|
|
945
953
|
ctx: ExtensionContext,
|
|
946
954
|
pi: ExtensionAPI,
|
|
947
955
|
loadConfig: typeof loadToolkitConfig,
|
|
@@ -1169,12 +1177,14 @@ export default function registerCompactionExtension(
|
|
|
1169
1177
|
const contextWindows = overrides.contextWindows ?? new CodexContextWindowManager((ctx, signal, gatewayModels) =>
|
|
1170
1178
|
loadHistoryNotesThreadHint(ctx, signal, gatewayModels)
|
|
1171
1179
|
);
|
|
1180
|
+
const manualCompact = new ManagedManualCompact(pi, contextWindows);
|
|
1172
1181
|
const dependencies: CompactionDependencies = {
|
|
1173
|
-
loadConfig,
|
|
1174
1182
|
remoteCompact: executeRemoteV2Compaction,
|
|
1175
1183
|
nativeFallback: runNativeFallbackCompaction,
|
|
1176
1184
|
contextWindows,
|
|
1177
1185
|
...overrides,
|
|
1186
|
+
manualCompact,
|
|
1187
|
+
loadConfig: () => manualCompact.snapshot ?? loadConfig(),
|
|
1178
1188
|
piContextHookPatch: overrides.piContextHookPatch ?? piContextHookPatch,
|
|
1179
1189
|
};
|
|
1180
1190
|
let tools!: ReturnType<typeof registerContextManagementTools>;
|
|
@@ -1193,6 +1203,8 @@ export default function registerCompactionExtension(
|
|
|
1193
1203
|
assertConfigValid(resolved, "context", "compatibility");
|
|
1194
1204
|
return { active: await isContextRuntimeActive(ctx, resolved.config.compaction), gatewayModels: resolved.gatewayModelKeys };
|
|
1195
1205
|
},
|
|
1206
|
+
undefined,
|
|
1207
|
+
manualCompact,
|
|
1196
1208
|
);
|
|
1197
1209
|
const remoteContextActive: RemoteContextActive = isContextRuntimeActive;
|
|
1198
1210
|
const syncTools = async (
|
|
@@ -1233,6 +1245,7 @@ export default function registerCompactionExtension(
|
|
|
1233
1245
|
pi.on("session_start", async (_event, ctx) => {
|
|
1234
1246
|
const resolved = contextOperation(dependencies.loadConfig, ctx);
|
|
1235
1247
|
const syncOutcome = await syncTools(ctx, ctx.model, { notifyWindowFailure: true }, resolved);
|
|
1248
|
+
await manualCompact.recover(ctx);
|
|
1236
1249
|
const active = syncOutcome.eligible && syncOutcome.toolsSynced && syncOutcome.windowInitialized;
|
|
1237
1250
|
const { source } = resolved;
|
|
1238
1251
|
const warnings = resolved.issues.map((issue) => `${issue.path}: ${issue.code}`);
|
|
@@ -1291,10 +1304,17 @@ export default function registerCompactionExtension(
|
|
|
1291
1304
|
}
|
|
1292
1305
|
});
|
|
1293
1306
|
|
|
1294
|
-
pi.on("
|
|
1307
|
+
pi.on("context_with_system", (event, ctx) => {
|
|
1308
|
+
manualCompact.guardRequest(ctx);
|
|
1309
|
+
return handleContext(event, ctx, pi, dependencies.loadConfig, contextWindows, remoteContextActive);
|
|
1310
|
+
});
|
|
1295
1311
|
pi.on("session_before_compact", (event, ctx) => handleSessionBeforeCompact(event, ctx, dependencies, remoteContextActive));
|
|
1296
1312
|
pi.on("session_compact", (event, _ctx) => contextWindows.recordCompaction(event.compactionEntry.details));
|
|
1297
|
-
pi.on("
|
|
1313
|
+
pi.on("session_compact_failed", async (event, ctx) => {
|
|
1314
|
+
if (event.reason === "manual" && !manualCompact.isMaintenance) await manualCompact.compactFailed(ctx);
|
|
1315
|
+
});
|
|
1316
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
1317
|
+
await manualCompact.shutdown(ctx);
|
|
1298
1318
|
contextWindowReady = false;
|
|
1299
1319
|
contextWindows.reset();
|
|
1300
1320
|
tools.reset();
|
|
@@ -1302,9 +1322,11 @@ export default function registerCompactionExtension(
|
|
|
1302
1322
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
1303
1323
|
// Idle: no retry, compaction, or queued continuation is left running, so this is the
|
|
1304
1324
|
// only safe place to spend a model call on the user's behalf.
|
|
1305
|
-
await
|
|
1325
|
+
if (await manualCompact.settled(ctx)) return;
|
|
1326
|
+
await handleWindowBulk(ctx, contextWindows, dependencies.loadConfig, "turn-end", undefined, () => manualCompact.beginMaintenance());
|
|
1306
1327
|
});
|
|
1307
1328
|
pi.on("model_select", async (event, ctx) => {
|
|
1329
|
+
manualCompact.modelSelected(event.model, ctx);
|
|
1308
1330
|
// Switching into a covered model mid-session must open the window
|
|
1309
1331
|
// lifecycle immediately: without an identity the request rewrite skips
|
|
1310
1332
|
// window metadata, the backend never ingests those turns, and the first
|
|
@@ -1314,12 +1336,24 @@ export default function registerCompactionExtension(
|
|
|
1314
1336
|
await syncTools(ctx, event.model, { notifyWindowFailure: true }, resolved);
|
|
1315
1337
|
// The switch itself is the moment the durable transcript changes consumer, and the
|
|
1316
1338
|
// session is idle here, so the close-out must happen now rather than mid-turn.
|
|
1317
|
-
await handleWindowBulk(ctx, contextWindows, dependencies.loadConfig, "model-switch", resolved);
|
|
1339
|
+
if (!manualCompact.busy) await handleWindowBulk(ctx, contextWindows, dependencies.loadConfig, "model-switch", resolved, () => manualCompact.beginMaintenance());
|
|
1318
1340
|
});
|
|
1319
1341
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
1320
1342
|
await syncTools(ctx, ctx.model, { notifyWindowFailure: true });
|
|
1321
1343
|
});
|
|
1322
|
-
pi.on("
|
|
1344
|
+
pi.on("thinking_level_select", (event, ctx) => manualCompact.thinkingSelected(event.level, ctx));
|
|
1345
|
+
pi.on("message_start", (event) => manualCompact.messageStarted(event.message));
|
|
1346
|
+
pi.on("turn_end", (_event, ctx) => manualCompact.turnEnded(ctx));
|
|
1347
|
+
pi.on("tool_call", (event) => manualCompact.guardTool(event.toolName));
|
|
1348
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
1349
|
+
contextWindows.synchronize(ctx);
|
|
1350
|
+
await manualCompact.recover(ctx);
|
|
1351
|
+
await syncTools(ctx);
|
|
1352
|
+
});
|
|
1353
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
1354
|
+
manualCompact.guardRequest(ctx);
|
|
1355
|
+
return handleBeforeProviderRequest(event, ctx, dependencies.loadConfig, contextWindows, remoteContextActive);
|
|
1356
|
+
});
|
|
1323
1357
|
pi.on("before_provider_headers", async (event, ctx) => {
|
|
1324
1358
|
const resolved = contextOperation(dependencies.loadConfig, ctx);
|
|
1325
1359
|
requireContextPolicy(resolved, ctx);
|
package/src/payload-rewrite.ts
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { buildSessionProjection, sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
3
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
4
|
-
import type {
|
|
5
|
-
BranchSummaryEntry,
|
|
6
|
-
CustomMessageEntry,
|
|
7
|
-
SessionEntry,
|
|
8
|
-
SessionMessageEntry,
|
|
9
|
-
} from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
10
5
|
import type { ResponsesCompatibleRequestPayload } from "./runtime";
|
|
11
6
|
import { hasVerifiedCompactionInputProvenance } from "./compaction-projection";
|
|
12
7
|
import { rewritePayloadWithDeferredToolCarryover } from "./deferred-tool-carryover";
|
|
@@ -40,7 +35,8 @@ export type NativeReplayPayloadRewriteFailureReason =
|
|
|
40
35
|
| "unexpected-compaction-after-boundary"
|
|
41
36
|
| "compaction-summary-not-found"
|
|
42
37
|
| "retained-context-mismatch"
|
|
43
|
-
| "unverified-compaction-input"
|
|
38
|
+
| "unverified-compaction-input"
|
|
39
|
+
| "checkpoint-context-edited";
|
|
44
40
|
|
|
45
41
|
export type NativeReplayPayloadRewriteFailure = {
|
|
46
42
|
ok: false;
|
|
@@ -200,46 +196,6 @@ function areEquivalentValues(left: unknown, right: unknown): boolean {
|
|
|
200
196
|
return false;
|
|
201
197
|
}
|
|
202
198
|
|
|
203
|
-
function toBranchSummaryMessage(entry: BranchSummaryEntry): AgentMessage {
|
|
204
|
-
return {
|
|
205
|
-
role: "branchSummary",
|
|
206
|
-
summary: entry.summary,
|
|
207
|
-
fromId: entry.fromId,
|
|
208
|
-
timestamp: new Date(entry.timestamp).getTime(),
|
|
209
|
-
} as AgentMessage;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function toCustomMessage(entry: CustomMessageEntry): AgentMessage {
|
|
213
|
-
return {
|
|
214
|
-
role: "custom",
|
|
215
|
-
customType: entry.customType,
|
|
216
|
-
content: entry.content,
|
|
217
|
-
display: entry.display,
|
|
218
|
-
details: entry.details,
|
|
219
|
-
timestamp: new Date(entry.timestamp).getTime(),
|
|
220
|
-
} as AgentMessage;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function toSessionMessage(entry: SessionMessageEntry): AgentMessage {
|
|
224
|
-
return entry.message;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function toReplayAgentMessage(entry: SessionEntry): AgentMessage | undefined {
|
|
228
|
-
if (entry.type === "message") {
|
|
229
|
-
return toSessionMessage(entry);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (entry.type === "custom_message") {
|
|
233
|
-
return toCustomMessage(entry);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (entry.type === "branch_summary") {
|
|
237
|
-
return toBranchSummaryMessage(entry);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return undefined;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
199
|
function extractUserItemText(item: Record<string, unknown>): unknown {
|
|
244
200
|
const { content } = item;
|
|
245
201
|
if (typeof content === "string") {
|
|
@@ -273,23 +229,29 @@ function findCompactionSummaryIndex(input: readonly unknown[], summaryMarker: st
|
|
|
273
229
|
}
|
|
274
230
|
|
|
275
231
|
function collectReplayMessages(entries: readonly SessionEntry[]): AgentMessage[] {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
for (const entry of entries) {
|
|
279
|
-
const message = toReplayAgentMessage(entry);
|
|
280
|
-
if (message) {
|
|
281
|
-
messages.push(message);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
232
|
+
return buildSessionProjection([...entries]).messages;
|
|
233
|
+
}
|
|
284
234
|
|
|
285
|
-
|
|
235
|
+
/** An opaque checkpoint cannot be selectively edited after it was produced. */
|
|
236
|
+
export function hasInvalidatedCompactionContext(
|
|
237
|
+
entries: readonly SessionEntry[],
|
|
238
|
+
compactionEntryId: string,
|
|
239
|
+
): boolean {
|
|
240
|
+
const boundary = findCompactionBoundaryIndex(entries, compactionEntryId);
|
|
241
|
+
if (boundary === undefined) return false;
|
|
242
|
+
const coveredIds = new Set(entries.slice(0, boundary).map((entry) => entry.id));
|
|
243
|
+
return entries.slice(boundary + 1).some((entry) =>
|
|
244
|
+
entry.type === "context_edit" && coveredIds.has(entry.targetId),
|
|
245
|
+
);
|
|
286
246
|
}
|
|
287
247
|
|
|
288
|
-
function
|
|
248
|
+
function findKeptEntryIndex(
|
|
289
249
|
entries: readonly SessionEntry[],
|
|
290
250
|
entryId: string,
|
|
291
251
|
boundaryIndex: number,
|
|
292
252
|
): number | undefined {
|
|
253
|
+
// Pi 0.87 uses the compaction itself as the retained boundary for retain-none.
|
|
254
|
+
if (entries[boundaryIndex]?.id === entryId) return boundaryIndex;
|
|
293
255
|
const index = entries.findIndex((entry, candidateIndex) => candidateIndex < boundaryIndex && entry.id === entryId);
|
|
294
256
|
return index >= 0 ? index : undefined;
|
|
295
257
|
}
|
|
@@ -319,7 +281,7 @@ export function collectLiveTailMessages(entries: readonly SessionEntry[]): Agent
|
|
|
319
281
|
}
|
|
320
282
|
|
|
321
283
|
/**
|
|
322
|
-
* Remote V2 covers the complete pre-compaction context. Pi
|
|
284
|
+
* Remote V2 covers the complete pre-compaction context. Pi also retains
|
|
323
285
|
* recent messages, so remove those verified copies before provider conversion
|
|
324
286
|
* (where IDs/fields may change across models). Persisted session entries are
|
|
325
287
|
* untouched. Preserve transient additions and all post-compaction messages.
|
|
@@ -335,7 +297,10 @@ export function removeNativeCompactionRetainedMessages(args: {
|
|
|
335
297
|
if (!hasVerifiedCompactionInputProvenance(args.compactionEntry.details, args.expectedInputProvenance)) {
|
|
336
298
|
return { ok: false, reason: "unverified-compaction-input" };
|
|
337
299
|
}
|
|
338
|
-
|
|
300
|
+
if (hasInvalidatedCompactionContext(args.branchEntries, args.compactionEntry.id)) {
|
|
301
|
+
return { ok: false, reason: "checkpoint-context-edited" };
|
|
302
|
+
}
|
|
303
|
+
const firstKept = findKeptEntryIndex(args.branchEntries, args.compactionEntry.firstKeptEntryId, boundary);
|
|
339
304
|
if (firstKept === undefined) return { ok: false, reason: "first-kept-entry-not-found" };
|
|
340
305
|
const summaryMessage = sessionEntryToContextMessages(args.compactionEntry).find(
|
|
341
306
|
(message) => message.role === "compactionSummary",
|
|
@@ -344,9 +309,10 @@ export function removeNativeCompactionRetainedMessages(args: {
|
|
|
344
309
|
const summary = structuredClone(summaryMessage);
|
|
345
310
|
const summaryIndex = args.messages.findIndex((message) => areEquivalentValues(message, summary));
|
|
346
311
|
if (summaryIndex < 0) return { ok: false, reason: "compaction-summary-not-found" };
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
.
|
|
312
|
+
const retainedIds = new Set(args.branchEntries.slice(firstKept, boundary).map((entry) => entry.id));
|
|
313
|
+
const retained = buildSessionProjection([...args.branchEntries]).entries
|
|
314
|
+
.filter((entry) => retainedIds.has(entry.sourceEntry.id))
|
|
315
|
+
.flatMap((entry) => entry.messages)
|
|
350
316
|
.map((message) => structuredClone(message));
|
|
351
317
|
// Custom messages are optional context-hook entries: a hook may filter or
|
|
352
318
|
// rewrite them before provider serialization. An exact custom copy is still
|
|
@@ -418,7 +384,11 @@ function buildNativeReplaySegmentsInternal<TApi extends Api>(args: {
|
|
|
418
384
|
};
|
|
419
385
|
}
|
|
420
386
|
|
|
421
|
-
|
|
387
|
+
if (hasInvalidatedCompactionContext(args.branchEntries, args.compactionEntry.id)) {
|
|
388
|
+
return { ok: false, reason: "checkpoint-context-edited" };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const firstKeptEntryIndex = findKeptEntryIndex(
|
|
422
392
|
args.branchEntries,
|
|
423
393
|
args.compactionEntry.firstKeptEntryId,
|
|
424
394
|
boundaryIndex,
|
package/src/pi-context-hook.ts
CHANGED
|
@@ -1,116 +1,117 @@
|
|
|
1
|
-
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import { ExtensionRunner, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
|
|
4
|
-
/** Context property added by the toolkit's host patch. */
|
|
5
|
-
export const PI_CONTEXT_PROJECTOR_FIELD = "projectContextForCompaction" as const;
|
|
6
|
-
const PI_CONTEXT_PROJECTION_REPORTER_FIELD = "__piOpenAiToolkitReportContextProjectionFailure" as const;
|
|
7
|
-
const PATCH_MARKER_FIELD = "__piOpenAiToolkitContextHookPatch" as const;
|
|
8
|
-
|
|
9
|
-
type ProjectionState = {
|
|
10
|
-
failure?: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const activeProjectionByRunner = new WeakMap<ExtensionRunner, ProjectionState>();
|
|
14
|
-
|
|
15
|
-
type PatchedExtensionContext = ExtensionContext & {
|
|
16
|
-
[PI_CONTEXT_PROJECTOR_FIELD]?: PiContextHookProjector;
|
|
17
|
-
[PI_CONTEXT_PROJECTION_REPORTER_FIELD]?: (reason: string) => void;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
type MarkedCreateContext = typeof ExtensionRunner.prototype.createContext & {
|
|
21
|
-
[PATCH_MARKER_FIELD]?: true;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/** The ordered context-hook chain, applied to a cloned compaction context. */
|
|
25
|
-
export type PiContextHookProjector = (
|
|
26
|
-
messages: readonly AgentMessage[],
|
|
27
|
-
) => Promise<readonly AgentMessage[]>;
|
|
28
|
-
|
|
29
|
-
export type PiContextHookPatchResult =
|
|
30
|
-
| { ok: true; installed: boolean; reason?: "already-installed" }
|
|
31
|
-
| { ok: false; reason: "emit-context-unavailable" | "create-context-unavailable" };
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Add a narrow projection method to every ExtensionContext created by Pi.
|
|
35
|
-
*
|
|
36
|
-
* Pi 0.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
1
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { ExtensionRunner, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
/** Context property added by the toolkit's host patch. */
|
|
5
|
+
export const PI_CONTEXT_PROJECTOR_FIELD = "projectContextForCompaction" as const;
|
|
6
|
+
const PI_CONTEXT_PROJECTION_REPORTER_FIELD = "__piOpenAiToolkitReportContextProjectionFailure" as const;
|
|
7
|
+
const PATCH_MARKER_FIELD = "__piOpenAiToolkitContextHookPatch" as const;
|
|
8
|
+
|
|
9
|
+
type ProjectionState = {
|
|
10
|
+
failure?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const activeProjectionByRunner = new WeakMap<ExtensionRunner, ProjectionState>();
|
|
14
|
+
|
|
15
|
+
type PatchedExtensionContext = ExtensionContext & {
|
|
16
|
+
[PI_CONTEXT_PROJECTOR_FIELD]?: PiContextHookProjector;
|
|
17
|
+
[PI_CONTEXT_PROJECTION_REPORTER_FIELD]?: (reason: string) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type MarkedCreateContext = typeof ExtensionRunner.prototype.createContext & {
|
|
21
|
+
[PATCH_MARKER_FIELD]?: true;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** The ordered context-hook chain, applied to a cloned compaction context. */
|
|
25
|
+
export type PiContextHookProjector = (
|
|
26
|
+
messages: readonly AgentMessage[],
|
|
27
|
+
) => Promise<readonly AgentMessage[]>;
|
|
28
|
+
|
|
29
|
+
export type PiContextHookPatchResult =
|
|
30
|
+
| { ok: true; installed: boolean; reason?: "already-installed" }
|
|
31
|
+
| { ok: false; reason: "emit-context-unavailable" | "create-context-unavailable" };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Add a narrow projection method to every ExtensionContext created by Pi.
|
|
35
|
+
*
|
|
36
|
+
* Pi 0.87's ordered `ExtensionRunner.emitContext()` runs both `context` and
|
|
37
|
+
* `context_with_system` phases,
|
|
38
|
+
* but the event-level ExtensionContext does not expose the runner. The patch
|
|
39
|
+
* bridges only the method needed by Remote V2; it does not expose the runner
|
|
40
|
+
* or alter Pi's scheduler.
|
|
41
|
+
*/
|
|
42
|
+
export function installPiContextHookPatch(): PiContextHookPatchResult {
|
|
43
|
+
const prototype = ExtensionRunner.prototype;
|
|
44
|
+
if (typeof prototype.emitContext !== "function") {
|
|
45
|
+
return { ok: false, reason: "emit-context-unavailable" };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const original = prototype.createContext;
|
|
49
|
+
if (typeof original !== "function") {
|
|
50
|
+
return { ok: false, reason: "create-context-unavailable" };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const markedOriginal = original as MarkedCreateContext;
|
|
54
|
+
if (markedOriginal[PATCH_MARKER_FIELD] === true) {
|
|
55
|
+
return { ok: true, installed: false, reason: "already-installed" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const patched = function patchedCreateContext(this: ExtensionRunner): ExtensionContext {
|
|
59
|
+
const context = original.call(this) as PatchedExtensionContext;
|
|
60
|
+
if (!(PI_CONTEXT_PROJECTOR_FIELD in context)) {
|
|
61
|
+
Object.defineProperty(context, PI_CONTEXT_PROJECTOR_FIELD, {
|
|
62
|
+
configurable: false,
|
|
63
|
+
enumerable: false,
|
|
64
|
+
writable: false,
|
|
65
|
+
value: async (messages: readonly AgentMessage[]) => {
|
|
66
|
+
const state: ProjectionState = {};
|
|
67
|
+
activeProjectionByRunner.set(this, state);
|
|
68
|
+
try {
|
|
69
|
+
const projected = await this.emitContext([...messages]);
|
|
70
|
+
if (state.failure) {
|
|
71
|
+
throw new Error(`context projection failed: ${state.failure}`);
|
|
72
|
+
}
|
|
73
|
+
return projected;
|
|
74
|
+
} finally {
|
|
75
|
+
if (activeProjectionByRunner.get(this) === state) {
|
|
76
|
+
activeProjectionByRunner.delete(this);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (!(PI_CONTEXT_PROJECTION_REPORTER_FIELD in context)) {
|
|
83
|
+
Object.defineProperty(context, PI_CONTEXT_PROJECTION_REPORTER_FIELD, {
|
|
84
|
+
configurable: false,
|
|
85
|
+
enumerable: false,
|
|
86
|
+
writable: false,
|
|
87
|
+
value: (reason: string) => {
|
|
88
|
+
const state = activeProjectionByRunner.get(this);
|
|
89
|
+
if (state) state.failure = reason;
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return context;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
Object.defineProperty(patched, PATCH_MARKER_FIELD, {
|
|
97
|
+
configurable: false,
|
|
98
|
+
enumerable: false,
|
|
99
|
+
writable: false,
|
|
100
|
+
value: true,
|
|
101
|
+
});
|
|
102
|
+
prototype.createContext = patched;
|
|
103
|
+
return { ok: true, installed: true };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Return the patched projection method, if this Pi process supports it. */
|
|
107
|
+
export function getPiContextHookProjector(
|
|
108
|
+
ctx: ExtensionContext,
|
|
109
|
+
): PiContextHookProjector | undefined {
|
|
110
|
+
const candidate = (ctx as PatchedExtensionContext)[PI_CONTEXT_PROJECTOR_FIELD];
|
|
111
|
+
return typeof candidate === "function" ? candidate : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Preserve fail-closed context-hook errors across Pi's emitContext boundary. */
|
|
115
|
+
export function reportPiContextHookFailure(ctx: ExtensionContext, reason: string): void {
|
|
116
|
+
(ctx as PatchedExtensionContext)[PI_CONTEXT_PROJECTION_REPORTER_FIELD]?.(reason);
|
|
117
|
+
}
|