@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
@@ -12,6 +12,7 @@
12
12
  *
13
13
  * Modes use this class and add their own I/O layer on top.
14
14
  */
15
+ import { randomUUID } from "node:crypto";
15
16
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
17
  import { basename, dirname } from "node:path";
17
18
  import { contentText } from "@earendil-works/pi-ai";
@@ -29,11 +30,14 @@ import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
29
30
  import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
30
31
  import { emitSessionShutdownEvent } from "./extensions/runner.js";
31
32
  import { ModelRegistry } from "./model-registry.js";
32
- import { compactPiServer, syncPiServerTree, } from "./pi-server-client.js";
33
+ import { acknowledgePiServerCompaction, acknowledgePiServerRunMessage, acquirePiServerSessionLease, compactPiServer, getPiServerRunDiagnostic, recordPiServerCompactionApplied, recoverPendingPiServerCompaction, releasePiServerRunLease, releasePiServerSessionLease, syncPiServerTree, } from "./pi-server-client.js";
34
+ import { getPiServerCompactStatePath, readPiServerPendingCompact } from "./pi-server-compact-state.js";
35
+ import { getPiServerRunStatePath, readPiServerPendingRun } from "./pi-server-run-state.js";
33
36
  import { expandPromptTemplate } from "./prompt-templates.js";
34
37
  import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
35
38
  import { createSyntheticSourceInfo } from "./source-info.js";
36
39
  import { buildSystemPrompt } from "./system-prompt.js";
40
+ import { beginSessionToolEffect, beginSessionToolEffectFinalization, commitSessionToolEffect, recoverToolEffects, writeSessionToolEffectFinalResult, writeSessionToolEffectResult, } from "./tool-effect-journal.js";
37
41
  import { createLocalBashOperations } from "./tools/bash.js";
38
42
  import { createAllToolDefinitions } from "./tools/index.js";
39
43
  import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
@@ -93,16 +97,26 @@ function getStringField(input, key) {
93
97
  const value = input[key];
94
98
  return typeof value === "string" && value.trim().length > 0 ? value : undefined;
95
99
  }
