@averyyy/pi-coding-agent 0.82.1-piclient.4 → 0.82.1-piclient.6

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 (33) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/core/agent-session.d.ts +0 -1
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +4 -27
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/model-runtime.d.ts +0 -7
  7. package/dist/core/model-runtime.d.ts.map +1 -1
  8. package/dist/core/model-runtime.js +0 -66
  9. package/dist/core/model-runtime.js.map +1 -1
  10. package/dist/core/pi-server-client.d.ts +1 -3
  11. package/dist/core/pi-server-client.d.ts.map +1 -1
  12. package/dist/core/pi-server-client.js +301 -55
  13. package/dist/core/pi-server-client.js.map +1 -1
  14. package/dist/core/pi-server-compact-state.d.ts +1 -1
  15. package/dist/core/pi-server-compact-state.d.ts.map +1 -1
  16. package/dist/core/pi-server-compact-state.js +2 -1
  17. package/dist/core/pi-server-compact-state.js.map +1 -1
  18. package/dist/core/pi-server-run-state.d.ts +2 -1
  19. package/dist/core/pi-server-run-state.d.ts.map +1 -1
  20. package/dist/core/pi-server-run-state.js +5 -3
  21. package/dist/core/pi-server-run-state.js.map +1 -1
  22. package/dist/core/sdk.d.ts.map +1 -1
  23. package/dist/core/sdk.js +0 -5
  24. package/dist/core/sdk.js.map +1 -1
  25. package/package.json +4 -4
  26. package/dist/core/pi-server-provider-execution.d.ts +0 -19
  27. package/dist/core/pi-server-provider-execution.d.ts.map +0 -1
  28. package/dist/core/pi-server-provider-execution.js +0 -43
  29. package/dist/core/pi-server-provider-execution.js.map +0 -1
  30. package/dist/core/provider-execution-contract.d.ts +0 -36
  31. package/dist/core/provider-execution-contract.d.ts.map +0 -1
  32. package/dist/core/provider-execution-contract.js +0 -30
  33. package/dist/core/provider-execution-contract.js.map +0 -1
@@ -50,7 +50,7 @@ const PI_SERVER_RESPONSE_NO_PROGRESS_TIMEOUT_MS = 90_000;
50
50
  const PI_SERVER_ERROR_BODY_MAX_BYTES = 64 * 1024;
51
51
  const PI_SERVER_EVENT_STREAM_MAX_LINE_BYTES = 256 * 1024 * 1024;
52
52
  const TRANSIENT_PI_SERVER_RETRY_DELAYS_MS = [250, 750, 1500];
53
- const TRANSIENT_PI_SERVER_STATUS_CODES = new Set([408, 425, 429, 502, 503, 504, 530]);
53
+ const TRANSIENT_PI_SERVER_STATUS_CODES = new Set([408, 425, 429, 502, 503, 504, 520, 521, 522, 523, 524, 525, 530]);
54
54
  export function hashStaticContext(ctx) {
55
55
  return hashPiServerStaticContext(ctx);
56
56
  }
