@averyyy/pi-coding-agent 0.85.1-piclient.2 → 0.85.1-piclient.3

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.
@@ -30,7 +30,7 @@ import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
30
30
  import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
31
31
  import { emitSessionShutdownEvent } from "./extensions/runner.js";
32
32
  import { ModelRegistry } from "./model-registry.js";
33
- import { compactPiServer } from "./pi-server-client.js";
33
+ import { cancelPiServerOperations, compactPiServer, } from "./pi-server-client.js";
34
34
  import { expandPromptTemplate } from "./prompt-templates.js";
35
35
  import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
36
36
  import { createSyntheticSourceInfo } from "./source-info.js";
@@ -152,6 +152,10 @@ export class AgentSession {
152
152
  _isAgentRunActive = false;
153
153
  _idleWaitPromise;
154
154
  _resolveIdleWait;
155
+ _abortPromise;
156
+ _isAborting = false;
157
+ _abortError;
158
+ _continueAfterAbortRequested = false;
155
159
  /** Tracks pending steering messages for UI display. Removed when delivered. */
156
160
  _steeringMessages = [];
157
161
  /** Tracks pending follow-up messages for UI display. Removed when delivered. */
@@ -163,15 +167,22 @@ export class AgentSession {
163
167
  // Compaction state
164
168
  _compactionAbortController = undefined;
165
169
  _autoCompactionAbortController = undefined;
170
+ _compactionCompletionPromise = undefined;
171
+ _prePromptAbortController = undefined;
172
+ _prePromptCompletionPromise = undefined;
166
173
  _overflowRecoveryAttempted = false;
167
174
  _lastServerCompactionTimestamp = undefined;
168
175
  // Branch summarization state
169
176
  _branchSummaryAbortController = undefined;
177
+ _branchSummaryCompletionPromise = undefined;
170
178
  // Retry state
171
179
  _retryAbortController = undefined;
172
180
  _retryAttempt = 0;
181
+ // Post-turn session naming is a provider request too, so cancellation must reach it.
182
+ _sessionNameAbortController = undefined;
173
183
  // Bash execution state
174
184
  _bashAbortControllers = new Set();
185
+ _bashCompletionPromises = new Set();
175
186
  _pendingBashMessages = [];
176
187
  // Extension system
177
188
  _extensionRunner;
@@ -743,6 +754,14 @@ export class AgentSession {
743
754
  get isStreaming() {
744
755
  return this._isAgentRunActive;
745
756
  }
757
+ /** Whether the current run is being cancelled. New queued intent is drained after cancellation. */
758
+ get isAborting() {
759
+ return this._isAborting;
760
+ }
761
+ /** Error from the most recent cancellation attempt, if it failed. */
762
+ get abortError() {
763
+ return this._abortError;
764
+ }
746
765
  /** Whether the session has no active agent run, compaction, branch summary, retry, or queued continuation. */
747
766
  get isIdle() {
748
767
  return !this._isAgentRunActive && !this.isCompacting;
@@ -896,11 +915,16 @@ export class AgentSession {
896
915
  // =========================================================================
897
916
  // Prompting
898
917
  // =========================================================================
899
- async _runAgentPrompt(messages) {
918
+ async _runAgentOperation(start) {
900
919
  this._isAgentRunActive = true;
901
920
  try {
902
- await this.agent.prompt(messages);
921
+ await start();
903
922
  while (await this._handlePostAgentRun()) {
923
+ // Esc cancels the current agent run. Keep messages queued during that
924
+ // cancellation for the single deferred continuation in abort().
925
+ if (this._isAborting) {
926
+ break;
927
+ }
904
928
  try {
905
929
  await this.agent.continue();
906
930
  }
@@ -918,7 +942,9 @@ export class AgentSession {
918
942
  throw error;
919
943
  }
920
944
  }
921
- await this._maybeGenerateSessionName();
945
+ if (!this._isAborting) {
946
+ await this._maybeGenerateSessionName();
947
+ }
922
948
  }
923
949
  finally {
924
950
  this._systemPromptOverride = undefined;
@@ -927,6 +953,9 @@ export class AgentSession {
927
953
  await this._emitAgentSettled();
928
954
  }
929
955
  }
956
+ async _runAgentPrompt(messages) {
957
+ await this._runAgentOperation(() => this.agent.prompt(messages));
958
+ }
930
959
  _sessionHasInfoEntry() {
931
960
  return this.sessionManager.getEntries().some((entry) => entry.type === "session_info");
932
961
  }
@@ -960,6 +989,7 @@ export class AgentSession {
960
989
  const transcript = this._buildSessionNameTranscript();
961
990
  if (!model || !transcript)
962
991
  return;
992
+ this._sessionNameAbortController = new AbortController();
963
993
  try {
964
994
  const stream = await this.agent.streamFunction(model, {
965
995
  systemPrompt: this.systemPrompt,
@@ -978,6 +1008,7 @@ export class AgentSession {
978
1008
  transport: this.agent.transport,
979
1009
  thinkingBudgets: this.agent.thinkingBudgets,
980
1010
  maxRetryDelayMs: this.agent.maxRetryDelayMs,
1011
+ signal: this._sessionNameAbortController.signal,
981
1012
  });
982
1013
  for await (const _event of stream) {
983
1014
  // Drain the title stream without surfacing it as a chat message.
@@ -992,6 +1023,9 @@ export class AgentSession {
992
1023
  catch {
993
1024
  // Session naming is a post-turn convenience; the completed assistant turn stays authoritative.
994
1025
  }
1026
+ finally {
1027
+ this._sessionNameAbortController = undefined;
1028
+ }
995
1029
  }
996
1030
  async _handlePostAgentRun() {
997
1031
  const msg = this._lastAssistantMessage;
@@ -999,9 +1033,18 @@ export class AgentSession {
999
1033
  if (!msg) {
1000
1034
  return false;
1001
1035
  }
1036
+ if (this._isAborting) {
1037
+ if (isPiServerMode() && this._isTerminalAssistantFailure(msg)) {
1038
+ this._detachPiServerTerminalAssistantFailure();
1039
+ }
1040
+ return false;
1041
+ }
1002
1042
  if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {
1003
1043
  return true;
1004
1044
  }
1045
+ if (this._isAborting) {
1046
+ return false;
1047
+ }
1005
1048
  if (msg.stopReason === "error" && this._retryAttempt > 0) {
1006
1049
  this._emit({
1007
1050
  type: "auto_retry_end",
@@ -1045,7 +1088,7 @@ export class AgentSession {
1045
1088
  return;
1046
1089
  }
1047
1090
  }
1048
- if (this._compactionAbortController !== undefined) {
1091
+ if (this._compactionAbortController !== undefined && !this._isAborting) {
1049
1092
  throw new Error("Cannot submit a prompt while compaction is in progress. Wait for compaction to finish and retry.");
1050
1093
  }
1051
1094
  // Emit input event for extension interception (before skill/template expansion)
@@ -1069,7 +1112,7 @@ export class AgentSession {
1069
1112
  expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
1070
1113
  }
1071
1114
  // If streaming, queue via steer() or followUp() based on option
1072
- if (this.isStreaming) {
1115
+ if (this.isStreaming || this._isAborting) {
1073
1116
  if (!options?.streamingBehavior) {
1074
1117
  throw new Error("Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.");
1075
1118
  }
@@ -1102,9 +1145,35 @@ export class AgentSession {
1102
1145
  }
1103
1146
  // Check if we need to compact before sending (catches aborted responses).
1104
1147
  // The user's new prompt is sent below, so do not call agent.continue() here.
1148
+ if (this._isAborting) {
1149
+ preflightResult?.(false);
1150
+ return;
1151
+ }
1105
1152
  const lastAssistant = this._findLastAssistantMessage();
1106
1153
  if (lastAssistant) {
1107
- await this._checkCompaction(lastAssistant, false);
1154
+ const prePromptAbortController = new AbortController();
1155
+ let resolvePrePromptCompletion;
1156
+ const prePromptCompletion = new Promise((resolve) => {
1157
+ resolvePrePromptCompletion = resolve;
1158
+ });
1159
+ this._prePromptAbortController = prePromptAbortController;
1160
+ this._prePromptCompletionPromise = prePromptCompletion;
1161
+ try {
1162
+ await this._checkCompaction(lastAssistant, false, prePromptAbortController.signal);
1163
+ }
1164
+ finally {
1165
+ if (this._prePromptAbortController === prePromptAbortController) {
1166
+ this._prePromptAbortController = undefined;
1167
+ }
1168
+ if (this._prePromptCompletionPromise === prePromptCompletion) {
1169
+ this._prePromptCompletionPromise = undefined;
1170
+ }
1171
+ resolvePrePromptCompletion?.();
1172
+ }
1173
+ if (prePromptAbortController.signal.aborted || this._isAborting) {
1174
+ preflightResult?.(false);
1175
+ return;
1176
+ }
1108
1177
  }
1109
1178
  if (isPiServerMode()) {
1110
1179
  this._detachPiServerTerminalAssistantFailure();
@@ -1270,6 +1339,9 @@ export class AgentSession {
1270
1339
  content,
1271
1340
  timestamp: Date.now(),
1272
1341
  });
1342
+ if (this._isAborting) {
1343
+ this._continueAfterAbortRequested = true;
1344
+ }
1273
1345
  }
1274
1346
  /**
1275
1347
  * Internal: Queue a follow-up message (already expanded, no extension command check).
@@ -1286,6 +1358,9 @@ export class AgentSession {
1286
1358
  content,
1287
1359
  timestamp: Date.now(),
1288
1360
  });
1361
+ if (this._isAborting) {
1362
+ this._continueAfterAbortRequested = true;
1363
+ }
1289
1364
  }
1290
1365
  /**
1291
1366
  * Throw an error if the text is an extension command.
@@ -1324,12 +1399,18 @@ export class AgentSession {
1324
1399
  if (options?.deliverAs === "nextTurn") {
1325
1400
  this._pendingNextTurnMessages.push(appMessage);
1326
1401
  }
1327
- else if (this.isStreaming && options?.triggerTurn !== false) {
1402
+ else if ((this.isStreaming || this._isAborting) && options?.triggerTurn !== false) {
1328
1403
  if (options?.deliverAs === "followUp") {
1329
1404
  this.agent.followUp(appMessage);
1405
+ if (this._isAborting) {
1406
+ this._continueAfterAbortRequested = true;
1407
+ }
1330
1408
  }
1331
1409
  else {
1332
1410
  this.agent.steer(appMessage);
1411
+ if (this._isAborting) {
1412
+ this._continueAfterAbortRequested = true;
1413
+ }
1333
1414
  }
1334
1415
  }
1335
1416
  else if (options?.triggerTurn) {
@@ -1431,15 +1512,79 @@ export class AgentSession {
1431
1512
  get resourceLoader() {
1432
1513
  return this._resourceLoader;
1433
1514
  }
1434
- /**
1435
- * Abort current operation and wait for agent to become idle.
1436
- */
1437
- async abort() {
1515
+ _startQueuedAgentRunAfterAbort() {
1516
+ if (this._isAgentRunActive || this._isAborting || !this.agent.hasQueuedMessages()) {
1517
+ return;
1518
+ }
1519
+ void this._runAgentOperation(() => this.agent.continue()).catch((error) => {
1520
+ this._extensionRunner.emitError({
1521
+ extensionPath: "<runtime>",
1522
+ event: "abort_continuation",
1523
+ error: error instanceof Error ? error.message : String(error),
1524
+ });
1525
+ });
1526
+ }
1527
+ async _performAbort() {
1438
1528
  this.abortRetry();
1439
1529
  this.abortCompaction();
1440
1530
  this.abortBranchSummary();
1531
+ this.abortBash();
1532
+ this._prePromptAbortController?.abort();
1533
+ this._sessionNameAbortController?.abort();
1441
1534
  this.agent.abort();
1442
- await this.waitForIdle();
1535
+ // _runAgentOperation owns the settled event and the idle waiter. Do not
1536
+ // start a queued continuation until that cleanup has completed.
1537
+ try {
1538
+ const cleanupPromises = [
1539
+ this.waitForIdle(),
1540
+ ...(this._compactionCompletionPromise ? [this._compactionCompletionPromise] : []),
1541
+ ...(this._prePromptCompletionPromise ? [this._prePromptCompletionPromise] : []),
1542
+ ...(this._branchSummaryCompletionPromise ? [this._branchSummaryCompletionPromise] : []),
1543
+ ...this._bashCompletionPromises,
1544
+ ];
1545
+ if (isPiServerMode()) {
1546
+ cleanupPromises.push(cancelPiServerOperations(this.sessionId));
1547
+ }
1548
+ const cleanupResults = await Promise.allSettled(cleanupPromises);
1549
+ const cleanupFailure = cleanupResults.find((result) => result.status === "rejected");
1550
+ if (cleanupFailure?.status === "rejected") {
1551
+ throw cleanupFailure.reason;
1552
+ }
1553
+ }
1554
+ catch (error) {
1555
+ this._abortError = error instanceof Error ? error.message : String(error);
1556
+ this._abortPromise = undefined;
1557
+ this._emit({ type: "abort_error", errorMessage: this._abortError });
1558
+ throw error;
1559
+ }
1560
+ this._isAborting = false;
1561
+ const continueAfterAbort = this._continueAfterAbortRequested && this.agent.hasQueuedMessages();
1562
+ this._continueAfterAbortRequested = false;
1563
+ this._abortError = undefined;
1564
+ this._abortPromise = undefined;
1565
+ this._emit({ type: "abort_end" });
1566
+ if (continueAfterAbort) {
1567
+ this._startQueuedAgentRunAfterAbort();
1568
+ }
1569
+ }
1570
+ /**
1571
+ * Abort the current operation and wait for cancellation cleanup to finish.
1572
+ * Messages queued while cancellation is in progress are sent in one deferred continuation.
1573
+ */
1574
+ abort() {
1575
+ if (this._abortPromise) {
1576
+ return this._abortPromise;
1577
+ }
1578
+ if (this._isAborting) {
1579
+ this._abortError = undefined;
1580
+ }
1581
+ else {
1582
+ this._isAborting = true;
1583
+ }
1584
+ const abortPromise = this._performAbort();
1585
+ this._abortPromise = abortPromise;
1586
+ this._emit({ type: "abort_start" });
1587
+ return abortPromise;
1443
1588
  }
1444
1589
  async waitForIdle() {
1445
1590
  if (this.isIdle) {
@@ -1706,8 +1851,13 @@ export class AgentSession {
1706
1851
  async compact(customInstructions) {
1707
1852
  await this.abort();
1708
1853
  this._compactionAbortController = new AbortController();
1709
- this._emit({ type: "compaction_start", reason: "manual" });
1854
+ let resolveCompactionCompletion;
1855
+ const compactionCompletion = new Promise((resolve) => {
1856
+ resolveCompactionCompletion = resolve;
1857
+ });
1858
+ this._compactionCompletionPromise = compactionCompletion;
1710
1859
  try {
1860
+ this._emit({ type: "compaction_start", reason: "manual" });
1711
1861
  if (!this.model) {
1712
1862
  throw new Error(formatNoModelSelectedMessage());
1713
1863
  }
@@ -1849,6 +1999,11 @@ export class AgentSession {
1849
1999
  }
1850
2000
  finally {
1851
2001
  this._clearManualCompactionState();
2002
+ this._compactionAbortController = undefined;
2003
+ if (this._compactionCompletionPromise === compactionCompletion) {
2004
+ this._compactionCompletionPromise = undefined;
2005
+ }
2006
+ resolveCompactionCompletion?.();
1852
2007
  }
1853
2008
  }
1854
2009
  /**
@@ -1876,7 +2031,9 @@ export class AgentSession {
1876
2031
  * @param assistantMessage The assistant message to check
1877
2032
  * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
1878
2033
  */
1879
- async _checkCompaction(assistantMessage, skipAbortedCheck = true) {
2034
+ async _checkCompaction(assistantMessage, skipAbortedCheck = true, externalSignal) {
2035
+ if (externalSignal?.aborted)
2036
+ return false;
1880
2037
  const settings = this.settingsManager.getCompactionSettings();
1881
2038
  if (!settings.enabled)
1882
2039
  return false;
@@ -1909,7 +2066,7 @@ export class AgentSession {
1909
2066
  if (contextOverflow || recoverableLength) {
1910
2067
  const willRetry = assistantMessage.stopReason !== "stop";
1911
2068
  if (!willRetry) {
1912
- return await this._runAutoCompaction("overflow", false);
2069
+ return await this._runAutoCompaction("overflow", false, undefined, undefined, externalSignal);
1913
2070
  }
1914
2071
  if (this._overflowRecoveryAttempted) {
1915
2072
  const errorMessage = contextOverflow
@@ -1940,7 +2097,7 @@ export class AgentSession {
1940
2097
  this.agent.state.messages = messages.slice(0, -1);
1941
2098
  }
1942
2099
  }
1943
- return await this._runAutoCompaction("overflow", willRetry, terminalFailurePreparation);
2100
+ return await this._runAutoCompaction("overflow", willRetry, terminalFailurePreparation, undefined, externalSignal);
1944
2101
  }
1945
2102
  // Case 2: Threshold - context is getting large
1946
2103
  // For error messages or all-zero usage messages, estimate from the last valid response.
@@ -1968,25 +2125,33 @@ export class AgentSession {
1968
2125
  contextTokens = directContextTokens;
1969
2126
  }
1970
2127
  if (shouldCompact(contextTokens, contextWindow, settings)) {
1971
- return await this._runAutoCompaction("threshold", false);
2128
+ return await this._runAutoCompaction("threshold", false, undefined, undefined, externalSignal);
1972
2129
  }
1973
2130
  return false;
1974
2131
  }
1975
2132
  /**
1976
2133
  * Internal: Run auto-compaction with events.
1977
2134
  */
1978
- async _runAutoCompaction(reason, willRetry, preparationOverride, preparationOptions) {
2135
+ async _runAutoCompaction(reason, willRetry, preparationOverride, preparationOptions, externalSignal) {
1979
2136
  const settings = this.settingsManager.getCompactionSettings();
1980
2137
  let started = false;
1981
2138
  let fromExtension = false;
2139
+ if (externalSignal?.aborted)
2140
+ return false;
2141
+ const abortExternal = () => this._autoCompactionAbortController?.abort();
2142
+ externalSignal?.addEventListener("abort", abortExternal, { once: true });
1982
2143
  try {
1983
2144
  if (!this.model) {
1984
2145
  return false;
1985
2146
  }
1986
2147
  const { model: requestModel, apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model);
2148
+ if (externalSignal?.aborted)
2149
+ return false;
1987
2150
  if (isPiServerMode()) {
1988
2151
  this._emit({ type: "compaction_start", reason });
1989
2152
  this._autoCompactionAbortController = new AbortController();
2153
+ if (externalSignal?.aborted)
2154
+ this._autoCompactionAbortController.abort();
1990
2155
  started = true;
1991
2156
  const signal = this._autoCompactionAbortController.signal;
1992
2157
  const piServerResult = await compactPiServer(requestModel, {
@@ -2004,18 +2169,26 @@ export class AgentSession {
2004
2169
  reasoning: toProviderReasoning(this.thinkingLevel),
2005
2170
  onHistoryReconciled: (snapshot) => this.reconcilePiServerHistory(snapshot),
2006
2171
  });
2172
+ if (externalSignal?.aborted || signal.aborted) {
2173
+ this._emit({ type: "compaction_end", reason, result: undefined, aborted: true, willRetry: false });
2174
+ return false;
2175
+ }
2007
2176
  const result = await this._applyPiServerCompactionResult(piServerResult, reason, willRetry);
2008
2177
  this._lastServerCompactionTimestamp = Date.now();
2009
2178
  this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
2010
2179
  return willRetry ? true : this.agent.hasQueuedMessages();
2011
2180
  }
2012
2181
  const pathEntries = this.sessionManager.getBranch();
2182
+ if (externalSignal?.aborted)
2183
+ return false;
2013
2184
  const preparation = preparationOverride ?? prepareCompaction(pathEntries, settings);
2014
2185
  if (!preparation) {
2015
2186
  return false;
2016
2187
  }
2017
2188
  this._emit({ type: "compaction_start", reason });
2018
2189
  this._autoCompactionAbortController = new AbortController();
2190
+ if (externalSignal?.aborted)
2191
+ this._autoCompactionAbortController.abort();
2019
2192
  started = true;
2020
2193
  let extensionCompaction;
2021
2194
  if (this._extensionRunner.hasHandlers("session_before_compact")) {
@@ -2071,7 +2244,7 @@ export class AgentSession {
2071
2244
  usage = compactResult.usage;
2072
2245
  details = compactResult.details;
2073
2246
  }
2074
- if (this._autoCompactionAbortController.signal.aborted) {
2247
+ if (externalSignal?.aborted || this._autoCompactionAbortController.signal.aborted) {
2075
2248
  this._emit({
2076
2249
  type: "compaction_end",
2077
2250
  reason,
@@ -2154,6 +2327,7 @@ export class AgentSession {
2154
2327
  return false;
2155
2328
  }
2156
2329
  finally {
2330
+ externalSignal?.removeEventListener("abort", abortExternal);
2157
2331
  this._autoCompactionAbortController = undefined;
2158
2332
  this._resolveIdleWaitIfIdle();
2159
2333
  }
@@ -2714,6 +2888,11 @@ export class AgentSession {
2714
2888
  async executeBash(command, onChunk, options) {
2715
2889
  const abortController = new AbortController();
2716
2890
  this._bashAbortControllers.add(abortController);
2891
+ let resolveBashCompletion;
2892
+ const bashCompletion = new Promise((resolve) => {
2893
+ resolveBashCompletion = resolve;
2894
+ });
2895
+ this._bashCompletionPromises.add(bashCompletion);
2717
2896
  // Apply command prefix if configured (e.g., "shopt -s expand_aliases" for alias support)
2718
2897
  const prefix = this.settingsManager.getShellCommandPrefix();
2719
2898
  const shellPath = this.settingsManager.getShellPath();
@@ -2731,6 +2910,8 @@ export class AgentSession {
2731
2910
  }
2732
2911
  finally {
2733
2912
  this._bashAbortControllers.delete(abortController);
2913
+ this._bashCompletionPromises.delete(bashCompletion);
2914
+ resolveBashCompletion?.();
2734
2915
  }
2735
2916
  }
2736
2917
  /**
@@ -2853,6 +3034,11 @@ export class AgentSession {
2853
3034
  };
2854
3035
  // Set up abort controller for summarization
2855
3036
  this._branchSummaryAbortController = new AbortController();
3037
+ let resolveBranchSummaryCompletion;
3038
+ const branchSummaryCompletion = new Promise((resolve) => {
3039
+ resolveBranchSummaryCompletion = resolve;
3040
+ });
3041
+ this._branchSummaryCompletionPromise = branchSummaryCompletion;
2856
3042
  try {
2857
3043
  let extensionSummary;
2858
3044
  let fromExtension = false;
@@ -2977,6 +3163,10 @@ export class AgentSession {
2977
3163
  }
2978
3164
  finally {
2979
3165
  this._branchSummaryAbortController = undefined;
3166
+ if (this._branchSummaryCompletionPromise === branchSummaryCompletion) {
3167
+ this._branchSummaryCompletionPromise = undefined;
3168
+ }
3169
+ resolveBranchSummaryCompletion?.();
2980
3170
  this._resolveIdleWaitIfIdle();
2981
3171
  }
2982
3172
  }