@averyyy/pi-coding-agent 0.82.1-piclient.3 → 0.82.1-piclient.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cli/args.d.ts.map +1 -1
  3. package/dist/cli/args.js +2 -1
  4. package/dist/cli/args.js.map +1 -1
  5. package/dist/cli/tool-effects.d.ts +6 -0
  6. package/dist/cli/tool-effects.d.ts.map +1 -0
  7. package/dist/cli/tool-effects.js +164 -0
  8. package/dist/cli/tool-effects.js.map +1 -0
  9. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  10. package/dist/core/agent-session-runtime.js +5 -0
  11. package/dist/core/agent-session-runtime.js.map +1 -1
  12. package/dist/core/agent-session.d.ts +12 -1
  13. package/dist/core/agent-session.d.ts.map +1 -1
  14. package/dist/core/agent-session.js +314 -89
  15. package/dist/core/agent-session.js.map +1 -1
  16. package/dist/core/pi-server-client.d.ts +53 -2
  17. package/dist/core/pi-server-client.d.ts.map +1 -1
  18. package/dist/core/pi-server-client.js +2082 -290
  19. package/dist/core/pi-server-client.js.map +1 -1
  20. package/dist/core/pi-server-compact-state.d.ts +81 -0
  21. package/dist/core/pi-server-compact-state.d.ts.map +1 -0
  22. package/dist/core/pi-server-compact-state.js +438 -0
  23. package/dist/core/pi-server-compact-state.js.map +1 -0
  24. package/dist/core/pi-server-protocol.d.ts +2 -0
  25. package/dist/core/pi-server-protocol.d.ts.map +1 -1
  26. package/dist/core/pi-server-protocol.js +24 -1
  27. package/dist/core/pi-server-protocol.js.map +1 -1
  28. package/dist/core/pi-server-request.d.ts +28 -0
  29. package/dist/core/pi-server-request.d.ts.map +1 -1
  30. package/dist/core/pi-server-request.js +492 -75
  31. package/dist/core/pi-server-request.js.map +1 -1
  32. package/dist/core/pi-server-run-state.d.ts +41 -0
  33. package/dist/core/pi-server-run-state.d.ts.map +1 -0
  34. package/dist/core/pi-server-run-state.js +318 -0
  35. package/dist/core/pi-server-run-state.js.map +1 -0
  36. package/dist/core/sdk.d.ts.map +1 -1
  37. package/dist/core/sdk.js +69 -53
  38. package/dist/core/sdk.js.map +1 -1
  39. package/dist/core/session-manager.d.ts +86 -2
  40. package/dist/core/session-manager.d.ts.map +1 -1
  41. package/dist/core/session-manager.js +737 -144
  42. package/dist/core/session-manager.js.map +1 -1
  43. package/dist/core/tool-effect-journal.d.ts +103 -0
  44. package/dist/core/tool-effect-journal.d.ts.map +1 -0
  45. package/dist/core/tool-effect-journal.js +1442 -0
  46. package/dist/core/tool-effect-journal.js.map +1 -0
  47. package/dist/core/tools/output-accumulator.d.ts +3 -1
  48. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  49. package/dist/core/tools/output-accumulator.js +46 -26
  50. package/dist/core/tools/output-accumulator.js.map +1 -1
  51. package/dist/main.d.ts.map +1 -1
  52. package/dist/main.js +13 -0
  53. package/dist/main.js.map +1 -1
  54. package/dist/modes/interactive/components/session-selector.d.ts +15 -0
  55. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  56. package/dist/modes/interactive/components/session-selector.js +98 -29
  57. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  58. package/package.json +4 -4
@@ -3,6 +3,55 @@ import { createHash, randomUUID } from "node:crypto";
3
3
  const DEFAULT_MAX_REQUEST_KB = 512;
4
4
  const CHUNK_ENDPOINT = "/api/request/chunk";
5
5
  const CHUNK_UPLOAD_CONCURRENCY = 4;
