@averyyy/pi-coding-agent 0.85.1-piclient.1 → 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";
@@ -74,9 +74,6 @@ function estimateMessagesTokens(messages) {
74
74
  // ============================================================================
75
75
  const INTRA_TURN_COMPACTION_MARKER_TYPE = "pi:intra-turn-compaction";
76
76
  const INTRA_TURN_COMPACTION_MARKER_TEXT = "Continue from the compaction summary above. The previous tool loop was compacted before the next provider request; use the summary's Operational State, file lists, failures, last command, and next steps.";
77
- const VALIDATION_HINT_MESSAGE_TYPE = "pi:validation-hint";
78
- const VALIDATION_HINT_FILE_LIMIT = 12;
79
- const VALIDATION_HINT_FAILURE_CHAR_LIMIT = 1200;
80
77
  const AUTO_SESSION_NAME_INPUT_CHAR_LIMIT = 4000;
81
78
  const AUTO_SESSION_NAME_MAX_CHARS = 80;
82
79
  const AUTO_SESSION_NAME_PROMPT = "Name this coding session from the transcript below. Return only a concise title, in the user's language when clear. No quotes, no markdown, no explanation. Do not call tools.";
@@ -86,12 +83,6 @@ function isPiServerMode() {
86
83
  function toProviderReasoning(thinkingLevel) {
87
84
  return thinkingLevel === "off" ? undefined : thinkingLevel;
88
85
  }
89
- function getStringField(input, key) {
90
- if (!input || typeof input !== "object" || Array.isArray(input))
91
- return undefined;
92
- const value = input[key];
93
- return typeof value === "string" && value.trim().length > 0 ? value : undefined;
94
- }
95
86
  function getPiServerFailurePhase(message) {
96
87
  for (const diagnostic of message.diagnostics ?? []) {
97
88
  if (diagnostic.type !== "pi_server_failure")
@@ -112,19 +103,6 @@ function getPiServerFailureRetryable(message) {
112
103
  }
113
104
  return undefined;
114
105
  }
115
- function toolTextContent(content) {
116
- return content
117
- .filter((part) => part.type === "text")
118
- .map((part) => part.text)
119
- .join("\n")
120
- .trim();
121
- }
122
- function compactValidationSummary(text) {
123
- const trimmed = text.trim();
124
- if (trimmed.length <= VALIDATION_HINT_FAILURE_CHAR_LIMIT)
125
- return trimmed;
126
- return `...${trimmed.slice(trimmed.length - VALIDATION_HINT_FAILURE_CHAR_LIMIT + 3)}`;
127
- }
128
106
  function messageTextContent(message) {
129
107
  if (!("content" in message))
130
108
  return "";
@@ -174,6 +152,10 @@ export class AgentSession {
174
152
  _isAgentRunActive = false;
175
153
  _idleWaitPromise;
176
154
  _resolveIdleWait;
155
+ _abortPromise;
156
+ _isAborting = false;
157
+ _abortError;
158
+ _continueAfterAbortRequested = false;
177
159
  /** Tracks pending steering messages for UI display. Removed when delivered. */
178
160
  _steeringMessages = [];
179
161
  /** Tracks pending follow-up messages for UI display. Removed when delivered. */
@@ -185,18 +167,23 @@ export class AgentSession {
185
167
  // Compaction state
186
168
  _compactionAbortController = undefined;
187
169
  _autoCompactionAbortController = undefined;
170
+ _compactionCompletionPromise = undefined;
171
+ _prePromptAbortController = undefined;
172
+ _prePromptCompletionPromise = undefined;
188
173
  _overflowRecoveryAttempted = false;
189
174
  _lastServerCompactionTimestamp = undefined;
190
175
  // Branch summarization state
191
176
  _branchSummaryAbortController = undefined;
177
+ _branchSummaryCompletionPromise = undefined;
192
178
  // Retry state
193
179
  _retryAbortController = undefined;
194
180
  _retryAttempt = 0;
181
+ // Post-turn session naming is a provider request too, so cancellation must reach it.
182
+ _sessionNameAbortController = undefined;
195
183
  // Bash execution state
196
184
  _bashAbortControllers = new Set();
185
+ _bashCompletionPromises = new Set();
197
186
  _pendingBashMessages = [];
198
- _validationModifiedFiles = new Set();
199
- _validationFailure = undefined;
200
187
  // Extension system
201
188
  _extensionRunner;
202
189
  _turnIndex = 0;
@@ -320,71 +307,6 @@ export class AgentSession {
320
307
  return { model };
321
308
  }
322
309
  }
323
- _resetValidationHintState() {
324
- this._validationModifiedFiles.clear();
325
- this._validationFailure = undefined;
326
- }
327
- _recordValidationToolResult(toolName, input, content, isError) {
328
- if ((toolName === "edit" || toolName === "write") && !isError) {
329
- const path = getStringField(input, "path") ?? getStringField(input, "file_path");
330
- if (path) {
331
- this._validationModifiedFiles.add(path);
332
- }
333
- return;
334
- }
335
- if (toolName !== "bash") {
336
- return;
337
- }
338
- const command = getStringField(input, "command");
339
- if (!command) {
340
- return;
341
- }
342
- if (isError) {
343
- this._validationFailure = {
344
- command,
345
- summary: compactValidationSummary(toolTextContent(content)),
346
- };
347
- }
348
- else if (this._validationFailure?.command === command) {
349
- this._resetValidationHintState();
350
- }
351
- else if (!this._validationFailure) {
352
- this._validationModifiedFiles.clear();
353
- }
354
- }
355
- _createValidationHintMessage() {
356
- const modifiedFiles = Array.from(this._validationModifiedFiles).slice(-VALIDATION_HINT_FILE_LIMIT);
357
- if (modifiedFiles.length === 0 && !this._validationFailure) {
358
- return undefined;
359
- }
360
- const lines = ["<validation-hint>"];
361
- if (modifiedFiles.length > 0) {
362
- lines.push("Recent modified files:", ...modifiedFiles.map((file) => `- ${file}`));
363
- }
364
- if (this._validationFailure) {
365
- lines.push("Recent failed command:", this._validationFailure.command, "Failure summary:", this._validationFailure.summary || "(no output)", "Suggested next validation command:", this._validationFailure.command);
366
- }
367
- else {
368
- lines.push("Suggested next step:", "Run the smallest project validation that covers the modified files.");
369
- }
370
- lines.push("Inspect failures, fix the root cause, then rerun validation before finalizing.", "</validation-hint>");
371
- return {
372
- role: "custom",
373
- customType: VALIDATION_HINT_MESSAGE_TYPE,
374
- content: [{ type: "text", text: lines.join("\n") }],
375
- display: false,
376
- details: { modifiedFiles, failedCommand: this._validationFailure?.command },
377
- timestamp: Date.now(),
378
- };
379
- }
380
- _withValidationHint(context) {
381
- const messages = context.messages.filter((message) => !(message.role === "custom" && message.customType === VALIDATION_HINT_MESSAGE_TYPE));
382
- const hint = this._createValidationHintMessage();
383
- return {
384
- ...context,
385
- messages: hint ? [...messages, hint] : messages,
386
- };
387
- }
388
310
  /**
389
311
  * Install tool hooks once on the Agent instance.
390
312
  *
@@ -433,8 +355,6 @@ export class AgentSession {
433
355
  const normalizedContent = await normalizeToolResultImages(content, {
434
356
  autoResizeImages: this.settingsManager.getImageAutoResize(),
435
357
  });
436
- const finalIsError = hookResult?.isError ?? isError;
437
- this._recordValidationToolResult(toolCall.name, args, normalizedContent, finalIsError);
438
358
  if (!hookResult && normalizedContent === content) {
439
359
  return undefined;
440
360
  }
@@ -458,11 +378,11 @@ export class AgentSession {
458
378
  const previousContext = previousSnapshot?.context ?? nextTurn.context;
459
379
  return {
460
380
  ...previousSnapshot,
461
- context: this._withValidationHint({
381
+ context: {
462
382
  ...previousContext,
463
383
  systemPrompt: this._systemPromptOverride ?? this._baseSystemPrompt,
464
384
  tools: this.agent.state.tools.slice(),
465
- }),
385
+ },
466
386
  model: this.agent.state.model,
467
387
  thinkingLevel: this.agent.state.thinkingLevel,
468
388
  };
@@ -834,6 +754,14 @@ export class AgentSession {
834
754
  get isStreaming() {
835
755
  return this._isAgentRunActive;
836
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
+ }
837
765
  /** Whether the session has no active agent run, compaction, branch summary, retry, or queued continuation. */
838
766
  get isIdle() {
839
767
  return !this._isAgentRunActive && !this.isCompacting;
@@ -987,12 +915,16 @@ export class AgentSession {
987
915
  // =========================================================================
988
916
  // Prompting
989
917
  // =========================================================================
990
- async _runAgentPrompt(messages) {
991
- this._resetValidationHintState();
918
+ async _runAgentOperation(start) {
992
919
  this._isAgentRunActive = true;
993
920
  try {
994
- await this.agent.prompt(messages);
921
+ await start();
995
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
+ }
996
928
  try {
997
929
  await this.agent.continue();
998
930
  }
@@ -1010,7 +942,9 @@ export class AgentSession {
1010
942
  throw error;
1011
943
  }
1012
944
  }
1013
- await this._maybeGenerateSessionName();
945
+ if (!this._isAborting) {
946
+ await this._maybeGenerateSessionName();
947
+ }
1014
948
  }
1015
949
  finally {
1016
950
  this._systemPromptOverride = undefined;
@@ -1019,6 +953,9 @@ export class AgentSession {
1019
953
  await this._emitAgentSettled();
1020
954
  }
1021
955
  }
956
+ async _runAgentPrompt(messages) {
957
+ await this._runAgentOperation(() => this.agent.prompt(messages));
958
+ }
1022
959
  _sessionHasInfoEntry() {
1023
960
  return this.sessionManager.getEntries().some((entry) => entry.type === "session_info");
1024
961
  }
@@ -1052,6 +989,7 @@ export class AgentSession {
1052
989
  const transcript = this._buildSessionNameTranscript();
1053
990
  if (!model || !transcript)
1054
991
  return;
992
+ this._sessionNameAbortController = new AbortController();
1055
993
  try {
1056
994
  const stream = await this.agent.streamFunction(model, {
1057
995
  systemPrompt: this.systemPrompt,
@@ -1070,6 +1008,7 @@ export class AgentSession {
1070
1008
  transport: this.agent.transport,
1071
1009
  thinkingBudgets: this.agent.thinkingBudgets,
1072
1010
  maxRetryDelayMs: this.agent.maxRetryDelayMs,
1011
+ signal: this._sessionNameAbortController.signal,
1073
1012
  });
1074
1013
  for await (const _event of stream) {
1075
1014
  // Drain the title stream without surfacing it as a chat message.
@@ -1084,6 +1023,9 @@ export class AgentSession {
1084
1023
  catch {
1085
1024
  // Session naming is a post-turn convenience; the completed assistant turn stays authoritative.
1086
1025
  }
1026
+ finally {
1027
+ this._sessionNameAbortController = undefined;
1028
+ }
1087
1029
  }
1088
1030
  async _handlePostAgentRun() {
1089
1031
  const msg = this._lastAssistantMessage;
@@ -1091,9 +1033,18 @@ export class AgentSession {
1091
1033
  if (!msg) {
1092
1034
  return false;
1093
1035
  }
1036
+ if (this._isAborting) {
1037
+ if (isPiServerMode() && this._isTerminalAssistantFailure(msg)) {
1038
+ this._detachPiServerTerminalAssistantFailure();
1039
+ }
1040
+ return false;
1041
+ }
1094
1042
  if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {
1095
1043
  return true;
1096
1044
  }
1045
+ if (this._isAborting) {
1046
+ return false;
1047
+ }
1097
1048
  if (msg.stopReason === "error" && this._retryAttempt > 0) {
1098
1049
  this._emit({
1099
1050
  type: "auto_retry_end",
@@ -1137,7 +1088,7 @@ export class AgentSession {
1137
1088
  return;
1138
1089
  }
1139
1090
  }
1140
- if (this._compactionAbortController !== undefined) {
1091
+ if (this._compactionAbortController !== undefined && !this._isAborting) {
1141
1092
  throw new Error("Cannot submit a prompt while compaction is in progress. Wait for compaction to finish and retry.");
1142
1093
  }
1143
1094
  // Emit input event for extension interception (before skill/template expansion)
@@ -1161,7 +1112,7 @@ export class AgentSession {
1161
1112
  expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
1162
1113
  }
1163
1114
  // If streaming, queue via steer() or followUp() based on option
1164
- if (this.isStreaming) {
1115
+ if (this.isStreaming || this._isAborting) {
1165
1116
  if (!options?.streamingBehavior) {
1166
1117
  throw new Error("Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.");
1167
1118
  }
@@ -1194,9 +1145,35 @@ export class AgentSession {
1194
1145
  }
1195
1146
  // Check if we need to compact before sending (catches aborted responses).
1196
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
+ }
1197
1152
  const lastAssistant = this._findLastAssistantMessage();
1198
1153
  if (lastAssistant) {
1199
- 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
+ }
1200
1177
  }
1201
1178
  if (isPiServerMode()) {
1202
1179
  this._detachPiServerTerminalAssistantFailure();
@@ -1362,6 +1339,9 @@ export class AgentSession {
1362
1339
  content,
1363
1340
  timestamp: Date.now(),
1364
1341
  });
1342
+ if (this._isAborting) {
1343
+ this._continueAfterAbortRequested = true;
1344
+ }
1365
1345
  }
1366
1346
  /**
1367
1347
  * Internal: Queue a follow-up message (already expanded, no extension command check).
@@ -1378,6 +1358,9 @@ export class AgentSession {
1378
1358
  content,
1379
1359
  timestamp: Date.now(),
1380
1360
  });
1361
+ if (this._isAborting) {
1362
+ this._continueAfterAbortRequested = true;
1363
+ }
1381
1364
  }
1382
1365
  /**
1383
1366
  * Throw an error if the text is an extension command.
@@ -1416,12 +1399,18 @@ export class AgentSession {
1416
1399
  if (options?.deliverAs === "nextTurn") {
1417
1400
  this._pendingNextTurnMessages.push(appMessage);
1418
1401
  }
1419
- else if (this.isStreaming && options?.triggerTurn !== false) {
1402
+ else if ((this.isStreaming || this._isAborting) && options?.triggerTurn !== false) {
1420
1403
  if (options?.deliverAs === "followUp") {
1421
1404
  this.agent.followUp(appMessage);
1405
+ if (this._isAborting) {
1406
+ this._continueAfterAbortRequested = true;
1407
+ }
1422
1408
  }
1423
1409
  else {
1424
1410
  this.agent.steer(appMessage);
1411
+ if (this._isAborting) {
1412
+ this._continueAfterAbortRequested = true;
1413
+ }
1425
1414
  }
1426
1415
  }
1427
1416
  else if (options?.triggerTurn) {
@@ -1523,15 +1512,79 @@ export class AgentSession {
1523
1512
  get resourceLoader() {
1524
1513
  return this._resourceLoader;
1525
1514
  }
1526
- /**
1527
- * Abort current operation and wait for agent to become idle.
1528
- */
1529
- 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() {
1530
1528
  this.abortRetry();
1531
1529
  this.abortCompaction();
1532
1530
  this.abortBranchSummary();
1531
+ this.abortBash();
1532
+ this._prePromptAbortController?.abort();
1533
+ this._sessionNameAbortController?.abort();
1533
1534
  this.agent.abort();
1534
- 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;
1535
1588
  }
1536
1589
  async waitForIdle() {
1537
1590
  if (this.isIdle) {
@@ -1798,8 +1851,13 @@ export class AgentSession {
1798
1851
  async compact(customInstructions) {
1799
1852
  await this.abort();
1800
1853
  this._compactionAbortController = new AbortController();
1801
- this._emit({ type: "compaction_start", reason: "manual" });
1854
+ let resolveCompactionCompletion;
1855
+ const compactionCompletion = new Promise((resolve) => {
1856
+ resolveCompactionCompletion = resolve;
1857
+ });
1858
+ this._compactionCompletionPromise = compactionCompletion;
1802
1859
  try {
1860
+ this._emit({ type: "compaction_start", reason: "manual" });
1803
1861
  if (!this.model) {
1804
1862
  throw new Error(formatNoModelSelectedMessage());
1805
1863
  }
@@ -1941,6 +1999,11 @@ export class AgentSession {
1941
1999
  }
1942
2000
  finally {
1943
2001
  this._clearManualCompactionState();
2002
+ this._compactionAbortController = undefined;
2003
+ if (this._compactionCompletionPromise === compactionCompletion) {
2004
+ this._compactionCompletionPromise = undefined;
2005
+ }
2006
+ resolveCompactionCompletion?.();
1944
2007
  }
1945
2008
  }
1946
2009
  /**
@@ -1968,7 +2031,9 @@ export class AgentSession {
1968
2031
  * @param assistantMessage The assistant message to check
1969
2032
  * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
1970
2033
  */
1971
- async _checkCompaction(assistantMessage, skipAbortedCheck = true) {
2034
+ async _checkCompaction(assistantMessage, skipAbortedCheck = true, externalSignal) {
2035
+ if (externalSignal?.aborted)
2036
+ return false;
1972
2037
  const settings = this.settingsManager.getCompactionSettings();
1973
2038
  if (!settings.enabled)
1974
2039
  return false;
@@ -2001,7 +2066,7 @@ export class AgentSession {
2001
2066
  if (contextOverflow || recoverableLength) {
2002
2067
  const willRetry = assistantMessage.stopReason !== "stop";
2003
2068
  if (!willRetry) {
2004
- return await this._runAutoCompaction("overflow", false);
2069
+ return await this._runAutoCompaction("overflow", false, undefined, undefined, externalSignal);
2005
2070
  }
2006
2071
  if (this._overflowRecoveryAttempted) {
2007
2072
  const errorMessage = contextOverflow
@@ -2032,7 +2097,7 @@ export class AgentSession {
2032
2097
  this.agent.state.messages = messages.slice(0, -1);
2033
2098
  }
2034
2099
  }
2035
- return await this._runAutoCompaction("overflow", willRetry, terminalFailurePreparation);
2100
+ return await this._runAutoCompaction("overflow", willRetry, terminalFailurePreparation, undefined, externalSignal);
2036
2101
  }
2037
2102
  // Case 2: Threshold - context is getting large
2038
2103
  // For error messages or all-zero usage messages, estimate from the last valid response.
@@ -2060,25 +2125,33 @@ export class AgentSession {
2060
2125
  contextTokens = directContextTokens;
2061
2126
  }
2062
2127
  if (shouldCompact(contextTokens, contextWindow, settings)) {
2063
- return await this._runAutoCompaction("threshold", false);
2128
+ return await this._runAutoCompaction("threshold", false, undefined, undefined, externalSignal);
2064
2129
  }
2065
2130
  return false;
2066
2131
  }
2067
2132
  /**
2068
2133
  * Internal: Run auto-compaction with events.
2069
2134
  */
2070
- async _runAutoCompaction(reason, willRetry, preparationOverride, preparationOptions) {
2135
+ async _runAutoCompaction(reason, willRetry, preparationOverride, preparationOptions, externalSignal) {
2071
2136
  const settings = this.settingsManager.getCompactionSettings();
2072
2137
  let started = false;
2073
2138
  let fromExtension = false;
2139
+ if (externalSignal?.aborted)
2140
+ return false;
2141
+ const abortExternal = () => this._autoCompactionAbortController?.abort();
2142
+ externalSignal?.addEventListener("abort", abortExternal, { once: true });
2074
2143
  try {
2075
2144
  if (!this.model) {
2076
2145
  return false;
2077
2146
  }
2078
2147
  const { model: requestModel, apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model);
2148
+ if (externalSignal?.aborted)
2149
+ return false;
2079
2150
  if (isPiServerMode()) {
2080
2151
  this._emit({ type: "compaction_start", reason });
2081
2152
  this._autoCompactionAbortController = new AbortController();
2153
+ if (externalSignal?.aborted)
2154
+ this._autoCompactionAbortController.abort();
2082
2155
  started = true;
2083
2156
  const signal = this._autoCompactionAbortController.signal;
2084
2157
  const piServerResult = await compactPiServer(requestModel, {
@@ -2096,18 +2169,26 @@ export class AgentSession {
2096
2169
  reasoning: toProviderReasoning(this.thinkingLevel),
2097
2170
  onHistoryReconciled: (snapshot) => this.reconcilePiServerHistory(snapshot),
2098
2171
  });
2172
+ if (externalSignal?.aborted || signal.aborted) {
2173
+ this._emit({ type: "compaction_end", reason, result: undefined, aborted: true, willRetry: false });
2174
+ return false;
2175
+ }
2099
2176
  const result = await this._applyPiServerCompactionResult(piServerResult, reason, willRetry);
2100
2177
  this._lastServerCompactionTimestamp = Date.now();
2101
2178
  this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
2102
2179
  return willRetry ? true : this.agent.hasQueuedMessages();
2103
2180
  }
2104
2181
  const pathEntries = this.sessionManager.getBranch();
2182
+ if (externalSignal?.aborted)
2183
+ return false;
2105
2184
  const preparation = preparationOverride ?? prepareCompaction(pathEntries, settings);
2106
2185
  if (!preparation) {
2107
2186
  return false;
2108
2187
  }
2109
2188
  this._emit({ type: "compaction_start", reason });
2110
2189
  this._autoCompactionAbortController = new AbortController();
2190
+ if (externalSignal?.aborted)
2191
+ this._autoCompactionAbortController.abort();
2111
2192
  started = true;
2112
2193
  let extensionCompaction;
2113
2194
  if (this._extensionRunner.hasHandlers("session_before_compact")) {
@@ -2163,7 +2244,7 @@ export class AgentSession {
2163
2244
  usage = compactResult.usage;
2164
2245
  details = compactResult.details;
2165
2246
  }
2166
- if (this._autoCompactionAbortController.signal.aborted) {
2247
+ if (externalSignal?.aborted || this._autoCompactionAbortController.signal.aborted) {
2167
2248
  this._emit({
2168
2249
  type: "compaction_end",
2169
2250
  reason,
@@ -2246,6 +2327,7 @@ export class AgentSession {
2246
2327
  return false;
2247
2328
  }
2248
2329
  finally {
2330
+ externalSignal?.removeEventListener("abort", abortExternal);
2249
2331
  this._autoCompactionAbortController = undefined;
2250
2332
  this._resolveIdleWaitIfIdle();
2251
2333
  }
@@ -2806,6 +2888,11 @@ export class AgentSession {
2806
2888
  async executeBash(command, onChunk, options) {
2807
2889
  const abortController = new AbortController();
2808
2890
  this._bashAbortControllers.add(abortController);
2891
+ let resolveBashCompletion;
2892
+ const bashCompletion = new Promise((resolve) => {
2893
+ resolveBashCompletion = resolve;
2894
+ });
2895
+ this._bashCompletionPromises.add(bashCompletion);
2809
2896
  // Apply command prefix if configured (e.g., "shopt -s expand_aliases" for alias support)
2810
2897
  const prefix = this.settingsManager.getShellCommandPrefix();
2811
2898
  const shellPath = this.settingsManager.getShellPath();
@@ -2823,6 +2910,8 @@ export class AgentSession {
2823
2910
  }
2824
2911
  finally {
2825
2912
  this._bashAbortControllers.delete(abortController);
2913
+ this._bashCompletionPromises.delete(bashCompletion);
2914
+ resolveBashCompletion?.();
2826
2915
  }
2827
2916
  }
2828
2917
  /**
@@ -2945,6 +3034,11 @@ export class AgentSession {
2945
3034
  };
2946
3035
  // Set up abort controller for summarization
2947
3036
  this._branchSummaryAbortController = new AbortController();
3037
+ let resolveBranchSummaryCompletion;
3038
+ const branchSummaryCompletion = new Promise((resolve) => {
3039
+ resolveBranchSummaryCompletion = resolve;
3040
+ });
3041
+ this._branchSummaryCompletionPromise = branchSummaryCompletion;
2948
3042
  try {
2949
3043
  let extensionSummary;
2950
3044
  let fromExtension = false;
@@ -3069,6 +3163,10 @@ export class AgentSession {
3069
3163
  }
3070
3164
  finally {
3071
3165
  this._branchSummaryAbortController = undefined;
3166
+ if (this._branchSummaryCompletionPromise === branchSummaryCompletion) {
3167
+ this._branchSummaryCompletionPromise = undefined;
3168
+ }
3169
+ resolveBranchSummaryCompletion?.();
3072
3170
  this._resolveIdleWaitIfIdle();
3073
3171
  }
3074
3172
  }