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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) 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 +13 -1
  13. package/dist/core/agent-session.d.ts.map +1 -1
  14. package/dist/core/agent-session.js +340 -89
  15. package/dist/core/agent-session.js.map +1 -1
  16. package/dist/core/model-runtime.d.ts +7 -0
  17. package/dist/core/model-runtime.d.ts.map +1 -1
  18. package/dist/core/model-runtime.js +66 -0
  19. package/dist/core/model-runtime.js.map +1 -1
  20. package/dist/core/pi-server-client.d.ts +56 -2
  21. package/dist/core/pi-server-client.d.ts.map +1 -1
  22. package/dist/core/pi-server-client.js +2090 -288
  23. package/dist/core/pi-server-client.js.map +1 -1
  24. package/dist/core/pi-server-compact-state.d.ts +81 -0
  25. package/dist/core/pi-server-compact-state.d.ts.map +1 -0
  26. package/dist/core/pi-server-compact-state.js +438 -0
  27. package/dist/core/pi-server-compact-state.js.map +1 -0
  28. package/dist/core/pi-server-protocol.d.ts +2 -0
  29. package/dist/core/pi-server-protocol.d.ts.map +1 -1
  30. package/dist/core/pi-server-protocol.js +24 -1
  31. package/dist/core/pi-server-protocol.js.map +1 -1
  32. package/dist/core/pi-server-provider-execution.d.ts +19 -0
  33. package/dist/core/pi-server-provider-execution.d.ts.map +1 -0
  34. package/dist/core/pi-server-provider-execution.js +43 -0
  35. package/dist/core/pi-server-provider-execution.js.map +1 -0
  36. package/dist/core/pi-server-request.d.ts +28 -0
  37. package/dist/core/pi-server-request.d.ts.map +1 -1
  38. package/dist/core/pi-server-request.js +492 -75
  39. package/dist/core/pi-server-request.js.map +1 -1
  40. package/dist/core/pi-server-run-state.d.ts +41 -0
  41. package/dist/core/pi-server-run-state.d.ts.map +1 -0
  42. package/dist/core/pi-server-run-state.js +318 -0
  43. package/dist/core/pi-server-run-state.js.map +1 -0
  44. package/dist/core/provider-execution-contract.d.ts +36 -0
  45. package/dist/core/provider-execution-contract.d.ts.map +1 -0
  46. package/dist/core/provider-execution-contract.js +30 -0
  47. package/dist/core/provider-execution-contract.js.map +1 -0
  48. package/dist/core/sdk.d.ts.map +1 -1
  49. package/dist/core/sdk.js +74 -53
  50. package/dist/core/sdk.js.map +1 -1
  51. package/dist/core/session-manager.d.ts +86 -2
  52. package/dist/core/session-manager.d.ts.map +1 -1
  53. package/dist/core/session-manager.js +737 -144
  54. package/dist/core/session-manager.js.map +1 -1
  55. package/dist/core/tool-effect-journal.d.ts +103 -0
  56. package/dist/core/tool-effect-journal.d.ts.map +1 -0
  57. package/dist/core/tool-effect-journal.js +1442 -0
  58. package/dist/core/tool-effect-journal.js.map +1 -0
  59. package/dist/core/tools/output-accumulator.d.ts +3 -1
  60. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  61. package/dist/core/tools/output-accumulator.js +46 -26
  62. package/dist/core/tools/output-accumulator.js.map +1 -1
  63. package/dist/main.d.ts.map +1 -1
  64. package/dist/main.js +13 -0
  65. package/dist/main.js.map +1 -1
  66. package/dist/modes/interactive/components/session-selector.d.ts +15 -0
  67. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  68. package/dist/modes/interactive/components/session-selector.js +98 -29
  69. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  70. 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,15 @@ 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 { assertPiServerProviderExecution, } from "./pi-server-provider-execution.js";
36
+ import { getPiServerRunStatePath, readPiServerPendingRun } from "./pi-server-run-state.js";
33
37
  import { expandPromptTemplate } from "./prompt-templates.js";
34
38
  import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
35
39
  import { createSyntheticSourceInfo } from "./source-info.js";
36
40
  import { buildSystemPrompt } from "./system-prompt.js";
41
+ import { beginSessionToolEffect, beginSessionToolEffectFinalization, commitSessionToolEffect, recoverToolEffects, writeSessionToolEffectFinalResult, writeSessionToolEffectResult, } from "./tool-effect-journal.js";
37
42
  import { createLocalBashOperations } from "./tools/bash.js";
38
43
  import { createAllToolDefinitions } from "./tools/index.js";
39
44
  import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
@@ -93,16 +98,26 @@ function getStringField(input, key) {
93
98
  const value = input[key];
94
99
  return typeof value === "string" && value.trim().length > 0 ? value : undefined;
95
100
  }