6
+ const CHUNK_MAX_TOTAL_CHUNKS = 4096;
7
+ const CHUNK_UPLOAD_RETRY_DELAYS_MS = [100, 250, 500];
8
+ const LEGACY_CHUNK_ACK_MAX_BYTES = 4096;
9
+ const DEFAULT_CHUNK_REQUEST_MEMO_LIMIT = 128;
10
+ const DEFAULT_RESPONSE_NO_PROGRESS_TIMEOUT_MS = 90_000;
11
+ export const PI_SERVER_CHUNK_ACK_HEADER = "X-Pi-Chunk-Ack";
12
+ export const PI_SERVER_CHUNK_ACK_VALUE = "1";
13
+ export const PI_SERVER_CHUNK_FINAL_VALUE = "0";
14
+ export const PI_SERVER_CHUNK_SEGMENT_ENCODING = "base64-segment-v1";
15
+ export const PI_SERVER_RESPONSE_HEADER_TIMEOUT_MS = 30_000;
16
+ export class PiServerTransportHeaderTimeoutError extends Error {
17
+ code = "PI_SERVER_TRANSPORT_HEADER_TIMEOUT";
18
+ timeoutMs;
19
+ constructor(method, endpoint, timeoutMs) {
20
+ super(`pi-server transport header timeout after ${timeoutMs} ms (ETIMEDOUT): ${method} ${endpoint}`);
21
+ this.name = "PiServerTransportHeaderTimeoutError";
22
+ this.timeoutMs = timeoutMs;
23
+ }
24
+ }
25
+ class PiServerTransportBodyNoProgressError extends Error {
26
+ constructor(timeoutMs) {
27
+ super(`pi-server response made no progress for ${timeoutMs}ms`);
28
+ this.name = "PiServerTransportBodyNoProgressError";
29
+ }
30
+ }
31
+ export class PiServerChunkRequestMemoLimitError extends Error {
32
+ code = "PI_SERVER_CHUNK_REQUEST_MEMO_LIMIT";
33
+ limit;
34
+ constructor(limit) {
35
+ super(`pi-server chunk request identity limit (${limit}) reached; refusing to allocate a new requestId while prior requests remain uncertain`);
36
+ this.name = "PiServerChunkRequestMemoLimitError";
37
+ this.limit = limit;
38
+ }
39
+ }
40
+ export class PiServerChunkAckProtocolError extends Error {
41
+ constructor(message) {
42
+ super(message);
43
+ this.name = "PiServerChunkAckProtocolError";
44
+ }
45
+ }
46
+ export class PiServerTransportBodyLimitError extends Error {
47
+ code = "PI_SERVER_TRANSPORT_BODY_LIMIT";
48
+ maxBytes;
49
+ constructor(maxBytes) {
50
+ super(`pi-server response body exceeded ${maxBytes} bytes`);
51
+ this.name = "PiServerTransportBodyLimitError";
52
+ this.maxBytes = maxBytes;
53
+ }
54
+ }
6
55
  function jsonByteLength(value) {
7
56
  return Buffer.byteLength(JSON.stringify(value), "utf-8");
8
57
  }