@@ -186,11 +186,6 @@ function isObject(value) {
186
186
  function isNonNegativeInteger(value) {
187
187
  return typeof value === "number" && Number.isInteger(value) && value >= 0;
188
188
  }
189
- function validateProviderExecutionFingerprint(value) {
190
- if (value !== undefined && !/^[a-f0-9]{64}$/u.test(value)) {
191
- throw new Error("pi-server provider execution fingerprint must be a lowercase SHA-256 digest");
192
- }
193
- }
194
189
  export function getPiServerRunDiagnostic(message) {
195
190
  const diagnostics = message.diagnostics?.filter((diagnostic) => diagnostic.type === "pi_server_run") ?? [];
196
191
  if (diagnostics.length === 0)
@@ -951,7 +946,7 @@ function isRecoverableTreeDivergenceError(error) {
951
946
  function isRecoverableMissingServerState(response, errorBody) {
952
947
  if (response.status !== 400 && response.status !== 404)
953
948
  return false;
954
- return /Session has no static context|session not found|parent entry .* does not exist|leafId .* does not exist|entry .* already exists/i.test(errorBody);
949
+ return /Session has no static context|session not found|(?:stream )?run not found|parent entry .* does not exist|leafId .* does not exist|entry .* already exists/i.test(errorBody);
955
950
  }
956
951
  async function syncFullPiServerTree(sessionId, tree, request) {
957
952
  await postTreeJson(request, "/api/session/tree/sync", {
@@ -1050,7 +1045,6 @@ export function hashPiServerCompactRequest(body) {
1050
1045
  const serialized = canonicalJsonStringify({
1051
1046
  protocolVersion: body.protocolVersion,
1052
1047
  sessionId: body.sessionId,
1053
- providerExecutionFingerprint: body.providerExecutionFingerprint,
1054
1048
  model: body.model,
1055
1049
  options: body.options,
1056
1050
  settings: body.settings,
@@ -1368,7 +1362,6 @@ async function persistAndAcknowledgePiServerCompactTerminal(payload, compactStat
1368
1362
  await acknowledgeObservedPiServerCompactTerminal(pending, compactStatePath, signal);
1369
1363
  }
1370
1364
  export async function compactPiServer(model, context, options) {
1371
- validateProviderExecutionFingerprint(options?.providerExecutionFingerprint);
1372
1365
  const sessionId = options?.sessionId ?? "default";
1373
1366
  const signal = options?.signal;
1374
1367
  const recoveryWindowMs = getPiServerCompactRecoveryWindowMs(options?.piServerRecoveryWindowMs);
@@ -1381,6 +1374,7 @@ export async function compactPiServer(model, context, options) {
1381
1374
  const recovered = await recoverPendingPiServerCompaction(sessionId, tree, compactStatePath, {
1382
1375
  signal,
1383
1376
  recoveryWindowMs,
1377
+ onHistoryReconciled: options?.onHistoryReconciled,
1384
1378
  });
1385
1379
  if (recovered)
1386
1380
  return recovered;
@@ -1416,7 +1410,6 @@ export async function compactPiServer(model, context, options) {
1416
1410
  protocolVersion: PI_SERVER_PROTOCOL_VERSION,
1417
1411
  sessionId,
1418
1412
  operationId,
1419
- providerExecutionFingerprint: options?.providerExecutionFingerprint,
1420
1413
  model,
1421
1414
  options: serializeOptions(options),
1422
1415
  settings: options?.settings,
@@ -1504,6 +1497,14 @@ export async function compactPiServer(model, context, options) {
1504
1497
  }
1505
1498
  return await persistAndCompletePiServerCompactTerminal(terminal, compactStatePath, signal);
1506
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
+ }
1507
1508
  if (isRecoverablePiServerCompactResponse(response)) {
1508
1509
  lastInterruption = new Error(`Server compaction failed (${failure.details})`);
1509
1510
  recoveryDeadline ??= Date.now() + recoveryWindowMs;
@@ -1580,25 +1581,20 @@ export async function compactPiServer(model, context, options) {
1580
1581
  signal?.removeEventListener("abort", onAbort);
1581
1582
  }
1582
1583
  }
1583
- function assertPendingPiServerCompactMatches(pending, sessionId, tree) {
1584
- if (pending.serverHash !== hashPiServerIdentity(getServerUrl()) ||
1585
- pending.sessionId !== sessionId ||
1586
- pending.baseEntryCount > tree.entries.length) {
1587
- throw new Error(`Cannot safely resume pending pi-server compaction ${pending.operationId}: server, session, or tree identity changed`);
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`);
1588
1587
  }
1588
+ if (pending.baseEntryCount > tree.entries.length)
1589
+ return undefined;
1589
1590
  const baseEntries = tree.entries.slice(0, pending.baseEntryCount);
1590
1591
  if (hashEntries(baseEntries) !== pending.baseTreeHash ||
1591
1592
  (pending.baseLeafId !== null && !baseEntries.some((entry) => entry.id === pending.baseLeafId))) {
1592
- throw new Error(`Cannot safely resume pending pi-server compaction ${pending.operationId}: local base tree no longer matches its durable marker`);
1593
- }
1594
- if (!pending.observation || pending.observation.kind === "terminal") {
1595
- if (tree.entries.length !== pending.baseEntryCount || tree.leafId !== pending.baseLeafId) {
1596
- throw new Error(`Cannot safely resume pending pi-server compaction ${pending.operationId}: local tree changed before a committed compaction was observed`);
1597
- }
1593
+ return undefined;
1598
1594
  }
1599
1595
  return { entries: baseEntries, leafId: pending.baseLeafId };
1600
1596
  }
1601
- function recoverLocallyAppliedPiServerCompaction(pending, tree, baseTree, compactStatePath) {
1597
+ function tryRecoverLocallyAppliedPiServerCompaction(pending, tree, baseTree, compactStatePath) {
1602
1598
  const applied = pending.observation;
1603
1599
  if (!applied || applied.kind !== "applied") {
1604
1600
  throw new Error("Pending pi-server compaction does not contain a durable local apply observation");
@@ -1612,7 +1608,7 @@ function recoverLocallyAppliedPiServerCompaction(pending, tree, baseTree, compac
1612
1608
  applied.updatedLeafId !== applied.entryId ||
1613
1609
  hashEntries(tree.entries) !== applied.updatedTreeHash ||
1614
1610
  applied.updatedRevision !== pending.baseRevision + 1) {
1615
- throw new Error(`Cannot safely resume pending pi-server compaction ${pending.operationId}: local applied tree does not match its durable observation`);
1611
+ return undefined;
1616
1612
  }
1617
1613
  return normalizePiServerCompactionResult({
1618
1614
  protocolVersion: PI_SERVER_PROTOCOL_VERSION,
@@ -1643,10 +1639,22 @@ export async function recoverPendingPiServerCompaction(sessionId, tree, compactS
1643
1639
  const pending = readPiServerPendingCompact(compactStatePath);
1644
1640
  if (!pending)
1645
1641
  return undefined;
1646
- const baseTree = assertPendingPiServerCompactMatches(pending, sessionId, tree);
1647
- const locallyApplied = pending.observation?.kind === "applied"
1648
- ? recoverLocallyAppliedPiServerCompaction(pending, tree, baseTree, compactStatePath)
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)
1649
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);
1650
1658
  if (pending.observation?.kind === "terminal" && pending.observation.operationDisposition === "not_started") {
1651
1659
  return await persistAndCompletePiServerCompactTerminal({
1652
1660
  protocolVersion: PI_SERVER_PROTOCOL_VERSION,
@@ -1704,8 +1712,15 @@ export async function recoverPendingPiServerCompaction(sessionId, tree, compactS
1704
1712
  await acknowledgeObservedPiServerCompactTerminal(pending, compactStatePath, options?.signal);
1705
1713
  throw new PiServerCompactOperationError(`Pending server compaction failed (${pending.observation.error})`, pending.observation);
1706
1714
  }
1707
- throw new Error(`Pending pi-server compaction ${pending.operationId} is missing from the durable server journal; ` +
1708
- "the local marker was retained and automatic provider resubmission is disabled");
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;
1709
1724
  }
1710
1725
  if (!isRecoverablePiServerCompactResponse(response)) {
1711
1726
  throw new Error(`Pending server compaction recovery failed (${failure.details})`);
@@ -1716,6 +1731,20 @@ export async function recoverPendingPiServerCompaction(sessionId, tree, compactS
1716
1731
  }
1717
1732
  try {
1718
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
+ }
1719
1748
  const result = normalizePiServerCompactionResult(responseResult, sessionId, pending.operationId, pending.requestHash, baseTree, {
1720
1749
  staticContextHash: pending.baseStaticContextHash,
1721
1750
  treeHash: pending.baseTreeHash,
@@ -1839,34 +1868,30 @@ function findPendingPiServerRunTerminal(entries, pending) {
1839
1868
  if (entry.type !== "message" || entry.message.role !== "assistant")
1840
1869
  continue;
1841
1870
  const message = entry.message;
1842
- const diagnostic = getPiServerRunDiagnostic(message);
1871
+ let diagnostic;
1872
+ try {
1873
+ diagnostic = getPiServerRunDiagnostic(message);
1874
+ }
1875
+ catch {
1876
+ continue;
1877
+ }
1843
1878
  if (!diagnostic || diagnostic.runId !== pending.runId)
1844
1879
  continue;
1845
1880
  if (diagnostic.sessionId !== pending.sessionId || diagnostic.requestMac !== pending.requestHash) {
1846
- throw new Error("Cannot recover pi-server run: persisted terminal diagnostic did not match pending state");
1881
+ continue;
1847
1882
  }
1848
1883
  return message;
1849
1884
  }
1850
1885
  return undefined;
1851
1886
  }
1852
- function assertPendingPiServerRunMatches(pending, serverHash, sessionId, tree) {
1853
- if (pending.serverHash !== serverHash ||
1887
+ function pendingPiServerRunBaseMatches(pending, serverHash, sessionId, tree) {
1888
+ return !(pending.serverHash !== serverHash ||
1854
1889
  pending.sessionId !== sessionId ||
1855
- pending.baseEntryCount > tree.entries.length ||
1890
+ pending.baseEntryCount !== tree.entries.length ||
1856
1891
  hashLocalTreePrefix(sessionId, tree.entries, pending.baseEntryCount) !== pending.baseTreeHash ||
1857
- pending.baseLeafId !== tree.leafId) {
1858
- throw new Error(`Cannot safely resume pending pi-server run ${pending.runId}: server, tree, or provider request identity changed`);
1859
- }
1860
- }
1861
- function missingDurablePiServerRunError(runId, cause) {
1862
- const prefix = cause ? `${cause}; ` : "";
1863
- return new Error(`${prefix}pi-server no longer has durable journal for pending run ${runId}. ` +
1864
- "The provider outcome is unknown and the pending marker was retained. Restore the server journal, " +
1865
- "or after independently verifying that the provider did not execute, start an explicit new session or fork; " +
1866
- "automatic provider resubmission is disabled.");
1892
+ pending.baseLeafId !== tree.leafId);
1867
1893
  }
1868
1894
  export async function streamPiServer(model, context, options) {
1869
- validateProviderExecutionFingerprint(options?.providerExecutionFingerprint);
1870
1895
  const runMode = options?.runMode;
1871
1896
  if (runMode === "auxiliary-transient" && options?.piServerRunStatePath) {
1872
1897
  throw new Error("Auxiliary pi-server streams cannot use the main durable run-state path");
@@ -2143,12 +2168,11 @@ export async function streamPiServer(model, context, options) {
2143
2168
  };
2144
2169
  try {
2145
2170
  const request = createPiServerRequest(options?.signal);
2146
- await ensureSessionInit(sessionId, context, request);
2147
2171
  const tree = options?.sessionTree ?? {
2148
2172
  ...getLinearTreeFromMessages(context.messages),
2149
2173
  replace: true,
2150
2174
  };
2151
- const syncTree = tree;
2175
+ let syncTree = tree;
2152
2176
  if (runStatePath) {
2153
2177
  attachRunStateLease();
2154
2178
  }
@@ -2157,6 +2181,9 @@ export async function streamPiServer(model, context, options) {
2157
2181
  : sessionPendingRuns.get(sessionId);
2158
2182
  unresolvedRunPending = pending !== undefined;
2159
2183
  let treeSynced = false;
2184
+ if (pending && pending.serverHash !== hashPiServerIdentity(getServerUrl())) {
2185
+ resetSessionTracking(sessionId);
2186
+ }
2160
2187
  if (pending) {
2161
2188
  const persistedTerminal = findPendingPiServerRunTerminal(syncTree.entries, pending);
2162
2189
  if (persistedTerminal) {
@@ -2171,12 +2198,14 @@ export async function streamPiServer(model, context, options) {
2171
2198
  unresolvedRunPending = false;
2172
2199
  }
2173
2200
  }
2201
+ if (!pending) {
2202
+ await ensureSessionInit(sessionId, context, request);
2203
+ }
2174
2204
  const buildRequestIdentity = (baseTreeHash, baseEntryCount, baseLeafId, baseStaticContextHash, baseRevision) => JSON.parse(JSON.stringify({
2175
2205
  sessionId,
2176
2206
  runMode,
2177
2207
  model,
2178
2208
  options: serializeOptions(options),
2179
- providerExecutionFingerprint: options?.providerExecutionFingerprint,
2180
2209
  ephemeralMessages: options?.ephemeralMessages,
2181
2210
  contextOverlay: options?.contextOverlay,
2182
2211
  baseTreeHash,
@@ -2195,14 +2224,95 @@ export async function streamPiServer(model, context, options) {
2195
2224
  let resumedPendingRun = false;
2196
2225
  let recoveredPendingRun;
2197
2226
  if (pending) {
2198
- assertPendingPiServerRunMatches(pending, hashPiServerIdentity(getServerUrl()), sessionId, syncTree);
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) {
2199
2240
  runId = pending.runId;
2200
2241
  phase = "provider_stream";
2201
2242
  recoveredPendingRun = await recoverPiServerRun(sessionId, runId, request, options?.signal);
2202
2243
  if (recoveredPendingRun.status === "not_found") {
2203
- throw missingDurablePiServerRunError(pending.runId);
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";
2204
2265
  }
2205
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
+ }
2206
2316
  resumedPendingRun = true;
2207
2317
  runSubmissionStarted = true;
2208
2318
  requestHash = pending.requestHash;
@@ -2241,6 +2351,7 @@ export async function streamPiServer(model, context, options) {
2241
2351
  };
2242
2352
  if (!runStatePath)
2243
2353
  sessionPendingRuns.set(sessionId, newPending);
2354
+ pending = newPending;
2244
2355
  unresolvedRunPending = true;
2245
2356
  }
2246
2357
  const makeBody = (eventCursor) => ({
@@ -2372,13 +2483,147 @@ export async function streamPiServer(model, context, options) {
2372
2483
  recoveredRun.status === "completed" ||
2373
2484
  recoveredRun.status === "failed";
2374
2485
  if (hasDurableRun && recoveredRun.requestMac !== requestHash) {
2375
- await settleStreamFailure(new Error("pi-server recovered run request identity did not match the pending provider request"), false);
2376
- return;
2486
+ phase = "history_reconcile";
2487
+ const snapshot = await fetchPiServerHistory(sessionId, request);
2488
+ if (snapshot) {
2489
+ syncTree = snapshot;
2490
+ await applyPiServerHistory(sessionId, snapshot, options?.onHistoryReconciled);
2491
+ treeSynced = true;
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";
2377
2523
  }
2378
2524
  if (recoveredRun.status === "not_found") {
2379
- const failureMessage = interruption instanceof Error ? interruption.message : String(interruption);
2380
- await settleStreamFailure(missingDurablePiServerRunError(runId, failureMessage), false);
2381
- return;
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;
2621
+ }
2622
+ interruption = error;
2623
+ activeResponse = undefined;
2624
+ recoveryDeadline = Date.now() + recoveryWindowMs;
2625
+ }
2626
+ continue;
2382
2627
  }
2383
2628
  if (recoveredRun.status === "unavailable") {
2384
2629
  const failureMessage = interruption instanceof Error ? interruption.message : String(interruption);
@@ -2406,7 +2651,8 @@ export async function streamPiServer(model, context, options) {
2406
2651
  const reconnectResponse = await getRunEvents(eventCursor);
2407
2652
  if (!reconnectResponse.ok) {
2408
2653
  const failure = await readPiServerFailure(reconnectResponse);
2409
- if (!isTransientPiServerResponse(reconnectResponse, failure.bodyText)) {
2654
+ if (!isTransientPiServerResponse(reconnectResponse, failure.bodyText) &&
2655
+ !isRecoverableMissingServerState(reconnectResponse, failure.matchText)) {
2410
2656
  await settleStreamFailure(new Error(`pi-server reconnect failed: ${failure.details}`), false);
2411
2657
  return;
2412
2658
  }