pi-openai-toolkit 0.22.3 → 0.23.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.
Files changed (40) hide show
  1. package/LICENSE +21 -21
  2. package/NOTICE +83 -83
  3. package/README.md +4 -2
  4. package/README.zh.md +4 -2
  5. package/config.schema.json +278 -278
  6. package/docs/configuration.md +298 -298
  7. package/docs/internals.md +181 -181
  8. package/package.json +134 -134
  9. package/src/compact-client.ts +412 -412
  10. package/src/compaction-projection.ts +35 -35
  11. package/src/config/migration.ts +103 -103
  12. package/src/config.ts +310 -310
  13. package/src/context-management/codex-provider.ts +270 -270
  14. package/src/context-management/history-notes.ts +357 -357
  15. package/src/context-management/manual-compact.ts +181 -4
  16. package/src/context-management/tools.ts +371 -371
  17. package/src/context-management/types.ts +168 -168
  18. package/src/context-management/window-manager.ts +905 -811
  19. package/src/debug.ts +238 -238
  20. package/src/deferred-tool-carryover.ts +282 -282
  21. package/src/details-store.ts +151 -151
  22. package/src/extension-runtime.ts +1986 -1661
  23. package/src/image-generation/extension.ts +189 -189
  24. package/src/image-generation/protocol.ts +579 -579
  25. package/src/image-generation/types.ts +300 -300
  26. package/src/payload-rewrite.ts +515 -515
  27. package/src/pi-context-hook.ts +117 -117
  28. package/src/remote-v2-client.ts +811 -811
  29. package/src/request-context-cache.ts +155 -155
  30. package/src/responses-headers.ts +180 -180
  31. package/src/runtime.ts +865 -865
  32. package/src/serializer.ts +882 -882
  33. package/src/supported-environment.ts +27 -27
  34. package/src/types.ts +565 -565
  35. package/src/web-search/alpha-client.ts +876 -876
  36. package/src/web-search/extension.ts +530 -530
  37. package/src/web-search/payload.ts +299 -299
  38. package/src/web-search/prompt.ts +60 -60
  39. package/src/web-search/types.ts +166 -166
  40. package/tsconfig.check.json +50 -50
@@ -1,9 +1,10 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { isDeepStrictEqual } from "node:util";
2
3
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
3
- import { buildSessionContext, estimateTokens, type ExtensionAPI, type ExtensionContext, type SessionBeforeCompactEvent, type SessionEntry } from "@earendil-works/pi-coding-agent";
4
+ import { buildSessionContext, estimateTokens, type CompactionResult, type ExtensionAPI, type ExtensionContext, type SessionBeforeCompactEvent, type SessionEntry } from "@earendil-works/pi-coding-agent";
4
5
  import { resolveToolkitConfig, type ResolvedToolkitConfig } from "../config";
5
6
  import { parseModelSpec } from "../runtime";
6
- import { THINKING_LEVELS } from "../types";
7
+ import { THINKING_LEVELS, type NativeCompactionDetails } from "../types";
7
8
  import { resolveCodexContextProvider } from "./codex-provider";
8
9
  import { findLatestNotesCheckpointSinceBoundary, findLatestWindowBoundaryEntry, type CodexContextWindowManager } from "./window-manager";
9
10
  import { isNonEmptyString, isRecord, type CodexContextProvider, type ContextWindowIdentity } from "./types";
@@ -79,7 +80,183 @@ function freshCheckpoint(entries: readonly SessionEntry[], record: ManualCompact
79
80
  return findLatestNotesCheckpointSinceBoundary(entries, record.sessionId, afterId);
80
81
  }
81
82
 