@@ -25,126 +74,494 @@ function makeHeaders(authToken) {
25
74
  function sha256(value) {
26
75
  return createHash("sha256").update(value).digest("hex");
27
76
  }
77
+ function isTransientChunkResponse(status) {
78
+ return status === 408 || status === 425 || status === 429 || status >= 500;
79
+ }
80
+ function waitForChunkRetry(delayMs, signal) {
81
+ if (signal?.aborted)
82
+ return Promise.reject(signal.reason);
83
+ return new Promise((resolve, reject) => {
84
+ const timeout = setTimeout(() => {
85
+ signal?.removeEventListener("abort", onAbort);
86
+ resolve();
87
+ }, delayMs);
88
+ const onAbort = () => {
89
+ clearTimeout(timeout);
90
+ signal?.removeEventListener("abort", onAbort);
91
+ reject(signal?.reason ?? new DOMException("The operation was aborted", "AbortError"));
92
+ };
93
+ signal?.addEventListener("abort", onAbort, { once: true });
94
+ });
95
+ }
28
96
  function isRecord(value) {
29
97
  return typeof value === "object" && value !== null;
30
98
  }
31
99
  function isChunkAckBody(value) {
32
100
  return (isRecord(value) && "received" in value && "requestId" in value && "chunkIndex" in value && "totalChunks" in value);
33
101
  }
34
- async function isChunkAck(response, requestId, chunkIndex, totalChunks) {
35
- if (!response.headers.get("Content-Type")?.includes("application/json"))
36
- return false;
102
+ function getResponseNoProgressTimeoutMs() {
103
+ const raw = process.env.PI_SERVER_RESPONSE_NO_PROGRESS_TIMEOUT_MS;
104
+ const value = raw === undefined ? DEFAULT_RESPONSE_NO_PROGRESS_TIMEOUT_MS : Number(raw);
105
+ if (!Number.isFinite(value) || value <= 0) {
106
+ throw new Error("PI_SERVER_RESPONSE_NO_PROGRESS_TIMEOUT_MS must be positive finite milliseconds");
107
+ }
108
+ return value;
109
+ }
110
+ function createBodyNoProgressState() {
111
+ const timeoutMs = getResponseNoProgressTimeoutMs();
112
+ return { timeoutMs, deadlineMs: Date.now() + timeoutMs };
113
+ }
114
+ async function readPiServerBodyChunk(reader, state) {
115
+ while (true) {
116
+ const remainingMs = state.deadlineMs - Date.now();
117
+ if (remainingMs <= 0) {
118
+ throw new PiServerTransportBodyNoProgressError(state.timeoutMs);
119
+ }
120
+ const result = await new Promise((resolve, reject) => {
121
+ let settled = false;
122
+ const timeout = setTimeout(() => {
123
+ if (settled)
124
+ return;
125
+ settled = true;
126
+ reject(new PiServerTransportBodyNoProgressError(state.timeoutMs));
127
+ }, remainingMs);
128
+ timeout.unref();
129
+ void reader.read().then((value) => {
130
+ if (settled)
131
+ return;
132
+ settled = true;
133
+ clearTimeout(timeout);
134
+ resolve(value);
135
+ }, (error) => {
136
+ if (settled)
137
+ return;
138
+ settled = true;
139
+ clearTimeout(timeout);
140
+ reject(error);
141
+ });
142
+ });
143
+ if (result.done || result.value.byteLength > 0) {
144
+ if (!result.done)
145
+ state.deadlineMs = Date.now() + state.timeoutMs;
146
+ return result;
147
+ }
148
+ await new Promise((resolve) => {
149
+ setTimeout(resolve, 0);
150
+ });
151
+ }
152
+ }
153
+ export async function readPiServerResponseText(response, maxBytes = Number.POSITIVE_INFINITY) {
154
+ if ((maxBytes !== Number.POSITIVE_INFINITY && (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)) ||
155
+ maxBytes === Number.NEGATIVE_INFINITY) {
156
+ throw new Error("pi-server response body limit must be positive safe-integer bytes or Infinity");
157
+ }
158
+ if (!response.body)
159
+ throw new Error("pi-server response body was missing");
160
+ const reader = response.body.getReader();
161
+ const decoder = new TextDecoder();
162
+ const noProgress = createBodyNoProgressState();
163
+ let body = "";
164
+ let bodyBytes = 0;
165
+ try {
166
+ while (true) {
167
+ const result = await readPiServerBodyChunk(reader, noProgress);
168
+ if (result.done) {
169
+ body += decoder.decode();
170
+ return body;
171
+ }
172
+ bodyBytes += result.value.byteLength;
173
+ if (bodyBytes > maxBytes) {
174
+ throw new PiServerTransportBodyLimitError(maxBytes);
175
+ }
176
+ body += decoder.decode(result.value, { stream: true });
177
+ }
178
+ }
179
+ catch (error) {
180
+ void reader.cancel().catch(() => undefined);
181
+ throw error;
182
+ }
183
+ }
184
+ export async function readPiServerResponseJson(response, maxBytes = Number.POSITIVE_INFINITY) {
185
+ return JSON.parse(await readPiServerResponseText(response, maxBytes));
186
+ }
187
+ function rebuildResponse(response, body) {
188
+ return new Response(body, {
189
+ status: response.status,
190
+ statusText: response.statusText,
191
+ headers: response.headers,
192
+ });
193
+ }
194
+ async function discardResponse(response) {
195
+ if (!response?.body || response.bodyUsed || response.body.locked)
196
+ return;
197
+ try {
198
+ await response.body.cancel();
199
+ }
200
+ catch {
201
+ // The response is already unusable and must not replace the selected final response.
202
+ }
203
+ }
204
+ async function inspectLegacyChunkAck(response, requestId, chunkIndex, totalChunks) {
205
+ const contentLength = Number(response.headers.get("Content-Length"));
206
+ if (Number.isFinite(contentLength) && contentLength > LEGACY_CHUNK_ACK_MAX_BYTES) {
207
+ return { acknowledged: false, response };
208
+ }
209
+ if (!response.body)
210
+ return { acknowledged: false, response };
211
+ const reader = response.body.getReader();
212
+ const noProgress = createBodyNoProgressState();
213
+ const chunks = [];
214
+ let bodyBytes = 0;
37
215
  try {
38
- const body = (await response.clone().json());
39
- return (isChunkAckBody(body) &&
216
+ while (true) {
217
+ const result = await readPiServerBodyChunk(reader, noProgress);
218
+ if (result.done)
219
+ break;
220
+ chunks.push(result.value);
221
+ bodyBytes += result.value.byteLength;
222
+ if (bodyBytes > LEGACY_CHUNK_ACK_MAX_BYTES) {
223
+ let prefixIndex = 0;
224
+ const rebuiltBody = new ReadableStream({
225
+ async pull(controller) {
226
+ if (prefixIndex < chunks.length) {
227
+ const prefix = chunks[prefixIndex];
228
+ chunks[prefixIndex] = new Uint8Array();
229
+ prefixIndex++;
230
+ controller.enqueue(prefix);
231
+ return;
232
+ }
233
+ try {
234
+ const next = await readPiServerBodyChunk(reader, noProgress);
235
+ if (next.done) {
236
+ controller.close();
237
+ }
238
+ else {
239
+ controller.enqueue(next.value);
240
+ }
241
+ }
242
+ catch (error) {
243
+ controller.error(error);
244
+ void reader.cancel(error).catch(() => undefined);
245
+ }
246
+ },
247
+ cancel(reason) {
248
+ return reader.cancel(reason);
249
+ },
250
+ });
251
+ return { acknowledged: false, response: rebuildResponse(response, rebuiltBody) };
252
+ }
253
+ }
254
+ }
255
+ catch (error) {
256
+ void reader.cancel().catch(() => undefined);
257
+ throw error;
258
+ }
259
+ const encoded = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), bodyBytes);
260
+ const rebuilt = rebuildResponse(response, encoded);
261
+ try {
262
+ const body = JSON.parse(encoded.toString("utf-8"));
263
+ const acknowledged = isChunkAckBody(body) &&
40
264
  body.received === true &&
41
265
  body.requestId === requestId &&
42
266
  body.chunkIndex === chunkIndex &&
43
- body.totalChunks === totalChunks);
267
+ body.totalChunks === totalChunks;
268
+ return acknowledged ? { acknowledged: true } : { acknowledged: false, response: rebuilt };
44
269
  }
45
270
  catch {
46
- return false;
47
- }
48
- }
49
- function chunkBodyFits(target, requestId, encodedLength, chunkLength, maxBytes) {
50
- const total = Math.ceil(encodedLength / chunkLength);
51
- const body = {
52
- requestId,
53
- target,
54
- chunkIndex: Math.max(0, total - 1),
55
- totalChunks: total,
56
- sha256: "0".repeat(64),
57
- chunk: "x".repeat(Math.min(chunkLength, encodedLength)),
58
- };
59
- return jsonByteLength(body) <= maxBytes;
271
+ return { acknowledged: false, response: rebuilt };
272
+ }
60
273
  }