96
- function getPiServerFailurePhase(message) {
100
+ function getPiServerFailureMetadata(message) {
97
101
  for (const diagnostic of message.diagnostics ?? []) {
98
102
  if (diagnostic.type !== "pi_server_failure")
99
103
  continue;
100
104
  const phase = diagnostic.details?.phase;
101
- if (typeof phase === "string")
102
- return phase;
105
+ const retryable = diagnostic.details?.retryable;
106
+ return {
107
+ phase: typeof phase === "string" ? phase : undefined,
108
+ retryable: typeof retryable === "boolean" ? retryable : undefined,
109
+ };
103
110
  }
104
111
  return undefined;
105
112
  }
113
+ function isPiServerControlPlaneFailure(message) {
114
+ const hasFailure = message.diagnostics?.some((diagnostic) => diagnostic.type === "pi_server_failure") === true;
115
+ return hasFailure && getPiServerRunDiagnostic(message) === undefined;
116
+ }
117
+ function isPiServerRestartUncertainty(message) {
118
+ return getPiServerRunDiagnostic(message)?.restartUnknown === true;
119
+ }
106
120
  function toolTextContent(content) {
107
121
  return content
108
122
  .filter((part) => part.type === "text")
@@ -165,6 +179,7 @@ export class AgentSession {
165
179
  _isAgentRunActive = false;
166
180
  _idleWaitPromise;
167
181
  _resolveIdleWait;
182
+ _toolResultsInFinalization = new WeakSet();
168
183
  /** Tracks pending steering messages for UI display. Removed when delivered. */
169
184
  _steeringMessages = [];
170
185
  /** Tracks pending follow-up messages for UI display. Removed when delivered. */
@@ -176,6 +191,7 @@ export class AgentSession {
176
191
  _autoCompactionAbortController = undefined;
177
192
  _overflowRecoveryAttempted = false;
178
193
  _lastServerCompactionTimestamp = undefined;
194
+ _piServerSessionLeaseOwner;
179
195
  // Branch summarization state
180
196
  _branchSummaryAbortController = undefined;
181
197
  // Retry state
@@ -208,6 +224,7 @@ export class AgentSession {
208
224
  _extensionErrorUnsubscriber;
209
225
  _autoSessionNameEnabled;
210
226
  _autoSessionNameAttempted = false;
227
+ _auxiliaryStreamFunction;
211
228
  _modelRuntime;
212
229
  // Tool registry for extension getTools/setTools
213
230
  _toolRegistry = new Map();
@@ -227,6 +244,7 @@ export class AgentSession {
227
244
  this._customTools = config.customTools ?? [];
228
245
  this._cwd = config.cwd;
229
246
  this._modelRuntime = config.modelRuntime;
247
+ this._auxiliaryStreamFunction = config.auxiliaryStreamFunction;
230
248
  this._extensionRunnerRef = config.extensionRunnerRef;
231
249
  this._initialActiveToolNames = config.initialActiveToolNames;
232
250
  this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
@@ -234,6 +252,10 @@ export class AgentSession {
234
252
  this._baseToolsOverride = config.baseToolsOverride;
235
253
  this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" };
236
254
  this._autoSessionNameEnabled = config.autoSessionName ?? true;
255
+ const toolEffectRecovery = recoverToolEffects(this.sessionManager);
256
+ if (toolEffectRecovery.recoveredToolCallIds.length > 0) {
257
+ this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
258
+ }
237
259
  if (isPiServerMode()) {
238
260
  this._detachPiServerTerminalAssistantFailure();
239
261
  }
@@ -250,6 +272,9 @@ export class AgentSession {
250
272
  get modelRuntime() {
251
273
  return this._modelRuntime;
252
274
  }
275
+ _getAuxiliaryStreamFunction() {
276
+ return this._auxiliaryStreamFunction ?? this.agent.streamFunction;
277
+ }
253
278
  reconcilePiServerHistory(snapshot) {
254
279
  this.sessionManager.replaceTree(snapshot.entries, snapshot.leafId);
255
280
  const sessionContext = this.sessionManager.buildSessionContext();
@@ -260,6 +285,102 @@ export class AgentSession {
260
285
  messageCount: snapshot.messages.length,
261
286
  });
262
287
  }
288
+ async _syncAndAcknowledgePiServerAssistantMessage(message, signal) {
289
+ const persistenceSignal = signal.aborted ? undefined : signal;
290
+ const sessionFile = this.sessionManager.getSessionFile();
291
+ const runStatePath = sessionFile ? getPiServerRunStatePath(sessionFile) : undefined;
292
+ try {
293
+ await syncPiServerTree(this.sessionId, {
294
+ systemPrompt: this.systemPrompt,
295
+ messages: this.agent.state.messages,
296
+ tools: this.agent.state.tools,
297
+ }, { entries: this.sessionManager.getEntries(), leafId: this.sessionManager.getLeafId() }, {
298
+ signal: persistenceSignal,
299
+ onHistoryReconciled: (snapshot) => this.reconcilePiServerHistory(snapshot),
300
+ });
301
+ this.sessionManager.flushSessionFile();
302
+ await acknowledgePiServerRunMessage(this.sessionId, message, runStatePath, persistenceSignal);
303
+ }
304
+ catch (error) {
305
+ releasePiServerRunLease(this.sessionId, runStatePath);
306
+ throw error;
307
+ }
308
+ }
309
+ _acquirePiServerOperationLease(reentrantOwner) {
310
+ if (!isPiServerMode())
311
+ return { path: undefined, acquired: false, owner: undefined };
312
+ const sessionFile = this.sessionManager.getSessionFile();
313
+ const path = sessionFile ? getPiServerRunStatePath(sessionFile) : undefined;
314
+ if (this._piServerSessionLeaseOwner) {
315
+ if (reentrantOwner !== this._piServerSessionLeaseOwner) {
316
+ throw new Error(`Cannot start a concurrent durable pi-server operation for session ${this.sessionId}`);
317
+ }
318
+ return { path, acquired: false, owner: this._piServerSessionLeaseOwner };
319
+ }
320
+ const owner = Symbol("pi-server-session-lease");
321
+ if (path)
322
+ acquirePiServerSessionLease(this.sessionId, path);
323
+ this._piServerSessionLeaseOwner = owner;
324
+ return { path, acquired: true, owner };
325
+ }
326
+ _releasePiServerOperationLease(path, acquired, owner) {
327
+ if (!acquired)
328
+ return;
329
+ if (!owner || this._piServerSessionLeaseOwner !== owner) {
330
+ throw new Error(`Cannot release a durable pi-server operation lease owned by another operation`);
331
+ }
332
+ if (path)
333
+ releasePiServerSessionLease(this.sessionId, path);
334
+ this._piServerSessionLeaseOwner = undefined;
335
+ }
336
+ async _recoverPendingPiServerRunBeforeControlOperation(signal) {
337
+ if (!isPiServerMode())
338
+ return;
339
+ const sessionFile = this.sessionManager.getSessionFile();
340
+ if (!sessionFile) {
341
+ throw new Error("Durable pi-server operations require a persisted session file");
342
+ }
343
+ const runStatePath = getPiServerRunStatePath(sessionFile);
344
+ const pending = readPiServerPendingRun(runStatePath);
345
+ if (!pending)
346
+ return;
347
+ let terminal;
348
+ const entries = this.sessionManager.getEntries();
349
+ for (let index = entries.length - 1; index >= 0; index--) {
350
+ const entry = entries[index];
351
+ if (entry.type !== "message" || entry.message.role !== "assistant")
352
+ continue;
353
+ const message = entry.message;
354
+ const diagnostic = getPiServerRunDiagnostic(message);
355
+ if (!diagnostic || diagnostic.runId !== pending.runId)
356
+ continue;
357
+ if (diagnostic.sessionId !== pending.sessionId ||
358
+ diagnostic.requestMac !== pending.requestHash ||
359
+ pending.sessionId !== this.sessionId) {
360
+ throw new Error("Pending pi-server run terminal does not match its durable client marker");
361
+ }
362
+ terminal = message;
363
+ break;
364
+ }
365
+ if (!terminal) {
366
+ throw new Error(`Cannot start compaction while pi-server run ${pending.runId} is pending without a durable terminal assistant`);
367
+ }
368
+ await this._syncAndAcknowledgePiServerAssistantMessage(terminal, signal ?? new AbortController().signal);
369
+ }
370
+ async _recoverPendingPiServerCompaction(reason, willRetry, signal) {
371
+ if (!isPiServerMode())
372
+ return undefined;
373
+ const sessionFile = this.sessionManager.getSessionFile();
374
+ if (!sessionFile)
375
+ return undefined;
376
+ const compactStatePath = getPiServerCompactStatePath(sessionFile);
377
+ if (!readPiServerPendingCompact(compactStatePath))
378
+ return undefined;
379
+ const result = await recoverPendingPiServerCompaction(this.sessionId, { entries: this.sessionManager.getEntries(), leafId: this.sessionManager.getLeafId() }, compactStatePath, { signal });
380
+ if (!result)
381
+ return undefined;
382
+ return this._applyPiServerCompactionResult(result, reason, willRetry);
383
+ }
263
384
  async _getRequiredRequestAuth(model) {
264
385
  let result;
265
386
  try {
@@ -288,7 +409,7 @@ export class AgentSession {
288
409
  throw new Error(formatNoApiKeyFoundMessage(model.provider));
289
410
  }
290
411
  async _getSummarizationRequestAuth(model) {
291
- if (this.agent.streamFunction === streamSimple) {
412
+ if (this._getAuxiliaryStreamFunction() === streamSimple) {
292
413
  return this._getRequiredRequestAuth(model);
293
414
  }
294
415
  try {
@@ -480,18 +601,33 @@ export class AgentSession {
480
601
  }
481
602
  async _emitAgentSettled() {
482
603
  this._isAgentRunActive = false;
483
- try {
484
- await this._extensionRunner.emit({ type: "agent_settled" });
485
- this._emit({ type: "agent_settled" });
486
- }
487
- finally {
488
- this._resolveIdleWaitIfIdle();
489
- }
604
+ await this._extensionRunner.emit({ type: "agent_settled" });
605
+ this._emit({ type: "agent_settled" });
490
606
  }
491
607
  // Track last assistant message for auto-compaction check
492
608
  _lastAssistantMessage = undefined;
493
609
  /** Internal handler for agent events - shared by subscribe and reconnect */
494
610
  _handleAgentEvent = async (event, signal) => {
611
+ if (event.type === "tool_execution_start") {
612
+ beginSessionToolEffect(this.sessionManager, event);
613
+ }
614
+ else if (event.type === "tool_execution_end") {
615
+ writeSessionToolEffectResult(this.sessionManager, event.toolResult, { finalizationPending: true });
616
+ this._toolResultsInFinalization.add(event.toolResult);
617
+ }
618
+ else if (event.type === "message_start" &&
619
+ event.message.role === "toolResult" &&
620
+ !this._toolResultsInFinalization.has(event.message)) {
621
+ beginSessionToolEffectFinalization(this.sessionManager, event.message);
622
+ this._toolResultsInFinalization.add(event.message);
623
+ }
624
+ else if (event.type === "message_end" &&
625
+ event.message.role === "toolResult" &&
626
+ !this._toolResultsInFinalization.has(event.message)) {
627
+ beginSessionToolEffectFinalization(this.sessionManager, event.message);
628
+ this._toolResultsInFinalization.add(event.message);
629
+ }
630
+ const finalizingToolResult = event.type === "message_end" && this._toolResultsInFinalization.has(event.message);
495
631
  // When a user message starts, check if it's from either queue and remove it BEFORE emitting
496
632
  // This ensures the UI sees the updated queue state
497
633
  if (event.type === "message_start" && event.message.role === "user") {
@@ -518,6 +654,13 @@ export class AgentSession {
518
654
  await this._emitExtensionEvent(event);
519
655
  // Notify all listeners
520
656
  this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event);
657
+ if (finalizingToolResult) {
658
+ if (event.message.role !== "toolResult") {
659
+ throw new Error("A toolResult message_end replacement must remain a toolResult");
660
+ }
661
+ writeSessionToolEffectFinalResult(this.sessionManager, event.message);
662
+ this._toolResultsInFinalization.delete(event.message);
663
+ }
521
664
  // Handle session persistence
522
665
  if (event.type === "message_end") {
523
666
  // Check if this is a custom message from extensions
@@ -529,7 +672,11 @@ export class AgentSession {
529
672
  event.message.role === "assistant" ||
530
673
  event.message.role === "toolResult") {
531
674
  // Regular LLM message - persist as SessionMessageEntry
532
- this.sessionManager.appendMessage(event.message);
675
+ const sessionEntryId = this.sessionManager.appendMessage(event.message);
676
+ if (event.message.role === "toolResult") {
677
+ this.sessionManager.flushSessionFile();
678
+ commitSessionToolEffect(this.sessionManager, event.message, sessionEntryId);
679
+ }
533
680
  }
534
681
  // Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
535
682
  // Track assistant message for auto-compaction (checked on agent_end)
@@ -549,12 +696,10 @@ export class AgentSession {
549
696
  });
550
697
  this._retryAttempt = 0;
551
698
  }
552
- if (isPiServerMode() && !this._isTerminalAssistantFailure(assistantMsg)) {
553
- await syncPiServerTree(this.sessionId, {
554
- systemPrompt: this.systemPrompt,
555
- messages: this.agent.state.messages,
556
- tools: this.agent.state.tools,
557
- }, { entries: this.sessionManager.getEntries(), leafId: this.sessionManager.getLeafId() }, { signal, onHistoryReconciled: (snapshot) => this.reconcilePiServerHistory(snapshot) });
699
+ if (isPiServerMode() &&
700
+ !this._isPostAssistantPiServerSyncFailure(assistantMsg) &&
701
+ !isPiServerControlPlaneFailure(assistantMsg)) {
702
+ await this._syncAndAcknowledgePiServerAssistantMessage(assistantMsg, signal);
558
703
  }
559
704
  }
560
705
  }
@@ -820,7 +965,7 @@ export class AgentSession {
820
965
  }
821
966
  /** Whether the session has no active agent run, retry, auto-compaction, or queued continuation. */
822
967
  get isIdle() {
823
- return !this._isAgentRunActive;
968
+ return !this._isAgentRunActive && !this._piServerSessionLeaseOwner;
824
969
  }
825
970
  /** Current effective system prompt (includes any per-turn extension modifications) */
826
971
  get systemPrompt() {
@@ -972,9 +1117,11 @@ export class AgentSession {
972
1117
  // Prompting
973
1118
  // =========================================================================
974
1119
  async _runAgentPrompt(messages) {
1120
+ const piServerLease = this._acquirePiServerOperationLease();
975
1121
  this._resetValidationHintState();
976
1122
  this._isAgentRunActive = true;
977
1123
  try {
1124
+ await this._recoverPendingPiServerCompaction("threshold", false);
978
1125
  await this.agent.prompt(messages);
979
1126
  while (await this._handlePostAgentRun()) {
980
1127
  try {
@@ -997,9 +1144,15 @@ export class AgentSession {
997
1144
  await this._maybeGenerateSessionName();
998
1145
  }
999
1146
  finally {
1000
- this._systemPromptOverride = undefined;
1001
- this._flushPendingBashMessages();
1002
- await this._emitAgentSettled();
1147
+ try {
1148
+ this._systemPromptOverride = undefined;
1149
+ this._flushPendingBashMessages();
1150
+ await this._emitAgentSettled();
1151
+ }
1152
+ finally {
1153
+ this._releasePiServerOperationLease(piServerLease.path, piServerLease.acquired, piServerLease.owner);
1154
+ this._resolveIdleWaitIfIdle();
1155
+ }
1003
1156
  }
1004
1157
  }
1005
1158
  _sessionHasInfoEntry() {
@@ -1036,7 +1189,7 @@ export class AgentSession {
1036
1189
  if (!model || !transcript)
1037
1190
  return;
1038
1191
  try {
1039
- const stream = await this.agent.streamFunction(model, {
1192
+ const stream = await this._getAuxiliaryStreamFunction()(model, {
1040
1193
  systemPrompt: this.systemPrompt,
1041
1194
  messages: [
1042
1195
  {
@@ -1049,7 +1202,7 @@ export class AgentSession {
1049
1202
  }, {
1050
1203
  maxTokens: 32,
1051
1204
  reasoning: undefined,
1052
- sessionId: this.sessionId,
1205
+ sessionId: randomUUID(),
1053
1206
  transport: this.agent.transport,
1054
1207
  thinkingBudgets: this.agent.thinkingBudgets,
1055
1208
  maxRetryDelayMs: this.agent.maxRetryDelayMs,
@@ -1682,9 +1835,26 @@ export class AgentSession {
1682
1835
  // Compaction
1683
1836
  // =========================================================================
1684
1837
  async _applyPiServerCompactionResult(result, reason, willRetry) {
1685
- this.sessionManager.replaceTree(result.entries, result.leafId);
1838
+ const compactionEntry = result.compactionEntry;
1839
+ const operation = compactionEntry.piServerCompactOperation;
1840
+ if (!operation ||
1841
+ !Number.isSafeInteger(operation.baseEntryCount) ||
1842
+ operation.baseEntryCount < 0 ||
1843
+ (operation.baseLeafId !== null &&
1844
+ (typeof operation.baseLeafId !== "string" || operation.baseLeafId.length === 0)) ||
1845
+ typeof operation.baseTreeHash !== "string" ||
1846
+ !/^[a-f0-9]{64}$/u.test(operation.baseTreeHash)) {
1847
+ throw new Error("Pi-server compaction result is missing a valid durable base identity");
1848
+ }
1849
+ this.sessionManager.appendCompactionEntry(compactionEntry, {
1850
+ baseEntryCount: operation.baseEntryCount,
1851
+ baseLeafId: operation.baseLeafId,
1852
+ baseTreeHash: operation.baseTreeHash,
1853
+ });
1854
+ recordPiServerCompactionApplied(result);
1686
1855
  const sessionContext = this.sessionManager.buildSessionContext();
1687
1856
  this.agent.state.messages = sessionContext.messages;
1857
+ await acknowledgePiServerCompaction(result);
1688
1858
  const firstKeptEntryId = result.compaction.firstKeptEntryId;
1689
1859
  if (!firstKeptEntryId) {
1690
1860
  throw new Error("Pi-server compaction result is missing firstKeptEntryId");
@@ -1703,7 +1873,7 @@ export class AgentSession {
1703
1873
  await this._extensionRunner.emit({
1704
1874
  type: "session_compact",
1705
1875
  compactionEntry: savedCompactionEntry,
1706
- fromExtension: false,
1876
+ fromExtension: savedCompactionEntry.fromHook === true,
1707
1877
  reason,
1708
1878
  willRetry,
1709
1879
  });
@@ -1720,38 +1890,31 @@ export class AgentSession {
1720
1890
  await this.abort();
1721
1891
  this._compactionAbortController = new AbortController();
1722
1892
  this._emit({ type: "compaction_start", reason: "manual" });
1893
+ let piServerLease = {
1894
+ path: undefined,
1895
+ acquired: false,
1896
+ owner: undefined,
1897
+ };
1723
1898
  try {
1724
- if (!this.model) {
1725
- throw new Error(formatNoModelSelectedMessage());
1726
- }
1727
- const { apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model);
1728
- if (isPiServerMode()) {
1729
- this._detachPiServerTerminalAssistantFailure();
1730
- const piServerResult = await compactPiServer(this.model, {
1731
- systemPrompt: this.systemPrompt,
1732
- messages: this.agent.state.messages,
1733
- tools: this.agent.state.tools,
1734
- }, {
1735
- sessionId: this.sessionId,
1736
- apiKey,
1737
- headers,
1738
- customInstructions,
1739
- settings: this.settingsManager.getCompactionSettings(),
1740
- sessionTree: { entries: this.sessionManager.getEntries(), leafId: this.sessionManager.getLeafId() },
1741
- signal: this._compactionAbortController.signal,
1742
- reasoning: toProviderReasoning(this.thinkingLevel),
1743
- onHistoryReconciled: (snapshot) => this.reconcilePiServerHistory(snapshot),
1744
- });
1745
- const result = await this._applyPiServerCompactionResult(piServerResult, "manual", false);
1746
- this._lastServerCompactionTimestamp = Date.now();
1899
+ piServerLease = this._acquirePiServerOperationLease();
1900
+ await this._recoverPendingPiServerRunBeforeControlOperation(this._compactionAbortController.signal);
1901
+ const recoveredCompaction = await this._recoverPendingPiServerCompaction("manual", false, this._compactionAbortController.signal);
1902
+ if (recoveredCompaction) {
1747
1903
  this._emit({
1748
1904
  type: "compaction_end",
1749
1905
  reason: "manual",
1750
- result,
1906
+ result: recoveredCompaction,
1751
1907
  aborted: false,
1752
1908
  willRetry: false,
1753
1909
  });
1754
- return result;
1910
+ return recoveredCompaction;
1911
+ }
1912
+ if (!this.model) {
1913
+ throw new Error(formatNoModelSelectedMessage());
1914
+ }
1915
+ const { apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model);
1916
+ if (isPiServerMode()) {
1917
+ this._detachPiServerTerminalAssistantFailure();
1755
1918
  }
1756
1919
  const pathEntries = this.sessionManager.getBranch();
1757
1920
  const settings = this.settingsManager.getCompactionSettings();
@@ -1784,6 +1947,42 @@ export class AgentSession {
1784
1947
  fromExtension = true;
1785
1948
  }
1786
1949
  }
1950
+ if (isPiServerMode()) {
1951
+ const sessionFile = this.sessionManager.getSessionFile();
1952
+ if (!sessionFile) {
1953
+ throw new Error("Durable pi-server compaction requires a persisted session file");
1954
+ }
1955
+ const piServerResult = await compactPiServer(this.model, {
1956
+ systemPrompt: this.systemPrompt,
1957
+ messages: this.agent.state.messages,
1958
+ tools: this.agent.state.tools,
1959
+ }, {
1960
+ sessionId: this.sessionId,
1961
+ apiKey,
1962
+ headers,
1963
+ env,
1964
+ customInstructions,
1965
+ settings,
1966
+ preparation: { firstKeptEntryId: preparation.firstKeptEntryId },
1967
+ extensionCompaction,
1968
+ retry: this.settingsManager.getRetrySettings(),
1969
+ sessionTree: { entries: this.sessionManager.getEntries(), leafId: this.sessionManager.getLeafId() },
1970
+ piServerCompactStatePath: getPiServerCompactStatePath(sessionFile),
1971
+ signal: this._compactionAbortController.signal,
1972
+ reasoning: toProviderReasoning(this.thinkingLevel),
1973
+ onHistoryReconciled: (snapshot) => this.reconcilePiServerHistory(snapshot),
1974
+ });
1975
+ const result = await this._applyPiServerCompactionResult(piServerResult, "manual", false);
1976
+ this._lastServerCompactionTimestamp = Date.now();
1977
+ this._emit({
1978
+ type: "compaction_end",
1979
+ reason: "manual",
1980
+ result,
1981
+ aborted: false,
1982
+ willRetry: false,
1983
+ });
1984
+ return result;
1985
+ }
1787
1986
  let summary;
1788
1987
  let firstKeptEntryId;
1789
1988
  let tokensBefore;
@@ -1799,7 +1998,7 @@ export class AgentSession {
1799
1998
  }
1800
1999
  else {
1801
2000
  // Generate compaction result
1802
- const result = await compact(preparation, this.model, apiKey, headers, customInstructions, this._compactionAbortController.signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason: "manual" }));
2001
+ const result = await compact(preparation, this.model, apiKey, headers, customInstructions, this._compactionAbortController.signal, this.thinkingLevel, this._getAuxiliaryStreamFunction(), env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason: "manual" }));
1803
2002
  summary = result.summary;
1804
2003
  firstKeptEntryId = result.firstKeptEntryId;
1805
2004
  tokensBefore = result.tokensBefore;
@@ -1856,6 +2055,7 @@ export class AgentSession {
1856
2055
  throw error;
1857
2056
  }
1858
2057
  finally {
2058
+ this._releasePiServerOperationLease(piServerLease.path, piServerLease.acquired, piServerLease.owner);
1859
2059
  this._compactionAbortController = undefined;
1860
2060
  this._reconnectToAgent();
1861
2061
  }
@@ -1979,45 +2179,33 @@ export class AgentSession {
1979
2179
  */
1980
2180
  async _runAutoCompaction(reason, willRetry, preparationOverride, preparationOptions) {
1981
2181
  const settings = this.settingsManager.getCompactionSettings();
2182
+ const model = this.model;
1982
2183
  let started = false;
2184
+ let piServerLease = {
2185
+ path: undefined,
2186
+ acquired: false,
2187
+ owner: undefined,
2188
+ };
1983
2189
  try {
1984
- if (!this.model) {
1985
- return false;
2190
+ piServerLease = this._acquirePiServerOperationLease(this._isAgentRunActive ? this._piServerSessionLeaseOwner : undefined);
2191
+ if (!this._isAgentRunActive) {
2192
+ await this._recoverPendingPiServerRunBeforeControlOperation();
1986
2193
  }
1987
- let apiKey;
1988
- let headers;
1989
- let env;
1990
- if (this.agent.streamFunction === streamSimple) {
1991
- ({ apiKey, headers, env } = await this._getRequiredRequestAuth(this.model));
1992
- }
1993
- else {
1994
- ({ apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model));
1995
- }
1996
- if (isPiServerMode()) {
2194
+ const recoveredCompaction = await this._recoverPendingPiServerCompaction(reason, willRetry);
2195
+ if (recoveredCompaction) {
1997
2196
  this._emit({ type: "compaction_start", reason });
1998
- this._autoCompactionAbortController = new AbortController();
1999
2197
  started = true;
2000
- const signal = this._autoCompactionAbortController.signal;
2001
- const piServerResult = await compactPiServer(this.model, {
2002
- systemPrompt: this.systemPrompt,
2003
- messages: this.agent.state.messages,
2004
- tools: this.agent.state.tools,
2005
- }, {
2006
- sessionId: this.sessionId,
2007
- apiKey,
2008
- headers,
2009
- settings,
2010
- preparation: preparationOptions,
2011
- sessionTree: { entries: this.sessionManager.getEntries(), leafId: this.sessionManager.getLeafId() },
2012
- signal,
2013
- reasoning: toProviderReasoning(this.thinkingLevel),
2014
- onHistoryReconciled: (snapshot) => this.reconcilePiServerHistory(snapshot),
2015
- });
2016
- const result = await this._applyPiServerCompactionResult(piServerResult, reason, willRetry);
2017
2198
  this._lastServerCompactionTimestamp = Date.now();
2018
- this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
2199
+ this._emit({ type: "compaction_end", reason, result: recoveredCompaction, aborted: false, willRetry });
2019
2200
  return willRetry ? true : this.agent.hasQueuedMessages();
2020
2201
  }
2202
+ if (!model) {
2203
+ return false;
2204
+ }
2205
+ let apiKey;
2206
+ let headers;
2207
+ let env;
2208
+ ({ apiKey, headers, env } = await this._getSummarizationRequestAuth(model));
2021
2209
  const pathEntries = this.sessionManager.getBranch();
2022
2210
  const preparation = preparationOverride ?? prepareCompaction(pathEntries, settings);
2023
2211
  if (!preparation) {
@@ -2026,6 +2214,7 @@ export class AgentSession {
2026
2214
  this._emit({ type: "compaction_start", reason });
2027
2215
  this._autoCompactionAbortController = new AbortController();
2028
2216
  started = true;
2217
+ const signal = this._autoCompactionAbortController.signal;
2029
2218
  let extensionCompaction;
2030
2219
  let fromExtension = false;
2031
2220
  if (this._extensionRunner.hasHandlers("session_before_compact")) {
@@ -2036,7 +2225,7 @@ export class AgentSession {
2036
2225
  customInstructions: undefined,
2037
2226
  reason,
2038
2227
  willRetry,
2039
- signal: this._autoCompactionAbortController.signal,
2228
+ signal,
2040
2229
  }));
2041
2230
  if (extensionResult?.cancel) {
2042
2231
  this._emit({
@@ -2053,6 +2242,35 @@ export class AgentSession {
2053
2242
  fromExtension = true;
2054
2243
  }
2055
2244
  }
2245
+ if (isPiServerMode()) {
2246
+ const sessionFile = this.sessionManager.getSessionFile();
2247
+ if (!sessionFile) {
2248
+ throw new Error("Durable pi-server compaction requires a persisted session file");
2249
+ }
2250
+ const piServerResult = await compactPiServer(model, {
2251
+ systemPrompt: this.systemPrompt,
2252
+ messages: this.agent.state.messages,
2253
+ tools: this.agent.state.tools,
2254
+ }, {
2255
+ sessionId: this.sessionId,
2256
+ apiKey,
2257
+ headers,
2258
+ env,
2259
+ settings,
2260
+ retry: this.settingsManager.getRetrySettings(),
2261
+ preparation: preparationOptions ?? { firstKeptEntryId: preparation.firstKeptEntryId },
2262
+ extensionCompaction,
2263
+ sessionTree: { entries: this.sessionManager.getEntries(), leafId: this.sessionManager.getLeafId() },
2264
+ piServerCompactStatePath: getPiServerCompactStatePath(sessionFile),
2265
+ signal,
2266
+ reasoning: toProviderReasoning(this.thinkingLevel),
2267
+ onHistoryReconciled: (snapshot) => this.reconcilePiServerHistory(snapshot),
2268
+ });
2269
+ const result = await this._applyPiServerCompactionResult(piServerResult, reason, willRetry);
2270
+ this._lastServerCompactionTimestamp = Date.now();
2271
+ this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
2272
+ return willRetry ? true : this.agent.hasQueuedMessages();
2273
+ }
2056
2274
  let summary;
2057
2275
  let firstKeptEntryId;
2058
2276
  let tokensBefore;
@@ -2068,7 +2286,7 @@ export class AgentSession {
2068
2286
  }
2069
2287
  else {
2070
2288
  // Generate compaction result
2071
- const compactResult = await compact(preparation, this.model, apiKey, headers, undefined, this._autoCompactionAbortController.signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }));
2289
+ const compactResult = await compact(preparation, model, apiKey, headers, undefined, this._autoCompactionAbortController.signal, this.thinkingLevel, this._getAuxiliaryStreamFunction(), env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }));
2072
2290
  summary = compactResult.summary;
2073
2291
  firstKeptEntryId = compactResult.firstKeptEntryId;
2074
2292
  tokensBefore = compactResult.tokensBefore;
@@ -2139,6 +2357,7 @@ export class AgentSession {
2139
2357
  return false;
2140
2358
  }
2141
2359
  finally {
2360
+ this._releasePiServerOperationLease(piServerLease.path, piServerLease.acquired, piServerLease.owner);
2142
2361
  this._autoCompactionAbortController = undefined;
2143
2362
  }
2144
2363
  }
@@ -2554,8 +2773,12 @@ export class AgentSession {
2554
2773
  return false;
2555
2774
  if (this._isPostAssistantPiServerSyncFailure(message))
2556
2775
  return false;
2557
- const piServerPhase = getPiServerFailurePhase(message);
2558
- if (piServerPhase && piServerPhase !== "provider_stream")
2776
+ if (isPiServerRestartUncertainty(message))
2777
+ return false;
2778
+ const piServerFailure = getPiServerFailureMetadata(message);
2779
+ if (piServerFailure?.phase && piServerFailure.phase !== "provider_stream")
2780
+ return false;
2781
+ if (piServerFailure?.retryable === false)
2559
2782
  return false;
2560
2783
  if (message.errorMessage && this._isNonRetryableProviderLimitError(message.errorMessage))
2561
2784
  return false;
@@ -2822,6 +3045,7 @@ export class AgentSession {
2822
3045
  replaceInstructions,
2823
3046
  label,
2824
3047
  };
3048
+ const piServerLease = this._acquirePiServerOperationLease();
2825
3049
  // Set up abort controller for summarization
2826
3050
  this._branchSummaryAbortController = new AbortController();
2827
3051
  try {
@@ -2869,7 +3093,7 @@ export class AgentSession {
2869
3093
  customInstructions,
2870
3094
  replaceInstructions,
2871
3095
  reserveTokens: branchSummarySettings.reserveTokens,
2872
- streamFn: this.agent.streamFunction,
3096
+ streamFn: this._getAuxiliaryStreamFunction(),
2873
3097
  retry: this.settingsManager.getRetrySettings(),
2874
3098
  callbacks: this._summarizationRetryCallbacks({ source: "branchSummary" }),
2875
3099
  });
@@ -2954,6 +3178,7 @@ export class AgentSession {
2954
3178
  return { editorText, cancelled: false, summaryEntry };
2955
3179
  }
2956
3180
  finally {
3181
+ this._releasePiServerOperationLease(piServerLease.path, piServerLease.acquired, piServerLease.owner);
2957
3182
  this._branchSummaryAbortController = undefined;
2958
3183
  }
2959
3184
  }