82
- /** Owns only manual managed handoffs, not normal window rollovers or Remote V2. */
83
+ function branchFingerprint(entries: readonly SessionEntry[]): string {
84
+ return createHash("sha256").update(JSON.stringify(entries)).digest("hex");
85
+ }
86
+
87
+ export type RemoteManualCompactOperation = {
88
+ generation: number;
89
+ sessionId: string;
90
+ sessionFile: string | undefined;
91
+ anchorId: string;
92
+ branchLength: number;
93
+ branchFingerprint: string;
94
+ windowId: string;
95
+ model: string;
96
+ api: string;
97
+ baseUrl: string;
98
+ thinking: ThinkingLevel;
99
+ controller: AbortController;
100
+ signal: AbortSignal;
101
+ removeAbortListeners: () => void;
102
+ policy: ResolvedToolkitConfig;
103
+ candidate?: CompactionResult<NativeCompactionDetails>;
104
+ persistedId?: string;
105
+ deadline?: number;
106
+ };
107
+
108
+ /**
109
+ * One transient Remote V2 operation. It never selects a model, writes recovery
110
+ * records, takes host queues, or resumes from reload. Legacy duty recovery below
111
+ * remains independent.
112
+ */
113
+ export class RemoteManualCompact {
114
+ private generation = 0;
115
+ private operation: RemoteManualCompactOperation | undefined;
116
+ private timer: ReturnType<typeof setTimeout> | undefined;
117
+
118
+ constructor(private readonly pi: ExtensionAPI, private readonly windows: CodexContextWindowManager,
119
+ private readonly idleTimeoutMs = 10_000) {}
120
+
121
+ get snapshot() { return this.operation?.policy.snapshot; }
122
+
123
+ invalidate(): void {
124
+ ++this.generation;
125
+ clearTimeout(this.timer);
126
+ this.timer = undefined;
127
+ const operation = this.operation;
128
+ this.operation = undefined;
129
+ operation?.removeAbortListeners();
130
+ operation?.controller.abort();
131
+ }
132
+
133
+ cancel(operation: RemoteManualCompactOperation | undefined): void {
134
+ if (operation && this.operation === operation) this.invalidate();
135
+ }
136
+
137
+ begin(event: SessionBeforeCompactEvent, ctx: ExtensionContext, policy: ResolvedToolkitConfig): RemoteManualCompactOperation | undefined {
138
+ this.invalidate();
139
+ const model = ctx.model;
140
+ const anchorId = ctx.sessionManager.getLeafId();
141
+ const windowId = this.windows.currentIdentity()?.currentWindowId;
142
+ if (!model || !anchorId || !windowId || event.signal.aborted) return undefined;
143
+ const branch = ctx.sessionManager.getBranch();
144
+ if (!isDeepStrictEqual(event.branchEntries, branch)) return undefined;
145
+ const controller = new AbortController();
146
+ const onAbort = () => { if (this.operation === operation) this.invalidate(); };
147
+ // Manual compact has already aborted the preceding agent run. Its old,
148
+ // aborted ctx.signal is not cancellation of this newly initiated operation.
149
+ const signals = [event.signal, ...(ctx.signal && !ctx.signal.aborted && ctx.signal !== event.signal ? [ctx.signal] : [])];
150
+ const operation: RemoteManualCompactOperation = {
151
+ generation: this.generation, sessionId: ctx.sessionManager.getSessionId(),
152
+ sessionFile: ctx.sessionManager.getSessionFile(), anchorId,
153
+ branchLength: branch.length, branchFingerprint: branchFingerprint(branch), windowId,
154
+ model: modelKey(model)!, api: model.api, baseUrl: model.baseUrl, thinking: this.pi.getThinkingLevel(),
155
+ controller, signal: controller.signal, policy,
156
+ removeAbortListeners: () => { for (const signal of signals) signal.removeEventListener("abort", onAbort); },
157
+ };
158
+ this.operation = operation;
159
+ for (const signal of signals) signal.addEventListener("abort", onAbort, { once: true });
160
+ return operation;
161
+ }
162
+
163
+ private owns(operation: RemoteManualCompactOperation, ctx: ExtensionContext): boolean {
164
+ if (this.operation !== operation || operation.generation !== this.generation || operation.signal.aborted
165
+ || ctx.sessionManager.getSessionId() !== operation.sessionId || ctx.sessionManager.getSessionFile() !== operation.sessionFile
166
+ || modelKey(ctx.model) !== operation.model || ctx.model?.api !== operation.api || ctx.model?.baseUrl !== operation.baseUrl
167
+ || this.pi.getThinkingLevel() !== operation.thinking || ctx.hasPendingMessages()) return false;
168
+ this.windows.synchronize(ctx);
169
+ return this.windows.currentIdentity()?.currentWindowId === operation.windowId && !this.windows.hasPendingRollover(ctx);
170
+ }
171
+
172
+ current(operation: RemoteManualCompactOperation, ctx: ExtensionContext): boolean {
173
+ try {
174
+ const branch = ctx.sessionManager.getBranch();
175
+ return this.owns(operation, ctx) && ctx.sessionManager.getLeafId() === operation.anchorId
176
+ && branch.length === operation.branchLength && branchFingerprint(branch) === operation.branchFingerprint;
177
+ } catch { return false; }
178
+ }
179
+
180
+ arm(
181
+ operation: RemoteManualCompactOperation,
182
+ ctx: ExtensionContext,
183
+ candidate: CompactionResult<NativeCompactionDetails>,
184
+ ): { readonly cancel: boolean; compaction: CompactionResult<NativeCompactionDetails> } | undefined {
185
+ if (!this.current(operation, ctx)) return undefined;
186
+ operation.candidate = structuredClone(candidate);
187
+ const owner = this;
188
+ return {
189
+ compaction: candidate,
190
+ // Pi reads this public result again after later before-compact observers.
191
+ // Revalidate at consumption, not just at return: newer work must revoke
192
+ // the candidate before persistence, without ctx.abort() draining TUI queues.
193
+ get cancel() { return !owner.current(operation, ctx); },
194
+ };
195
+ }
196
+
197
+ private durableCandidate(operation: RemoteManualCompactOperation, ctx: ExtensionContext): string | undefined {
198
+ if (!this.owns(operation, ctx) || !operation.candidate) return undefined;
199
+ const branch = ctx.sessionManager.getBranch();
200
+ // The event's ID can refer to an OLD equal-summary checkpoint in Pi 1.0.
201
+ // Only the new actual branch child and its complete candidate are proof.
202
+ if (branch.length !== operation.branchLength + 1
203
+ || branchFingerprint(branch.slice(0, operation.branchLength)) !== operation.branchFingerprint) return undefined;
204
+ const entry = branch[operation.branchLength];
205
+ const candidate = operation.candidate;
206
+ if (!entry || entry.type !== "compaction" || entry.parentId !== operation.anchorId
207
+ || entry.id === operation.anchorId || ctx.sessionManager.getLeafId() !== entry.id || entry.fromHook !== true
208
+ || entry.summary !== candidate.summary || entry.firstKeptEntryId !== candidate.firstKeptEntryId
209
+ || entry.tokensBefore !== candidate.tokensBefore
210
+ // Persistence omits undefined optional fields; compare the actual JSON
211
+ // data, not object identity, summary text, or provider response ID alone.
212
+ || !isDeepStrictEqual(JSON.parse(JSON.stringify(entry.details)), JSON.parse(JSON.stringify(candidate.details)))) return undefined;
213
+ return entry.id;
214
+ }
215
+
216
+ private requestManualResume(ctx: ExtensionContext, message = "Remote /compact saved its checkpoint; resume manually."): void {
217
+ try { if (ctx.hasUI) ctx.ui.notify(message, "warning"); } catch { /* stale UI must not escape a deferred callback */ }
218
+ }
219
+
220
+ /** Observer only: never await idle or submit a prompt inside session_compact. */
221
+ persisted(ctx: ExtensionContext): void {
222
+ const operation = this.operation;
223
+ if (!operation?.candidate || operation.persistedId) return;
224
+ try { operation.persistedId = this.durableCandidate(operation, ctx); } catch { /* fail closed */ }
225
+ if (!operation.persistedId) {
226
+ this.invalidate();
227
+ this.requestManualResume(ctx, "Remote /compact could not verify its exact saved checkpoint; resume manually.");
228
+ return;
229
+ }
230
+ operation.deadline = Date.now() + this.idleTimeoutMs;
231
+ this.schedule(operation, ctx, 0);
232
+ }
233
+
234
+ private schedule(operation: RemoteManualCompactOperation, ctx: ExtensionContext, delay: number): void {
235
+ this.timer = setTimeout(() => {
236
+ this.timer = undefined;
237
+ if (this.operation !== operation) return;
238
+ try {
239
+ if (this.durableCandidate(operation, ctx) !== operation.persistedId) { this.invalidate(); return; }
240
+ if (Date.now() >= operation.deadline!) {
241
+ this.invalidate();
242
+ this.requestManualResume(ctx, "Remote /compact saved its checkpoint; automatic continuation could not safely reach idle. Resume manually.");
243
+ return;
244
+ }
245
+ if (!ctx.isIdle()) { this.schedule(operation, ctx, 25); return; }
246
+ // No await between final public ownership/queue/idle proof and send.
247
+ // Disarm first, so our own input/run events cannot invalidate a newer operation.
248
+ this.invalidate();
249
+ this.pi.sendUserMessage("Context compressed. Resume the active user task.");
250
+ } catch {
251
+ this.invalidate();
252
+ this.requestManualResume(ctx);
253
+ }
254
+ }, delay);
255
+ this.timer.unref?.();
256
+ }
257
+ }
258
+
259
+ /** Owns only legacy managed handoffs, not normal window rollovers or Remote V2. */
83
260
  export class ManagedManualCompact implements ContextRolloverHandoff {
84
261
  private record: ManualCompactRecord | undefined;
85
262
  private generation = 0;