@averyyy/pi-coding-agent 0.82.1-piclient.2 → 0.82.1-piclient.4
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/CHANGELOG.md +12 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +2 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/tool-effects.d.ts +6 -0
- package/dist/cli/tool-effects.d.ts.map +1 -0
- package/dist/cli/tool-effects.js +164 -0
- package/dist/cli/tool-effects.js.map +1 -0
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +5 -0
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session.d.ts +13 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +340 -89
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/model-runtime.d.ts +7 -0
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +66 -0
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/pi-server-client.d.ts +56 -2
- package/dist/core/pi-server-client.d.ts.map +1 -1
- package/dist/core/pi-server-client.js +2090 -288
- package/dist/core/pi-server-client.js.map +1 -1
- package/dist/core/pi-server-compact-state.d.ts +81 -0
- package/dist/core/pi-server-compact-state.d.ts.map +1 -0
- package/dist/core/pi-server-compact-state.js +438 -0
- package/dist/core/pi-server-compact-state.js.map +1 -0
- package/dist/core/pi-server-protocol.d.ts +2 -0
- package/dist/core/pi-server-protocol.d.ts.map +1 -1
- package/dist/core/pi-server-protocol.js +24 -1
- package/dist/core/pi-server-protocol.js.map +1 -1
- package/dist/core/pi-server-provider-execution.d.ts +19 -0
- package/dist/core/pi-server-provider-execution.d.ts.map +1 -0
- package/dist/core/pi-server-provider-execution.js +43 -0
- package/dist/core/pi-server-provider-execution.js.map +1 -0
- package/dist/core/pi-server-request.d.ts +28 -0
- package/dist/core/pi-server-request.d.ts.map +1 -1
- package/dist/core/pi-server-request.js +492 -75
- package/dist/core/pi-server-request.js.map +1 -1
- package/dist/core/pi-server-run-state.d.ts +41 -0
- package/dist/core/pi-server-run-state.d.ts.map +1 -0
- package/dist/core/pi-server-run-state.js +318 -0
- package/dist/core/pi-server-run-state.js.map +1 -0
- package/dist/core/provider-execution-contract.d.ts +36 -0
- package/dist/core/provider-execution-contract.d.ts.map +1 -0
- package/dist/core/provider-execution-contract.js +30 -0
- package/dist/core/provider-execution-contract.js.map +1 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +74 -53
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +86 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +737 -144
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/tool-effect-journal.d.ts +103 -0
- package/dist/core/tool-effect-journal.d.ts.map +1 -0
- package/dist/core/tool-effect-journal.js +1442 -0
- package/dist/core/tool-effect-journal.js.map +1 -0
- package/dist/core/tools/output-accumulator.d.ts +3 -1
- package/dist/core/tools/output-accumulator.d.ts.map +1 -1
- package/dist/core/tools/output-accumulator.js +46 -26
- package/dist/core/tools/output-accumulator.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +13 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts +15 -0
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +98 -29
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { resolve } from "node:path";
|
|
2
3
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
|
+
import { buildSessionContext, convertToLlm, } from "@earendil-works/pi-agent-core";
|
|
3
5
|
import { EventStream, parseStreamingJson, } from "@earendil-works/pi-ai";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
+
import { acknowledgePiServerPendingCompact, readPiServerPendingCompact, writePiServerAppliedCompact, writePiServerPendingCompact, writePiServerTerminalCompact, } from "./pi-server-compact-state.js";
|
|
7
|
+
import { appendPiServerTreeHash, canonicalJsonStringify, hashPiServerSessionEntries, hashPiServerStaticContext, hashPiServerTreeEntry, PI_SERVER_EMPTY_TREE_HASH, PI_SERVER_PROTOCOL_VERSION, } from "./pi-server-protocol.js";
|
|
8
|
+
import { ChunkRequest, PiServerTransportBodyLimitError, readPiServerResponseText } from "./pi-server-request.js";
|
|
9
|
+
import { acknowledgePiServerPendingRun, acquirePiServerRunStateLease, hashPiServerIdentity, readPiServerPendingRun, releasePiServerRunStateLease, writePiServerPendingRun, } from "./pi-server-run-state.js";
|
|
6
10
|
class PiServerEventStream extends EventStream {
|
|
7
11
|
constructor() {
|
|
8
12
|
super((event) => event.type === "done" || event.type === "error", (event) => {
|
|
@@ -28,14 +32,25 @@ function createPiServerRequest(signal) {
|
|
|
28
32
|
});
|
|
29
33
|
}
|
|
30
34
|
const sessionStaticContextHashes = new Map();
|
|
31
|
-
const sessionSyncedEntryIds = new Map();
|
|
32
35
|
const sessionTreeHashes = new Map();
|
|
33
36
|
const sessionTreeEntryCounts = new Map();
|
|
34
37
|
const sessionTreeLeafIds = new Map();
|
|
38
|
+
const sessionTreeRevisions = new Map();
|
|
35
39
|
const sessionHasTemporaryTree = new Set();
|
|
40
|
+
const sessionLocalTreeHashes = new Map();
|
|
41
|
+
const sessionPendingRuns = new Map();
|
|
42
|
+
const activePersistentRunLeases = new Map();
|
|
36
43
|
const RESPONSE_BODY_EXCERPT_CHARS = 500;
|
|
44
|
+
const PI_SERVER_CANCEL_TIMEOUT_MS = 5000;
|
|
45
|
+
const PI_SERVER_COMPACT_DEFAULT_RECOVERY_WINDOW_MS = 6 * 60 * 60_000;
|
|
46
|
+
const PI_SERVER_COMPACT_MAX_RECONNECT_DELAY_MS = 30_000;
|
|
47
|
+
const PI_SERVER_STREAM_DEFAULT_RECOVERY_WINDOW_MS = 6 * 60 * 60_000;
|
|
48
|
+
const PI_SERVER_STREAM_MAX_RECONNECT_DELAY_MS = 30_000;
|
|
49
|
+
const PI_SERVER_RESPONSE_NO_PROGRESS_TIMEOUT_MS = 90_000;
|
|
50
|
+
const PI_SERVER_ERROR_BODY_MAX_BYTES = 64 * 1024;
|
|
51
|
+
const PI_SERVER_EVENT_STREAM_MAX_LINE_BYTES = 256 * 1024 * 1024;
|
|
37
52
|
const TRANSIENT_PI_SERVER_RETRY_DELAYS_MS = [250, 750, 1500];
|
|
38
|
-
const TRANSIENT_PI_SERVER_STATUS_CODES = new Set([502, 503, 504, 530]);
|
|
53
|
+
const TRANSIENT_PI_SERVER_STATUS_CODES = new Set([408, 425, 429, 502, 503, 504, 530]);
|
|
39
54
|
export function hashStaticContext(ctx) {
|
|
40
55
|
return hashPiServerStaticContext(ctx);
|
|
41
56
|
}
|
|
@@ -60,23 +75,224 @@ function getLinearTreeFromMessages(messages) {
|
|
|
60
75
|
}
|
|
61
76
|
export function resetSessionTracking(sessionId) {
|
|
62
77
|
sessionStaticContextHashes.delete(sessionId);
|
|
63
|
-
sessionSyncedEntryIds.delete(sessionId);
|
|
64
78
|
sessionTreeHashes.delete(sessionId);
|
|
65
79
|
sessionTreeEntryCounts.delete(sessionId);
|
|
66
80
|
sessionTreeLeafIds.delete(sessionId);
|
|
81
|
+
sessionTreeRevisions.delete(sessionId);
|
|
67
82
|
sessionHasTemporaryTree.delete(sessionId);
|
|
83
|
+
sessionLocalTreeHashes.delete(sessionId);
|
|
68
84
|
}
|
|
69
85
|
export function resetAllSessionTracking() {
|
|
86
|
+
for (const { lease } of activePersistentRunLeases.values()) {
|
|
87
|
+
releasePiServerRunStateLease(lease);
|
|
88
|
+
}
|
|
89
|
+
activePersistentRunLeases.clear();
|
|
70
90
|
sessionStaticContextHashes.clear();
|
|
71
|
-
sessionSyncedEntryIds.clear();
|
|
72
91
|
sessionTreeHashes.clear();
|
|
73
92
|
sessionTreeEntryCounts.clear();
|
|
74
93
|
sessionTreeLeafIds.clear();
|
|
94
|
+
sessionTreeRevisions.clear();
|
|
75
95
|
sessionHasTemporaryTree.clear();
|
|
96
|
+
sessionLocalTreeHashes.clear();
|
|
97
|
+
sessionPendingRuns.clear();
|
|
98
|
+
}
|
|
99
|
+
function acquirePersistentRunLease(sessionId, path) {
|
|
100
|
+
const normalizedPath = resolve(path);
|
|
101
|
+
if (activePersistentRunLeases.has(normalizedPath)) {
|
|
102
|
+
throw new Error(`Cannot start pi-server run for ${sessionId}; this process already owns the session lease`);
|
|
103
|
+
}
|
|
104
|
+
const lease = acquirePiServerRunStateLease(normalizedPath);
|
|
105
|
+
activePersistentRunLeases.set(normalizedPath, { sessionId, lease, scope: "run" });
|
|
106
|
+
return lease;
|
|
107
|
+
}
|
|
108
|
+
export function releasePiServerRunLease(sessionId, path) {
|
|
109
|
+
if (!path)
|
|
110
|
+
return;
|
|
111
|
+
const normalizedPath = resolve(path);
|
|
112
|
+
const active = activePersistentRunLeases.get(normalizedPath);
|
|
113
|
+
if (!active || active.sessionId !== sessionId || active.scope !== "run")
|
|
114
|
+
return;
|
|
115
|
+
activePersistentRunLeases.delete(normalizedPath);
|
|
116
|
+
releasePiServerRunStateLease(active.lease);
|
|
117
|
+
}
|
|
118
|
+
export function acquirePiServerSessionLease(sessionId, path) {
|
|
119
|
+
const normalizedPath = resolve(path);
|
|
120
|
+
if (activePersistentRunLeases.has(normalizedPath)) {
|
|
121
|
+
throw new Error(`Cannot start pi-server session ${sessionId}; this process already owns the session lease`);
|
|
122
|
+
}
|
|
123
|
+
const lease = acquirePiServerRunStateLease(normalizedPath);
|
|
124
|
+
activePersistentRunLeases.set(normalizedPath, { sessionId, lease, scope: "session" });
|
|
125
|
+
}
|
|
126
|
+
export function releasePiServerSessionLease(sessionId, path) {
|
|
127
|
+
if (!path)
|
|
128
|
+
return;
|
|
129
|
+
const normalizedPath = resolve(path);
|
|
130
|
+
const active = activePersistentRunLeases.get(normalizedPath);
|
|
131
|
+
if (!active || active.sessionId !== sessionId || active.scope !== "session")
|
|
132
|
+
return;
|
|
133
|
+
activePersistentRunLeases.delete(normalizedPath);
|
|
134
|
+
releasePiServerRunStateLease(active.lease);
|
|
135
|
+
}
|
|
136
|
+
function getPersistentRunLease(sessionId, path) {
|
|
137
|
+
const active = activePersistentRunLeases.get(resolve(path));
|
|
138
|
+
if (!active)
|
|
139
|
+
return undefined;
|
|
140
|
+
if (active.sessionId !== sessionId) {
|
|
141
|
+
throw new Error(`Pi-server run lease belongs to session ${active.sessionId}, not ${sessionId}`);
|
|
142
|
+
}
|
|
143
|
+
return active;
|
|
144
|
+
}
|
|
145
|
+
class PiServerCompactOperationError extends Error {
|
|
146
|
+
payload;
|
|
147
|
+
constructor(message, payload) {
|
|
148
|
+
super(message);
|
|
149
|
+
this.name = "PiServerCompactOperationError";
|
|
150
|
+
this.payload = payload;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
class PiServerCompactProtocolError extends Error {
|
|
154
|
+
constructor(message) {
|
|
155
|
+
super(message);
|
|
156
|
+
this.name = "PiServerCompactProtocolError";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
class PiServerCompactInterruptionError extends Error {
|
|
160
|
+
observedProgress;
|
|
161
|
+
constructor(message, observedProgress) {
|
|
162
|
+
super(message);
|
|
163
|
+
this.name = "PiServerCompactInterruptionError";
|
|
164
|
+
this.observedProgress = observedProgress;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
class PiServerStreamProtocolError extends Error {
|
|
168
|
+
constructor(message) {
|
|
169
|
+
super(message);
|
|
170
|
+
this.name = "PiServerStreamProtocolError";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
class PiServerHttpResponseError extends Error {
|
|
174
|
+
status;
|
|
175
|
+
retryable;
|
|
176
|
+
constructor(message, status, retryable) {
|
|
177
|
+
super(message);
|
|
178
|
+
this.name = "PiServerHttpResponseError";
|
|
179
|
+
this.status = status;
|
|
180
|
+
this.retryable = retryable;
|
|
181
|
+
}
|
|
76
182
|
}
|
|
77
183
|
function isObject(value) {
|
|
78
184
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
79
185
|
}
|
|
186
|
+
function isNonNegativeInteger(value) {
|
|
187
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
188
|
+
}
|
|
189
|
+
function validateProviderExecutionFingerprint(value) {
|
|
190
|
+
if (value !== undefined && !/^[a-f0-9]{64}$/u.test(value)) {
|
|
191
|
+
throw new Error("pi-server provider execution fingerprint must be a lowercase SHA-256 digest");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
export function getPiServerRunDiagnostic(message) {
|
|
195
|
+
const diagnostics = message.diagnostics?.filter((diagnostic) => diagnostic.type === "pi_server_run") ?? [];
|
|
196
|
+
if (diagnostics.length === 0)
|
|
197
|
+
return undefined;
|
|
198
|
+
if (diagnostics.length !== 1) {
|
|
199
|
+
throw new Error("pi-server assistant message contained multiple run diagnostics");
|
|
200
|
+
}
|
|
201
|
+
const details = diagnostics[0].details;
|
|
202
|
+
if (!isObject(details) ||
|
|
203
|
+
typeof details.sessionId !== "string" ||
|
|
204
|
+
details.sessionId.length === 0 ||
|
|
205
|
+
typeof details.runId !== "string" ||
|
|
206
|
+
details.runId.length === 0 ||
|
|
207
|
+
typeof details.requestMac !== "string" ||
|
|
208
|
+
!/^[a-f0-9]{64}$/u.test(details.requestMac) ||
|
|
209
|
+
(details.restartUnknown !== undefined && typeof details.restartUnknown !== "boolean")) {
|
|
210
|
+
throw new Error("pi-server assistant message contained an invalid run diagnostic");
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
sessionId: details.sessionId,
|
|
214
|
+
runId: details.runId,
|
|
215
|
+
requestMac: details.requestMac,
|
|
216
|
+
restartUnknown: details.restartUnknown === true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function hashPiServerStreamRequest(value) {
|
|
220
|
+
const serialized = canonicalJsonStringify(value);
|
|
221
|
+
if (serialized === undefined) {
|
|
222
|
+
throw new Error("Failed to serialize pi-server stream request identity");
|
|
223
|
+
}
|
|
224
|
+
return createHash("sha256").update(serialized).digest("hex");
|
|
225
|
+
}
|
|
226
|
+
function parsePiServerSessionSummary(value, expectedSessionId, errorPrefix) {
|
|
227
|
+
if (!isObject(value)) {
|
|
228
|
+
throw new Error(`${errorPrefix} (response was not an object)`);
|
|
229
|
+
}
|
|
230
|
+
if (value.protocolVersion !== PI_SERVER_PROTOCOL_VERSION) {
|
|
231
|
+
const received = value.protocolVersion === undefined ? "missing" : JSON.stringify(value.protocolVersion);
|
|
232
|
+
throw new Error(`${errorPrefix} (unsupported pi-server protocol version: ${received}; expected ${PI_SERVER_PROTOCOL_VERSION})`);
|
|
233
|
+
}
|
|
234
|
+
if (value.sessionId !== expectedSessionId) {
|
|
235
|
+
throw new Error(`${errorPrefix} (response sessionId did not match the requested session)`);
|
|
236
|
+
}
|
|
237
|
+
if (typeof value.staticContextHash !== "string" ||
|
|
238
|
+
(value.staticContextHash !== "" && !/^[a-f0-9]{64}$/i.test(value.staticContextHash))) {
|
|
239
|
+
throw new Error(`${errorPrefix} (response staticContextHash was not a valid digest)`);
|
|
240
|
+
}
|
|
241
|
+
if (typeof value.treeHash !== "string" || !/^[a-f0-9]{64}$/i.test(value.treeHash)) {
|
|
242
|
+
throw new Error(`${errorPrefix} (response treeHash was not a valid digest)`);
|
|
243
|
+
}
|
|
244
|
+
if (!isNonNegativeInteger(value.messageCount)) {
|
|
245
|
+
throw new Error(`${errorPrefix} (response messageCount was not a non-negative integer)`);
|
|
246
|
+
}
|
|
247
|
+
if (!isNonNegativeInteger(value.entryCount)) {
|
|
248
|
+
throw new Error(`${errorPrefix} (response entryCount was not a non-negative integer)`);
|
|
249
|
+
}
|
|
250
|
+
if (value.leafId !== null && typeof value.leafId !== "string") {
|
|
251
|
+
throw new Error(`${errorPrefix} (response leafId was not a string or null)`);
|
|
252
|
+
}
|
|
253
|
+
if (!isNonNegativeInteger(value.revision)) {
|
|
254
|
+
throw new Error(`${errorPrefix} (response revision was not a non-negative integer)`);
|
|
255
|
+
}
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
258
|
+
function recordPiServerSessionSummary(sessionId, summary) {
|
|
259
|
+
sessionTreeHashes.set(sessionId, summary.treeHash);
|
|
260
|
+
sessionTreeEntryCounts.set(sessionId, summary.entryCount);
|
|
261
|
+
sessionTreeLeafIds.set(sessionId, summary.leafId);
|
|
262
|
+
sessionTreeRevisions.set(sessionId, summary.revision);
|
|
263
|
+
}
|
|
264
|
+
function extendsCachedImmutableTree(cachedEntries, entries) {
|
|
265
|
+
if (cachedEntries.length > entries.length)
|
|
266
|
+
return false;
|
|
267
|
+
if (cachedEntries.length === 0)
|
|
268
|
+
return true;
|
|
269
|
+
return cachedEntries[cachedEntries.length - 1] === entries[cachedEntries.length - 1];
|
|
270
|
+
}
|
|
271
|
+
function hashLocalTree(sessionId, entries) {
|
|
272
|
+
const cached = sessionLocalTreeHashes.get(sessionId);
|
|
273
|
+
let treeHash = PI_SERVER_EMPTY_TREE_HASH;
|
|
274
|
+
let startIndex = 0;
|
|
275
|
+
if (cached && extendsCachedImmutableTree(cached.entries, entries)) {
|
|
276
|
+
treeHash = cached.treeHash;
|
|
277
|
+
startIndex = cached.entries.length;
|
|
278
|
+
}
|
|
279
|
+
for (let index = startIndex; index < entries.length; index++) {
|
|
280
|
+
treeHash = appendPiServerTreeHash(treeHash, hashPiServerTreeEntry(entries[index]));
|
|
281
|
+
}
|
|
282
|
+
sessionLocalTreeHashes.set(sessionId, { entries, treeHash });
|
|
283
|
+
return treeHash;
|
|
284
|
+
}
|
|
285
|
+
function hashLocalTreePrefix(sessionId, entries, entryCount) {
|
|
286
|
+
const cached = sessionLocalTreeHashes.get(sessionId);
|
|
287
|
+
if (cached && cached.entries.length === entryCount && extendsCachedImmutableTree(cached.entries, entries)) {
|
|
288
|
+
return cached.treeHash;
|
|
289
|
+
}
|
|
290
|
+
let treeHash = PI_SERVER_EMPTY_TREE_HASH;
|
|
291
|
+
for (let index = 0; index < entryCount; index++) {
|
|
292
|
+
treeHash = appendPiServerTreeHash(treeHash, hashPiServerTreeEntry(entries[index]));
|
|
293
|
+
}
|
|
294
|
+
return treeHash;
|
|
295
|
+
}
|
|
80
296
|
function getResponseStatus(response) {
|
|
81
297
|
const status = response.statusText ? `${response.status} ${response.statusText}` : String(response.status);
|
|
82
298
|
return response.status >= 500 ? `${status} (server error)` : status;
|
|
@@ -91,6 +307,60 @@ function getBodyExcerpt(text) {
|
|
|
91
307
|
const excerpt = trimmed.slice(0, RESPONSE_BODY_EXCERPT_CHARS);
|
|
92
308
|
return excerpt.length < trimmed.length ? `${excerpt}...` : excerpt;
|
|
93
309
|
}
|
|
310
|
+
function getPiServerResponseNoProgressTimeoutMs() {
|
|
311
|
+
const environmentValue = process.env.PI_SERVER_RESPONSE_NO_PROGRESS_TIMEOUT_MS;
|
|
312
|
+
const value = environmentValue === undefined ? PI_SERVER_RESPONSE_NO_PROGRESS_TIMEOUT_MS : Number(environmentValue);
|
|
313
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
314
|
+
throw new Error("PI_SERVER_RESPONSE_NO_PROGRESS_TIMEOUT_MS must be positive finite milliseconds");
|
|
315
|
+
}
|
|
316
|
+
return value;
|
|
317
|
+
}
|
|
318
|
+
function createPiServerBodyNoProgressState() {
|
|
319
|
+
const timeoutMs = getPiServerResponseNoProgressTimeoutMs();
|
|
320
|
+
return { timeoutMs, deadlineMs: Date.now() + timeoutMs };
|
|
321
|
+
}
|
|
322
|
+
function createPiServerResponseNoProgressError(timeoutMs) {
|
|
323
|
+
const error = new Error(`pi-server response made no progress for ${timeoutMs}ms`);
|
|
324
|
+
error.name = "PiServerResponseNoProgressError";
|
|
325
|
+
return error;
|
|
326
|
+
}
|
|
327
|
+
async function readPiServerBodyChunk(reader, state) {
|
|
328
|
+
while (true) {
|
|
329
|
+
const remainingMs = state.deadlineMs - Date.now();
|
|
330
|
+
if (remainingMs <= 0) {
|
|
331
|
+
throw createPiServerResponseNoProgressError(state.timeoutMs);
|
|
332
|
+
}
|
|
333
|
+
const result = await new Promise((resolve, reject) => {
|
|
334
|
+
let settled = false;
|
|
335
|
+
const timer = setTimeout(() => {
|
|
336
|
+
if (settled)
|
|
337
|
+
return;
|
|
338
|
+
settled = true;
|
|
339
|
+
reject(createPiServerResponseNoProgressError(state.timeoutMs));
|
|
340
|
+
}, remainingMs);
|
|
341
|
+
timer.unref();
|
|
342
|
+
void reader.read().then((value) => {
|
|
343
|
+
if (settled)
|
|
344
|
+
return;
|
|
345
|
+
settled = true;
|
|
346
|
+
clearTimeout(timer);
|
|
347
|
+
resolve(value);
|
|
348
|
+
}, (error) => {
|
|
349
|
+
if (settled)
|
|
350
|
+
return;
|
|
351
|
+
settled = true;
|
|
352
|
+
clearTimeout(timer);
|
|
353
|
+
reject(error);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
if (result.done || result.value.byteLength > 0) {
|
|
357
|
+
if (!result.done)
|
|
358
|
+
state.deadlineMs = Date.now() + state.timeoutMs;
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
94
364
|
function getJsonErrorText(text) {
|
|
95
365
|
if (!text)
|
|
96
366
|
return undefined;
|
|
@@ -110,16 +380,17 @@ function formatResponseDetails(response, bodyText) {
|
|
|
110
380
|
return `${getResponseStatus(response)}; content-type: ${getResponseContentType(response)}; body excerpt: ${body}`;
|
|
111
381
|
}
|
|
112
382
|
async function readPiServerFailure(response) {
|
|
113
|
-
const bodyText = await response
|
|
383
|
+
const bodyText = await readPiServerResponseText(response, PI_SERVER_ERROR_BODY_MAX_BYTES);
|
|
114
384
|
return {
|
|
115
385
|
details: formatResponseDetails(response, bodyText),
|
|
116
386
|
matchText: getJsonErrorText(bodyText) ?? bodyText,
|
|
387
|
+
bodyText,
|
|
117
388
|
};
|
|
118
389
|
}
|
|
119
390
|
async function readPiServerJson(response, errorPrefix) {
|
|
120
|
-
const bodyText = await response.
|
|
391
|
+
const bodyText = await readPiServerResponseText(response, response.ok ? Number.POSITIVE_INFINITY : PI_SERVER_ERROR_BODY_MAX_BYTES);
|
|
121
392
|
if (!response.ok) {
|
|
122
|
-
throw new
|
|
393
|
+
throw new PiServerHttpResponseError(`${errorPrefix} (${formatResponseDetails(response, bodyText)})`, response.status, isTransientPiServerResponse(response, bodyText));
|
|
123
394
|
}
|
|
124
395
|
try {
|
|
125
396
|
return JSON.parse(bodyText);
|
|
@@ -133,41 +404,41 @@ function getServerSentEventFieldValue(line, field) {
|
|
|
133
404
|
const value = line.slice(prefix.length);
|
|
134
405
|
return value.startsWith(" ") ? value.slice(1) : value;
|
|
135
406
|
}
|
|
136
|
-
function
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (dataLines.length === 0)
|
|
142
|
-
return;
|
|
143
|
-
events.push({ event, data: dataLines.join("\n") });
|
|
144
|
-
event = "message";
|
|
145
|
-
dataLines = [];
|
|
146
|
-
};
|
|
147
|
-
for (const line of bodyText.split(/\r?\n/)) {
|
|
148
|
-
if (line === "") {
|
|
149
|
-
flush();
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
if (line.startsWith(":"))
|
|
153
|
-
continue;
|
|
154
|
-
if (line.startsWith("event:")) {
|
|
155
|
-
event = getServerSentEventFieldValue(line, "event");
|
|
407
|
+
function updateEventStreamLineBytes(chunk, currentLineBytes) {
|
|
408
|
+
let lineBytes = currentLineBytes;
|
|
409
|
+
for (const byte of chunk) {
|
|
410
|
+
if (byte === 0x0a) {
|
|
411
|
+
lineBytes = 0;
|
|
156
412
|
continue;
|
|
157
413
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
414
|
+
lineBytes++;
|
|
415
|
+
if (lineBytes > PI_SERVER_EVENT_STREAM_MAX_LINE_BYTES)
|
|
416
|
+
return -1;
|
|
417
|
+
}
|
|
418
|
+
return lineBytes;
|
|
419
|
+
}
|
|
420
|
+
function extractDecodedEventStreamLines(decoded, incompleteLineParts) {
|
|
421
|
+
const lines = [];
|
|
422
|
+
let start = 0;
|
|
423
|
+
let newlineIndex = decoded.indexOf("\n");
|
|
424
|
+
while (newlineIndex >= 0) {
|
|
425
|
+
incompleteLineParts.push(decoded.slice(start, newlineIndex));
|
|
426
|
+
lines.push(incompleteLineParts.join(""));
|
|
427
|
+
incompleteLineParts.length = 0;
|
|
428
|
+
start = newlineIndex + 1;
|
|
429
|
+
newlineIndex = decoded.indexOf("\n", start);
|
|
430
|
+
}
|
|
431
|
+
if (start < decoded.length) {
|
|
432
|
+
incompleteLineParts.push(decoded.slice(start));
|
|
161
433
|
}
|
|
162
|
-
|
|
163
|
-
return events;
|
|
434
|
+
return lines;
|
|
164
435
|
}
|
|
165
436
|
function parseServerSentEventData(event, errorPrefix) {
|
|
166
437
|
try {
|
|
167
438
|
return JSON.parse(event.data);
|
|
168
439
|
}
|
|
169
440
|
catch {
|
|
170
|
-
throw new
|
|
441
|
+
throw new PiServerCompactProtocolError(`${errorPrefix} (invalid event-stream JSON data: ${getBodyExcerpt(event.data)})`);
|
|
171
442
|
}
|
|
172
443
|
}
|
|
173
444
|
function formatServerSentEventError(response, payload) {
|
|
@@ -175,37 +446,139 @@ function formatServerSentEventError(response, payload) {
|
|
|
175
446
|
return `${getResponseStatus(response)}; content-type: ${getResponseContentType(response)}; body excerpt: ${getBodyExcerpt(message)}`;
|
|
176
447
|
}
|
|
177
448
|
async function readPiServerEventStreamJson(response, errorPrefix) {
|
|
178
|
-
|
|
179
|
-
|
|
449
|
+
if (!response.body) {
|
|
450
|
+
throw new PiServerCompactInterruptionError(`${errorPrefix} (response body was missing)`, false);
|
|
451
|
+
}
|
|
452
|
+
const reader = response.body.getReader();
|
|
453
|
+
const decoder = new TextDecoder();
|
|
454
|
+
const noProgress = createPiServerBodyNoProgressState();
|
|
455
|
+
let observedProgress = false;
|
|
456
|
+
const incompleteLineParts = [];
|
|
457
|
+
let eventName = "message";
|
|
458
|
+
let dataLines = [];
|
|
459
|
+
let eventDataBytes = 0;
|
|
460
|
+
let incompleteLineBytes = 0;
|
|
461
|
+
const dispatchEvent = () => {
|
|
462
|
+
if (dataLines.length === 0) {
|
|
463
|
+
eventName = "message";
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
const event = { event: eventName, data: dataLines.join("\n") };
|
|
467
|
+
eventName = "message";
|
|
468
|
+
dataLines = [];
|
|
469
|
+
eventDataBytes = 0;
|
|
180
470
|
if (event.event === "error") {
|
|
181
|
-
|
|
471
|
+
const payload = parseServerSentEventData(event, errorPrefix);
|
|
472
|
+
throw new PiServerCompactOperationError(`${errorPrefix} (${formatServerSentEventError(response, payload)})`, payload);
|
|
182
473
|
}
|
|
183
474
|
if (event.event === "result") {
|
|
184
|
-
return parseServerSentEventData(event, errorPrefix);
|
|
475
|
+
return { result: parseServerSentEventData(event, errorPrefix) };
|
|
476
|
+
}
|
|
477
|
+
return undefined;
|
|
478
|
+
};
|
|
479
|
+
const consumeLine = (rawLine) => {
|
|
480
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
481
|
+
if (line === "")
|
|
482
|
+
return dispatchEvent();
|
|
483
|
+
if (line.startsWith(":"))
|
|
484
|
+
return undefined;
|
|
485
|
+
if (line.startsWith("event:")) {
|
|
486
|
+
eventName = getServerSentEventFieldValue(line, "event");
|
|
487
|
+
}
|
|
488
|
+
else if (line.startsWith("data:")) {
|
|
489
|
+
const data = getServerSentEventFieldValue(line, "data");
|
|
490
|
+
eventDataBytes += Buffer.byteLength(data, "utf-8") + (dataLines.length === 0 ? 0 : 1);
|
|
491
|
+
if (eventDataBytes > PI_SERVER_EVENT_STREAM_MAX_LINE_BYTES) {
|
|
492
|
+
throw new PiServerCompactProtocolError(`${errorPrefix} (event-stream frame exceeded ${PI_SERVER_EVENT_STREAM_MAX_LINE_BYTES} bytes)`);
|
|
493
|
+
}
|
|
494
|
+
dataLines.push(data);
|
|
495
|
+
}
|
|
496
|
+
return undefined;
|
|
497
|
+
};
|
|
498
|
+
try {
|
|
499
|
+
while (true) {
|
|
500
|
+
let readResult;
|
|
501
|
+
try {
|
|
502
|
+
readResult = await readPiServerBodyChunk(reader, noProgress);
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
throw new PiServerCompactInterruptionError(error instanceof Error ? error.message : String(error), observedProgress);
|
|
506
|
+
}
|
|
507
|
+
if (readResult.done) {
|
|
508
|
+
const finalLines = extractDecodedEventStreamLines(decoder.decode(), incompleteLineParts);
|
|
509
|
+
for (const line of finalLines) {
|
|
510
|
+
const result = consumeLine(line);
|
|
511
|
+
if (result)
|
|
512
|
+
return result.result;
|
|
513
|
+
}
|
|
514
|
+
if (incompleteLineParts.length > 0) {
|
|
515
|
+
const result = consumeLine(incompleteLineParts.join(""));
|
|
516
|
+
if (result)
|
|
517
|
+
return result.result;
|
|
518
|
+
incompleteLineParts.length = 0;
|
|
519
|
+
}
|
|
520
|
+
const result = dispatchEvent();
|
|
521
|
+
if (result)
|
|
522
|
+
return result.result;
|
|
523
|
+
throw new PiServerCompactInterruptionError(`${errorPrefix} (event stream ended before a result event)`, observedProgress);
|
|
524
|
+
}
|
|
525
|
+
if (readResult.value.byteLength > 0)
|
|
526
|
+
observedProgress = true;
|
|
527
|
+
incompleteLineBytes = updateEventStreamLineBytes(readResult.value, incompleteLineBytes);
|
|
528
|
+
if (incompleteLineBytes < 0) {
|
|
529
|
+
throw new PiServerCompactProtocolError(`${errorPrefix} (event-stream line exceeded ${PI_SERVER_EVENT_STREAM_MAX_LINE_BYTES} bytes)`);
|
|
530
|
+
}
|
|
531
|
+
const lines = extractDecodedEventStreamLines(decoder.decode(readResult.value, { stream: true }), incompleteLineParts);
|
|
532
|
+
for (const line of lines) {
|
|
533
|
+
const result = consumeLine(line);
|
|
534
|
+
if (result)
|
|
535
|
+
return result.result;
|
|
536
|
+
}
|
|
185
537
|
}
|
|
186
538
|
}
|
|
187
|
-
|
|
539
|
+
finally {
|
|
540
|
+
void reader.cancel().catch(() => undefined);
|
|
541
|
+
}
|
|
188
542
|
}
|
|
189
543
|
async function readPiServerCompactResponse(response, errorPrefix) {
|
|
190
544
|
const contentType = getResponseContentType(response).toLowerCase().split(";")[0]?.trim();
|
|
191
545
|
if (contentType === "text/event-stream") {
|
|
192
546
|
return readPiServerEventStreamJson(response, errorPrefix);
|
|
193
547
|
}
|
|
194
|
-
|
|
548
|
+
let bodyText;
|
|
549
|
+
try {
|
|
550
|
+
bodyText = await readPiServerResponseText(response);
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
throw new PiServerCompactInterruptionError(error instanceof Error ? error.message : String(error), false);
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
return JSON.parse(bodyText);
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
throw new PiServerCompactProtocolError(`${errorPrefix} (${formatResponseDetails(response, bodyText)}; expected JSON)`);
|
|
560
|
+
}
|
|
195
561
|
}
|
|
196
562
|
function isAbortError(error, signal) {
|
|
197
563
|
return signal?.aborted === true || (error instanceof Error && error.name === "AbortError");
|
|
198
564
|
}
|
|
199
565
|
function isTransientPiServerFetchError(error) {
|
|
200
566
|
const message = error instanceof Error ? error.message : String(error);
|
|
201
|
-
return /fetch failed|network|socket hang up|terminated|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|CONNECT timeout/i.test(message);
|
|
567
|
+
return /fetch failed|network|socket (?:hang up|closed|lost)|terminated|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|CONNECT timeout|response made no progress/i.test(message);
|
|
202
568
|
}
|
|
203
|
-
function isTransientPiServerResponse(response,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
569
|
+
function isTransientPiServerResponse(response, _bodyText) {
|
|
570
|
+
return TRANSIENT_PI_SERVER_STATUS_CODES.has(response.status);
|
|
571
|
+
}
|
|
572
|
+
function isRetryablePiServerInterruption(error) {
|
|
573
|
+
if (error instanceof PiServerHttpResponseError)
|
|
574
|
+
return error.retryable;
|
|
575
|
+
if (error instanceof PiServerCompactInterruptionError)
|
|
207
576
|
return true;
|
|
208
|
-
|
|
577
|
+
if (error instanceof PiServerCompactProtocolError || error instanceof PiServerStreamProtocolError)
|
|
578
|
+
return false;
|
|
579
|
+
if (error instanceof PiServerTransportBodyLimitError)
|
|
580
|
+
return false;
|
|
581
|
+
return isTransientPiServerFetchError(error);
|
|
209
582
|
}
|
|
210
583
|
async function waitForPiServerRetry(attempt, signal) {
|
|
211
584
|
await sleep(TRANSIENT_PI_SERVER_RETRY_DELAYS_MS[attempt], undefined, { signal });
|
|
@@ -225,7 +598,19 @@ async function postPiServerJsonWithTransientRetry(request, endpoint, body, error
|
|
|
225
598
|
}
|
|
226
599
|
throw error;
|
|
227
600
|
}
|
|
228
|
-
|
|
601
|
+
let bodyText;
|
|
602
|
+
try {
|
|
603
|
+
bodyText = await readPiServerResponseText(response, response.ok ? Number.POSITIVE_INFINITY : PI_SERVER_ERROR_BODY_MAX_BYTES);
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
if (attempt < TRANSIENT_PI_SERVER_RETRY_DELAYS_MS.length &&
|
|
607
|
+
!request.options.signal?.aborted &&
|
|
608
|
+
isTransientPiServerFetchError(error)) {
|
|
609
|
+
await waitForPiServerRetry(attempt, request.options.signal);
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
throw error;
|
|
613
|
+
}
|
|
229
614
|
if (!response.ok) {
|
|
230
615
|
if (attempt < TRANSIENT_PI_SERVER_RETRY_DELAYS_MS.length &&
|
|
231
616
|
!request.options.signal?.aborted &&
|
|
@@ -233,7 +618,7 @@ async function postPiServerJsonWithTransientRetry(request, endpoint, body, error
|
|
|
233
618
|
await waitForPiServerRetry(attempt, request.options.signal);
|
|
234
619
|
continue;
|
|
235
620
|
}
|
|
236
|
-
throw new
|
|
621
|
+
throw new PiServerHttpResponseError(`${errorPrefix} (${formatResponseDetails(response, bodyText)})`, response.status, isTransientPiServerResponse(response, bodyText));
|
|
237
622
|
}
|
|
238
623
|
try {
|
|
239
624
|
return JSON.parse(bodyText);
|
|
@@ -248,46 +633,37 @@ async function ensurePiServerEventStream(response) {
|
|
|
248
633
|
if (contentType.toLowerCase().split(";")[0]?.trim() === "text/event-stream") {
|
|
249
634
|
return;
|
|
250
635
|
}
|
|
251
|
-
const bodyText = await response
|
|
636
|
+
const bodyText = await readPiServerResponseText(response, PI_SERVER_ERROR_BODY_MAX_BYTES);
|
|
252
637
|
throw new Error(`pi-server error: ${formatResponseDetails(response, bodyText)}; expected text/event-stream`);
|
|
253
638
|
}
|
|
254
639
|
async function ensureSessionInit(sessionId, context, request) {
|
|
255
640
|
const currentHash = hashStaticContext(context);
|
|
256
641
|
const previousHash = sessionStaticContextHashes.get(sessionId);
|
|
257
642
|
if (previousHash === currentHash) {
|
|
258
|
-
return
|
|
259
|
-
sessionId,
|
|
260
|
-
staticContextHash: previousHash,
|
|
261
|
-
messageCount: 0,
|
|
262
|
-
fromCache: true,
|
|
263
|
-
};
|
|
643
|
+
return;
|
|
264
644
|
}
|
|
265
645
|
const staticContext = {
|
|
266
646
|
systemPrompt: context.systemPrompt,
|
|
267
647
|
tools: context.tools,
|
|
268
648
|
};
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
sessionTreeHashes.set(sessionId, result.treeHash);
|
|
274
|
-
sessionTreeLeafIds.set(sessionId, result.leafId ?? null);
|
|
275
|
-
if (result.entryCount !== undefined) {
|
|
276
|
-
sessionTreeEntryCounts.set(sessionId, result.entryCount);
|
|
277
|
-
}
|
|
649
|
+
const probeValue = await postPiServerJsonWithTransientRetry(request, "/api/session/init", { sessionId, staticContextHash: currentHash }, "Session init failed");
|
|
650
|
+
const probe = parsePiServerSessionSummary(probeValue, sessionId, "Session init failed");
|
|
651
|
+
if (!isObject(probeValue) || typeof probeValue.staticContextRequired !== "boolean") {
|
|
652
|
+
throw new Error("Session init failed (protocol v2 response did not include staticContextRequired)");
|
|
278
653
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
654
|
+
let result = probe;
|
|
655
|
+
if (probeValue.staticContextRequired) {
|
|
656
|
+
const updateValue = await postPiServerJsonWithTransientRetry(request, "/api/session/update", { sessionId, staticContext }, "Session static context update failed");
|
|
657
|
+
result = parsePiServerSessionSummary(updateValue, sessionId, "Session static context update failed");
|
|
658
|
+
}
|
|
659
|
+
if (result.staticContextHash !== currentHash) {
|
|
660
|
+
throw new Error("Session init failed (server staticContextHash did not match the requested context)");
|
|
661
|
+
}
|
|
662
|
+
sessionStaticContextHashes.set(sessionId, currentHash);
|
|
663
|
+
recordPiServerSessionSummary(sessionId, result);
|
|
286
664
|
}
|
|
287
665
|
function markTreeSynced(sessionId, tree) {
|
|
288
|
-
|
|
289
|
-
const prefixHashes = buildPiServerTreePrefixHashes(tree.entries);
|
|
290
|
-
sessionTreeHashes.set(sessionId, prefixHashes[prefixHashes.length - 1]);
|
|
666
|
+
sessionTreeHashes.set(sessionId, hashLocalTree(sessionId, tree.entries));
|
|
291
667
|
sessionTreeEntryCounts.set(sessionId, tree.entries.length);
|
|
292
668
|
sessionTreeLeafIds.set(sessionId, tree.leafId);
|
|
293
669
|
if (tree.replace) {
|
|
@@ -298,58 +674,275 @@ function markTreeSynced(sessionId, tree) {
|
|
|
298
674
|
}
|
|
299
675
|
}
|
|
300
676
|
async function postTreeJson(request, endpoint, body, errorPrefix) {
|
|
301
|
-
|
|
677
|
+
if (!isObject(body) || typeof body.sessionId !== "string") {
|
|
678
|
+
throw new Error(`${errorPrefix} (request sessionId was missing)`);
|
|
679
|
+
}
|
|
680
|
+
const value = await postPiServerJsonWithTransientRetry(request, endpoint, body, errorPrefix);
|
|
681
|
+
const summary = parsePiServerSessionSummary(value, body.sessionId, errorPrefix);
|
|
682
|
+
recordPiServerSessionSummary(body.sessionId, summary);
|
|
683
|
+
}
|
|
684
|
+
function parsePiServerTreeEntries(value, errorPrefix) {
|
|
685
|
+
if (!Array.isArray(value) ||
|
|
686
|
+
value.some((entry) => !isObject(entry) ||
|
|
687
|
+
typeof entry.type !== "string" ||
|
|
688
|
+
typeof entry.id !== "string" ||
|
|
689
|
+
(entry.parentId !== null && typeof entry.parentId !== "string") ||
|
|
690
|
+
typeof entry.timestamp !== "string")) {
|
|
691
|
+
throw new Error(`${errorPrefix} (response entries were not a valid session tree array)`);
|
|
692
|
+
}
|
|
693
|
+
return value;
|
|
694
|
+
}
|
|
695
|
+
function buildPiServerActiveBranch(entries, leafId, errorPrefix) {
|
|
696
|
+
const entriesById = new Map();
|
|
697
|
+
for (const entry of entries) {
|
|
698
|
+
if (entriesById.has(entry.id)) {
|
|
699
|
+
throw new Error(`${errorPrefix} (response session tree contained duplicate entry ids)`);
|
|
700
|
+
}
|
|
701
|
+
entriesById.set(entry.id, entry);
|
|
702
|
+
}
|
|
703
|
+
const branch = [];
|
|
704
|
+
const branchIds = new Set();
|
|
705
|
+
let current = leafId === null ? undefined : entriesById.get(leafId);
|
|
706
|
+
if (leafId !== null && !current) {
|
|
707
|
+
throw new Error(`${errorPrefix} (response leafId did not exist in the session tree)`);
|
|
708
|
+
}
|
|
709
|
+
while (current) {
|
|
710
|
+
if (branchIds.has(current.id)) {
|
|
711
|
+
throw new Error(`${errorPrefix} (response session tree contained a parent cycle)`);
|
|
712
|
+
}
|
|
713
|
+
branchIds.add(current.id);
|
|
714
|
+
branch.push(current);
|
|
715
|
+
if (current.parentId === null)
|
|
716
|
+
break;
|
|
717
|
+
const parent = entriesById.get(current.parentId);
|
|
718
|
+
if (!parent) {
|
|
719
|
+
throw new Error(`${errorPrefix} (response session tree contained a missing parent)`);
|
|
720
|
+
}
|
|
721
|
+
current = parent;
|
|
722
|
+
}
|
|
723
|
+
branch.reverse();
|
|
724
|
+
return branch;
|
|
725
|
+
}
|
|
726
|
+
function buildPiServerHistorySnapshot(summary, entries, errorPrefix) {
|
|
727
|
+
if (entries.length !== summary.entryCount) {
|
|
728
|
+
throw new Error(`${errorPrefix} (response entryCount did not match the session tree)`);
|
|
729
|
+
}
|
|
730
|
+
if (hashEntries(entries) !== summary.treeHash) {
|
|
731
|
+
throw new Error(`${errorPrefix} (response treeHash did not match the session tree)`);
|
|
732
|
+
}
|
|
733
|
+
const branch = buildPiServerActiveBranch(entries, summary.leafId, errorPrefix);
|
|
734
|
+
const messages = convertToLlm(buildSessionContext(branch).messages);
|
|
735
|
+
if (messages.length !== summary.messageCount) {
|
|
736
|
+
throw new Error(`${errorPrefix} (response messageCount did not match the active session branch)`);
|
|
737
|
+
}
|
|
738
|
+
return {
|
|
739
|
+
entries,
|
|
740
|
+
leafId: summary.leafId,
|
|
741
|
+
messages,
|
|
742
|
+
treeHash: summary.treeHash,
|
|
743
|
+
revision: summary.revision,
|
|
744
|
+
};
|
|
302
745
|
}
|
|
303
746
|
async function fetchPiServerHistory(sessionId, request, localTree) {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
747
|
+
const query = new URLSearchParams({ protocolVersion: String(PI_SERVER_PROTOCOL_VERSION) });
|
|
748
|
+
if (localTree) {
|
|
749
|
+
query.set("entriesFrom", String(localTree.entries.length));
|
|
750
|
+
query.set("baseTreeHash", hashLocalTree(sessionId, localTree.entries));
|
|
751
|
+
const revision = sessionTreeRevisions.get(sessionId);
|
|
752
|
+
if (revision !== undefined)
|
|
753
|
+
query.set("revision", String(revision));
|
|
754
|
+
}
|
|
755
|
+
const historyUrl = `/api/session/${encodeURIComponent(sessionId)}/history?${query.toString()}`;
|
|
308
756
|
const response = await request.getJson(historyUrl);
|
|
309
757
|
if (response.status === 404) {
|
|
310
|
-
|
|
758
|
+
const failure = await readPiServerFailure(response);
|
|
759
|
+
if (/session not found/i.test(failure.matchText))
|
|
760
|
+
return undefined;
|
|
761
|
+
throw new Error(`Session history reconciliation failed (pi-server protocol v${PI_SERVER_PROTOCOL_VERSION} history endpoint unavailable; ${failure.details})`);
|
|
311
762
|
}
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
messages: history.messages ?? [],
|
|
318
|
-
};
|
|
763
|
+
const historyValue = await readPiServerJson(response, "Session history reconciliation failed");
|
|
764
|
+
const summary = parsePiServerSessionSummary(historyValue, sessionId, "Session history reconciliation failed");
|
|
765
|
+
const expectedStaticContextHash = sessionStaticContextHashes.get(sessionId);
|
|
766
|
+
if (expectedStaticContextHash !== undefined && summary.staticContextHash !== expectedStaticContextHash) {
|
|
767
|
+
throw new Error("Session history reconciliation failed (server staticContextHash changed unexpectedly)");
|
|
319
768
|
}
|
|
320
|
-
|
|
321
|
-
|
|
769
|
+
const history = historyValue;
|
|
770
|
+
if (history.treePatch !== undefined) {
|
|
771
|
+
if (!localTree || !isObject(history.treePatch)) {
|
|
772
|
+
throw new Error("Session history reconciliation failed (unexpected tree patch response)");
|
|
773
|
+
}
|
|
774
|
+
const patch = history.treePatch;
|
|
775
|
+
const expectedBaseRevision = sessionTreeRevisions.get(sessionId);
|
|
776
|
+
if (!isNonNegativeInteger(patch.entriesFrom) ||
|
|
777
|
+
patch.entriesFrom !== localTree.entries.length ||
|
|
778
|
+
!isNonNegativeInteger(patch.revision) ||
|
|
779
|
+
patch.revision !== summary.revision ||
|
|
780
|
+
(patch.baseRevision !== undefined && !isNonNegativeInteger(patch.baseRevision)) ||
|
|
781
|
+
(expectedBaseRevision !== undefined && patch.baseRevision !== expectedBaseRevision) ||
|
|
782
|
+
(patch.leafId !== null && typeof patch.leafId !== "string") ||
|
|
783
|
+
patch.leafId !== summary.leafId) {
|
|
784
|
+
throw new Error("Session history reconciliation failed (response tree patch metadata was invalid)");
|
|
785
|
+
}
|
|
786
|
+
const patchEntries = parsePiServerTreeEntries(patch.entries, "Session history reconciliation failed");
|
|
787
|
+
return buildPiServerHistorySnapshot(summary, [...localTree.entries.slice(0, patch.entriesFrom), ...patchEntries], "Session history reconciliation failed");
|
|
322
788
|
}
|
|
323
|
-
return
|
|
324
|
-
entries: history.entries,
|
|
325
|
-
leafId: history.leafId,
|
|
326
|
-
messages: history.messages,
|
|
327
|
-
};
|
|
789
|
+
return buildPiServerHistorySnapshot(summary, parsePiServerTreeEntries(history.entries, "Session history reconciliation failed"), "Session history reconciliation failed");
|
|
328
790
|
}
|
|
329
791
|
async function applyPiServerHistory(sessionId, snapshot, onHistoryReconciled) {
|
|
330
792
|
markTreeSynced(sessionId, snapshot);
|
|
793
|
+
sessionTreeRevisions.set(sessionId, snapshot.revision);
|
|
331
794
|
await onHistoryReconciled?.(snapshot);
|
|
332
795
|
}
|
|
333
796
|
async function fetchPiServerRun(sessionId, runId, request) {
|
|
334
797
|
const response = await request.getJson(`/api/session/${encodeURIComponent(sessionId)}/runs/${encodeURIComponent(runId)}`);
|
|
335
798
|
if (response.status === 404) {
|
|
336
|
-
|
|
799
|
+
const contentType = getResponseContentType(response).toLowerCase();
|
|
800
|
+
const failure = await readPiServerFailure(response);
|
|
801
|
+
if (contentType.includes("application/json") && /stream run not found|run not found/i.test(failure.matchText)) {
|
|
802
|
+
return undefined;
|
|
803
|
+
}
|
|
804
|
+
throw new Error(`Session run recovery failed (${failure.details}; expected structured run-not-found response)`);
|
|
805
|
+
}
|
|
806
|
+
const value = await readPiServerJson(response, "Session run recovery failed");
|
|
807
|
+
if (!isObject(value)) {
|
|
808
|
+
throw new Error("Session run recovery failed (response was not an object)");
|
|
809
|
+
}
|
|
810
|
+
if (value.sessionId !== sessionId) {
|
|
811
|
+
throw new Error("Session run recovery failed (response sessionId did not match the requested session)");
|
|
812
|
+
}
|
|
813
|
+
if (value.runId !== runId) {
|
|
814
|
+
throw new Error("Session run recovery failed (response runId did not match the requested run)");
|
|
815
|
+
}
|
|
816
|
+
if (typeof value.requestMac !== "string" || !/^[a-f0-9]{64}$/u.test(value.requestMac)) {
|
|
817
|
+
throw new Error("Session run recovery failed (response requestMac was not a lowercase SHA-256 digest)");
|
|
818
|
+
}
|
|
819
|
+
if (value.status !== "running" && value.status !== "completed" && value.status !== "failed") {
|
|
820
|
+
throw new Error("Session run recovery failed (response included an unsupported run status)");
|
|
821
|
+
}
|
|
822
|
+
if (!isNonNegativeInteger(value.nextSeq)) {
|
|
823
|
+
throw new Error("Session run recovery failed (response nextSeq was not a non-negative integer)");
|
|
824
|
+
}
|
|
825
|
+
if (value.message !== undefined && !isObject(value.message)) {
|
|
826
|
+
throw new Error("Session run recovery failed (response message was not an object)");
|
|
827
|
+
}
|
|
828
|
+
if (value.errorMessage !== undefined && typeof value.errorMessage !== "string") {
|
|
829
|
+
throw new Error("Session run recovery failed (response errorMessage was not a string)");
|
|
830
|
+
}
|
|
831
|
+
if (value.acknowledgedAt !== undefined && !isNonNegativeInteger(value.acknowledgedAt)) {
|
|
832
|
+
throw new Error("Session run recovery failed (response acknowledgedAt was not a non-negative integer)");
|
|
833
|
+
}
|
|
834
|
+
return value;
|
|
835
|
+
}
|
|
836
|
+
async function cancelPiServerRun(sessionId, runId) {
|
|
837
|
+
const request = createPiServerRequest(AbortSignal.timeout(PI_SERVER_CANCEL_TIMEOUT_MS));
|
|
838
|
+
const response = await request.postJson("/api/session/run/cancel", { sessionId, runId });
|
|
839
|
+
await readPiServerJson(response, "Session run cancel failed");
|
|
840
|
+
}
|
|
841
|
+
export async function acknowledgePiServerRunMessage(sessionId, message, runStatePath, signal) {
|
|
842
|
+
const activeLease = runStatePath ? getPersistentRunLease(sessionId, runStatePath) : undefined;
|
|
843
|
+
let lease = activeLease?.lease;
|
|
844
|
+
let releaseLeaseAfterAcknowledgement = activeLease?.scope === "run";
|
|
845
|
+
try {
|
|
846
|
+
const diagnostic = getPiServerRunDiagnostic(message);
|
|
847
|
+
if (!diagnostic)
|
|
848
|
+
return;
|
|
849
|
+
if (runStatePath && !lease) {
|
|
850
|
+
lease = acquirePersistentRunLease(sessionId, runStatePath);
|
|
851
|
+
releaseLeaseAfterAcknowledgement = true;
|
|
852
|
+
}
|
|
853
|
+
const pending = runStatePath ? readPiServerPendingRun(runStatePath, lease) : sessionPendingRuns.get(sessionId);
|
|
854
|
+
if (!pending)
|
|
855
|
+
return;
|
|
856
|
+
if (diagnostic.sessionId !== sessionId ||
|
|
857
|
+
diagnostic.sessionId !== pending.sessionId ||
|
|
858
|
+
diagnostic.runId !== pending.runId ||
|
|
859
|
+
diagnostic.requestMac !== pending.requestHash) {
|
|
860
|
+
throw new Error("Cannot acknowledge pi-server run: terminal diagnostic did not match durable pending state");
|
|
861
|
+
}
|
|
862
|
+
const ackSignal = signal?.aborted ? AbortSignal.timeout(PI_SERVER_CANCEL_TIMEOUT_MS) : signal;
|
|
863
|
+
const request = createPiServerRequest(ackSignal);
|
|
864
|
+
const response = await request.postJson("/api/session/run/ack", { sessionId, runId: pending.runId });
|
|
865
|
+
if (response.status === 404) {
|
|
866
|
+
const failure = await readPiServerFailure(response);
|
|
867
|
+
if (!/stream run not found|run not found/i.test(failure.matchText)) {
|
|
868
|
+
throw new Error(`Session run acknowledgement failed (${failure.details})`);
|
|
869
|
+
}
|
|
870
|
+
// The caller only reaches this point after the matching terminal assistant and tree
|
|
871
|
+
// are fsynced locally. A server-side acknowledged journal may already have expired.
|
|
872
|
+
if (runStatePath) {
|
|
873
|
+
acknowledgePiServerPendingRun(runStatePath, pending.runId, Date.now(), lease);
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
sessionPendingRuns.delete(sessionId);
|
|
877
|
+
}
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const value = await readPiServerJson(response, "Session run acknowledgement failed");
|
|
881
|
+
if (!isObject(value) ||
|
|
882
|
+
value.acknowledged !== true ||
|
|
883
|
+
value.sessionId !== sessionId ||
|
|
884
|
+
value.runId !== pending.runId ||
|
|
885
|
+
value.requestMac !== pending.requestHash ||
|
|
886
|
+
(value.status !== "completed" && value.status !== "failed") ||
|
|
887
|
+
!isNonNegativeInteger(value.acknowledgedAt)) {
|
|
888
|
+
throw new Error("Session run acknowledgement failed (response did not match the terminal pending run)");
|
|
889
|
+
}
|
|
890
|
+
if (runStatePath) {
|
|
891
|
+
acknowledgePiServerPendingRun(runStatePath, pending.runId, Date.now(), lease);
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
sessionPendingRuns.delete(sessionId);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
finally {
|
|
898
|
+
if (runStatePath && lease && releaseLeaseAfterAcknowledgement) {
|
|
899
|
+
releasePiServerRunLease(sessionId, runStatePath);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
async function cancelPiServerCompact(sessionId, operationId, requestHash) {
|
|
904
|
+
const request = createPiServerRequest(AbortSignal.timeout(PI_SERVER_CANCEL_TIMEOUT_MS));
|
|
905
|
+
const response = await request.postJson("/api/session/compact/cancel", { sessionId, operationId, requestHash });
|
|
906
|
+
const value = await readPiServerJson(response, "Session compaction cancel failed");
|
|
907
|
+
if (!isObject(value) ||
|
|
908
|
+
(value.canceled !== true && value.canceled !== false) ||
|
|
909
|
+
value.sessionId !== sessionId ||
|
|
910
|
+
value.operationId !== operationId ||
|
|
911
|
+
value.requestHash !== requestHash ||
|
|
912
|
+
value.status !== "failed" ||
|
|
913
|
+
value.resultStatus !== 499) {
|
|
914
|
+
throw new PiServerCompactProtocolError("Session compaction cancel failed (response identity was invalid)");
|
|
915
|
+
}
|
|
916
|
+
return parsePiServerCompactTerminalPayload(value.terminal, sessionId, operationId, requestHash, "Session compaction cancel failed");
|
|
917
|
+
}
|
|
918
|
+
async function recoverPiServerRun(sessionId, runId, request, signal) {
|
|
919
|
+
try {
|
|
920
|
+
const run = await fetchPiServerRun(sessionId, runId, request);
|
|
921
|
+
return run ? { status: run.status, requestMac: run.requestMac } : { status: "not_found" };
|
|
922
|
+
}
|
|
923
|
+
catch (error) {
|
|
924
|
+
if (signal?.aborted) {
|
|
925
|
+
throw error;
|
|
926
|
+
}
|
|
927
|
+
if (!isRetryablePiServerInterruption(error)) {
|
|
928
|
+
throw error;
|
|
929
|
+
}
|
|
930
|
+
return {
|
|
931
|
+
status: "unavailable",
|
|
932
|
+
details: `recovery request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
933
|
+
};
|
|
337
934
|
}
|
|
338
|
-
return readPiServerJson(response, "Session run recovery failed");
|
|
339
935
|
}
|
|
340
|
-
function
|
|
936
|
+
function getKnownServerPrefixCount(sessionId, entries) {
|
|
341
937
|
const entryCount = sessionTreeEntryCounts.get(sessionId);
|
|
342
938
|
const treeHash = sessionTreeHashes.get(sessionId);
|
|
343
939
|
if (entryCount === undefined || treeHash === undefined || entryCount > entries.length) {
|
|
344
940
|
return undefined;
|
|
345
941
|
}
|
|
346
|
-
|
|
347
|
-
if (prefixHashes[entryCount] !== treeHash) {
|
|
942
|
+
if (hashLocalTreePrefix(sessionId, entries, entryCount) !== treeHash) {
|
|
348
943
|
return undefined;
|
|
349
944
|
}
|
|
350
|
-
|
|
351
|
-
sessionSyncedEntryIds.set(sessionId, ids);
|
|
352
|
-
return ids;
|
|
945
|
+
return entryCount;
|
|
353
946
|
}
|
|
354
947
|
function isRecoverableTreeDivergenceError(error) {
|
|
355
948
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -360,19 +953,18 @@ function isRecoverableMissingServerState(response, errorBody) {
|
|
|
360
953
|
return false;
|
|
361
954
|
return /Session has no static context|session not found|parent entry .* does not exist|leafId .* does not exist|entry .* already exists/i.test(errorBody);
|
|
362
955
|
}
|
|
363
|
-
async function syncFullPiServerTree(sessionId,
|
|
956
|
+
async function syncFullPiServerTree(sessionId, tree, request) {
|
|
364
957
|
await postTreeJson(request, "/api/session/tree/sync", {
|
|
365
958
|
sessionId,
|
|
366
959
|
entries: tree.entries,
|
|
367
960
|
leafId: tree.leafId,
|
|
368
|
-
staticContext: getStaticContext(context),
|
|
369
961
|
}, "Session tree sync failed");
|
|
370
962
|
markTreeSynced(sessionId, tree);
|
|
371
963
|
}
|
|
372
964
|
function shouldUseServerHistory(sessionId, tree) {
|
|
373
965
|
return !tree.replace && !sessionHasTemporaryTree.has(sessionId) && (sessionTreeEntryCounts.get(sessionId) ?? 0) > 0;
|
|
374
966
|
}
|
|
375
|
-
async function recoverPiServerTreeDivergence(sessionId,
|
|
967
|
+
async function recoverPiServerTreeDivergence(sessionId, tree, request, onHistoryReconciled) {
|
|
376
968
|
if (shouldUseServerHistory(sessionId, tree)) {
|
|
377
969
|
const snapshot = await fetchPiServerHistory(sessionId, request, tree);
|
|
378
970
|
if (snapshot && snapshot.entries.length > 0) {
|
|
@@ -380,16 +972,19 @@ async function recoverPiServerTreeDivergence(sessionId, context, tree, request,
|
|
|
380
972
|
throw new Error("pi-server history differed from local history; local session was reconciled to server history");
|
|
381
973
|
}
|
|
382
974
|
}
|
|
383
|
-
await syncFullPiServerTree(sessionId,
|
|
975
|
+
await syncFullPiServerTree(sessionId, tree, request);
|
|
384
976
|
}
|
|
385
977
|
export async function syncPiServerTree(sessionId, context, tree, options) {
|
|
386
978
|
const request = createPiServerRequest(options?.signal);
|
|
387
979
|
await ensureSessionInit(sessionId, context, request);
|
|
388
|
-
await syncPiServerTreeWithRequest(sessionId,
|
|
980
|
+
await syncPiServerTreeWithRequest(sessionId, tree, request, options?.onHistoryReconciled);
|
|
389
981
|
}
|
|
390
|
-
async function syncPiServerTreeWithRequest(sessionId,
|
|
982
|
+
async function syncPiServerTreeWithRequest(sessionId, tree, request, onHistoryReconciled) {
|
|
391
983
|
const syncTree = tree;
|
|
392
|
-
const
|
|
984
|
+
const knownServerPrefixCount = !tree.replace && !sessionHasTemporaryTree.has(sessionId)
|
|
985
|
+
? getKnownServerPrefixCount(sessionId, syncTree.entries)
|
|
986
|
+
: undefined;
|
|
987
|
+
const currentHash = hashLocalTree(sessionId, syncTree.entries);
|
|
393
988
|
const previousHash = sessionTreeHashes.get(sessionId);
|
|
394
989
|
const previousLeafId = sessionTreeLeafIds.get(sessionId);
|
|
395
990
|
if (!tree.replace && previousHash === currentHash) {
|
|
@@ -401,7 +996,7 @@ async function syncPiServerTreeWithRequest(sessionId, context, tree, request, on
|
|
|
401
996
|
if (!isRecoverableTreeDivergenceError(error)) {
|
|
402
997
|
throw error;
|
|
403
998
|
}
|
|
404
|
-
await recoverPiServerTreeDivergence(sessionId,
|
|
999
|
+
await recoverPiServerTreeDivergence(sessionId, syncTree, request, onHistoryReconciled);
|
|
405
1000
|
return;
|
|
406
1001
|
}
|
|
407
1002
|
sessionTreeLeafIds.set(sessionId, syncTree.leafId);
|
|
@@ -409,32 +1004,28 @@ async function syncPiServerTreeWithRequest(sessionId, context, tree, request, on
|
|
|
409
1004
|
markTreeSynced(sessionId, syncTree);
|
|
410
1005
|
return;
|
|
411
1006
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
: undefined;
|
|
415
|
-
if (syncedIds) {
|
|
416
|
-
const deltaEntries = syncTree.entries.filter((entry) => !syncedIds.has(entry.id));
|
|
1007
|
+
if (knownServerPrefixCount !== undefined) {
|
|
1008
|
+
const deltaEntries = syncTree.entries.slice(knownServerPrefixCount);
|
|
417
1009
|
if (deltaEntries.length > 0) {
|
|
418
1010
|
try {
|
|
419
1011
|
await postTreeJson(request, "/api/session/tree/append", {
|
|
420
1012
|
sessionId,
|
|
421
1013
|
entries: deltaEntries,
|
|
422
1014
|
leafId: syncTree.leafId,
|
|
423
|
-
staticContext: getStaticContext(context),
|
|
424
1015
|
}, "Session tree append failed");
|
|
425
1016
|
}
|
|
426
1017
|
catch (error) {
|
|
427
1018
|
if (!isRecoverableTreeDivergenceError(error)) {
|
|
428
1019
|
throw error;
|
|
429
1020
|
}
|
|
430
|
-
await recoverPiServerTreeDivergence(sessionId,
|
|
1021
|
+
await recoverPiServerTreeDivergence(sessionId, syncTree, request, onHistoryReconciled);
|
|
431
1022
|
return;
|
|
432
1023
|
}
|
|
433
1024
|
markTreeSynced(sessionId, syncTree);
|
|
434
1025
|
return;
|
|
435
1026
|
}
|
|
436
1027
|
}
|
|
437
|
-
await recoverPiServerTreeDivergence(sessionId,
|
|
1028
|
+
await recoverPiServerTreeDivergence(sessionId, syncTree, request, onHistoryReconciled);
|
|
438
1029
|
}
|
|
439
1030
|
function serializeOptions(options) {
|
|
440
1031
|
return {
|
|
@@ -452,93 +1043,1104 @@ function serializeOptions(options) {
|
|
|
452
1043
|
websocketConnectTimeoutMs: options?.websocketConnectTimeoutMs,
|
|
453
1044
|
maxRetries: options?.maxRetries,
|
|
454
1045
|
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
1046
|
+
env: options?.env,
|
|
455
1047
|
};
|
|
456
1048
|
}
|
|
457
|
-
export
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
1049
|
+
export function hashPiServerCompactRequest(body) {
|
|
1050
|
+
const serialized = canonicalJsonStringify({
|
|
1051
|
+
protocolVersion: body.protocolVersion,
|
|
1052
|
+
sessionId: body.sessionId,
|
|
1053
|
+
providerExecutionFingerprint: body.providerExecutionFingerprint,
|
|
1054
|
+
model: body.model,
|
|
1055
|
+
options: body.options,
|
|
1056
|
+
settings: body.settings,
|
|
1057
|
+
preparation: body.preparation,
|
|
1058
|
+
extensionCompaction: body.extensionCompaction,
|
|
1059
|
+
customInstructions: body.customInstructions,
|
|
1060
|
+
baseStaticContextHash: body.baseStaticContextHash,
|
|
1061
|
+
baseTreeHash: body.baseTreeHash,
|
|
1062
|
+
baseEntryCount: body.baseEntryCount,
|
|
1063
|
+
baseLeafId: body.baseLeafId,
|
|
1064
|
+
baseRevision: body.baseRevision,
|
|
1065
|
+
streamResponse: body.streamResponse,
|
|
1066
|
+
retry: body.retry,
|
|
472
1067
|
});
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
let failure = await readPiServerFailure(response);
|
|
476
|
-
if (!options?.signal?.aborted && isRecoverableMissingServerState(response, failure.matchText)) {
|
|
477
|
-
resetSessionTracking(sessionId);
|
|
478
|
-
await ensureSessionInit(sessionId, context, request);
|
|
479
|
-
await syncPiServerTreeWithRequest(sessionId, context, tree, request, options?.onHistoryReconciled);
|
|
480
|
-
response = await request.postJson("/api/session/compact", makeBody());
|
|
481
|
-
if (response.ok) {
|
|
482
|
-
failure = { details: "", matchText: "" };
|
|
483
|
-
}
|
|
484
|
-
else {
|
|
485
|
-
failure = await readPiServerFailure(response);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
if (!response.ok) {
|
|
489
|
-
throw new Error(`Server compaction failed (${failure.details})`);
|
|
490
|
-
}
|
|
1068
|
+
if (serialized === undefined) {
|
|
1069
|
+
throw new Error("Failed to serialize pi-server compaction request identity");
|
|
491
1070
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
1071
|
+
return createHash("sha256").update(serialized).digest("hex");
|
|
1072
|
+
}
|
|
1073
|
+
function createPiServerCompactAbortError(cancelFailure) {
|
|
1074
|
+
const cancelDetails = cancelFailure === undefined
|
|
1075
|
+
? ""
|
|
1076
|
+
: ` (cancel request failed: ${cancelFailure instanceof Error ? cancelFailure.message : String(cancelFailure)})`;
|
|
1077
|
+
const error = new Error(`Compaction cancelled${cancelDetails}`);
|
|
1078
|
+
error.name = "AbortError";
|
|
1079
|
+
return error;
|
|
1080
|
+
}
|
|
1081
|
+
function getPiServerCompactReconnectDelay(attempt) {
|
|
1082
|
+
if (attempt === 0)
|
|
1083
|
+
return 0;
|
|
1084
|
+
return Math.min(1000 * 2 ** Math.min(attempt - 1, 5), PI_SERVER_COMPACT_MAX_RECONNECT_DELAY_MS);
|
|
1085
|
+
}
|
|
1086
|
+
function getPiServerCompactRecoveryWindowMs(configuredWindowMs) {
|
|
1087
|
+
const environmentValue = process.env.PI_SERVER_COMPACT_RECOVERY_WINDOW_MS;
|
|
1088
|
+
const value = configuredWindowMs ??
|
|
1089
|
+
(environmentValue === undefined ? PI_SERVER_COMPACT_DEFAULT_RECOVERY_WINDOW_MS : Number(environmentValue));
|
|
1090
|
+
if ((!Number.isFinite(value) && value !== Number.POSITIVE_INFINITY) || value <= 0) {
|
|
1091
|
+
throw new Error("pi-server compact recovery window must be positive milliseconds or Infinity (PiServerCompactOptions.piServerRecoveryWindowMs or PI_SERVER_COMPACT_RECOVERY_WINDOW_MS)");
|
|
495
1092
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
if (!result.compactionEntry || !resultEntries || resultLeafId === undefined) {
|
|
502
|
-
throw new Error("Server compaction response did not include the updated session tree");
|
|
1093
|
+
return value;
|
|
1094
|
+
}
|
|
1095
|
+
async function waitForPiServerCompactReconnect(delayMs, signal) {
|
|
1096
|
+
if (signal?.aborted) {
|
|
1097
|
+
throw createPiServerCompactAbortError();
|
|
503
1098
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
1099
|
+
if (delayMs === 0)
|
|
1100
|
+
return;
|
|
1101
|
+
await new Promise((resolve, reject) => {
|
|
1102
|
+
let timer;
|
|
1103
|
+
const onAbort = () => {
|
|
1104
|
+
if (timer !== undefined)
|
|
1105
|
+
clearTimeout(timer);
|
|
1106
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1107
|
+
reject(createPiServerCompactAbortError());
|
|
1108
|
+
};
|
|
1109
|
+
timer = setTimeout(() => {
|
|
1110
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1111
|
+
resolve();
|
|
1112
|
+
}, delayMs);
|
|
1113
|
+
timer.unref();
|
|
1114
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1115
|
+
if (signal?.aborted)
|
|
1116
|
+
onAbort();
|
|
1117
|
+
});
|
|
512
1118
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
1119
|
+
function getPiServerStreamReconnectDelay(attempt) {
|
|
1120
|
+
if (attempt === 0)
|
|
1121
|
+
return 0;
|
|
1122
|
+
return Math.min(1000 * 2 ** Math.min(attempt - 1, 5), PI_SERVER_STREAM_MAX_RECONNECT_DELAY_MS);
|
|
517
1123
|
}
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
1124
|
+
function getPiServerStreamRecoveryWindowMs(configuredWindowMs) {
|
|
1125
|
+
const environmentValue = process.env.PI_SERVER_STREAM_RECOVERY_WINDOW_MS;
|
|
1126
|
+
const value = configuredWindowMs ??
|
|
1127
|
+
(environmentValue === undefined ? PI_SERVER_STREAM_DEFAULT_RECOVERY_WINDOW_MS : Number(environmentValue));
|
|
1128
|
+
if ((!Number.isFinite(value) && value !== Number.POSITIVE_INFINITY) || value <= 0) {
|
|
1129
|
+
throw new Error("pi-server stream recovery window must be positive milliseconds or Infinity (PiServerStreamOptions.piServerRecoveryWindowMs or PI_SERVER_STREAM_RECOVERY_WINDOW_MS)");
|
|
1130
|
+
}
|
|
1131
|
+
return value;
|
|
1132
|
+
}
|
|
1133
|
+
function createPiServerStreamAbortError() {
|
|
1134
|
+
const error = new Error("Request aborted by user");
|
|
1135
|
+
error.name = "AbortError";
|
|
1136
|
+
return error;
|
|
1137
|
+
}
|
|
1138
|
+
async function waitForPiServerStreamReconnect(delayMs, signal) {
|
|
1139
|
+
if (signal?.aborted) {
|
|
1140
|
+
throw createPiServerStreamAbortError();
|
|
1141
|
+
}
|
|
1142
|
+
if (delayMs === 0)
|
|
1143
|
+
return;
|
|
1144
|
+
await new Promise((resolve, reject) => {
|
|
1145
|
+
let timer;
|
|
1146
|
+
const onAbort = () => {
|
|
1147
|
+
if (timer !== undefined)
|
|
1148
|
+
clearTimeout(timer);
|
|
1149
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1150
|
+
reject(createPiServerStreamAbortError());
|
|
1151
|
+
};
|
|
1152
|
+
timer = setTimeout(() => {
|
|
1153
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1154
|
+
resolve();
|
|
1155
|
+
}, delayMs);
|
|
1156
|
+
timer.unref();
|
|
1157
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1158
|
+
if (signal?.aborted)
|
|
1159
|
+
onAbort();
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
function isRecoverablePiServerCompactResponse(response) {
|
|
1163
|
+
return response.status >= 500 || response.status === 408 || response.status === 425 || response.status === 429;
|
|
1164
|
+
}
|
|
1165
|
+
function parsePiServerCompactTerminalPayload(value, sessionId, operationId, requestHash, errorPrefix) {
|
|
1166
|
+
if (!isObject(value) ||
|
|
1167
|
+
value.protocolVersion !== PI_SERVER_PROTOCOL_VERSION ||
|
|
1168
|
+
value.sessionId !== sessionId ||
|
|
1169
|
+
value.operationId !== operationId ||
|
|
1170
|
+
value.requestHash !== requestHash ||
|
|
1171
|
+
(value.operationDisposition !== "terminal" && value.operationDisposition !== "not_started") ||
|
|
1172
|
+
(value.status !== "failed" && value.status !== "rejected") ||
|
|
1173
|
+
(value.operationDisposition === "terminal" && value.status !== "failed") ||
|
|
1174
|
+
(value.operationDisposition === "not_started" && value.status !== "rejected") ||
|
|
1175
|
+
!Number.isSafeInteger(value.httpStatus) ||
|
|
1176
|
+
value.httpStatus < 100 ||
|
|
1177
|
+
value.httpStatus > 599 ||
|
|
1178
|
+
typeof value.error !== "string" ||
|
|
1179
|
+
value.error.length === 0) {
|
|
1180
|
+
throw new PiServerCompactProtocolError(`${errorPrefix} (terminal operation identity was invalid)`);
|
|
1181
|
+
}
|
|
1182
|
+
return value;
|
|
1183
|
+
}
|
|
1184
|
+
function normalizePiServerCompactionResult(result, sessionId, operationId, requestHash, tree, base, compactStatePath) {
|
|
1185
|
+
const errorPrefix = "Server compaction failed";
|
|
1186
|
+
const protocolError = (details) => new PiServerCompactProtocolError(`${errorPrefix} (${details})`);
|
|
1187
|
+
if (!isObject(result)) {
|
|
1188
|
+
throw protocolError("protocol v2 response was not an object");
|
|
1189
|
+
}
|
|
1190
|
+
if (result.protocolVersion !== PI_SERVER_PROTOCOL_VERSION) {
|
|
1191
|
+
const received = result.protocolVersion === undefined ? "missing" : JSON.stringify(result.protocolVersion);
|
|
1192
|
+
throw protocolError(`unsupported pi-server protocol version: ${received}; expected ${PI_SERVER_PROTOCOL_VERSION}`);
|
|
1193
|
+
}
|
|
1194
|
+
if (result.sessionId !== sessionId) {
|
|
1195
|
+
throw protocolError("response sessionId did not match the requested session");
|
|
1196
|
+
}
|
|
1197
|
+
if (result.operationId !== operationId) {
|
|
1198
|
+
throw protocolError("response operationId did not match the requested operation");
|
|
1199
|
+
}
|
|
1200
|
+
if (result.requestHash !== requestHash) {
|
|
1201
|
+
throw protocolError("response requestHash did not match the requested operation");
|
|
1202
|
+
}
|
|
1203
|
+
if (!isObject(result.treePatch)) {
|
|
1204
|
+
throw protocolError("protocol v2 response did not include a treePatch");
|
|
1205
|
+
}
|
|
1206
|
+
const patch = result.treePatch;
|
|
1207
|
+
if (patch.baseStaticContextHash !== base.staticContextHash) {
|
|
1208
|
+
throw protocolError("treePatch baseStaticContextHash did not match the submitted context");
|
|
1209
|
+
}
|
|
1210
|
+
if (patch.baseTreeHash !== base.treeHash) {
|
|
1211
|
+
throw protocolError("treePatch baseTreeHash did not match the submitted tree");
|
|
1212
|
+
}
|
|
1213
|
+
if (patch.baseEntryCount !== base.entryCount ||
|
|
1214
|
+
patch.entriesFrom !== base.entryCount ||
|
|
1215
|
+
base.entryCount !== tree.entries.length) {
|
|
1216
|
+
throw protocolError("treePatch entriesFrom did not match the submitted tree");
|
|
1217
|
+
}
|
|
1218
|
+
if (patch.baseLeafId !== base.leafId || tree.leafId !== base.leafId) {
|
|
1219
|
+
throw protocolError("treePatch baseLeafId did not match the submitted tree");
|
|
1220
|
+
}
|
|
1221
|
+
if (patch.baseRevision !== base.revision) {
|
|
1222
|
+
throw protocolError("treePatch baseRevision did not match the submitted revision");
|
|
1223
|
+
}
|
|
1224
|
+
if (!Array.isArray(patch.entries) || patch.entries.length !== 1) {
|
|
1225
|
+
throw protocolError("treePatch must contain exactly one compaction entry");
|
|
1226
|
+
}
|
|
1227
|
+
const rawEntry = patch.entries[0];
|
|
1228
|
+
if (!isObject(rawEntry) ||
|
|
1229
|
+
rawEntry.type !== "compaction" ||
|
|
1230
|
+
typeof rawEntry.id !== "string" ||
|
|
1231
|
+
rawEntry.id.length === 0 ||
|
|
1232
|
+
typeof rawEntry.timestamp !== "string" ||
|
|
1233
|
+
typeof rawEntry.summary !== "string" ||
|
|
1234
|
+
typeof rawEntry.firstKeptEntryId !== "string" ||
|
|
1235
|
+
rawEntry.firstKeptEntryId.length === 0 ||
|
|
1236
|
+
typeof rawEntry.tokensBefore !== "number" ||
|
|
1237
|
+
!Number.isFinite(rawEntry.tokensBefore) ||
|
|
1238
|
+
rawEntry.tokensBefore < 0 ||
|
|
1239
|
+
(rawEntry.retainedTail !== undefined && !Array.isArray(rawEntry.retainedTail)) ||
|
|
1240
|
+
(rawEntry.usage !== undefined && !isObject(rawEntry.usage)) ||
|
|
1241
|
+
(rawEntry.fromHook !== undefined && typeof rawEntry.fromHook !== "boolean") ||
|
|
1242
|
+
!isObject(rawEntry.piServerCompactOperation)) {
|
|
1243
|
+
throw protocolError("treePatch entry was not a valid compaction entry");
|
|
1244
|
+
}
|
|
1245
|
+
const operation = rawEntry.piServerCompactOperation;
|
|
1246
|
+
if (operation.version !== 1 ||
|
|
1247
|
+
operation.operationId !== operationId ||
|
|
1248
|
+
operation.requestHash !== requestHash ||
|
|
1249
|
+
operation.baseStaticContextHash !== base.staticContextHash ||
|
|
1250
|
+
operation.baseTreeHash !== base.treeHash ||
|
|
1251
|
+
operation.baseEntryCount !== base.entryCount ||
|
|
1252
|
+
operation.baseLeafId !== base.leafId ||
|
|
1253
|
+
operation.baseRevision !== base.revision) {
|
|
1254
|
+
throw protocolError("compaction entry durable operation metadata did not match the request");
|
|
1255
|
+
}
|
|
1256
|
+
if (rawEntry.parentId !== tree.leafId) {
|
|
1257
|
+
throw protocolError("compaction entry parentId did not match the submitted leaf");
|
|
1258
|
+
}
|
|
1259
|
+
if (tree.entries.some((entry) => entry.id === rawEntry.id)) {
|
|
1260
|
+
throw protocolError("compaction entry id already existed in the submitted tree");
|
|
1261
|
+
}
|
|
1262
|
+
if (patch.leafId !== rawEntry.id) {
|
|
1263
|
+
throw protocolError("treePatch leafId did not match the compaction entry");
|
|
1264
|
+
}
|
|
1265
|
+
if (!isNonNegativeInteger(patch.revision) || patch.revision !== base.revision + 1) {
|
|
1266
|
+
throw protocolError("treePatch revision did not advance the submitted revision exactly once");
|
|
1267
|
+
}
|
|
1268
|
+
const compactionEntry = rawEntry;
|
|
1269
|
+
const entries = [...tree.entries, compactionEntry];
|
|
1270
|
+
if (typeof patch.treeHash !== "string" || !/^[a-f0-9]{64}$/i.test(patch.treeHash)) {
|
|
1271
|
+
throw protocolError("treePatch treeHash was not a valid digest");
|
|
1272
|
+
}
|
|
1273
|
+
if (hashEntries(entries) !== patch.treeHash) {
|
|
1274
|
+
throw protocolError("treePatch treeHash did not match the updated tree");
|
|
1275
|
+
}
|
|
1276
|
+
const branch = buildPiServerActiveBranch(entries, compactionEntry.id, errorPrefix);
|
|
1277
|
+
const compaction = {
|
|
1278
|
+
summary: compactionEntry.summary,
|
|
1279
|
+
firstKeptEntryId: compactionEntry.firstKeptEntryId,
|
|
1280
|
+
tokensBefore: compactionEntry.tokensBefore,
|
|
1281
|
+
retainedTail: compactionEntry.retainedTail,
|
|
1282
|
+
details: compactionEntry.details,
|
|
1283
|
+
usage: compactionEntry.usage,
|
|
1284
|
+
};
|
|
1285
|
+
return {
|
|
1286
|
+
sessionId,
|
|
1287
|
+
compaction,
|
|
1288
|
+
compactionEntry,
|
|
1289
|
+
entries,
|
|
1290
|
+
leafId: compactionEntry.id,
|
|
1291
|
+
messages: convertToLlm(buildSessionContext(branch).messages),
|
|
1292
|
+
operationId,
|
|
1293
|
+
requestHash,
|
|
1294
|
+
revision: patch.revision,
|
|
1295
|
+
treeHash: patch.treeHash,
|
|
1296
|
+
compactStatePath,
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
function parseCompactTerminalFailureBody(bodyText, sessionId, operationId, requestHash, errorPrefix) {
|
|
1300
|
+
let value;
|
|
1301
|
+
try {
|
|
1302
|
+
value = JSON.parse(bodyText);
|
|
1303
|
+
}
|
|
1304
|
+
catch {
|
|
1305
|
+
return undefined;
|
|
1306
|
+
}
|
|
1307
|
+
if (!isObject(value) || value.operationDisposition === undefined)
|
|
1308
|
+
return undefined;
|
|
1309
|
+
return parsePiServerCompactTerminalPayload(value, sessionId, operationId, requestHash, errorPrefix);
|
|
1310
|
+
}
|
|
1311
|
+
async function acknowledgeObservedPiServerCompactTerminal(pending, compactStatePath, signal) {
|
|
1312
|
+
const observation = pending.observation;
|
|
1313
|
+
if (!observation || observation.kind !== "terminal") {
|
|
1314
|
+
throw new Error("Cannot acknowledge pi-server compaction failure without a durable terminal observation");
|
|
1315
|
+
}
|
|
1316
|
+
if (observation.operationDisposition === "not_started") {
|
|
1317
|
+
acknowledgePiServerPendingCompact(compactStatePath, pending.operationId, "server_rejected_not_started");
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
const ackSignal = signal?.aborted ? AbortSignal.timeout(PI_SERVER_CANCEL_TIMEOUT_MS) : signal;
|
|
1321
|
+
const request = createPiServerRequest(ackSignal);
|
|
1322
|
+
const response = await request.postJson("/api/session/compact/ack", {
|
|
1323
|
+
sessionId: pending.sessionId,
|
|
1324
|
+
operationId: pending.operationId,
|
|
1325
|
+
requestHash: pending.requestHash,
|
|
1326
|
+
});
|
|
1327
|
+
if (response.status === 404) {
|
|
1328
|
+
const contentType = getResponseContentType(response).toLowerCase();
|
|
1329
|
+
const failure = await readPiServerFailure(response);
|
|
1330
|
+
if (!contentType.includes("application/json") || !/compaction operation not found/i.test(failure.matchText)) {
|
|
1331
|
+
throw new Error(`Session compaction acknowledgement failed (${failure.details})`);
|
|
1332
|
+
}
|
|
1333
|
+
acknowledgePiServerPendingCompact(compactStatePath, pending.operationId, "server_missing_after_terminal_failure");
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
const value = await readPiServerJson(response, "Session compaction acknowledgement failed");
|
|
1337
|
+
if (!isObject(value) ||
|
|
1338
|
+
value.acknowledged !== true ||
|
|
1339
|
+
value.sessionId !== pending.sessionId ||
|
|
1340
|
+
value.operationId !== pending.operationId ||
|
|
1341
|
+
value.requestHash !== pending.requestHash ||
|
|
1342
|
+
value.status !== "failed" ||
|
|
1343
|
+
!isNonNegativeInteger(value.acknowledgedAt)) {
|
|
1344
|
+
throw new Error("Session compaction acknowledgement failed (terminal response identity was invalid)");
|
|
1345
|
+
}
|
|
1346
|
+
acknowledgePiServerPendingCompact(compactStatePath, pending.operationId);
|
|
1347
|
+
}
|
|
1348
|
+
async function persistAndCompletePiServerCompactTerminal(payload, compactStatePath, signal) {
|
|
1349
|
+
await persistAndAcknowledgePiServerCompactTerminal(payload, compactStatePath, signal);
|
|
1350
|
+
throw new PiServerCompactOperationError(`Server compaction failed (${payload.error})`, payload);
|
|
1351
|
+
}
|
|
1352
|
+
async function persistAndAcknowledgePiServerCompactTerminal(payload, compactStatePath, signal) {
|
|
1353
|
+
if (!compactStatePath) {
|
|
1354
|
+
throw new Error("Cannot complete pi-server compaction failure without a durable compact-state path");
|
|
1355
|
+
}
|
|
1356
|
+
writePiServerTerminalCompact(compactStatePath, {
|
|
1357
|
+
operationId: payload.operationId,
|
|
1358
|
+
requestHash: payload.requestHash,
|
|
1359
|
+
httpStatus: payload.httpStatus,
|
|
1360
|
+
error: payload.error,
|
|
1361
|
+
operationDisposition: payload.operationDisposition,
|
|
1362
|
+
status: payload.status,
|
|
1363
|
+
});
|
|
1364
|
+
const pending = readPiServerPendingCompact(compactStatePath);
|
|
1365
|
+
if (!pending) {
|
|
1366
|
+
throw new Error("Pi-server compact terminal observation disappeared before acknowledgement");
|
|
1367
|
+
}
|
|
1368
|
+
await acknowledgeObservedPiServerCompactTerminal(pending, compactStatePath, signal);
|
|
1369
|
+
}
|
|
1370
|
+
export async function compactPiServer(model, context, options) {
|
|
1371
|
+
validateProviderExecutionFingerprint(options?.providerExecutionFingerprint);
|
|
1372
|
+
const sessionId = options?.sessionId ?? "default";
|
|
1373
|
+
const signal = options?.signal;
|
|
1374
|
+
const recoveryWindowMs = getPiServerCompactRecoveryWindowMs(options?.piServerRecoveryWindowMs);
|
|
1375
|
+
const request = createPiServerRequest(options?.signal);
|
|
1376
|
+
const tree = options?.sessionTree ?? getLinearTreeFromMessages(context.messages);
|
|
1377
|
+
const compactStatePath = options?.piServerCompactStatePath;
|
|
1378
|
+
if (!compactStatePath) {
|
|
1379
|
+
throw new Error("Durable pi-server compaction requires a compact-state path");
|
|
1380
|
+
}
|
|
1381
|
+
const recovered = await recoverPendingPiServerCompaction(sessionId, tree, compactStatePath, {
|
|
1382
|
+
signal,
|
|
1383
|
+
recoveryWindowMs,
|
|
1384
|
+
});
|
|
1385
|
+
if (recovered)
|
|
1386
|
+
return recovered;
|
|
1387
|
+
const operationId = randomUUID();
|
|
1388
|
+
let operationSubmissionStarted = false;
|
|
1389
|
+
let requestHash;
|
|
1390
|
+
let cancelPromise;
|
|
1391
|
+
const cancelOperation = () => {
|
|
1392
|
+
if (!operationSubmissionStarted || !requestHash)
|
|
1393
|
+
return Promise.resolve(undefined);
|
|
1394
|
+
cancelPromise ??= cancelPiServerCompact(sessionId, operationId, requestHash);
|
|
1395
|
+
void cancelPromise.catch(() => undefined);
|
|
1396
|
+
return cancelPromise;
|
|
1397
|
+
};
|
|
1398
|
+
const onAbort = () => {
|
|
1399
|
+
void cancelOperation();
|
|
1400
|
+
};
|
|
1401
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1402
|
+
try {
|
|
1403
|
+
await ensureSessionInit(sessionId, context, request);
|
|
1404
|
+
await syncPiServerTreeWithRequest(sessionId, tree, request, options?.onHistoryReconciled);
|
|
1405
|
+
const base = {
|
|
1406
|
+
staticContextHash: sessionStaticContextHashes.get(sessionId) ?? hashStaticContext(context),
|
|
1407
|
+
treeHash: sessionTreeHashes.get(sessionId) ?? hashEntries(tree.entries),
|
|
1408
|
+
entryCount: sessionTreeEntryCounts.get(sessionId) ?? tree.entries.length,
|
|
1409
|
+
leafId: sessionTreeLeafIds.get(sessionId) ?? tree.leafId,
|
|
1410
|
+
revision: sessionTreeRevisions.get(sessionId) ?? 0,
|
|
1411
|
+
};
|
|
1412
|
+
if (base.entryCount !== tree.entries.length || base.leafId !== tree.leafId) {
|
|
1413
|
+
throw new Error("Cannot begin pi-server compaction from an unverified local/server tree identity");
|
|
1414
|
+
}
|
|
1415
|
+
const body = Object.freeze(JSON.parse(JSON.stringify({
|
|
1416
|
+
protocolVersion: PI_SERVER_PROTOCOL_VERSION,
|
|
1417
|
+
sessionId,
|
|
1418
|
+
operationId,
|
|
1419
|
+
providerExecutionFingerprint: options?.providerExecutionFingerprint,
|
|
1420
|
+
model,
|
|
1421
|
+
options: serializeOptions(options),
|
|
1422
|
+
settings: options?.settings,
|
|
1423
|
+
preparation: options?.preparation,
|
|
1424
|
+
extensionCompaction: options?.extensionCompaction,
|
|
1425
|
+
customInstructions: options?.customInstructions,
|
|
1426
|
+
retry: options?.retry,
|
|
1427
|
+
baseStaticContextHash: base.staticContextHash,
|
|
1428
|
+
baseTreeHash: base.treeHash,
|
|
1429
|
+
baseEntryCount: base.entryCount,
|
|
1430
|
+
baseLeafId: base.leafId,
|
|
1431
|
+
baseRevision: base.revision,
|
|
1432
|
+
streamResponse: true,
|
|
1433
|
+
})));
|
|
1434
|
+
const compactRequestHash = hashPiServerCompactRequest(body);
|
|
1435
|
+
requestHash = compactRequestHash;
|
|
1436
|
+
writePiServerPendingCompact(compactStatePath, {
|
|
1437
|
+
serverHash: hashPiServerIdentity(getServerUrl()),
|
|
1438
|
+
sessionId,
|
|
1439
|
+
operationId,
|
|
1440
|
+
requestHash: compactRequestHash,
|
|
1441
|
+
baseStaticContextHash: base.staticContextHash,
|
|
1442
|
+
baseTreeHash: base.treeHash,
|
|
1443
|
+
baseEntryCount: base.entryCount,
|
|
1444
|
+
baseLeafId: base.leafId,
|
|
1445
|
+
baseRevision: base.revision,
|
|
1446
|
+
});
|
|
1447
|
+
let recoveryDeadline;
|
|
1448
|
+
let reconnectAttempt = 0;
|
|
1449
|
+
let lastInterruption;
|
|
1450
|
+
for (;;) {
|
|
1451
|
+
if (signal?.aborted) {
|
|
1452
|
+
throw createPiServerCompactAbortError();
|
|
1453
|
+
}
|
|
1454
|
+
if (recoveryDeadline !== undefined) {
|
|
1455
|
+
const remainingMs = recoveryDeadline - Date.now();
|
|
1456
|
+
if (remainingMs <= 0) {
|
|
1457
|
+
const details = lastInterruption instanceof Error ? lastInterruption.message : String(lastInterruption);
|
|
1458
|
+
throw new Error(`Server compaction recovery exhausted for operation ${operationId}: ${details}`);
|
|
1459
|
+
}
|
|
1460
|
+
const delayMs = Math.min(getPiServerCompactReconnectDelay(reconnectAttempt), remainingMs);
|
|
1461
|
+
reconnectAttempt++;
|
|
1462
|
+
await waitForPiServerCompactReconnect(delayMs, signal);
|
|
1463
|
+
if (Date.now() >= recoveryDeadline) {
|
|
1464
|
+
const details = lastInterruption instanceof Error ? lastInterruption.message : String(lastInterruption);
|
|
1465
|
+
throw new Error(`Server compaction recovery exhausted for operation ${operationId}: ${details}`);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
let response;
|
|
1469
|
+
try {
|
|
1470
|
+
operationSubmissionStarted = true;
|
|
1471
|
+
response = await request.postJson("/api/session/compact", body);
|
|
1472
|
+
}
|
|
1473
|
+
catch (error) {
|
|
1474
|
+
if (isAbortError(error, signal)) {
|
|
1475
|
+
throw error;
|
|
1476
|
+
}
|
|
1477
|
+
if (!isRetryablePiServerInterruption(error)) {
|
|
1478
|
+
throw error;
|
|
1479
|
+
}
|
|
1480
|
+
lastInterruption = error;
|
|
1481
|
+
recoveryDeadline ??= Date.now() + recoveryWindowMs;
|
|
1482
|
+
continue;
|
|
1483
|
+
}
|
|
1484
|
+
if (!response.ok) {
|
|
1485
|
+
let failure;
|
|
1486
|
+
try {
|
|
1487
|
+
failure = await readPiServerFailure(response);
|
|
1488
|
+
}
|
|
1489
|
+
catch (error) {
|
|
1490
|
+
if (isAbortError(error, signal)) {
|
|
1491
|
+
throw error;
|
|
1492
|
+
}
|
|
1493
|
+
if (!isRecoverablePiServerCompactResponse(response) || !isRetryablePiServerInterruption(error)) {
|
|
1494
|
+
throw error;
|
|
1495
|
+
}
|
|
1496
|
+
lastInterruption = error;
|
|
1497
|
+
recoveryDeadline ??= Date.now() + recoveryWindowMs;
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
const terminal = parseCompactTerminalFailureBody(failure.bodyText, sessionId, operationId, compactRequestHash, "Server compaction failed");
|
|
1501
|
+
if (terminal) {
|
|
1502
|
+
if (terminal.httpStatus !== response.status) {
|
|
1503
|
+
throw new PiServerCompactProtocolError("Server compaction failed (terminal HTTP status did not match the response)");
|
|
1504
|
+
}
|
|
1505
|
+
return await persistAndCompletePiServerCompactTerminal(terminal, compactStatePath, signal);
|
|
1506
|
+
}
|
|
1507
|
+
if (isRecoverablePiServerCompactResponse(response)) {
|
|
1508
|
+
lastInterruption = new Error(`Server compaction failed (${failure.details})`);
|
|
1509
|
+
recoveryDeadline ??= Date.now() + recoveryWindowMs;
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
throw new Error(`Server compaction failed (${failure.details})`);
|
|
1513
|
+
}
|
|
1514
|
+
try {
|
|
1515
|
+
const responseResult = await readPiServerCompactResponse(response, "Server compaction failed");
|
|
1516
|
+
const result = normalizePiServerCompactionResult(responseResult, sessionId, operationId, compactRequestHash, tree, base, compactStatePath);
|
|
1517
|
+
if (signal?.aborted) {
|
|
1518
|
+
throw createPiServerCompactAbortError();
|
|
1519
|
+
}
|
|
1520
|
+
markTreeSynced(sessionId, { entries: result.entries, leafId: result.leafId });
|
|
1521
|
+
sessionTreeRevisions.set(sessionId, result.revision);
|
|
1522
|
+
return result;
|
|
1523
|
+
}
|
|
1524
|
+
catch (error) {
|
|
1525
|
+
if (error instanceof PiServerCompactOperationError) {
|
|
1526
|
+
const terminal = parsePiServerCompactTerminalPayload(error.payload, sessionId, operationId, compactRequestHash, "Server compaction failed");
|
|
1527
|
+
return await persistAndCompletePiServerCompactTerminal(terminal, compactStatePath, signal);
|
|
1528
|
+
}
|
|
1529
|
+
if (error instanceof PiServerCompactProtocolError) {
|
|
1530
|
+
throw error;
|
|
1531
|
+
}
|
|
1532
|
+
if (isAbortError(error, signal)) {
|
|
1533
|
+
throw error;
|
|
1534
|
+
}
|
|
1535
|
+
if (!isRetryablePiServerInterruption(error)) {
|
|
1536
|
+
throw error;
|
|
1537
|
+
}
|
|
1538
|
+
lastInterruption = error;
|
|
1539
|
+
if (error instanceof PiServerCompactInterruptionError && error.observedProgress) {
|
|
1540
|
+
recoveryDeadline = Date.now() + recoveryWindowMs;
|
|
1541
|
+
reconnectAttempt = 0;
|
|
1542
|
+
}
|
|
1543
|
+
else {
|
|
1544
|
+
recoveryDeadline ??= Date.now() + recoveryWindowMs;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
catch (error) {
|
|
1550
|
+
if (isAbortError(error, signal)) {
|
|
1551
|
+
let cancelFailure;
|
|
1552
|
+
try {
|
|
1553
|
+
if (!operationSubmissionStarted && requestHash) {
|
|
1554
|
+
await persistAndAcknowledgePiServerCompactTerminal({
|
|
1555
|
+
protocolVersion: PI_SERVER_PROTOCOL_VERSION,
|
|
1556
|
+
sessionId,
|
|
1557
|
+
operationId,
|
|
1558
|
+
requestHash,
|
|
1559
|
+
status: "rejected",
|
|
1560
|
+
httpStatus: 499,
|
|
1561
|
+
operationDisposition: "not_started",
|
|
1562
|
+
error: "Compaction cancelled before server submission",
|
|
1563
|
+
}, compactStatePath);
|
|
1564
|
+
}
|
|
1565
|
+
else {
|
|
1566
|
+
const terminal = await cancelOperation();
|
|
1567
|
+
if (terminal) {
|
|
1568
|
+
await persistAndAcknowledgePiServerCompactTerminal(terminal, compactStatePath, AbortSignal.timeout(PI_SERVER_CANCEL_TIMEOUT_MS));
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
catch (cancelError) {
|
|
1573
|
+
cancelFailure = cancelError;
|
|
1574
|
+
}
|
|
1575
|
+
throw createPiServerCompactAbortError(cancelFailure);
|
|
1576
|
+
}
|
|
1577
|
+
throw error;
|
|
1578
|
+
}
|
|
1579
|
+
finally {
|
|
1580
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
function assertPendingPiServerCompactMatches(pending, sessionId, tree) {
|
|
1584
|
+
if (pending.serverHash !== hashPiServerIdentity(getServerUrl()) ||
|
|
1585
|
+
pending.sessionId !== sessionId ||
|
|
1586
|
+
pending.baseEntryCount > tree.entries.length) {
|
|
1587
|
+
throw new Error(`Cannot safely resume pending pi-server compaction ${pending.operationId}: server, session, or tree identity changed`);
|
|
1588
|
+
}
|
|
1589
|
+
const baseEntries = tree.entries.slice(0, pending.baseEntryCount);
|
|
1590
|
+
if (hashEntries(baseEntries) !== pending.baseTreeHash ||
|
|
1591
|
+
(pending.baseLeafId !== null && !baseEntries.some((entry) => entry.id === pending.baseLeafId))) {
|
|
1592
|
+
throw new Error(`Cannot safely resume pending pi-server compaction ${pending.operationId}: local base tree no longer matches its durable marker`);
|
|
1593
|
+
}
|
|
1594
|
+
if (!pending.observation || pending.observation.kind === "terminal") {
|
|
1595
|
+
if (tree.entries.length !== pending.baseEntryCount || tree.leafId !== pending.baseLeafId) {
|
|
1596
|
+
throw new Error(`Cannot safely resume pending pi-server compaction ${pending.operationId}: local tree changed before a committed compaction was observed`);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
return { entries: baseEntries, leafId: pending.baseLeafId };
|
|
1600
|
+
}
|
|
1601
|
+
function recoverLocallyAppliedPiServerCompaction(pending, tree, baseTree, compactStatePath) {
|
|
1602
|
+
const applied = pending.observation;
|
|
1603
|
+
if (!applied || applied.kind !== "applied") {
|
|
1604
|
+
throw new Error("Pending pi-server compaction does not contain a durable local apply observation");
|
|
1605
|
+
}
|
|
1606
|
+
const entry = tree.entries[pending.baseEntryCount];
|
|
1607
|
+
if (tree.entries.length !== pending.baseEntryCount + 1 ||
|
|
1608
|
+
!entry ||
|
|
1609
|
+
entry.id !== applied.entryId ||
|
|
1610
|
+
hashPiServerTreeEntry(entry) !== applied.entryHash ||
|
|
1611
|
+
tree.leafId !== applied.updatedLeafId ||
|
|
1612
|
+
applied.updatedLeafId !== applied.entryId ||
|
|
1613
|
+
hashEntries(tree.entries) !== applied.updatedTreeHash ||
|
|
1614
|
+
applied.updatedRevision !== pending.baseRevision + 1) {
|
|
1615
|
+
throw new Error(`Cannot safely resume pending pi-server compaction ${pending.operationId}: local applied tree does not match its durable observation`);
|
|
1616
|
+
}
|
|
1617
|
+
return normalizePiServerCompactionResult({
|
|
1618
|
+
protocolVersion: PI_SERVER_PROTOCOL_VERSION,
|
|
1619
|
+
sessionId: pending.sessionId,
|
|
1620
|
+
operationId: pending.operationId,
|
|
1621
|
+
requestHash: pending.requestHash,
|
|
1622
|
+
treePatch: {
|
|
1623
|
+
baseStaticContextHash: pending.baseStaticContextHash,
|
|
1624
|
+
baseTreeHash: pending.baseTreeHash,
|
|
1625
|
+
baseEntryCount: pending.baseEntryCount,
|
|
1626
|
+
baseLeafId: pending.baseLeafId,
|
|
1627
|
+
baseRevision: pending.baseRevision,
|
|
1628
|
+
entriesFrom: pending.baseEntryCount,
|
|
1629
|
+
entries: [entry],
|
|
1630
|
+
leafId: applied.updatedLeafId,
|
|
1631
|
+
revision: applied.updatedRevision,
|
|
1632
|
+
treeHash: applied.updatedTreeHash,
|
|
1633
|
+
},
|
|
1634
|
+
}, pending.sessionId, pending.operationId, pending.requestHash, baseTree, {
|
|
1635
|
+
staticContextHash: pending.baseStaticContextHash,
|
|
1636
|
+
treeHash: pending.baseTreeHash,
|
|
1637
|
+
entryCount: pending.baseEntryCount,
|
|
1638
|
+
leafId: pending.baseLeafId,
|
|
1639
|
+
revision: pending.baseRevision,
|
|
1640
|
+
}, compactStatePath);
|
|
1641
|
+
}
|
|
1642
|
+
export async function recoverPendingPiServerCompaction(sessionId, tree, compactStatePath, options) {
|
|
1643
|
+
const pending = readPiServerPendingCompact(compactStatePath);
|
|
1644
|
+
if (!pending)
|
|
1645
|
+
return undefined;
|
|
1646
|
+
const baseTree = assertPendingPiServerCompactMatches(pending, sessionId, tree);
|
|
1647
|
+
const locallyApplied = pending.observation?.kind === "applied"
|
|
1648
|
+
? recoverLocallyAppliedPiServerCompaction(pending, tree, baseTree, compactStatePath)
|
|
1649
|
+
: undefined;
|
|
1650
|
+
if (pending.observation?.kind === "terminal" && pending.observation.operationDisposition === "not_started") {
|
|
1651
|
+
return await persistAndCompletePiServerCompactTerminal({
|
|
1652
|
+
protocolVersion: PI_SERVER_PROTOCOL_VERSION,
|
|
1653
|
+
sessionId: pending.sessionId,
|
|
1654
|
+
operationId: pending.operationId,
|
|
1655
|
+
requestHash: pending.requestHash,
|
|
1656
|
+
status: pending.observation.status,
|
|
1657
|
+
httpStatus: pending.observation.httpStatus,
|
|
1658
|
+
operationDisposition: pending.observation.operationDisposition,
|
|
1659
|
+
error: pending.observation.error,
|
|
1660
|
+
}, compactStatePath, options?.signal);
|
|
1661
|
+
}
|
|
1662
|
+
const recoveryWindowMs = getPiServerCompactRecoveryWindowMs(options?.recoveryWindowMs);
|
|
1663
|
+
const request = createPiServerRequest(options?.signal);
|
|
1664
|
+
let recoveryDeadline;
|
|
1665
|
+
let reconnectAttempt = 0;
|
|
1666
|
+
let lastInterruption = new Error("pending compaction has not reached a terminal response");
|
|
1667
|
+
for (;;) {
|
|
1668
|
+
if (options?.signal?.aborted) {
|
|
1669
|
+
let cancelFailure;
|
|
1670
|
+
try {
|
|
1671
|
+
const terminal = await cancelPiServerCompact(sessionId, pending.operationId, pending.requestHash);
|
|
1672
|
+
await persistAndAcknowledgePiServerCompactTerminal(terminal, compactStatePath, AbortSignal.timeout(PI_SERVER_CANCEL_TIMEOUT_MS));
|
|
1673
|
+
}
|
|
1674
|
+
catch (error) {
|
|
1675
|
+
cancelFailure = error;
|
|
1676
|
+
}
|
|
1677
|
+
throw createPiServerCompactAbortError(cancelFailure);
|
|
1678
|
+
}
|
|
1679
|
+
if (recoveryDeadline !== undefined) {
|
|
1680
|
+
const remainingMs = recoveryDeadline - Date.now();
|
|
1681
|
+
if (remainingMs <= 0) {
|
|
1682
|
+
throw new Error(`Pending server compaction recovery exhausted for operation ${pending.operationId}: ${lastInterruption instanceof Error ? lastInterruption.message : String(lastInterruption)}`);
|
|
1683
|
+
}
|
|
1684
|
+
await waitForPiServerCompactReconnect(Math.min(getPiServerCompactReconnectDelay(reconnectAttempt), remainingMs), options?.signal);
|
|
1685
|
+
reconnectAttempt++;
|
|
1686
|
+
}
|
|
1687
|
+
let response;
|
|
1688
|
+
try {
|
|
1689
|
+
response = await request.getJson(`/api/session/${encodeURIComponent(sessionId)}/compactions/${encodeURIComponent(pending.operationId)}?requestHash=${encodeURIComponent(pending.requestHash)}`);
|
|
1690
|
+
}
|
|
1691
|
+
catch (error) {
|
|
1692
|
+
if (!isRetryablePiServerInterruption(error))
|
|
1693
|
+
throw error;
|
|
1694
|
+
lastInterruption = error;
|
|
1695
|
+
recoveryDeadline ??= Date.now() + recoveryWindowMs;
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
if (!response.ok) {
|
|
1699
|
+
const failure = await readPiServerFailure(response);
|
|
1700
|
+
if (response.status === 404) {
|
|
1701
|
+
if (locallyApplied)
|
|
1702
|
+
return locallyApplied;
|
|
1703
|
+
if (pending.observation?.kind === "terminal") {
|
|
1704
|
+
await acknowledgeObservedPiServerCompactTerminal(pending, compactStatePath, options?.signal);
|
|
1705
|
+
throw new PiServerCompactOperationError(`Pending server compaction failed (${pending.observation.error})`, pending.observation);
|
|
1706
|
+
}
|
|
1707
|
+
throw new Error(`Pending pi-server compaction ${pending.operationId} is missing from the durable server journal; ` +
|
|
1708
|
+
"the local marker was retained and automatic provider resubmission is disabled");
|
|
1709
|
+
}
|
|
1710
|
+
if (!isRecoverablePiServerCompactResponse(response)) {
|
|
1711
|
+
throw new Error(`Pending server compaction recovery failed (${failure.details})`);
|
|
1712
|
+
}
|
|
1713
|
+
lastInterruption = new Error(`Pending server compaction recovery failed (${failure.details})`);
|
|
1714
|
+
recoveryDeadline ??= Date.now() + recoveryWindowMs;
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
try {
|
|
1718
|
+
const responseResult = await readPiServerCompactResponse(response, "Pending server compaction recovery failed");
|
|
1719
|
+
const result = normalizePiServerCompactionResult(responseResult, sessionId, pending.operationId, pending.requestHash, baseTree, {
|
|
1720
|
+
staticContextHash: pending.baseStaticContextHash,
|
|
1721
|
+
treeHash: pending.baseTreeHash,
|
|
1722
|
+
entryCount: pending.baseEntryCount,
|
|
1723
|
+
leafId: pending.baseLeafId,
|
|
1724
|
+
revision: pending.baseRevision,
|
|
1725
|
+
}, compactStatePath);
|
|
1726
|
+
if (locallyApplied &&
|
|
1727
|
+
(canonicalJsonStringify(result.compactionEntry) !==
|
|
1728
|
+
canonicalJsonStringify(locallyApplied.compactionEntry) ||
|
|
1729
|
+
result.treeHash !== locallyApplied.treeHash ||
|
|
1730
|
+
result.revision !== locallyApplied.revision)) {
|
|
1731
|
+
throw new PiServerCompactProtocolError("Pending server compaction recovery returned a result that differed from the durably applied tree");
|
|
1732
|
+
}
|
|
1733
|
+
markTreeSynced(sessionId, { entries: result.entries, leafId: result.leafId });
|
|
1734
|
+
sessionTreeRevisions.set(sessionId, result.revision);
|
|
1735
|
+
return result;
|
|
1736
|
+
}
|
|
1737
|
+
catch (error) {
|
|
1738
|
+
if (error instanceof PiServerCompactOperationError) {
|
|
1739
|
+
const terminal = parsePiServerCompactTerminalPayload(error.payload, sessionId, pending.operationId, pending.requestHash, "Pending server compaction recovery failed");
|
|
1740
|
+
return await persistAndCompletePiServerCompactTerminal(terminal, compactStatePath, options?.signal);
|
|
1741
|
+
}
|
|
1742
|
+
if (error instanceof PiServerCompactProtocolError) {
|
|
1743
|
+
throw error;
|
|
1744
|
+
}
|
|
1745
|
+
if (!isRetryablePiServerInterruption(error))
|
|
1746
|
+
throw error;
|
|
1747
|
+
lastInterruption = error;
|
|
1748
|
+
if (error instanceof PiServerCompactInterruptionError && error.observedProgress) {
|
|
1749
|
+
recoveryDeadline = Date.now() + recoveryWindowMs;
|
|
1750
|
+
reconnectAttempt = 0;
|
|
1751
|
+
}
|
|
1752
|
+
else {
|
|
1753
|
+
recoveryDeadline ??= Date.now() + recoveryWindowMs;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
export function recordPiServerCompactionApplied(result) {
|
|
1759
|
+
const compactStatePath = result.compactStatePath;
|
|
1760
|
+
if (!compactStatePath) {
|
|
1761
|
+
throw new Error("Cannot record pi-server compaction apply without a durable compact-state path");
|
|
1762
|
+
}
|
|
1763
|
+
const pending = readPiServerPendingCompact(compactStatePath);
|
|
1764
|
+
if (!pending ||
|
|
1765
|
+
pending.sessionId !== result.sessionId ||
|
|
1766
|
+
pending.operationId !== result.operationId ||
|
|
1767
|
+
pending.requestHash !== result.requestHash) {
|
|
1768
|
+
throw new Error("Cannot record pi-server compaction apply: durable pending marker did not match the result");
|
|
1769
|
+
}
|
|
1770
|
+
writePiServerAppliedCompact(compactStatePath, {
|
|
1771
|
+
operationId: result.operationId,
|
|
1772
|
+
requestHash: result.requestHash,
|
|
1773
|
+
entryId: result.compactionEntry.id,
|
|
1774
|
+
entryHash: hashPiServerTreeEntry(result.compactionEntry),
|
|
1775
|
+
updatedTreeHash: result.treeHash,
|
|
1776
|
+
updatedLeafId: result.compactionEntry.id,
|
|
1777
|
+
updatedRevision: result.revision,
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
export async function acknowledgePiServerCompaction(result, signal) {
|
|
1781
|
+
const compactStatePath = result.compactStatePath;
|
|
1782
|
+
if (!compactStatePath) {
|
|
1783
|
+
throw new Error("Cannot acknowledge pi-server compaction without a durable compact-state path");
|
|
1784
|
+
}
|
|
1785
|
+
const pending = readPiServerPendingCompact(compactStatePath);
|
|
1786
|
+
if (!pending ||
|
|
1787
|
+
pending.sessionId !== result.sessionId ||
|
|
1788
|
+
pending.operationId !== result.operationId ||
|
|
1789
|
+
pending.requestHash !== result.requestHash) {
|
|
1790
|
+
throw new Error("Cannot acknowledge pi-server compaction: durable pending marker did not match the result");
|
|
1791
|
+
}
|
|
1792
|
+
const applied = pending.observation;
|
|
1793
|
+
if (!applied ||
|
|
1794
|
+
applied.kind !== "applied" ||
|
|
1795
|
+
applied.entryId !== result.compactionEntry.id ||
|
|
1796
|
+
applied.entryHash !== hashPiServerTreeEntry(result.compactionEntry) ||
|
|
1797
|
+
applied.updatedTreeHash !== result.treeHash ||
|
|
1798
|
+
applied.updatedLeafId !== result.leafId ||
|
|
1799
|
+
applied.updatedRevision !== result.revision ||
|
|
1800
|
+
result.entries.length !== pending.baseEntryCount + 1 ||
|
|
1801
|
+
hashEntries(result.entries) !== result.treeHash) {
|
|
1802
|
+
throw new Error("Cannot acknowledge pi-server compaction: durable local apply observation did not match the result");
|
|
1803
|
+
}
|
|
1804
|
+
const request = createPiServerRequest(signal);
|
|
1805
|
+
const response = await request.postJson("/api/session/compact/ack", {
|
|
1806
|
+
sessionId: pending.sessionId,
|
|
1807
|
+
operationId: pending.operationId,
|
|
1808
|
+
requestHash: pending.requestHash,
|
|
1809
|
+
});
|
|
1810
|
+
if (response.status === 404) {
|
|
1811
|
+
const contentType = getResponseContentType(response).toLowerCase();
|
|
1812
|
+
const failure = await readPiServerFailure(response);
|
|
1813
|
+
if (!contentType.includes("application/json") || !/compaction operation not found/i.test(failure.matchText)) {
|
|
1814
|
+
throw new Error(`Session compaction acknowledgement failed (${failure.details})`);
|
|
1815
|
+
}
|
|
1816
|
+
acknowledgePiServerPendingCompact(compactStatePath, pending.operationId, "server_missing_after_tree_applied");
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
const value = await readPiServerJson(response, "Session compaction acknowledgement failed");
|
|
1820
|
+
if (!isObject(value) ||
|
|
1821
|
+
value.acknowledged !== true ||
|
|
1822
|
+
value.sessionId !== pending.sessionId ||
|
|
1823
|
+
value.operationId !== pending.operationId ||
|
|
1824
|
+
value.requestHash !== pending.requestHash ||
|
|
1825
|
+
value.status !== "completed" ||
|
|
1826
|
+
!isNonNegativeInteger(value.acknowledgedAt)) {
|
|
1827
|
+
throw new Error("Session compaction acknowledgement failed (response identity was invalid)");
|
|
1828
|
+
}
|
|
1829
|
+
acknowledgePiServerPendingCompact(compactStatePath, pending.operationId);
|
|
1830
|
+
}
|
|
1831
|
+
export async function dropLastPiServerAssistantError(sessionId) {
|
|
1832
|
+
const request = createPiServerRequest();
|
|
1833
|
+
const response = await request.postJson("/api/session/drop-last-assistant-error", { sessionId });
|
|
1834
|
+
await readPiServerJson(response, "Dropping server assistant error failed");
|
|
1835
|
+
}
|
|
1836
|
+
function findPendingPiServerRunTerminal(entries, pending) {
|
|
1837
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
1838
|
+
const entry = entries[index];
|
|
1839
|
+
if (entry.type !== "message" || entry.message.role !== "assistant")
|
|
1840
|
+
continue;
|
|
1841
|
+
const message = entry.message;
|
|
1842
|
+
const diagnostic = getPiServerRunDiagnostic(message);
|
|
1843
|
+
if (!diagnostic || diagnostic.runId !== pending.runId)
|
|
1844
|
+
continue;
|
|
1845
|
+
if (diagnostic.sessionId !== pending.sessionId || diagnostic.requestMac !== pending.requestHash) {
|
|
1846
|
+
throw new Error("Cannot recover pi-server run: persisted terminal diagnostic did not match pending state");
|
|
1847
|
+
}
|
|
1848
|
+
return message;
|
|
1849
|
+
}
|
|
1850
|
+
return undefined;
|
|
1851
|
+
}
|
|
1852
|
+
function assertPendingPiServerRunMatches(pending, serverHash, sessionId, tree) {
|
|
1853
|
+
if (pending.serverHash !== serverHash ||
|
|
1854
|
+
pending.sessionId !== sessionId ||
|
|
1855
|
+
pending.baseEntryCount > tree.entries.length ||
|
|
1856
|
+
hashLocalTreePrefix(sessionId, tree.entries, pending.baseEntryCount) !== pending.baseTreeHash ||
|
|
1857
|
+
pending.baseLeafId !== tree.leafId) {
|
|
1858
|
+
throw new Error(`Cannot safely resume pending pi-server run ${pending.runId}: server, tree, or provider request identity changed`);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
function missingDurablePiServerRunError(runId, cause) {
|
|
1862
|
+
const prefix = cause ? `${cause}; ` : "";
|
|
1863
|
+
return new Error(`${prefix}pi-server no longer has durable journal for pending run ${runId}. ` +
|
|
1864
|
+
"The provider outcome is unknown and the pending marker was retained. Restore the server journal, " +
|
|
1865
|
+
"or after independently verifying that the provider did not execute, start an explicit new session or fork; " +
|
|
1866
|
+
"automatic provider resubmission is disabled.");
|
|
1867
|
+
}
|
|
1868
|
+
export async function streamPiServer(model, context, options) {
|
|
1869
|
+
validateProviderExecutionFingerprint(options?.providerExecutionFingerprint);
|
|
1870
|
+
const runMode = options?.runMode;
|
|
1871
|
+
if (runMode === "auxiliary-transient" && options?.piServerRunStatePath) {
|
|
1872
|
+
throw new Error("Auxiliary pi-server streams cannot use the main durable run-state path");
|
|
1873
|
+
}
|
|
1874
|
+
const sessionId = options?.sessionId ?? randomUUID();
|
|
1875
|
+
let runId = randomUUID();
|
|
1876
|
+
const recoveryWindowMs = getPiServerStreamRecoveryWindowMs(options?.piServerRecoveryWindowMs);
|
|
1877
|
+
const isEphemeralSession = runMode === "auxiliary-transient" || options?.sessionId === undefined;
|
|
1878
|
+
const stream = new PiServerEventStream();
|
|
1879
|
+
const partial = {
|
|
1880
|
+
role: "assistant",
|
|
1881
|
+
stopReason: "stop",
|
|
1882
|
+
content: [],
|
|
1883
|
+
api: model.api,
|
|
1884
|
+
provider: model.provider,
|
|
1885
|
+
model: model.id,
|
|
1886
|
+
usage: {
|
|
1887
|
+
input: 0,
|
|
1888
|
+
output: 0,
|
|
1889
|
+
cacheRead: 0,
|
|
1890
|
+
cacheWrite: 0,
|
|
1891
|
+
totalTokens: 0,
|
|
1892
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
537
1893
|
},
|
|
538
1894
|
timestamp: Date.now(),
|
|
539
1895
|
};
|
|
540
1896
|
(async () => {
|
|
541
1897
|
let phase = "session_init";
|
|
1898
|
+
let receivedProxyEventCount = 0;
|
|
1899
|
+
let runSubmissionStarted = false;
|
|
1900
|
+
let unresolvedRunPending = false;
|
|
1901
|
+
let cancelFailure;
|
|
1902
|
+
let cancelPromise;
|
|
1903
|
+
let transientRunAcknowledged = false;
|
|
1904
|
+
let pendingTransientTerminalEvent;
|
|
1905
|
+
const runStatePath = options?.piServerRunStatePath;
|
|
1906
|
+
let runStateLease;
|
|
1907
|
+
const attachRunStateLease = () => {
|
|
1908
|
+
if (!runStatePath)
|
|
1909
|
+
return;
|
|
1910
|
+
const active = getPersistentRunLease(sessionId, runStatePath);
|
|
1911
|
+
if (active) {
|
|
1912
|
+
if (active.scope !== "session") {
|
|
1913
|
+
throw new Error(`Cannot start another pi-server run while session ${sessionId} is already streaming`);
|
|
1914
|
+
}
|
|
1915
|
+
runStateLease = active.lease;
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
runStateLease = acquirePersistentRunLease(sessionId, runStatePath);
|
|
1919
|
+
};
|
|
1920
|
+
const releaseRunStateLease = () => {
|
|
1921
|
+
if (!runStatePath || !runStateLease)
|
|
1922
|
+
return;
|
|
1923
|
+
releasePiServerRunLease(sessionId, runStatePath);
|
|
1924
|
+
runStateLease = undefined;
|
|
1925
|
+
};
|
|
1926
|
+
const requestRunCancel = () => {
|
|
1927
|
+
if (!runSubmissionStarted)
|
|
1928
|
+
return Promise.resolve();
|
|
1929
|
+
cancelPromise ??= cancelPiServerRun(sessionId, runId).catch((error) => {
|
|
1930
|
+
cancelFailure = error;
|
|
1931
|
+
});
|
|
1932
|
+
return cancelPromise;
|
|
1933
|
+
};
|
|
1934
|
+
const onAbort = () => {
|
|
1935
|
+
void requestRunCancel();
|
|
1936
|
+
};
|
|
1937
|
+
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1938
|
+
const endStream = () => {
|
|
1939
|
+
options?.signal?.removeEventListener("abort", onAbort);
|
|
1940
|
+
if (isEphemeralSession) {
|
|
1941
|
+
resetSessionTracking(sessionId);
|
|
1942
|
+
}
|
|
1943
|
+
stream.end();
|
|
1944
|
+
};
|
|
1945
|
+
const validateTerminalPendingRun = () => {
|
|
1946
|
+
const diagnostic = getPiServerRunDiagnostic(partial);
|
|
1947
|
+
const pending = runStatePath
|
|
1948
|
+
? readPiServerPendingRun(runStatePath, runStateLease)
|
|
1949
|
+
: sessionPendingRuns.get(sessionId);
|
|
1950
|
+
if (!diagnostic ||
|
|
1951
|
+
!pending ||
|
|
1952
|
+
diagnostic.sessionId !== sessionId ||
|
|
1953
|
+
diagnostic.sessionId !== pending.sessionId ||
|
|
1954
|
+
diagnostic.runId !== pending.runId ||
|
|
1955
|
+
diagnostic.requestMac !== pending.requestHash) {
|
|
1956
|
+
throw new PiServerStreamProtocolError("Pi-server terminal run diagnostic did not match the pending run");
|
|
1957
|
+
}
|
|
1958
|
+
return runStatePath !== undefined && runStateLease !== undefined;
|
|
1959
|
+
};
|
|
1960
|
+
const getDurableRunEvents = (request, eventCursor) => request.getJson(`/api/session/${encodeURIComponent(sessionId)}/runs/${encodeURIComponent(runId)}/events?from=${eventCursor}`);
|
|
1961
|
+
const acknowledgeTransientTerminal = async (event) => {
|
|
1962
|
+
const message = event.type === "done" ? event.message : event.error;
|
|
1963
|
+
if (!transientRunAcknowledged) {
|
|
1964
|
+
validateTerminalPendingRun();
|
|
1965
|
+
await acknowledgePiServerRunMessage(sessionId, message, undefined, options?.signal);
|
|
1966
|
+
if (sessionPendingRuns.has(sessionId)) {
|
|
1967
|
+
throw new PiServerStreamProtocolError("Auxiliary pi-server terminal acknowledgement did not clear transient pending state");
|
|
1968
|
+
}
|
|
1969
|
+
transientRunAcknowledged = true;
|
|
1970
|
+
}
|
|
1971
|
+
const cleanupSignal = options?.signal?.aborted
|
|
1972
|
+
? AbortSignal.timeout(PI_SERVER_CANCEL_TIMEOUT_MS)
|
|
1973
|
+
: options?.signal;
|
|
1974
|
+
const cleanupRequest = createPiServerRequest(cleanupSignal);
|
|
1975
|
+
const cleanupResponse = await cleanupRequest.deleteJson(`/api/session/${encodeURIComponent(sessionId)}`);
|
|
1976
|
+
const cleanupResult = await readPiServerJson(cleanupResponse, "Auxiliary pi-server session cleanup failed");
|
|
1977
|
+
if (!isObject(cleanupResult) || cleanupResult.deleted !== sessionId) {
|
|
1978
|
+
throw new PiServerStreamProtocolError("Auxiliary pi-server session cleanup response did not match the transient session");
|
|
1979
|
+
}
|
|
1980
|
+
unresolvedRunPending = false;
|
|
1981
|
+
pendingTransientTerminalEvent = undefined;
|
|
1982
|
+
stream.push(event);
|
|
1983
|
+
};
|
|
1984
|
+
const settleStreamFailure = async (failure, retryable = phase === "provider_stream") => {
|
|
1985
|
+
const reason = options?.signal?.aborted ? "aborted" : "error";
|
|
1986
|
+
if (reason === "aborted") {
|
|
1987
|
+
await requestRunCancel();
|
|
1988
|
+
if (cancelFailure === undefined && runSubmissionStarted) {
|
|
1989
|
+
try {
|
|
1990
|
+
const recoveryRequest = createPiServerRequest(AbortSignal.timeout(PI_SERVER_CANCEL_TIMEOUT_MS));
|
|
1991
|
+
const response = await getDurableRunEvents(recoveryRequest, receivedProxyEventCount);
|
|
1992
|
+
if (response.ok) {
|
|
1993
|
+
const recovered = await readStreamResponse(response, receivedProxyEventCount, true);
|
|
1994
|
+
if (recovered.terminal) {
|
|
1995
|
+
endStream();
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
cancelFailure = recovered.failure;
|
|
1999
|
+
}
|
|
2000
|
+
else {
|
|
2001
|
+
const cancelRecoveryFailure = await readPiServerFailure(response);
|
|
2002
|
+
cancelFailure = new Error(`pi-server canceled run replay failed: ${cancelRecoveryFailure.details}`);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
catch (error) {
|
|
2006
|
+
cancelFailure = error;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
const initialErrorMessage = failure instanceof Error ? failure.message : String(failure);
|
|
2011
|
+
const errorMessage = cancelFailure === undefined
|
|
2012
|
+
? initialErrorMessage
|
|
2013
|
+
: `${initialErrorMessage}; pi-server run cancel failed: ${cancelFailure instanceof Error ? cancelFailure.message : String(cancelFailure)}`;
|
|
2014
|
+
partial.stopReason = reason;
|
|
2015
|
+
partial.errorMessage = errorMessage;
|
|
2016
|
+
partial.diagnostics = partial.diagnostics?.filter((diagnostic) => diagnostic.type !== "pi_server_run");
|
|
2017
|
+
partial.diagnostics = [
|
|
2018
|
+
...(partial.diagnostics ?? []),
|
|
2019
|
+
{
|
|
2020
|
+
type: "pi_server_failure",
|
|
2021
|
+
timestamp: Date.now(),
|
|
2022
|
+
error: { name: failure instanceof Error ? failure.name : "Error", message: errorMessage },
|
|
2023
|
+
details: {
|
|
2024
|
+
phase,
|
|
2025
|
+
source: "pi-server",
|
|
2026
|
+
retryable: reason === "aborted" ? false : retryable,
|
|
2027
|
+
runUnresolved: unresolvedRunPending,
|
|
2028
|
+
},
|
|
2029
|
+
},
|
|
2030
|
+
];
|
|
2031
|
+
stream.push({
|
|
2032
|
+
type: "error",
|
|
2033
|
+
reason,
|
|
2034
|
+
error: partial,
|
|
2035
|
+
});
|
|
2036
|
+
releaseRunStateLease();
|
|
2037
|
+
endStream();
|
|
2038
|
+
};
|
|
2039
|
+
const readStreamResponse = async (response, eventCursor, ignoreCallerAbort = false) => {
|
|
2040
|
+
let reader;
|
|
2041
|
+
let observedProgress = false;
|
|
2042
|
+
try {
|
|
2043
|
+
try {
|
|
2044
|
+
await ensurePiServerEventStream(response);
|
|
2045
|
+
}
|
|
2046
|
+
catch (error) {
|
|
2047
|
+
throw new PiServerStreamProtocolError(`Invalid pi-server stream response: ${error instanceof Error ? error.message : String(error)}`);
|
|
2048
|
+
}
|
|
2049
|
+
reader = response.body.getReader();
|
|
2050
|
+
const decoder = new TextDecoder();
|
|
2051
|
+
const noProgress = createPiServerBodyNoProgressState();
|
|
2052
|
+
const incompleteLineParts = [];
|
|
2053
|
+
let incompleteLineBytes = 0;
|
|
2054
|
+
let replayEventIndex = eventCursor;
|
|
2055
|
+
while (true) {
|
|
2056
|
+
const { done, value } = await readPiServerBodyChunk(reader, noProgress);
|
|
2057
|
+
if (done) {
|
|
2058
|
+
return {
|
|
2059
|
+
terminal: false,
|
|
2060
|
+
failure: new Error("pi-server stream ended before a terminal event"),
|
|
2061
|
+
observedProgress,
|
|
2062
|
+
recoverable: true,
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
if (value.byteLength > 0)
|
|
2066
|
+
observedProgress = true;
|
|
2067
|
+
if (!ignoreCallerAbort && options?.signal?.aborted) {
|
|
2068
|
+
throw new Error("Request aborted by user");
|
|
2069
|
+
}
|
|
2070
|
+
incompleteLineBytes = updateEventStreamLineBytes(value, incompleteLineBytes);
|
|
2071
|
+
if (incompleteLineBytes < 0) {
|
|
2072
|
+
throw new PiServerStreamProtocolError(`Invalid pi-server stream response: event line exceeded ${PI_SERVER_EVENT_STREAM_MAX_LINE_BYTES} bytes`);
|
|
2073
|
+
}
|
|
2074
|
+
const lines = extractDecodedEventStreamLines(decoder.decode(value, { stream: true }), incompleteLineParts);
|
|
2075
|
+
for (const rawLine of lines) {
|
|
2076
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
2077
|
+
if (!line.startsWith("data: "))
|
|
2078
|
+
continue;
|
|
2079
|
+
const data = line.slice(6).trim();
|
|
2080
|
+
if (!data)
|
|
2081
|
+
continue;
|
|
2082
|
+
const eventIndex = replayEventIndex;
|
|
2083
|
+
replayEventIndex++;
|
|
2084
|
+
if (eventIndex < receivedProxyEventCount)
|
|
2085
|
+
continue;
|
|
2086
|
+
let proxyEvent;
|
|
2087
|
+
try {
|
|
2088
|
+
proxyEvent = JSON.parse(data);
|
|
2089
|
+
}
|
|
2090
|
+
catch {
|
|
2091
|
+
throw new PiServerStreamProtocolError(`Invalid pi-server stream event JSON: ${getBodyExcerpt(data)}`);
|
|
2092
|
+
}
|
|
2093
|
+
let event;
|
|
2094
|
+
try {
|
|
2095
|
+
event = processProxyEvent(proxyEvent, partial);
|
|
2096
|
+
}
|
|
2097
|
+
catch (error) {
|
|
2098
|
+
throw new PiServerStreamProtocolError(`Invalid pi-server stream event: ${error instanceof Error ? error.message : String(error)}`);
|
|
2099
|
+
}
|
|
2100
|
+
receivedProxyEventCount++;
|
|
2101
|
+
if (!event)
|
|
2102
|
+
continue;
|
|
2103
|
+
if (event.type === "done" || event.type === "error") {
|
|
2104
|
+
if (runMode === "auxiliary-transient") {
|
|
2105
|
+
pendingTransientTerminalEvent = event;
|
|
2106
|
+
try {
|
|
2107
|
+
await acknowledgeTransientTerminal(event);
|
|
2108
|
+
}
|
|
2109
|
+
catch (error) {
|
|
2110
|
+
return {
|
|
2111
|
+
terminal: false,
|
|
2112
|
+
failure: error,
|
|
2113
|
+
observedProgress: true,
|
|
2114
|
+
recoverable: isRetryablePiServerInterruption(error),
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
else {
|
|
2119
|
+
const retainLease = runMode === "main-durable" || runStatePath !== undefined
|
|
2120
|
+
? validateTerminalPendingRun()
|
|
2121
|
+
: false;
|
|
2122
|
+
if (!retainLease) {
|
|
2123
|
+
releaseRunStateLease();
|
|
2124
|
+
}
|
|
2125
|
+
stream.push(event);
|
|
2126
|
+
}
|
|
2127
|
+
void reader.cancel().catch(() => undefined);
|
|
2128
|
+
return { terminal: true };
|
|
2129
|
+
}
|
|
2130
|
+
stream.push(event);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
catch (error) {
|
|
2135
|
+
void reader?.cancel().catch(() => undefined);
|
|
2136
|
+
return {
|
|
2137
|
+
terminal: false,
|
|
2138
|
+
failure: error,
|
|
2139
|
+
observedProgress,
|
|
2140
|
+
recoverable: isRetryablePiServerInterruption(error),
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
542
2144
|
try {
|
|
543
2145
|
const request = createPiServerRequest(options?.signal);
|
|
544
2146
|
await ensureSessionInit(sessionId, context, request);
|
|
@@ -547,120 +2149,287 @@ export async function streamPiServer(model, context, options) {
|
|
|
547
2149
|
replace: true,
|
|
548
2150
|
};
|
|
549
2151
|
const syncTree = tree;
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
2152
|
+
if (runStatePath) {
|
|
2153
|
+
attachRunStateLease();
|
|
2154
|
+
}
|
|
2155
|
+
let pending = runStatePath
|
|
2156
|
+
? readPiServerPendingRun(runStatePath, runStateLease)
|
|
2157
|
+
: sessionPendingRuns.get(sessionId);
|
|
2158
|
+
unresolvedRunPending = pending !== undefined;
|
|
2159
|
+
let treeSynced = false;
|
|
2160
|
+
if (pending) {
|
|
2161
|
+
const persistedTerminal = findPendingPiServerRunTerminal(syncTree.entries, pending);
|
|
2162
|
+
if (persistedTerminal) {
|
|
2163
|
+
phase = "tree_sync";
|
|
2164
|
+
await syncPiServerTreeWithRequest(sessionId, syncTree, request, options?.onHistoryReconciled);
|
|
2165
|
+
treeSynced = true;
|
|
2166
|
+
await acknowledgePiServerRunMessage(sessionId, persistedTerminal, runStatePath, options?.signal);
|
|
2167
|
+
if (runStatePath) {
|
|
2168
|
+
attachRunStateLease();
|
|
2169
|
+
}
|
|
2170
|
+
pending = undefined;
|
|
2171
|
+
unresolvedRunPending = false;
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
const buildRequestIdentity = (baseTreeHash, baseEntryCount, baseLeafId, baseStaticContextHash, baseRevision) => JSON.parse(JSON.stringify({
|
|
553
2175
|
sessionId,
|
|
554
|
-
|
|
2176
|
+
runMode,
|
|
555
2177
|
model,
|
|
556
2178
|
options: serializeOptions(options),
|
|
2179
|
+
providerExecutionFingerprint: options?.providerExecutionFingerprint,
|
|
557
2180
|
ephemeralMessages: options?.ephemeralMessages,
|
|
558
2181
|
contextOverlay: options?.contextOverlay,
|
|
2182
|
+
baseTreeHash,
|
|
2183
|
+
baseEntryCount,
|
|
2184
|
+
baseLeafId,
|
|
2185
|
+
baseStaticContextHash,
|
|
2186
|
+
baseRevision,
|
|
2187
|
+
}));
|
|
2188
|
+
let baseTreeHash = pending?.baseTreeHash ?? hashLocalTree(sessionId, syncTree.entries);
|
|
2189
|
+
let baseEntryCount = pending?.baseEntryCount ?? syncTree.entries.length;
|
|
2190
|
+
let baseLeafId = pending?.baseLeafId ?? syncTree.leafId;
|
|
2191
|
+
let baseStaticContextHash = hashStaticContext(context);
|
|
2192
|
+
let baseRevision = sessionTreeRevisions.get(sessionId) ?? 0;
|
|
2193
|
+
let requestIdentity = buildRequestIdentity(baseTreeHash, baseEntryCount, baseLeafId, baseStaticContextHash, baseRevision);
|
|
2194
|
+
let requestHash = hashPiServerStreamRequest(requestIdentity);
|
|
2195
|
+
let resumedPendingRun = false;
|
|
2196
|
+
let recoveredPendingRun;
|
|
2197
|
+
if (pending) {
|
|
2198
|
+
assertPendingPiServerRunMatches(pending, hashPiServerIdentity(getServerUrl()), sessionId, syncTree);
|
|
2199
|
+
runId = pending.runId;
|
|
2200
|
+
phase = "provider_stream";
|
|
2201
|
+
recoveredPendingRun = await recoverPiServerRun(sessionId, runId, request, options?.signal);
|
|
2202
|
+
if (recoveredPendingRun.status === "not_found") {
|
|
2203
|
+
throw missingDurablePiServerRunError(pending.runId);
|
|
2204
|
+
}
|
|
2205
|
+
else {
|
|
2206
|
+
resumedPendingRun = true;
|
|
2207
|
+
runSubmissionStarted = true;
|
|
2208
|
+
requestHash = pending.requestHash;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
if (!pending) {
|
|
2212
|
+
if (!treeSynced) {
|
|
2213
|
+
phase = "tree_sync";
|
|
2214
|
+
await syncPiServerTreeWithRequest(sessionId, syncTree, request, options?.onHistoryReconciled);
|
|
2215
|
+
treeSynced = true;
|
|
2216
|
+
}
|
|
2217
|
+
baseTreeHash = hashLocalTree(sessionId, syncTree.entries);
|
|
2218
|
+
baseEntryCount = syncTree.entries.length;
|
|
2219
|
+
baseLeafId = syncTree.leafId;
|
|
2220
|
+
baseStaticContextHash = sessionStaticContextHashes.get(sessionId) ?? hashStaticContext(context);
|
|
2221
|
+
baseRevision = sessionTreeRevisions.get(sessionId) ?? 0;
|
|
2222
|
+
requestIdentity = buildRequestIdentity(baseTreeHash, baseEntryCount, baseLeafId, baseStaticContextHash, baseRevision);
|
|
2223
|
+
requestHash = hashPiServerStreamRequest(requestIdentity);
|
|
2224
|
+
const pendingInput = {
|
|
2225
|
+
serverHash: hashPiServerIdentity(getServerUrl()),
|
|
2226
|
+
sessionId,
|
|
2227
|
+
runId,
|
|
2228
|
+
baseTreeHash,
|
|
2229
|
+
baseEntryCount,
|
|
2230
|
+
baseLeafId,
|
|
2231
|
+
requestHash,
|
|
2232
|
+
};
|
|
2233
|
+
const newPending = runStatePath
|
|
2234
|
+
? writePiServerPendingRun(runStatePath, pendingInput, runStateLease)
|
|
2235
|
+
: {
|
|
2236
|
+
version: 1,
|
|
2237
|
+
kind: "run",
|
|
2238
|
+
sequence: 0,
|
|
2239
|
+
timestamp: Date.now(),
|
|
2240
|
+
...pendingInput,
|
|
2241
|
+
};
|
|
2242
|
+
if (!runStatePath)
|
|
2243
|
+
sessionPendingRuns.set(sessionId, newPending);
|
|
2244
|
+
unresolvedRunPending = true;
|
|
2245
|
+
}
|
|
2246
|
+
const makeBody = (eventCursor) => ({
|
|
2247
|
+
...requestIdentity,
|
|
2248
|
+
runId,
|
|
2249
|
+
eventCursor,
|
|
559
2250
|
});
|
|
2251
|
+
const getRunEvents = (eventCursor) => getDurableRunEvents(request, eventCursor);
|
|
560
2252
|
phase = "provider_stream";
|
|
561
|
-
|
|
562
|
-
|
|
2253
|
+
const postStream = async (eventCursor) => {
|
|
2254
|
+
runSubmissionStarted = true;
|
|
2255
|
+
if (options?.signal?.aborted) {
|
|
2256
|
+
throw new Error("Request aborted by user");
|
|
2257
|
+
}
|
|
2258
|
+
return request.postJson("/api/stream", makeBody(eventCursor));
|
|
2259
|
+
};
|
|
2260
|
+
let response;
|
|
2261
|
+
let activeResponse;
|
|
2262
|
+
let activeEventCursor = 0;
|
|
2263
|
+
let interruption = new Error("pi-server stream ended before a terminal event");
|
|
2264
|
+
try {
|
|
2265
|
+
if (resumedPendingRun) {
|
|
2266
|
+
if (recoveredPendingRun?.status === "running" ||
|
|
2267
|
+
recoveredPendingRun?.status === "completed" ||
|
|
2268
|
+
recoveredPendingRun?.status === "failed") {
|
|
2269
|
+
if (recoveredPendingRun.requestMac !== requestHash) {
|
|
2270
|
+
throw new Error("Pending pi-server run request identity did not match durable server state");
|
|
2271
|
+
}
|
|
2272
|
+
response = await getRunEvents(0);
|
|
2273
|
+
}
|
|
2274
|
+
else {
|
|
2275
|
+
interruption = new Error(`pi-server pending run recovery was unavailable: ${recoveredPendingRun?.status === "unavailable"
|
|
2276
|
+
? recoveredPendingRun.details
|
|
2277
|
+
: "unknown recovery state"}`);
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
else {
|
|
2281
|
+
response = await postStream(0);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
catch (error) {
|
|
2285
|
+
if (isAbortError(error, options?.signal))
|
|
2286
|
+
throw error;
|
|
2287
|
+
if (!isRetryablePiServerInterruption(error))
|
|
2288
|
+
throw error;
|
|
2289
|
+
interruption = error;
|
|
2290
|
+
}
|
|
2291
|
+
if (response && !response.ok) {
|
|
563
2292
|
let failure = await readPiServerFailure(response);
|
|
564
2293
|
if (!options?.signal?.aborted && isRecoverableMissingServerState(response, failure.matchText)) {
|
|
565
2294
|
resetSessionTracking(sessionId);
|
|
2295
|
+
phase = "session_init";
|
|
566
2296
|
await ensureSessionInit(sessionId, context, request);
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
2297
|
+
phase = "tree_sync";
|
|
2298
|
+
await syncPiServerTreeWithRequest(sessionId, syncTree, request, options?.onHistoryReconciled);
|
|
2299
|
+
phase = "provider_stream";
|
|
2300
|
+
try {
|
|
2301
|
+
response = await postStream(receivedProxyEventCount);
|
|
571
2302
|
}
|
|
572
|
-
|
|
2303
|
+
catch (error) {
|
|
2304
|
+
if (isAbortError(error, options?.signal))
|
|
2305
|
+
throw error;
|
|
2306
|
+
if (!isRetryablePiServerInterruption(error))
|
|
2307
|
+
throw error;
|
|
2308
|
+
response = undefined;
|
|
2309
|
+
interruption = error;
|
|
2310
|
+
}
|
|
2311
|
+
if (response && !response.ok) {
|
|
573
2312
|
failure = await readPiServerFailure(response);
|
|
574
2313
|
}
|
|
575
2314
|
}
|
|
576
|
-
if (!response.ok) {
|
|
577
|
-
|
|
2315
|
+
if (response && !response.ok) {
|
|
2316
|
+
const retryable = isTransientPiServerResponse(response, failure.bodyText);
|
|
2317
|
+
const responseError = new PiServerHttpResponseError(`pi-server error: ${failure.details}`, response.status, retryable);
|
|
2318
|
+
if (!retryable)
|
|
2319
|
+
throw responseError;
|
|
2320
|
+
interruption = responseError;
|
|
578
2321
|
}
|
|
579
2322
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
2323
|
+
if (response?.ok) {
|
|
2324
|
+
activeResponse = response;
|
|
2325
|
+
activeEventCursor = receivedProxyEventCount;
|
|
2326
|
+
}
|
|
2327
|
+
let reconnectAttempt = 0;
|
|
2328
|
+
let recoveryDeadline;
|
|
584
2329
|
while (true) {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
2330
|
+
if (pendingTransientTerminalEvent) {
|
|
2331
|
+
try {
|
|
2332
|
+
await acknowledgeTransientTerminal(pendingTransientTerminalEvent);
|
|
2333
|
+
endStream();
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
catch (error) {
|
|
2337
|
+
if (!isRetryablePiServerInterruption(error)) {
|
|
2338
|
+
await settleStreamFailure(error, false);
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
interruption = error;
|
|
2342
|
+
recoveryDeadline = Date.now() + recoveryWindowMs;
|
|
2343
|
+
reconnectAttempt = 0;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
if (activeResponse) {
|
|
2347
|
+
const result = await readStreamResponse(activeResponse, activeEventCursor);
|
|
2348
|
+
if (result.terminal) {
|
|
2349
|
+
endStream();
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
if (!result.recoverable) {
|
|
2353
|
+
await settleStreamFailure(result.failure, false);
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
interruption = result.failure;
|
|
2357
|
+
activeResponse = undefined;
|
|
2358
|
+
if (result.observedProgress) {
|
|
2359
|
+
recoveryDeadline = Date.now() + recoveryWindowMs;
|
|
2360
|
+
reconnectAttempt = 0;
|
|
2361
|
+
}
|
|
2362
|
+
else {
|
|
2363
|
+
recoveryDeadline ??= Date.now() + recoveryWindowMs;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
588
2366
|
if (options?.signal?.aborted) {
|
|
589
2367
|
throw new Error("Request aborted by user");
|
|
590
2368
|
}
|
|
591
|
-
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
2369
|
+
recoveryDeadline ??= Date.now() + recoveryWindowMs;
|
|
2370
|
+
const recoveredRun = await recoverPiServerRun(sessionId, runId, request, options?.signal);
|
|
2371
|
+
const hasDurableRun = recoveredRun.status === "running" ||
|
|
2372
|
+
recoveredRun.status === "completed" ||
|
|
2373
|
+
recoveredRun.status === "failed";
|
|
2374
|
+
if (hasDurableRun && recoveredRun.requestMac !== requestHash) {
|
|
2375
|
+
await settleStreamFailure(new Error("pi-server recovered run request identity did not match the pending provider request"), false);
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
if (recoveredRun.status === "not_found") {
|
|
2379
|
+
const failureMessage = interruption instanceof Error ? interruption.message : String(interruption);
|
|
2380
|
+
await settleStreamFailure(missingDurablePiServerRunError(runId, failureMessage), false);
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
if (recoveredRun.status === "unavailable") {
|
|
2384
|
+
const failureMessage = interruption instanceof Error ? interruption.message : String(interruption);
|
|
2385
|
+
interruption = new Error(`${failureMessage}; pi-server run recovery was unavailable: ${recoveredRun.details}`);
|
|
2386
|
+
}
|
|
2387
|
+
const remainingMs = recoveryDeadline - Date.now();
|
|
2388
|
+
if (remainingMs <= 0) {
|
|
2389
|
+
const failureMessage = interruption instanceof Error ? interruption.message : String(interruption);
|
|
2390
|
+
await settleStreamFailure(new Error(`${failureMessage}; pi-server stream recovery exhausted after ${recoveryWindowMs}ms`), false);
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
const delayMs = Math.min(getPiServerStreamReconnectDelay(reconnectAttempt), remainingMs);
|
|
2394
|
+
reconnectAttempt++;
|
|
2395
|
+
await waitForPiServerStreamReconnect(delayMs, options?.signal);
|
|
2396
|
+
if (Date.now() >= recoveryDeadline) {
|
|
2397
|
+
const failureMessage = interruption instanceof Error ? interruption.message : String(interruption);
|
|
2398
|
+
await settleStreamFailure(new Error(`${failureMessage}; pi-server stream recovery exhausted after ${recoveryWindowMs}ms`), false);
|
|
2399
|
+
return;
|
|
605
2400
|
}
|
|
606
|
-
}
|
|
607
|
-
if (options?.signal?.aborted) {
|
|
608
|
-
throw new Error("Request aborted by user");
|
|
609
|
-
}
|
|
610
|
-
if (isEphemeralSession) {
|
|
611
|
-
resetSessionTracking(sessionId);
|
|
612
|
-
}
|
|
613
|
-
stream.end();
|
|
614
|
-
}
|
|
615
|
-
catch (error) {
|
|
616
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
617
|
-
if (!options?.signal?.aborted && phase === "provider_stream") {
|
|
618
2401
|
try {
|
|
619
|
-
const
|
|
620
|
-
if (recoveredRun
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
});
|
|
630
|
-
break;
|
|
631
|
-
case "aborted":
|
|
632
|
-
stream.push({ type: "error", reason: "aborted", error: recoveredRun.message });
|
|
633
|
-
break;
|
|
634
|
-
case "error":
|
|
635
|
-
stream.push({ type: "error", reason: "error", error: recoveredRun.message });
|
|
636
|
-
break;
|
|
2402
|
+
const eventCursor = receivedProxyEventCount;
|
|
2403
|
+
if (recoveredRun.status === "unavailable") {
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
const reconnectResponse = await getRunEvents(eventCursor);
|
|
2407
|
+
if (!reconnectResponse.ok) {
|
|
2408
|
+
const failure = await readPiServerFailure(reconnectResponse);
|
|
2409
|
+
if (!isTransientPiServerResponse(reconnectResponse, failure.bodyText)) {
|
|
2410
|
+
await settleStreamFailure(new Error(`pi-server reconnect failed: ${failure.details}`), false);
|
|
2411
|
+
return;
|
|
637
2412
|
}
|
|
638
|
-
|
|
639
|
-
|
|
2413
|
+
interruption = new Error(`pi-server reconnect failed: ${failure.details}`);
|
|
2414
|
+
continue;
|
|
640
2415
|
}
|
|
2416
|
+
activeResponse = reconnectResponse;
|
|
2417
|
+
activeEventCursor = eventCursor;
|
|
641
2418
|
}
|
|
642
|
-
catch {
|
|
643
|
-
|
|
2419
|
+
catch (error) {
|
|
2420
|
+
if (isAbortError(error, options?.signal)) {
|
|
2421
|
+
throw error;
|
|
2422
|
+
}
|
|
2423
|
+
if (!isRetryablePiServerInterruption(error)) {
|
|
2424
|
+
await settleStreamFailure(error, false);
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
interruption = error;
|
|
644
2428
|
}
|
|
645
2429
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
partial.diagnostics = [
|
|
650
|
-
...(partial.diagnostics ?? []),
|
|
651
|
-
{
|
|
652
|
-
type: "pi_server_failure",
|
|
653
|
-
timestamp: Date.now(),
|
|
654
|
-
error: { name: error instanceof Error ? error.name : "Error", message: errorMessage },
|
|
655
|
-
details: { phase, source: "pi-server", retryable: phase === "provider_stream" },
|
|
656
|
-
},
|
|
657
|
-
];
|
|
658
|
-
stream.push({
|
|
659
|
-
type: "error",
|
|
660
|
-
reason,
|
|
661
|
-
error: partial,
|
|
662
|
-
});
|
|
663
|
-
stream.end();
|
|
2430
|
+
}
|
|
2431
|
+
catch (error) {
|
|
2432
|
+
await settleStreamFailure(error, false);
|
|
664
2433
|
}
|
|
665
2434
|
})();
|
|
666
2435
|
return stream;
|
|
@@ -683,6 +2452,8 @@ function processProxyEvent(proxyEvent, partial) {
|
|
|
683
2452
|
case "text_end": {
|
|
684
2453
|
const content = partial.content[proxyEvent.contentIndex];
|
|
685
2454
|
if (content?.type === "text") {
|
|
2455
|
+
if (proxyEvent.content !== undefined)
|
|
2456
|
+
content.text = proxyEvent.content;
|
|
686
2457
|
content.textSignature = proxyEvent.contentSignature;
|
|
687
2458
|
return { type: "text_end", contentIndex: proxyEvent.contentIndex, content: content.text, partial };
|
|
688
2459
|
}
|
|
@@ -702,25 +2473,34 @@ function processProxyEvent(proxyEvent, partial) {
|
|
|
702
2473
|
case "thinking_end": {
|
|
703
2474
|
const content = partial.content[proxyEvent.contentIndex];
|
|
704
2475
|
if (content?.type === "thinking") {
|
|
2476
|
+
if (proxyEvent.content !== undefined)
|
|
2477
|
+
content.thinking = proxyEvent.content;
|
|
705
2478
|
content.thinkingSignature = proxyEvent.contentSignature;
|
|
2479
|
+
content.redacted = proxyEvent.redacted;
|
|
706
2480
|
return { type: "thinking_end", contentIndex: proxyEvent.contentIndex, content: content.thinking, partial };
|
|
707
2481
|
}
|
|
708
2482
|
throw new Error("Received thinking_end for non-thinking content");
|
|
709
2483
|
}
|
|
710
|
-
case "toolcall_start":
|
|
711
|
-
|
|
2484
|
+
case "toolcall_start": {
|
|
2485
|
+
const partialToolCall = {
|
|
712
2486
|
type: "toolCall",
|
|
713
2487
|
id: proxyEvent.id,
|
|
714
2488
|
name: proxyEvent.toolName,
|
|
715
2489
|
arguments: {},
|
|
716
2490
|
partialJson: "",
|
|
717
2491
|
};
|
|
2492
|
+
partial.content[proxyEvent.contentIndex] = partialToolCall;
|
|
718
2493
|
return { type: "toolcall_start", contentIndex: proxyEvent.contentIndex, partial };
|
|
2494
|
+
}
|
|
719
2495
|
case "toolcall_delta": {
|
|
720
2496
|
const content = partial.content[proxyEvent.contentIndex];
|
|
721
2497
|
if (content?.type === "toolCall") {
|
|
722
|
-
|
|
723
|
-
|
|
2498
|
+
const partialToolCall = content;
|
|
2499
|
+
if (partialToolCall.partialJson === undefined) {
|
|
2500
|
+
throw new Error("Received toolcall_delta without an active partial tool call");
|
|
2501
|
+
}
|
|
2502
|
+
partialToolCall.partialJson += proxyEvent.delta;
|
|
2503
|
+
content.arguments = parseStreamingJson(partialToolCall.partialJson) || {};
|
|
724
2504
|
partial.content[proxyEvent.contentIndex] = { ...content };
|
|
725
2505
|
return { type: "toolcall_delta", contentIndex: proxyEvent.contentIndex, delta: proxyEvent.delta, partial };
|
|
726
2506
|
}
|
|
@@ -729,24 +2509,46 @@ function processProxyEvent(proxyEvent, partial) {
|
|
|
729
2509
|
case "toolcall_end": {
|
|
730
2510
|
const content = partial.content[proxyEvent.contentIndex];
|
|
731
2511
|
if (content?.type === "toolCall") {
|
|
732
|
-
|
|
733
|
-
|
|
2512
|
+
const toolCall = proxyEvent.toolCall ?? content;
|
|
2513
|
+
delete toolCall.partialJson;
|
|
2514
|
+
toolCall.thoughtSignature = proxyEvent.thoughtSignature ?? toolCall.thoughtSignature;
|
|
2515
|
+
partial.content[proxyEvent.contentIndex] = toolCall;
|
|
2516
|
+
return { type: "toolcall_end", contentIndex: proxyEvent.contentIndex, toolCall, partial };
|
|
734
2517
|
}
|
|
735
2518
|
return undefined;
|
|
736
2519
|
}
|
|
737
2520
|
case "done":
|
|
2521
|
+
if (proxyEvent.reason !== "stop" && proxyEvent.reason !== "length" && proxyEvent.reason !== "toolUse") {
|
|
2522
|
+
throw new Error(`Received invalid terminal done reason: ${String(proxyEvent.reason)}`);
|
|
2523
|
+
}
|
|
738
2524
|
partial.stopReason = proxyEvent.reason;
|
|
739
2525
|
partial.usage = proxyEvent.usage;
|
|
2526
|
+
partial.api = proxyEvent.api ?? partial.api;
|
|
2527
|
+
partial.provider = proxyEvent.provider ?? partial.provider;
|
|
2528
|
+
partial.model = proxyEvent.model ?? partial.model;
|
|
2529
|
+
partial.timestamp = proxyEvent.timestamp ?? partial.timestamp;
|
|
2530
|
+
partial.responseModel = proxyEvent.responseModel;
|
|
2531
|
+
partial.responseId = proxyEvent.responseId;
|
|
2532
|
+
partial.diagnostics = proxyEvent.diagnostics;
|
|
740
2533
|
return { type: "done", reason: proxyEvent.reason, message: partial };
|
|
741
2534
|
case "error":
|
|
2535
|
+
if (proxyEvent.reason !== "aborted" && proxyEvent.reason !== "error") {
|
|
2536
|
+
throw new Error(`Received invalid terminal error reason: ${String(proxyEvent.reason)}`);
|
|
2537
|
+
}
|
|
742
2538
|
partial.stopReason = proxyEvent.reason;
|
|
743
2539
|
partial.errorMessage = proxyEvent.errorMessage;
|
|
744
2540
|
partial.usage = proxyEvent.usage;
|
|
2541
|
+
partial.api = proxyEvent.api ?? partial.api;
|
|
2542
|
+
partial.provider = proxyEvent.provider ?? partial.provider;
|
|
2543
|
+
partial.model = proxyEvent.model ?? partial.model;
|
|
2544
|
+
partial.timestamp = proxyEvent.timestamp ?? partial.timestamp;
|
|
2545
|
+
partial.responseModel = proxyEvent.responseModel;
|
|
2546
|
+
partial.responseId = proxyEvent.responseId;
|
|
2547
|
+
partial.diagnostics = proxyEvent.diagnostics;
|
|
745
2548
|
return { type: "error", reason: proxyEvent.reason, error: partial };
|
|
746
2549
|
default: {
|
|
747
2550
|
const _exhaustiveCheck = proxyEvent;
|
|
748
|
-
|
|
749
|
-
return undefined;
|
|
2551
|
+
throw new Error(`Unhandled proxy event: ${String(_exhaustiveCheck)}`);
|
|
750
2552
|
}
|
|
751
2553
|
}
|
|
752
2554
|
}
|