61
- function splitEncodedBody(target, requestId, encoded, maxBytes) {
62
- if (!chunkBodyFits(target, requestId, encoded.length, 1, maxBytes)) {
63
- throw new Error("PI_CLIENT_MAX_REQUEST_KB is too small for pi-server chunk envelopes");
274
+ async function inspectChunkResponse(response, requestId, chunkIndex, totalChunks) {
275
+ const marker = response.headers.get(PI_SERVER_CHUNK_ACK_HEADER);
276
+ if (marker === PI_SERVER_CHUNK_FINAL_VALUE) {
277
+ return { acknowledged: false, response };
64
278
  }
65
- let low = 1;
66
- let high = encoded.length;
67
- let best = 1;
68
- while (low <= high) {
69
- const mid = Math.floor((low + high) / 2);
70
- if (chunkBodyFits(target, requestId, encoded.length, mid, maxBytes)) {
71
- best = mid;
72
- low = mid + 1;
279
+ if (marker === PI_SERVER_CHUNK_ACK_VALUE) {
280
+ if (!response.headers.get("Content-Type")?.includes("application/json")) {
281
+ await discardResponse(response);
282
+ throw new PiServerChunkAckProtocolError("pi-server chunk acknowledgement must use application/json");
283
+ }
284
+ const text = await readPiServerResponseText(response, LEGACY_CHUNK_ACK_MAX_BYTES);
285
+ let body;
286
+ try {
287
+ body = JSON.parse(text);
288
+ }
289
+ catch {
290
+ throw new PiServerChunkAckProtocolError("pi-server chunk acknowledgement was not valid JSON");
73
291
  }
74
- else {
75
- high = mid - 1;
292
+ if (!isChunkAckBody(body) ||
293
+ body.received !== true ||
294
+ body.requestId !== requestId ||
295
+ body.chunkIndex !== chunkIndex ||
296
+ body.totalChunks !== totalChunks) {
297
+ throw new PiServerChunkAckProtocolError("pi-server chunk acknowledgement did not match its request envelope");
76
298
  }
299
+ return { acknowledged: true };
77
300
  }
78
- const chunks = [];
79
- for (let offset = 0; offset < encoded.length; offset += best) {
80
- chunks.push(encoded.slice(offset, offset + best));
301
+ if (marker !== null) {
302
+ await discardResponse(response);
303
+ throw new PiServerChunkAckProtocolError(`pi-server returned unsupported ${PI_SERVER_CHUNK_ACK_HEADER} value: ${marker}`);
304
+ }
305
+ if (!response.headers.get("Content-Type")?.includes("application/json")) {
306
+ return { acknowledged: false, response };
307
+ }
308
+ return inspectLegacyChunkAck(response, requestId, chunkIndex, totalChunks);
309
+ }
310
+ function getRawChunkLayout(target, requestId, rawBodyBytes, maxBytes) {
311
+ let totalChunks = 1;
312
+ while (true) {
313
+ const emptyEnvelopeBytes = jsonByteLength({
314
+ requestId,
315
+ target,
316
+ chunkIndex: totalChunks - 1,
317
+ totalChunks,
318
+ sha256: "0".repeat(64),
319
+ chunk: "",
320
+ encoding: PI_SERVER_CHUNK_SEGMENT_ENCODING,
321
+ rawTotalBytes: rawBodyBytes,
322
+ // Using the total body size here is conservative because every
323
+ // derived chunk size is no larger and therefore has no more digits.
324
+ rawChunkBytes: rawBodyBytes,
325
+ });
326
+ const encodedChunkBytes = Math.floor((maxBytes - emptyEnvelopeBytes) / 4) * 4;
327
+ const rawChunkBytes = Math.floor(encodedChunkBytes / 4) * 3;
328
+ if (rawChunkBytes <= 0) {
329
+ throw new Error("PI_CLIENT_MAX_REQUEST_KB is too small for pi-server chunk envelopes");
330
+ }
331
+ const nextTotalChunks = Math.ceil(rawBodyBytes / rawChunkBytes);
332
+ if (nextTotalChunks > CHUNK_MAX_TOTAL_CHUNKS) {
333
+ throw new Error(`pi-server chunk request exceeds the ${CHUNK_MAX_TOTAL_CHUNKS}-chunk protocol limit`);
334
+ }
335
+ if (nextTotalChunks === totalChunks) {
336
+ return { rawChunkBytes, totalChunks };
337
+ }
338
+ if (nextTotalChunks < totalChunks) {
339
+ throw new Error("pi-server chunk layout did not converge monotonically");
340
+ }
341
+ totalChunks = nextTotalChunks;
81
342
  }
82
- return chunks;
83
343
  }
84
344
  export class ChunkRequest {
85
345
  options;
346
+ #chunkRequestMemos = new Map();
347
+ #maxChunkRequestMemos;
86
348
  constructor(options) {
349
+ const maxChunkRequestMemos = options.maxChunkRequestMemos ?? DEFAULT_CHUNK_REQUEST_MEMO_LIMIT;
350
+ if (!Number.isSafeInteger(maxChunkRequestMemos) || maxChunkRequestMemos <= 0) {
351
+ throw new Error("maxChunkRequestMemos must be a positive safe integer");
352
+ }
87
353
  this.options = options;
354
+ this.#maxChunkRequestMemos = maxChunkRequestMemos;
88
355
  }
89
356
  async getJson(endpoint) {
90
- return fetch(`${this.options.serverUrl}${endpoint}`, {
357
+ return this.#fetchWithResponseHeaderTimeout(endpoint, {
91
358
  method: "GET",
92
359
  headers: makeHeaders(this.options.authToken),
93
- signal: this.options.signal,
360
+ });
361
+ }
362
+ async deleteJson(endpoint) {
363
+ return this.#fetchWithResponseHeaderTimeout(endpoint, {
364
+ method: "DELETE",
365
+ headers: makeHeaders(this.options.authToken),
94
366
  });
95
367
  }
96
368
  async postJson(endpoint, body) {
97
- const rawJson = JSON.stringify(body);
98
- const maxBytes = getMaxRequestBytes();
99
- if (Buffer.byteLength(rawJson, "utf-8") <= maxBytes) {
100
- return this.#postRawJson(endpoint, rawJson);
101
- }
102
- const requestId = randomUUID();
103
- const encoded = Buffer.from(rawJson, "utf-8").toString("base64");
104
- const chunks = splitEncodedBody(endpoint, requestId, encoded, maxBytes);
369
+ const rawBody = Buffer.from(JSON.stringify(body), "utf-8");
370
+ const memoKey = `${endpoint}\0${sha256(rawBody)}`;
371
+ let memo = this.#chunkRequestMemos.get(memoKey);
372
+ const maxBytes = memo?.maxBytes ?? getMaxRequestBytes();
373
+ if (memo === undefined && rawBody.byteLength <= maxBytes) {
374
+ return this.#postRawJson(endpoint, rawBody.toString("utf-8"));
375
+ }
376
+ if (memo === undefined) {
377
+ if (this.#chunkRequestMemos.size >= this.#maxChunkRequestMemos) {
378
+ throw new PiServerChunkRequestMemoLimitError(this.#maxChunkRequestMemos);
379
+ }
380
+ memo = { requestId: randomUUID(), maxBytes };
381
+ this.#chunkRequestMemos.set(memoKey, memo);
382
+ }
383
+ const { requestId } = memo;
384
+ let layout;
385
+ try {
386
+ layout = getRawChunkLayout(endpoint, requestId, rawBody.byteLength, memo.maxBytes);
387
+ }
388
+ catch (error) {
389
+ if (this.#chunkRequestMemos.get(memoKey) === memo) {
390
+ this.#chunkRequestMemos.delete(memoKey);
391
+ }
392
+ throw error;
393
+ }
105
394
  let nextIndex = 0;
106
395
  let finalResponse;
107
- let failureResponse;
396
+ let transientFailureResponse;
397
+ let fatalError;
398
+ const stopped = () => finalResponse !== undefined || fatalError !== undefined;
399
+ const replaceTransientFailureResponse = async (response) => {
400
+ const previous = transientFailureResponse;
401
+ transientFailureResponse = response;
402
+ if (previous !== response)
403
+ await discardResponse(previous);
404
+ };
405
+ const uploadChunk = async (index) => {
406
+ const start = index * layout.rawChunkBytes;
407
+ const chunk = rawBody
408
+ .subarray(start, Math.min(start + layout.rawChunkBytes, rawBody.byteLength))
409
+ .toString("base64");
410
+ let lastError;
411
+ let lastTransientResponse;
412
+ try {
413
+ for (let attempt = 0; attempt <= CHUNK_UPLOAD_RETRY_DELAYS_MS.length; attempt++) {
414
+ if (stopped())
415
+ return;
416
+ if (attempt > 0) {
417
+ await waitForChunkRetry(CHUNK_UPLOAD_RETRY_DELAYS_MS[attempt - 1], this.options.signal);
418
+ if (stopped())
419
+ return;
420
+ }
421
+ const chunkBody = {
422
+ requestId,
423
+ target: endpoint,
424
+ chunkIndex: index,
425
+ totalChunks: layout.totalChunks,
426
+ sha256: sha256(chunk),
427
+ chunk,
428
+ encoding: PI_SERVER_CHUNK_SEGMENT_ENCODING,
429
+ rawTotalBytes: rawBody.byteLength,
430
+ rawChunkBytes: layout.rawChunkBytes,
431
+ };
432
+ try {
433
+ let response = await this.#postRawJson(CHUNK_ENDPOINT, JSON.stringify(chunkBody));
434
+ if (stopped()) {
435
+ await discardResponse(response);
436
+ return;
437
+ }
438
+ if (response.ok) {
439
+ const inspection = await inspectChunkResponse(response, requestId, index, layout.totalChunks);
440
+ if (inspection.acknowledged)
441
+ return;
442
+ if (!inspection.response) {
443
+ throw new PiServerChunkAckProtocolError("pi-server chunk response inspection lost its response");
444
+ }
445
+ response = inspection.response;
446
+ if (stopped()) {
447
+ await discardResponse(response);
448
+ return;
449
+ }
450
+ }
451
+ if (isTransientChunkResponse(response.status)) {
452
+ await discardResponse(lastTransientResponse);
453
+ lastTransientResponse = response;
454
+ lastError = new Error(`pi-server chunk ${index + 1}/${layout.totalChunks} returned HTTP ${response.status}`);
455
+ continue;
456
+ }
457
+ if (finalResponse === undefined && fatalError === undefined) {
458
+ finalResponse = response;
459
+ }
460
+ else {
461
+ await discardResponse(response);
462
+ }
463
+ return;
464
+ }
465
+ catch (error) {
466
+ if (this.options.signal?.aborted) {
467
+ throw this.options.signal.reason ?? error;
468
+ }
469
+ if (error instanceof PiServerChunkAckProtocolError ||
470
+ error instanceof PiServerTransportBodyLimitError) {
471
+ fatalError = error;
472
+ throw error;
473
+ }
474
+ lastError = error;
475
+ }
476
+ }
477
+ if (lastTransientResponse !== undefined) {
478
+ await replaceTransientFailureResponse(lastTransientResponse);
479
+ lastTransientResponse = undefined;
480
+ }
481
+ if (lastError instanceof Error) {
482
+ throw lastError;
483
+ }
484
+ throw new Error(`pi-server chunk ${index + 1}/${layout.totalChunks} failed after bounded retries`);
485
+ }
486
+ finally {
487
+ if (stopped())
488
+ await discardResponse(lastTransientResponse);
489
+ }
490
+ };
108
491
  const uploadWorker = async () => {
109
- while (nextIndex < chunks.length && finalResponse === undefined && failureResponse === undefined) {
492
+ while (!stopped()) {
110
493
  const index = nextIndex;
111
- nextIndex++;
112
- const chunk = chunks[index];
113
- const chunkBody = {
114
- requestId,
115
- target: endpoint,
116
- chunkIndex: index,
117
- totalChunks: chunks.length,
118
- sha256: sha256(chunk),
119
- chunk,
120
- };
121
- const response = await this.#postRawJson(CHUNK_ENDPOINT, JSON.stringify(chunkBody));
122
- if (finalResponse !== undefined || failureResponse !== undefined)
494
+ if (index >= layout.totalChunks)
123
495
  return;
124
- if (!response.ok) {
125
- failureResponse = response;
126
- return;
127
- }
128
- if (!(await isChunkAck(response, requestId, index, chunks.length))) {
129
- finalResponse = response;
130
- return;
131
- }
496
+ nextIndex++;
497
+ await uploadChunk(index);
132
498
  }
133
499
  };
134
- await Promise.all(Array.from({ length: Math.min(CHUNK_UPLOAD_CONCURRENCY, chunks.length) }, () => uploadWorker()));
135
- if (failureResponse)
136
- return failureResponse;
137
- if (finalResponse)
500
+ const results = await Promise.allSettled(Array.from({ length: Math.min(CHUNK_UPLOAD_CONCURRENCY, layout.totalChunks) }, () => uploadWorker()));
501
+ if (fatalError !== undefined) {
502
+ await discardResponse(finalResponse);
503
+ await discardResponse(transientFailureResponse);
504
+ throw fatalError;
505
+ }
506
+ if (this.options.signal?.aborted) {
507
+ await discardResponse(finalResponse);
508
+ await discardResponse(transientFailureResponse);
509
+ throw this.options.signal.reason ?? new DOMException("The operation was aborted", "AbortError");
510
+ }
511
+ if (finalResponse !== undefined) {
512
+ await discardResponse(transientFailureResponse);
513
+ if (this.#chunkRequestMemos.get(memoKey) === memo) {
514
+ this.#chunkRequestMemos.delete(memoKey);
515
+ }
138
516
  return finalResponse;
517
+ }
518
+ if (transientFailureResponse !== undefined) {
519
+ return transientFailureResponse;
520
+ }
521
+ for (const result of results) {
522
+ if (result.status === "rejected") {
523
+ throw result.reason;
524
+ }
525
+ }
139
526
  throw new Error("No final chunk response received from pi-server");
140
527
  }
141
528
  async #postRawJson(endpoint, rawJson) {
142
- return fetch(`${this.options.serverUrl}${endpoint}`, {
529
+ return this.#fetchWithResponseHeaderTimeout(endpoint, {
143
530
  method: "POST",
144
531
  headers: makeHeaders(this.options.authToken),
145
532
  body: rawJson,
146
- signal: this.options.signal,
147
533
  });
148
534
  }
535
+ async #fetchWithResponseHeaderTimeout(endpoint, init) {
536
+ const timeoutMs = this.options.responseHeaderTimeoutMs ?? PI_SERVER_RESPONSE_HEADER_TIMEOUT_MS;
537
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
538
+ throw new Error("responseHeaderTimeoutMs must be a positive finite number");
539
+ }
540
+ const method = init.method ?? "GET";
541
+ const timeoutError = new PiServerTransportHeaderTimeoutError(method, endpoint, timeoutMs);
542
+ const timeoutController = new AbortController();
543
+ const signal = this.options.signal
544
+ ? AbortSignal.any([this.options.signal, timeoutController.signal])
545
+ : timeoutController.signal;
546
+ const timeout = setTimeout(() => {
547
+ timeoutController.abort(timeoutError);
548
+ }, timeoutMs);
549
+ timeout.unref();
550
+ try {
551
+ return await fetch(`${this.options.serverUrl}${endpoint}`, {
552
+ ...init,
553
+ signal,
554
+ });
555
+ }
556
+ catch (error) {
557
+ if (timeoutController.signal.aborted && !this.options.signal?.aborted) {
558
+ throw timeoutError;
559
+ }
560
+ throw error;
561
+ }
562
+ finally {
563
+ clearTimeout(timeout);
564
+ }
565
+ }
149
566
  }
150
567
  //# sourceMappingURL=pi-server-request.js.map