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