96
- function getPiServerFailurePhase(message) {
101
+ function getPiServerFailureMetadata(message) {
97
102
  for (const diagnostic of message.diagnostics ?? []) {
98
103
  if (diagnostic.type !== "pi_server_failure")
99
104
  continue;
100
105
  const phase = diagnostic.details?.phase;
101
- if (typeof phase === "string")
102
- return phase;
106
+ const retryable = diagnostic.details?.retryable;
107
+ return {
108
+ phase: typeof phase === "string" ? phase : undefined,
109
+ retryable: typeof retryable === "boolean" ? retryable : undefined,
110
+ };
103
111
  }
104
112
  return undefined;
105
113
  }
114
+ function isPiServerControlPlaneFailure(message) {
115
+ const hasFailure = message.diagnostics?.some((diagnostic) => diagnostic.type === "pi_server_failure") === true;
116
+ return hasFailure && getPiServerRunDiagnostic(message) === undefined;
117
+ }
118
+ function isPiServerRestartUncertainty(message) {
119
+ return getPiServerRunDiagnostic(message)?.restartUnknown === true;
120
+ }
106
121
  function toolTextContent(content) {
107
122
  return content
108
123
  .filter((part) => part.type === "text")
@@ -165,6 +180,7 @@ export class AgentSession {
165
180
  _isAgentRunActive = false;
166
181
  _idleWaitPromise;
167
182
  _resolveIdleWait;
183
+ _toolResultsInFinalization = new WeakSet();
168
184
  /** Tracks pending steering messages for UI display. Removed when delivered. */
169
185
  _steeringMessages = [];
170
186
  /** Tracks pending follow-up messages for UI display. Removed when delivered. */
@@ -176,6 +192,7 @@ export class AgentSession {
176
192
  _autoCompactionAbortController = undefined;
177
193
  _overflowRecoveryAttempted = false;
178
194
  _lastServerCompactionTimestamp = undefined;
195
+ _piServerSessionLeaseOwner;
179
196
  // Branch summarization state
180
197
  _branchSummaryAbortController = undefined;
181
198
  // Retry state
@@ -208,6 +225,7 @@ export class AgentSession {
208
225
  _extensionErrorUnsubscriber;
209
226
  _autoSessionNameEnabled;
210
227
  _autoSessionNameAttempted = false;
228
+ _auxiliaryStreamFunction;
211
229
  _modelRuntime;
212
230
  // Tool registry for extension getTools/setTools
213
231
  _toolRegistry = new Map();
@@ -227,6 +245,7 @@ export class AgentSession {
227
245
  this._customTools = config.customTools ?? [];
228
246
  this._cwd = config.cwd;
229
247
  this._modelRuntime = config.modelRuntime;
248
+ this._auxiliaryStreamFunction = config.auxiliaryStreamFunction;
230
249
  this._extensionRunnerRef = config.extensionRunnerRef;
231
250
  this._initialActiveToolNames = config.initialActiveToolNames;
232
251
  this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
@@ -234,6 +253,10 @@ export class AgentSession {
234
253
  this._baseToolsOverride = config.baseToolsOverride;
235
254
  this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" };
236
255
  this._autoSessionNameEnabled = config.autoSessionName ?? true;
256
+ const toolEffectRecovery = recoverToolEffects(this.sessionManager);
257
+ if (toolEffectRecovery.recoveredToolCallIds.length > 0) {
258
+ this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
259
+ }
237
260
  if (isPiServerMode()) {
238
261
  this._detachPiServerTerminalAssistantFailure();
239
262
  }
@@ -250,6 +273,9 @@ export class AgentSession {
250
273
  get modelRuntime() {
251
274
  return this._modelRuntime;
252
275
  }
276
+ _getAuxiliaryStreamFunction() {
277
+ return this._auxiliaryStreamFunction ?? this.agent.streamFunction;
278
+ }
253
279
  reconcilePiServerHistory(snapshot) {
254
280
  this.sessionManager.replaceTree(snapshot.entries, snapshot.leafId);
255
281
  const sessionContext = this.sessionManager.buildSessionContext();
@@ -260,6 +286,105 @@ export class AgentSession {
260
286
  messageCount: snapshot.messages.length,
261
287
  });
262
288
  }
289
+ async _syncAndAcknowledgePiServerAssistantMessage(message, signal) {
290
+ const persistenceSignal = signal.aborted ? undefined : signal;
291
+ const sessionFile = this.sessionManager.getSessionFile();
292
+ const runStatePath = sessionFile ? getPiServerRunStatePath(sessionFile) : undefined;
293
+ try {
294
+ await syncPiServerTree(this.sessionId, {
295
+ systemPrompt: this.systemPrompt,
296
+ messages: this.agent.state.messages,
297
+ tools: this.agent.state.tools,
298
+ }, { entries: this.sessionManager.getEntries(), leafId: this.sessionManager.getLeafId() }, {
299
+ signal: persistenceSignal,
300
+ onHistoryReconciled: (snapshot) => this.reconcilePiServerHistory(snapshot),
301
+ });
302
+ this.sessionManager.flushSessionFile();
303
+ await acknowledgePiServerRunMessage(this.sessionId, message, runStatePath, persistenceSignal);
304
+ }
305
+ catch (error) {
306
+ releasePiServerRunLease(this.sessionId, runStatePath);
307
+ throw error;
308
+ }
309
+ }
310
+ _acquirePiServerOperationLease(reentrantOwner) {
311
+ if (!isPiServerMode())
312
+ return { path: undefined, acquired: false, owner: undefined };
313
+ const sessionFile = this.sessionManager.getSessionFile();
314
+ const path = sessionFile ? getPiServerRunStatePath(sessionFile) : undefined;
315
+ if (this._piServerSessionLeaseOwner) {
316
+ if (reentrantOwner !== this._piServerSessionLeaseOwner) {
317
+ throw new Error(`Cannot start a concurrent durable pi-server operation for session ${this.sessionId}`);
318
+ }
319
+ return { path, acquired: false, owner: this._piServerSessionLeaseOwner };
320
+ }
321
+ const owner = Symbol("pi-server-session-lease");
322
+ if (path)
323
+ acquirePiServerSessionLease(this.sessionId, path);
324
+ this._piServerSessionLeaseOwner = owner;
325
+ return { path, acquired: true, owner };
326
+ }
327
+ _releasePiServerOperationLease(path, acquired, owner) {
328
+ if (!acquired)
329
+ return;
330
+ if (!owner || this._piServerSessionLeaseOwner !== owner) {
331
+ throw new Error(`Cannot release a durable pi-server operation lease owned by another operation`);
332
+ }
333
+ if (path)
334
+ releasePiServerSessionLease(this.sessionId, path);
335
+ this._piServerSessionLeaseOwner = undefined;
336
+ }
337
+ async _recoverPendingPiServerRunBeforeControlOperation(signal) {
338
+ if (!isPiServerMode())
339
+ return;
340
+ const sessionFile = this.sessionManager.getSessionFile();
341
+ if (!sessionFile) {
342
+ throw new Error("Durable pi-server operations require a persisted session file");
343
+ }
344
+ const runStatePath = getPiServerRunStatePath(sessionFile);
345
+ const pending = readPiServerPendingRun(runStatePath);
346
+ if (!pending)
347
+ return;
348
+ let terminal;
349
+ const entries = this.sessionManager.getEntries();
350
+ for (let index = entries.length - 1; index >= 0; index--) {
351
+ const entry = entries[index];
352
+ if (entry.type !== "message" || entry.message.role !== "assistant")
353
+ continue;
354
+ const message = entry.message;
355
+ const diagnostic = getPiServerRunDiagnostic(message);
356
+ if (!diagnostic || diagnostic.runId !== pending.runId)
357
+ continue;
358
+ if (diagnostic.sessionId !== pending.sessionId ||
359
+ diagnostic.requestMac !== pending.requestHash ||
360
+ pending.sessionId !== this.sessionId) {
361
+ throw new Error("Pending pi-server run terminal does not match its durable client marker");
362
+ }
363
+ terminal = message;
364
+ break;
365
+ }
366
+ if (!terminal) {
367
+ throw new Error(`Cannot start compaction while pi-server run ${pending.runId} is pending without a durable terminal assistant`);
368
+ }
369
+ await this._syncAndAcknowledgePiServerAssistantMessage(terminal, signal ?? new AbortController().signal);
370
+ }
371
+ async _recoverPendingPiServerCompaction(reason, willRetry, signal) {
372
+ if (!isPiServerMode())
373
+ return undefined;
374
+ const sessionFile = this.sessionManager.getSessionFile();
375
+ if (!sessionFile)
376
+ return undefined;
377
+ const compactStatePath = getPiServerCompactStatePath(sessionFile);
378
+ if (!readPiServerPendingCompact(compactStatePath))
379
+ return undefined;
380
+ const result = await recoverPendingPiServerCompaction(this.sessionId, { entries: this.sessionManager.getEntries(), leafId: this.sessionManager.getLeafId() }, compactStatePath, { signal });
381
+ if (!result)
382
+ return undefined;
383
+ return this._applyPiServerCompactionResult(result, reason, willRetry);
384
+ }
385
+ _assertPiServerProviderExecution(model) {
386
+ return assertPiServerProviderExecution(this._modelRuntime, model, this._extensionRunner);
387
+ }
263
388
  async _getRequiredRequestAuth(model) {
264
389
  let result;
265
390
  try {
@@ -288,7 +413,7 @@ export class AgentSession {
288
413
  throw new Error(formatNoApiKeyFoundMessage(model.provider));
289
414
  }
290
415
  async _getSummarizationRequestAuth(model) {
291
- if (this.agent.streamFunction === streamSimple) {
416
+ if (this._getAuxiliaryStreamFunction() === streamSimple) {
292
417
  return this._getRequiredRequestAuth(model);
293
418
  }
294
419
  try {
@@ -480,18 +605,33 @@ export class AgentSession {
480
605
  }
481
606
  async _emitAgentSettled() {
482
607
  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
- }
608
+ await this._extensionRunner.emit({ type: "agent_settled" });
609
+ this._emit({ type: "agent_settled" });
490
610
  }
491
611
  // Track last assistant message for auto-compaction check
492
612
  _lastAssistantMessage = undefined;
493
613
  /** Internal handler for agent events - shared by subscribe and reconnect */
494
614
  _handleAgentEvent = async (event, signal) => {
615
+ if (event.type === "tool_execution_start") {
616
+ beginSessionToolEffect(this.sessionManager, event);
617
+ }
618
+ else if (event.type === "tool_execution_end") {
619
+ writeSessionToolEffectResult(this.sessionManager, event.toolResult, { finalizationPending: true });
620
+ this._toolResultsInFinalization.add(event.toolResult);
621
+ }
622
+ else if (event.type === "message_start" &&
623
+ event.message.role === "toolResult" &&
624
+ !this._toolResultsInFinalization.has(event.message)) {
625
+ beginSessionToolEffectFinalization(this.sessionManager, event.message);
626
+ this._toolResultsInFinalization.add(event.message);
627
+ }
628
+ else if (event.type === "message_end" &&
629
+ event.message.role === "toolResult" &&
630
+ !this._toolResultsInFinalization.has(event.message)) {
631
+ beginSessionToolEffectFinalization(this.sessionManager, event.message);
632
+ this._toolResultsInFinalization.add(event.message);
633
+ }
634
+ const finalizingToolResult = event.type === "message_end" && this._toolResultsInFinalization.has(event.message);
495
635
  // When a user message starts, check if it's from either queue and remove it BEFORE emitting
496
636
  // This ensures the UI sees the updated queue state
497
637
  if (event.type === "message_start" && event.message.role === "user") {
@@ -518,6 +658,13 @@ export class AgentSession {
518
658
  await this._emitExtensionEvent(event);
519
659
  // Notify all listeners
520
660
  this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event);
661
+ if (finalizingToolResult) {
662
+ if (event.message.role !== "toolResult") {
663
+ throw new Error("A toolResult message_end replacement must remain a toolResult");
664
+ }
665
+ writeSessionToolEffectFinalResult(this.sessionManager, event.message);
666
+ this._toolResultsInFinalization.delete(event.message);
667
+ }
521
668
  // Handle session persistence
522
669
  if (event.type === "message_end") {
523
670
  // Check if this is a custom message from extensions
@@ -529,7 +676,11 @@ export class AgentSession {
529
676
  event.message.role === "assistant" ||
530
677
  event.message.role === "toolResult") {
531
678
  // Regular LLM message - persist as SessionMessageEntry
532
- this.sessionManager.appendMessage(event.message);
679
+ const sessionEntryId = this.sessionManager.appendMessage(event.message);
680
+ if (event.message.role === "toolResult") {
681
+ this.sessionManager.flushSessionFile();
682
+ commitSessionToolEffect(this.sessionManager, event.message, sessionEntryId);
683
+ }
533
684
  }
534
685
  // Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
535
686
  // Track assistant message for auto-compaction (checked on agent_end)
@@ -549,12 +700,10 @@ export class AgentSession {
549
700
  });
550
701
  this._retryAttempt = 0;
551
702
  }
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) });
703
+ if (isPiServerMode() &&
704
+ !this._isPostAssistantPiServerSyncFailure(assistantMsg) &&
705
+ !isPiServerControlPlaneFailure(assistantMsg)) {
706
+ await this._syncAndAcknowledgePiServerAssistantMessage(assistantMsg, signal);
558
707
  }
559
708
  }
560
709
  }
@@ -820,7 +969,7 @@ export class AgentSession {
820
969
  }
821
970
  /** Whether the session has no active agent run, retry, auto-compaction, or queued continuation. */
822
971
  get isIdle() {
823
- return !this._isAgentRunActive;
972
+ return !this._isAgentRunActive && !this._piServerSessionLeaseOwner;
824
973
  }
825
974
  /** Current effective system prompt (includes any per-turn extension modifications) */
826
975
  get systemPrompt() {
@@ -972,9 +1121,14 @@ export class AgentSession {
972
1121
  // Prompting
973
1122
  // =========================================================================
974
1123
  async _runAgentPrompt(messages) {
1124
+ if (isPiServerMode() && this.model) {
1125
+ this._assertPiServerProviderExecution(this.model);
1126
+ }
1127
+ const piServerLease = this._acquirePiServerOperationLease();
975
1128
  this._resetValidationHintState();
976
1129
  this._isAgentRunActive = true;
977
1130
  try {
1131
+ await this._recoverPendingPiServerCompaction("threshold", false);
978
1132
  await this.agent.prompt(messages);
979
1133
  while (await this._handlePostAgentRun()) {
980
1134
  try {
@@ -997,9 +1151,15 @@ export class AgentSession {
997
1151
  await this._maybeGenerateSessionName();
998
1152
  }
999
1153
  finally {
1000
- this._systemPromptOverride = undefined;
1001
- this._flushPendingBashMessages();
1002
- await this._emitAgentSettled();
1154
+ try {
1155
+ this._systemPromptOverride = undefined;
1156
+ this._flushPendingBashMessages();
1157
+ await this._emitAgentSettled();
1158
+ }
1159
+ finally {
1160
+ this._releasePiServerOperationLease(piServerLease.path, piServerLease.acquired, piServerLease.owner);
1161
+ this._resolveIdleWaitIfIdle();
1162
+ }
1003
1163
  }
1004
1164
  }
1005
1165
  _sessionHasInfoEntry() {
@@ -1036,7 +1196,7 @@ export class AgentSession {
1036
1196
  if (!model || !transcript)
1037
1197
  return;
1038
1198
  try {
1039
- const stream = await this.agent.streamFunction(model, {
1199
+ const stream = await this._getAuxiliaryStreamFunction()(model, {
1040
1200
  systemPrompt: this.systemPrompt,
1041
1201
  messages: [
1042
1202
  {
@@ -1049,7 +1209,7 @@ export class AgentSession {
1049
1209
  }, {
1050
1210
  maxTokens: 32,
1051
1211
  reasoning: undefined,
1052
- sessionId: this.sessionId,
1212
+ sessionId: randomUUID(),
1053
1213
  transport: this.agent.transport,
1054
1214
  thinkingBudgets: this.agent.thinkingBudgets,
1055
1215
  maxRetryDelayMs: this.agent.maxRetryDelayMs,
@@ -1160,6 +1320,9 @@ export class AgentSession {
1160
1320
  if (!this.model) {
1161
1321
  throw new Error(formatNoModelSelectedMessage());
1162
1322
  }
1323
+ if (isPiServerMode()) {
1324
+ this._assertPiServerProviderExecution(this.model);
1325
+ }
1163
1326
  const hasConfiguredAuth = this._modelRuntime.hasConfiguredAuth(this.model.provider) ||
1164
1327
  (await this._modelRuntime.checkAuth(this.model.provider)) !== undefined;
1165
1328
  if (!hasConfiguredAuth) {
@@ -1682,9 +1845,26 @@ export class AgentSession {
1682
1845
  // Compaction
1683
1846
  // =========================================================================
1684
1847
  async _applyPiServerCompactionResult(result, reason, willRetry) {
1685
- this.sessionManager.replaceTree(result.entries, result.leafId);
1848
+ const compactionEntry = result.compactionEntry;
1849
+ const operation = compactionEntry.piServerCompactOperation;
1850
+ if (!operation ||
1851
+ !Number.isSafeInteger(operation.baseEntryCount) ||
1852
+ operation.baseEntryCount < 0 ||
1853
+ (operation.baseLeafId !== null &&
1854
+ (typeof operation.baseLeafId !== "string" || operation.baseLeafId.length === 0)) ||
1855
+ typeof operation.baseTreeHash !== "string" ||
1856
+ !/^[a-f0-9]{64}$/u.test(operation.baseTreeHash)) {
1857
+ throw new Error("Pi-server compaction result is missing a valid durable base identity");
1858
+ }
1859
+ this.sessionManager.appendCompactionEntry(compactionEntry, {
1860
+ baseEntryCount: operation.baseEntryCount,
1861
+ baseLeafId: operation.baseLeafId,
1862
+ baseTreeHash: operation.baseTreeHash,
1863
+ });
1864
+ recordPiServerCompactionApplied(result);
1686
1865
  const sessionContext = this.sessionManager.buildSessionContext();
1687
1866
  this.agent.state.messages = sessionContext.messages;
1867
+ await acknowledgePiServerCompaction(result);
1688
1868
  const firstKeptEntryId = result.compaction.firstKeptEntryId;
1689
1869
  if (!firstKeptEntryId) {
1690
1870
  throw new Error("Pi-server compaction result is missing firstKeptEntryId");
@@ -1703,7 +1883,7 @@ export class AgentSession {
1703
1883
  await this._extensionRunner.emit({
1704
1884
  type: "session_compact",
1705
1885
  compactionEntry: savedCompactionEntry,
1706
- fromExtension: false,
1886
+ fromExtension: savedCompactionEntry.fromHook === true,
1707
1887
  reason,
1708
1888
  willRetry,
1709
1889
  });
@@ -1716,42 +1896,36 @@ export class AgentSession {
1716
1896
  * @param customInstructions Optional instructions for the compaction summary
1717
1897
  */
1718
1898
  async compact(customInstructions) {
1899
+ const piServerExecution = isPiServerMode() && this.model ? this._assertPiServerProviderExecution(this.model) : undefined;
1719
1900
  this._disconnectFromAgent();
1720
1901
  await this.abort();
1721
1902
  this._compactionAbortController = new AbortController();
1722
1903
  this._emit({ type: "compaction_start", reason: "manual" });
1904
+ let piServerLease = {
1905
+ path: undefined,
1906
+ acquired: false,
1907
+ owner: undefined,
1908
+ };
1723
1909
  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();
1910
+ piServerLease = this._acquirePiServerOperationLease();
1911
+ await this._recoverPendingPiServerRunBeforeControlOperation(this._compactionAbortController.signal);
1912
+ const recoveredCompaction = await this._recoverPendingPiServerCompaction("manual", false, this._compactionAbortController.signal);
1913
+ if (recoveredCompaction) {
1747
1914
  this._emit({
1748
1915
  type: "compaction_end",
1749
1916
  reason: "manual",
1750
- result,
1917
+ result: recoveredCompaction,
1751
1918
  aborted: false,
1752
1919
  willRetry: false,
1753
1920
  });
1754
- return result;
1921
+ return recoveredCompaction;
1922
+ }
1923
+ if (!this.model) {
1924
+ throw new Error(formatNoModelSelectedMessage());
1925
+ }
1926
+ const { apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model);
1927
+ if (isPiServerMode()) {
1928
+ this._detachPiServerTerminalAssistantFailure();
1755
1929
  }
1756
1930
  const pathEntries = this.sessionManager.getBranch();
1757
1931
  const settings = this.settingsManager.getCompactionSettings();
@@ -1784,6 +1958,43 @@ export class AgentSession {
1784
1958
  fromExtension = true;
1785
1959
  }
1786
1960
  }
1961
+ if (isPiServerMode()) {
1962
+ const sessionFile = this.sessionManager.getSessionFile();
1963
+ if (!sessionFile) {
1964
+ throw new Error("Durable pi-server compaction requires a persisted session file");
1965
+ }
1966
+ const piServerResult = await compactPiServer(this.model, {
1967
+ systemPrompt: this.systemPrompt,
1968
+ messages: this.agent.state.messages,
1969
+ tools: this.agent.state.tools,
1970
+ }, {
1971
+ sessionId: this.sessionId,
1972
+ apiKey,
1973
+ headers,
1974
+ env,
1975
+ customInstructions,
1976
+ settings,
1977
+ preparation: { firstKeptEntryId: preparation.firstKeptEntryId },
1978
+ extensionCompaction,
1979
+ retry: this.settingsManager.getRetrySettings(),
1980
+ sessionTree: { entries: this.sessionManager.getEntries(), leafId: this.sessionManager.getLeafId() },
1981
+ providerExecutionFingerprint: piServerExecution?.providerExecutionFingerprint,
1982
+ piServerCompactStatePath: getPiServerCompactStatePath(sessionFile),
1983
+ signal: this._compactionAbortController.signal,
1984
+ reasoning: toProviderReasoning(this.thinkingLevel),
1985
+ onHistoryReconciled: (snapshot) => this.reconcilePiServerHistory(snapshot),
1986
+ });
1987
+ const result = await this._applyPiServerCompactionResult(piServerResult, "manual", false);
1988
+ this._lastServerCompactionTimestamp = Date.now();
1989
+ this._emit({
1990
+ type: "compaction_end",
1991
+ reason: "manual",
1992
+ result,
1993
+ aborted: false,
1994
+ willRetry: false,
1995
+ });
1996
+ return result;
1997
+ }
1787
1998
  let summary;
1788
1999
  let firstKeptEntryId;
1789
2000
  let tokensBefore;
@@ -1799,7 +2010,7 @@ export class AgentSession {
1799
2010
  }
1800
2011
  else {
1801
2012
  // 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" }));
2013
+ 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
2014
  summary = result.summary;
1804
2015
  firstKeptEntryId = result.firstKeptEntryId;
1805
2016
  tokensBefore = result.tokensBefore;
@@ -1856,6 +2067,7 @@ export class AgentSession {
1856
2067
  throw error;
1857
2068
  }
1858
2069
  finally {
2070
+ this._releasePiServerOperationLease(piServerLease.path, piServerLease.acquired, piServerLease.owner);
1859
2071
  this._compactionAbortController = undefined;
1860
2072
  this._reconnectToAgent();
1861
2073
  }
@@ -1912,6 +2124,9 @@ export class AgentSession {
1912
2124
  // but must not retry: the assistant answer already completed and agent.continue() cannot
1913
2125
  // continue from an assistant message.
1914
2126
  if (sameModel && isContextOverflow(assistantMessage, contextWindow)) {
2127
+ if (isPiServerMode() && this.model) {
2128
+ this._assertPiServerProviderExecution(this.model);
2129
+ }
1915
2130
  const willRetry = assistantMessage.stopReason !== "stop";
1916
2131
  if (!willRetry) {
1917
2132
  return await this._runAutoCompaction("overflow", false);
@@ -1970,6 +2185,9 @@ export class AgentSession {
1970
2185
  contextTokens = directContextTokens;
1971
2186
  }
1972
2187
  if (shouldCompact(contextTokens, contextWindow, settings)) {
2188
+ if (isPiServerMode() && this.model) {
2189
+ this._assertPiServerProviderExecution(this.model);
2190
+ }
1973
2191
  return await this._runAutoCompaction("threshold", false);
1974
2192
  }
1975
2193
  return false;
@@ -1979,45 +2197,34 @@ export class AgentSession {
1979
2197
  */
1980
2198
  async _runAutoCompaction(reason, willRetry, preparationOverride, preparationOptions) {
1981
2199
  const settings = this.settingsManager.getCompactionSettings();
2200
+ const model = this.model;
2201
+ const piServerExecution = isPiServerMode() && model ? this._assertPiServerProviderExecution(model) : undefined;
1982
2202
  let started = false;
2203
+ let piServerLease = {
2204
+ path: undefined,
2205
+ acquired: false,
2206
+ owner: undefined,
2207
+ };
1983
2208
  try {
1984
- if (!this.model) {
1985
- return false;
2209
+ piServerLease = this._acquirePiServerOperationLease(this._isAgentRunActive ? this._piServerSessionLeaseOwner : undefined);
2210
+ if (!this._isAgentRunActive) {
2211
+ await this._recoverPendingPiServerRunBeforeControlOperation();
1986
2212
  }
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()) {
2213
+ const recoveredCompaction = await this._recoverPendingPiServerCompaction(reason, willRetry);
2214
+ if (recoveredCompaction) {
1997
2215
  this._emit({ type: "compaction_start", reason });
1998
- this._autoCompactionAbortController = new AbortController();
1999
2216
  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
2217
  this._lastServerCompactionTimestamp = Date.now();
2018
- this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
2218
+ this._emit({ type: "compaction_end", reason, result: recoveredCompaction, aborted: false, willRetry });
2019
2219
  return willRetry ? true : this.agent.hasQueuedMessages();
2020
2220
  }
2221
+ if (!model) {
2222
+ return false;
2223
+ }
2224
+ let apiKey;
2225
+ let headers;
2226
+ let env;
2227
+ ({ apiKey, headers, env } = await this._getSummarizationRequestAuth(model));
2021
2228
  const pathEntries = this.sessionManager.getBranch();
2022
2229
  const preparation = preparationOverride ?? prepareCompaction(pathEntries, settings);
2023
2230
  if (!preparation) {
@@ -2026,6 +2233,7 @@ export class AgentSession {
2026
2233
  this._emit({ type: "compaction_start", reason });
2027
2234
  this._autoCompactionAbortController = new AbortController();
2028
2235
  started = true;
2236
+ const signal = this._autoCompactionAbortController.signal;
2029
2237
  let extensionCompaction;
2030
2238
  let fromExtension = false;
2031
2239
  if (this._extensionRunner.hasHandlers("session_before_compact")) {
@@ -2036,7 +2244,7 @@ export class AgentSession {
2036
2244
  customInstructions: undefined,
2037
2245
  reason,
2038
2246
  willRetry,
2039
- signal: this._autoCompactionAbortController.signal,
2247
+ signal,
2040
2248
  }));
2041
2249
  if (extensionResult?.cancel) {
2042
2250
  this._emit({
@@ -2053,6 +2261,36 @@ export class AgentSession {
2053
2261
  fromExtension = true;
2054
2262
  }
2055
2263
  }
2264
+ if (isPiServerMode()) {
2265
+ const sessionFile = this.sessionManager.getSessionFile();
2266
+ if (!sessionFile) {
2267
+ throw new Error("Durable pi-server compaction requires a persisted session file");
2268
+ }
2269
+ const piServerResult = await compactPiServer(model, {
2270
+ systemPrompt: this.systemPrompt,
2271
+ messages: this.agent.state.messages,
2272
+ tools: this.agent.state.tools,
2273
+ }, {
2274
+ sessionId: this.sessionId,
2275
+ apiKey,
2276
+ headers,
2277
+ env,
2278
+ settings,
2279
+ retry: this.settingsManager.getRetrySettings(),
2280
+ preparation: preparationOptions ?? { firstKeptEntryId: preparation.firstKeptEntryId },
2281
+ extensionCompaction,
2282
+ sessionTree: { entries: this.sessionManager.getEntries(), leafId: this.sessionManager.getLeafId() },
2283
+ providerExecutionFingerprint: piServerExecution?.providerExecutionFingerprint,
2284
+ piServerCompactStatePath: getPiServerCompactStatePath(sessionFile),
2285
+ signal,
2286
+ reasoning: toProviderReasoning(this.thinkingLevel),
2287
+ onHistoryReconciled: (snapshot) => this.reconcilePiServerHistory(snapshot),
2288
+ });
2289
+ const result = await this._applyPiServerCompactionResult(piServerResult, reason, willRetry);
2290
+ this._lastServerCompactionTimestamp = Date.now();
2291
+ this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
2292
+ return willRetry ? true : this.agent.hasQueuedMessages();
2293
+ }
2056
2294
  let summary;
2057
2295
  let firstKeptEntryId;
2058
2296
  let tokensBefore;
@@ -2068,7 +2306,7 @@ export class AgentSession {
2068
2306
  }
2069
2307
  else {
2070
2308
  // 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 }));
2309
+ 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
2310
  summary = compactResult.summary;
2073
2311
  firstKeptEntryId = compactResult.firstKeptEntryId;
2074
2312
  tokensBefore = compactResult.tokensBefore;
@@ -2139,6 +2377,7 @@ export class AgentSession {
2139
2377
  return false;
2140
2378
  }
2141
2379
  finally {
2380
+ this._releasePiServerOperationLease(piServerLease.path, piServerLease.acquired, piServerLease.owner);
2142
2381
  this._autoCompactionAbortController = undefined;
2143
2382
  }
2144
2383
  }
@@ -2189,6 +2428,9 @@ export class AgentSession {
2189
2428
  }
2190
2429
  const settings = this.settingsManager.getCompactionSettings();
2191
2430
  const markerParentId = this.sessionManager.getLeafId();
2431
+ if (isPiServerMode() && this.model) {
2432
+ this._assertPiServerProviderExecution(this.model);
2433
+ }
2192
2434
  const markerId = this._appendIntraTurnCompactionMarker();
2193
2435
  const preparationOptions = { firstKeptEntryId: markerId };
2194
2436
  const branchWithMarker = this.sessionManager.getBranch();
@@ -2554,8 +2796,12 @@ export class AgentSession {
2554
2796
  return false;
2555
2797
  if (this._isPostAssistantPiServerSyncFailure(message))
2556
2798
  return false;
2557
- const piServerPhase = getPiServerFailurePhase(message);
2558
- if (piServerPhase && piServerPhase !== "provider_stream")
2799
+ if (isPiServerRestartUncertainty(message))
2800
+ return false;
2801
+ const piServerFailure = getPiServerFailureMetadata(message);
2802
+ if (piServerFailure?.phase && piServerFailure.phase !== "provider_stream")
2803
+ return false;
2804
+ if (piServerFailure?.retryable === false)
2559
2805
  return false;
2560
2806
  if (message.errorMessage && this._isNonRetryableProviderLimitError(message.errorMessage))
2561
2807
  return false;
@@ -2793,6 +3039,9 @@ export class AgentSession {
2793
3039
  * @returns Result with editorText (if user message) and cancelled status
2794
3040
  */
2795
3041
  async navigateTree(targetId, options = {}) {
3042
+ if (isPiServerMode() && options.summarize && this.model) {
3043
+ this._assertPiServerProviderExecution(this.model);
3044
+ }
2796
3045
  const oldLeafId = this.sessionManager.getLeafId();
2797
3046
  // No-op if already at target
2798
3047
  if (targetId === oldLeafId) {
@@ -2822,6 +3071,7 @@ export class AgentSession {
2822
3071
  replaceInstructions,
2823
3072
  label,
2824
3073
  };
3074
+ const piServerLease = this._acquirePiServerOperationLease();
2825
3075
  // Set up abort controller for summarization
2826
3076
  this._branchSummaryAbortController = new AbortController();
2827
3077
  try {
@@ -2869,7 +3119,7 @@ export class AgentSession {
2869
3119
  customInstructions,
2870
3120
  replaceInstructions,
2871
3121
  reserveTokens: branchSummarySettings.reserveTokens,
2872
- streamFn: this.agent.streamFunction,
3122
+ streamFn: this._getAuxiliaryStreamFunction(),
2873
3123
  retry: this.settingsManager.getRetrySettings(),
2874
3124
  callbacks: this._summarizationRetryCallbacks({ source: "branchSummary" }),
2875
3125
  });
@@ -2954,6 +3204,7 @@ export class AgentSession {
2954
3204
  return { editorText, cancelled: false, summaryEntry };
2955
3205
  }
2956
3206
  finally {
3207
+ this._releasePiServerOperationLease(piServerLease.path, piServerLease.acquired, piServerLease.owner);
2957
3208
  this._branchSummaryAbortController = undefined;
2958
3209
  }
2959
3210
  }