@oh-my-pi/pi-coding-agent 13.3.7 → 13.3.9

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 (49) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/package.json +9 -18
  3. package/scripts/format-prompts.ts +7 -172
  4. package/src/config/prompt-templates.ts +2 -54
  5. package/src/config/settings-schema.ts +24 -0
  6. package/src/discovery/codex.ts +1 -2
  7. package/src/discovery/helpers.ts +0 -5
  8. package/src/lsp/client.ts +8 -0
  9. package/src/lsp/config.ts +2 -3
  10. package/src/lsp/index.ts +379 -99
  11. package/src/lsp/render.ts +21 -31
  12. package/src/lsp/types.ts +21 -8
  13. package/src/lsp/utils.ts +193 -1
  14. package/src/mcp/config-writer.ts +3 -0
  15. package/src/modes/components/settings-defs.ts +9 -0
  16. package/src/modes/interactive-mode.ts +8 -1
  17. package/src/modes/theme/mermaid-cache.ts +4 -4
  18. package/src/modes/theme/theme.ts +33 -0
  19. package/src/prompts/system/subagent-user-prompt.md +2 -0
  20. package/src/prompts/system/system-prompt.md +12 -1
  21. package/src/prompts/tools/ast-find.md +20 -0
  22. package/src/prompts/tools/ast-replace.md +21 -0
  23. package/src/prompts/tools/bash.md +2 -0
  24. package/src/prompts/tools/hashline.md +26 -8
  25. package/src/prompts/tools/lsp.md +22 -5
  26. package/src/sdk.ts +11 -1
  27. package/src/session/agent-session.ts +261 -82
  28. package/src/task/executor.ts +8 -5
  29. package/src/tools/ast-find.ts +316 -0
  30. package/src/tools/ast-replace.ts +294 -0
  31. package/src/tools/bash.ts +2 -1
  32. package/src/tools/browser.ts +2 -8
  33. package/src/tools/fetch.ts +55 -18
  34. package/src/tools/index.ts +8 -0
  35. package/src/tools/path-utils.ts +34 -0
  36. package/src/tools/python.ts +2 -1
  37. package/src/tools/renderers.ts +4 -0
  38. package/src/tools/ssh.ts +2 -1
  39. package/src/tools/todo-write.ts +34 -0
  40. package/src/tools/tool-timeouts.ts +29 -0
  41. package/src/utils/mime.ts +37 -14
  42. package/src/utils/prompt-format.ts +172 -0
  43. package/src/web/scrapers/arxiv.ts +12 -12
  44. package/src/web/scrapers/go-pkg.ts +2 -2
  45. package/src/web/scrapers/iacr.ts +17 -9
  46. package/src/web/scrapers/readthedocs.ts +3 -3
  47. package/src/web/scrapers/twitter.ts +11 -11
  48. package/src/web/scrapers/wikipedia.ts +4 -5
  49. package/src/utils/ignore-files.ts +0 -119
@@ -1,16 +1,33 @@
1
1
  Interacts with Language Server Protocol servers for code intelligence.
2
2
 
3
3
  <operations>
4
- - `definition`: Go to symbol definition file path + position
5
- - `references`: Find all referenceslist of locations (file + position)
4
+ - `diagnostics`: Get errors/warnings for file, glob, or entire workspace (no file)
5
+ - `definition`: Go to symbol definition file path + position + 3-line source context
6
+ - `type_definition`: Go to symbol type definition → file path + position + 3-line source context
7
+ - `implementation`: Find concrete implementations → file path + position + 3-line source context
8
+ - `references`: Find references → locations with 3-line source context (first 50), remaining location-only
6
9
  - `hover`: Get type info and documentation → type signature + docs
7
- - `symbols`: List symbols in file, or search workspace (with query, no file) → names, kinds, locations
8
- - `rename`: Rename symbol across codebase → confirmation of changes
9
- - `diagnostics`: Get errors/warnings for file, or check entire project (no file) list with severity + message
10
+ - `symbols`: List symbols in file, or search workspace (with query, no file)
11
+ - `rename`: Rename symbol across codebase → preview or apply edits
12
+ - `code_actions`: List available quick-fixes/refactors/import actions; apply one when `apply: true` and `query` matches title or index
13
+ - `status`: Show active language servers
10
14
  - `reload`: Restart the language server
11
15
  </operations>
12
16
 
17
+ <parameters>
18
+ - `file`: File path; for diagnostics it may be a glob pattern (e.g., `src/**/*.ts`)
19
+ - `line`: 1-indexed line number for position-based actions
20
+ - `symbol`: Substring on the target line used to resolve column automatically
21
+ - `occurrence`: 1-indexed match index when `symbol` appears multiple times on the same line
22
+ - `query`: Symbol search query, code-action kind filter (list mode), or code-action selector (apply mode)
23
+ - `new_name`: Required for rename
24
+ - `apply`: Apply edits for rename/code_actions (default true for rename, list mode for code_actions unless explicitly true)
25
+ - `timeout`: Request timeout in seconds (clamped to 5-60, default 20)
26
+ </parameters>
27
+
13
28
  <caution>
14
29
  - Requires running LSP server for target language
15
30
  - Some operations require file to be saved to disk
31
+ - Diagnostics glob mode samples up to 20 files per request to avoid long-running stalls on broad patterns
32
+ - When `symbol` is provided for position-based actions, missing symbols or out-of-bounds `occurrence` values return an explicit error instead of silently falling back
16
33
  </caution>
package/src/sdk.ts CHANGED
@@ -1295,7 +1295,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1295
1295
  return key;
1296
1296
  },
1297
1297
  cursorExecHandlers,
1298
- transformToolCallArguments: obfuscator?.hasSecrets() ? args => obfuscator!.deobfuscateObject(args) : undefined,
1298
+ transformToolCallArguments: (args, _toolName) => {
1299
+ let result = args;
1300
+ const maxTimeout = settings.get("tools.maxTimeout");
1301
+ if (maxTimeout > 0 && typeof result.timeout === "number") {
1302
+ result = { ...result, timeout: Math.min(result.timeout, maxTimeout) };
1303
+ }
1304
+ if (obfuscator?.hasSecrets()) {
1305
+ result = obfuscator.deobfuscateObject(result);
1306
+ }
1307
+ return result;
1308
+ },
1299
1309
  intentTracing: !!intentField,
1300
1310
  });
1301
1311
  cursorEventEmitter = event => agent.emitExternalEvent(event);
@@ -355,6 +355,13 @@ export class AgentSession {
355
355
  #pendingTtsrInjections: Rule[] = [];
356
356
  #ttsrAbortPending = false;
357
357
  #ttsrRetryToken = 0;
358
+ #ttsrResumePromise: Promise<void> | undefined = undefined;
359
+ #ttsrResumeResolve: (() => void) | undefined = undefined;
360
+ #postPromptTaskCounter = 0;
361
+ #postPromptTaskIds = new Set<number>();
362
+ #postPromptTasksPromise: Promise<void> | undefined = undefined;
363
+ #postPromptTasksResolve: (() => void) | undefined = undefined;
364
+ #postPromptTasksAbortController = new AbortController();
358
365
 
359
366
  #streamingEditAbortTriggered = false;
360
367
  #streamingEditCheckedLineCounts = new Map<string, number>();
@@ -509,6 +516,7 @@ export class AgentSession {
509
516
  if (this.#shouldInterruptForTtsrMatch(matchContext)) {
510
517
  // Abort the stream immediately — do not gate on extension callbacks
511
518
  this.#ttsrAbortPending = true;
519
+ this.#ensureTtsrResumePromise();
512
520
  this.agent.abort();
513
521
  // Notify extensions (fire-and-forget, does not block abort)
514
522
  this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
@@ -517,49 +525,58 @@ export class AgentSession {
517
525
  const generation = this.#promptGeneration;
518
526
  const targetMessageTimestamp =
519
527
  event.message.role === "assistant" ? event.message.timestamp : undefined;
520
- setTimeout(async () => {
521
- if (this.#ttsrRetryToken !== retryToken) {
522
- return;
523
- }
528
+ this.#schedulePostPromptTask(
529
+ async () => {
530
+ if (this.#ttsrRetryToken !== retryToken) {
531
+ this.#resolveTtsrResume();
532
+ return;
533
+ }
524
534
 
525
- const targetAssistantIndex = this.#findTtsrAssistantIndex(targetMessageTimestamp);
526
- if (
527
- !this.#ttsrAbortPending ||
528
- this.#promptGeneration !== generation ||
529
- targetAssistantIndex === -1
530
- ) {
535
+ const targetAssistantIndex = this.#findTtsrAssistantIndex(targetMessageTimestamp);
536
+ if (
537
+ !this.#ttsrAbortPending ||
538
+ this.#promptGeneration !== generation ||
539
+ targetAssistantIndex === -1
540
+ ) {
541
+ this.#ttsrAbortPending = false;
542
+ this.#pendingTtsrInjections = [];
543
+ this.#resolveTtsrResume();
544
+ return;
545
+ }
531
546
  this.#ttsrAbortPending = false;
532
- this.#pendingTtsrInjections = [];
533
- return;
534
- }
535
- this.#ttsrAbortPending = false;
536
- const ttsrSettings = this.#ttsrManager?.getSettings();
537
- if (ttsrSettings?.contextMode === "discard") {
538
- // Remove the partial/aborted assistant turn from agent state
539
- this.agent.replaceMessages(this.agent.state.messages.slice(0, targetAssistantIndex));
540
- }
541
- // Inject TTSR rules as system reminder before retry
542
- const injection = this.#getTtsrInjectionContent();
543
- if (injection) {
544
- const details = { rules: injection.rules.map(rule => rule.name) };
545
- this.agent.appendMessage({
546
- role: "custom",
547
- customType: "ttsr-injection",
548
- content: injection.content,
549
- display: false,
550
- details,
551
- timestamp: Date.now(),
552
- });
553
- this.sessionManager.appendCustomMessageEntry(
554
- "ttsr-injection",
555
- injection.content,
556
- false,
557
- details,
558
- );
559
- this.#markTtsrInjected(details.rules);
560
- }
561
- this.agent.continue().catch(() => {});
562
- }, 50);
547
+ const ttsrSettings = this.#ttsrManager?.getSettings();
548
+ if (ttsrSettings?.contextMode === "discard") {
549
+ // Remove the partial/aborted assistant turn from agent state
550
+ this.agent.replaceMessages(this.agent.state.messages.slice(0, targetAssistantIndex));
551
+ }
552
+ // Inject TTSR rules as system reminder before retry
553
+ const injection = this.#getTtsrInjectionContent();
554
+ if (injection) {
555
+ const details = { rules: injection.rules.map(rule => rule.name) };
556
+ this.agent.appendMessage({
557
+ role: "custom",
558
+ customType: "ttsr-injection",
559
+ content: injection.content,
560
+ display: false,
561
+ details,
562
+ timestamp: Date.now(),
563
+ });
564
+ this.sessionManager.appendCustomMessageEntry(
565
+ "ttsr-injection",
566
+ injection.content,
567
+ false,
568
+ details,
569
+ );
570
+ this.#markTtsrInjected(details.rules);
571
+ }
572
+ try {
573
+ await this.agent.continue();
574
+ } catch {
575
+ this.#resolveTtsrResume();
576
+ }
577
+ },
578
+ { delayMs: 50 },
579
+ );
563
580
  return;
564
581
  }
565
582
  }
@@ -607,6 +624,13 @@ export class AgentSession {
607
624
  if (event.message.role === "assistant") {
608
625
  this.#lastAssistantMessage = event.message;
609
626
  const assistantMsg = event.message as AssistantMessage;
627
+ // Resolve TTSR resume gate before checking for new deferred injections.
628
+ // Gate on #ttsrAbortPending, not stopReason: a non-TTSR abort (e.g. streaming
629
+ // edit) also produces stopReason === "aborted" but has no continuation coming.
630
+ // Only skip when #ttsrAbortPending is true (TTSR continuation is imminent).
631
+ if (!this.#ttsrAbortPending) {
632
+ this.#resolveTtsrResume();
633
+ }
610
634
  this.#queueDeferredTtsrInjectionIfNeeded(assistantMsg);
611
635
  if (this.#handoffAbortController) {
612
636
  this.#skipPostTurnMaintenanceAssistantTimestamp = assistantMsg.timestamp;
@@ -683,8 +707,9 @@ export class AgentSession {
683
707
  if (didRetry) return; // Retry was initiated, don't proceed to compaction
684
708
  }
685
709
 
686
- await this.#checkCompaction(msg);
687
-
710
+ const compactionTask = this.#checkCompaction(msg);
711
+ this.#trackPostPromptTask(compactionTask);
712
+ await compactionTask;
688
713
  // Check for incomplete todos (unless there was an error or abort)
689
714
  if (msg.stopReason !== "error" && msg.stopReason !== "aborted") {
690
715
  await this.#checkTodoCompletion();
@@ -701,6 +726,141 @@ export class AgentSession {
701
726
  }
702
727
  }
703
728
 
729
+ /** Create the TTSR resume gate promise if one doesn't already exist. */
730
+ #ensureTtsrResumePromise(): void {
731
+ if (this.#ttsrResumePromise) return;
732
+ const { promise, resolve } = Promise.withResolvers<void>();
733
+ this.#ttsrResumePromise = promise;
734
+ this.#ttsrResumeResolve = resolve;
735
+ }
736
+
737
+ /** Resolve and clear the TTSR resume gate. */
738
+ #resolveTtsrResume(): void {
739
+ if (!this.#ttsrResumeResolve) return;
740
+ this.#ttsrResumeResolve();
741
+ this.#ttsrResumeResolve = undefined;
742
+ this.#ttsrResumePromise = undefined;
743
+ }
744
+
745
+ #ensurePostPromptTasksPromise(): void {
746
+ if (this.#postPromptTasksPromise) return;
747
+ const { promise, resolve } = Promise.withResolvers<void>();
748
+ this.#postPromptTasksPromise = promise;
749
+ this.#postPromptTasksResolve = resolve;
750
+ }
751
+
752
+ #resolvePostPromptTasks(): void {
753
+ if (!this.#postPromptTasksResolve) return;
754
+ this.#postPromptTasksResolve();
755
+ this.#postPromptTasksResolve = undefined;
756
+ this.#postPromptTasksPromise = undefined;
757
+ }
758
+
759
+ #trackPostPromptTask(task: Promise<void>): void {
760
+ const taskId = ++this.#postPromptTaskCounter;
761
+ this.#postPromptTaskIds.add(taskId);
762
+ this.#ensurePostPromptTasksPromise();
763
+ void task
764
+ .catch(() => {})
765
+ .finally(() => {
766
+ this.#postPromptTaskIds.delete(taskId);
767
+ if (this.#postPromptTaskIds.size === 0) {
768
+ this.#resolvePostPromptTasks();
769
+ }
770
+ });
771
+ }
772
+
773
+ #schedulePostPromptTask(
774
+ task: (signal: AbortSignal) => Promise<void>,
775
+ options?: { delayMs?: number; generation?: number; onSkip?: () => void },
776
+ ): void {
777
+ const delayMs = options?.delayMs ?? 0;
778
+ const signal = this.#postPromptTasksAbortController.signal;
779
+ const scheduled = (async () => {
780
+ if (delayMs > 0) {
781
+ try {
782
+ await abortableSleep(delayMs, signal);
783
+ } catch {
784
+ return;
785
+ }
786
+ }
787
+ if (signal.aborted) {
788
+ options?.onSkip?.();
789
+ return;
790
+ }
791
+ if (options?.generation !== undefined && this.#promptGeneration !== options.generation) {
792
+ options.onSkip?.();
793
+ return;
794
+ }
795
+ await task(signal);
796
+ })();
797
+ this.#trackPostPromptTask(scheduled);
798
+ }
799
+
800
+ #scheduleAgentContinue(options?: {
801
+ delayMs?: number;
802
+ generation?: number;
803
+ shouldContinue?: () => boolean;
804
+ onSkip?: () => void;
805
+ onError?: () => void;
806
+ }): void {
807
+ this.#schedulePostPromptTask(
808
+ async () => {
809
+ if (options?.shouldContinue && !options.shouldContinue()) {
810
+ options.onSkip?.();
811
+ return;
812
+ }
813
+ try {
814
+ await this.agent.continue();
815
+ } catch {
816
+ options?.onError?.();
817
+ }
818
+ },
819
+ {
820
+ delayMs: options?.delayMs,
821
+ generation: options?.generation,
822
+ onSkip: options?.onSkip,
823
+ },
824
+ );
825
+ }
826
+
827
+ #cancelPostPromptTasks(): void {
828
+ this.#postPromptTasksAbortController.abort();
829
+ this.#postPromptTasksAbortController = new AbortController();
830
+ this.#postPromptTaskIds.clear();
831
+ this.#resolvePostPromptTasks();
832
+ }
833
+ /**
834
+ * Wait for retry, TTSR resume, and any background continuation to settle.
835
+ * Loops because a TTSR continuation can trigger a retry (or vice-versa),
836
+ * and fire-and-forget `agent.continue()` may still be streaming after
837
+ * the TTSR resume gate resolves.
838
+ */
839
+ async #waitForPostPromptRecovery(): Promise<void> {
840
+ while (true) {
841
+ if (this.#retryPromise) {
842
+ await this.#retryPromise;
843
+ continue;
844
+ }
845
+ if (this.#ttsrResumePromise) {
846
+ await this.#ttsrResumePromise;
847
+ continue;
848
+ }
849
+ if (this.#postPromptTasksPromise) {
850
+ await this.#postPromptTasksPromise;
851
+ continue;
852
+ }
853
+ // Tracked post-prompt tasks cover deferred continuations scheduled from
854
+ // event handlers. Keep the streaming fallback for direct agent activity
855
+ // outside the scheduler.
856
+ if (this.agent.state.isStreaming) {
857
+ await this.agent.waitForIdle();
858
+ continue;
859
+ }
860
+ break;
861
+ }
862
+ }
863
+
704
864
  /** Get TTSR injection payload and clear pending injections. */
705
865
  #getTtsrInjectionContent(): { content: string; rules: Rule[] } | undefined {
706
866
  if (this.#pendingTtsrInjections.length === 0) return undefined;
@@ -792,14 +952,26 @@ export class AgentSession {
792
952
  details: { rules: injection.rules.map(rule => rule.name) },
793
953
  timestamp: Date.now(),
794
954
  });
955
+ this.#ensureTtsrResumePromise();
795
956
  // Mark as injected after this custom message is delivered and persisted (handled in message_end).
796
957
  // followUp() only enqueues; resume on the next tick once streaming settles.
797
- setTimeout(() => {
798
- if (this.agent.state.isStreaming || !this.agent.hasQueuedMessages()) {
799
- return;
800
- }
801
- this.agent.continue().catch(() => {});
802
- }, 0);
958
+ this.#scheduleAgentContinue({
959
+ delayMs: 1,
960
+ generation: this.#promptGeneration,
961
+ onSkip: () => {
962
+ this.#resolveTtsrResume();
963
+ },
964
+ shouldContinue: () => {
965
+ if (this.agent.state.isStreaming || !this.agent.hasQueuedMessages()) {
966
+ this.#resolveTtsrResume();
967
+ return false;
968
+ }
969
+ return true;
970
+ },
971
+ onError: () => {
972
+ this.#resolveTtsrResume();
973
+ },
974
+ });
803
975
  }
804
976
 
805
977
  /** Build TTSR match context for tool call argument deltas. */
@@ -1271,6 +1443,7 @@ export class AgentSession {
1271
1443
  } catch (error) {
1272
1444
  logger.warn("Failed to emit session_shutdown event", { error: String(error) });
1273
1445
  }
1446
+ this.#cancelPostPromptTasks();
1274
1447
  const drained = await this.#asyncJobManager?.dispose({ timeoutMs: 3_000 });
1275
1448
  const deliveryState = this.#asyncJobManager?.getDeliveryState();
1276
1449
  if (drained === false && deliveryState) {
@@ -1322,6 +1495,16 @@ export class AgentSession {
1322
1495
  return this.agent.state.isStreaming || this.#promptInFlight;
1323
1496
  }
1324
1497
 
1498
+ /** Wait until streaming and deferred recovery work are fully settled. */
1499
+ async waitForIdle(): Promise<void> {
1500
+ await this.agent.waitForIdle();
1501
+ await this.#waitForPostPromptRecovery();
1502
+ }
1503
+
1504
+ /** Most recent assistant message in agent state. */
1505
+ getLastAssistantMessage(): AssistantMessage | undefined {
1506
+ return this.#findLastAssistantMessage();
1507
+ }
1325
1508
  /** Current effective system prompt (includes any per-turn extension modifications) */
1326
1509
  get systemPrompt(): string {
1327
1510
  return this.agent.state.systemPrompt;
@@ -1721,7 +1904,7 @@ export class AgentSession {
1721
1904
  async #promptWithMessage(
1722
1905
  message: AgentMessage,
1723
1906
  expandedText: string,
1724
- options?: Pick<PromptOptions, "toolChoice" | "images">,
1907
+ options?: Pick<PromptOptions, "toolChoice" | "images"> & { skipPostPromptRecoveryWait?: boolean },
1725
1908
  ): Promise<void> {
1726
1909
  this.#promptInFlight = true;
1727
1910
  const generation = this.#promptGeneration;
@@ -1826,7 +2009,9 @@ export class AgentSession {
1826
2009
 
1827
2010
  const agentPromptOptions = options?.toolChoice ? { toolChoice: options.toolChoice } : undefined;
1828
2011
  await this.#promptAgentWithIdleRetry(messages, agentPromptOptions);
1829
- await this.#waitForRetry();
2012
+ if (!options?.skipPostPromptRecoveryWait) {
2013
+ await this.#waitForPostPromptRecovery();
2014
+ }
1830
2015
  } finally {
1831
2016
  this.#promptInFlight = false;
1832
2017
  }
@@ -1886,7 +2071,7 @@ export class AgentSession {
1886
2071
  },
1887
2072
  hasQueuedMessages: () => this.queuedMessageCount > 0,
1888
2073
  getContextUsage: () => this.getContextUsage(),
1889
- waitForIdle: () => this.agent.waitForIdle(),
2074
+ waitForIdle: () => this.waitForIdle(),
1890
2075
  newSession: async options => {
1891
2076
  const success = await this.newSession({ parentSession: options?.parentSession });
1892
2077
  if (!success) {
@@ -2217,6 +2402,8 @@ export class AgentSession {
2217
2402
  async abort(): Promise<void> {
2218
2403
  this.abortRetry();
2219
2404
  this.#promptGeneration++;
2405
+ this.#resolveTtsrResume();
2406
+ this.#cancelPostPromptTasks();
2220
2407
  this.agent.abort();
2221
2408
  await this.agent.waitForIdle();
2222
2409
  // Clear promptInFlight: waitForIdle resolves when the agent loop's finally
@@ -3004,6 +3191,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
3004
3191
  // Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
3005
3192
  if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return;
3006
3193
  const contextWindow = this.model?.contextWindow ?? 0;
3194
+ const generation = this.#promptGeneration;
3007
3195
  // Skip overflow check if the message came from a different model.
3008
3196
  // This handles the case where user switched from a smaller-context model (e.g. opus)
3009
3197
  // to a larger-context model (e.g. codex) - the overflow error from the old model
@@ -3029,9 +3217,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
3029
3217
  const promoted = await this.#tryContextPromotion(assistantMessage);
3030
3218
  if (promoted) {
3031
3219
  // Retry on the promoted (larger) model without compacting
3032
- setTimeout(() => {
3033
- this.agent.continue().catch(() => {});
3034
- }, 100);
3220
+ this.#scheduleAgentContinue({ delayMs: 100, generation });
3035
3221
  return;
3036
3222
  }
3037
3223
 
@@ -3132,7 +3318,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
3132
3318
  content: [{ type: "text", text: reminder }],
3133
3319
  timestamp: Date.now(),
3134
3320
  });
3135
- this.agent.continue().catch(() => {});
3321
+ this.#scheduleAgentContinue({ generation: this.#promptGeneration });
3136
3322
  }
3137
3323
 
3138
3324
  /**
@@ -3302,6 +3488,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
3302
3488
  async #runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise<void> {
3303
3489
  const compactionSettings = this.settings.getGroup("compaction");
3304
3490
 
3491
+ const generation = this.#promptGeneration;
3305
3492
  await this.#emitSessionEvent({ type: "auto_compaction_start", reason });
3306
3493
  // Properly abort and null existing controller before replacing
3307
3494
  if (this.#autoCompactionAbortController) {
@@ -3541,10 +3728,15 @@ Be thorough - include exact file paths, function names, error messages, and tech
3541
3728
  await this.#emitSessionEvent({ type: "auto_compaction_end", result, aborted: false, willRetry });
3542
3729
 
3543
3730
  if (!willRetry && compactionSettings.autoContinue !== false) {
3544
- await this.prompt("Continue if you have next steps.", {
3545
- expandPromptTemplates: false,
3546
- synthetic: true,
3547
- });
3731
+ await this.#promptWithMessage(
3732
+ {
3733
+ role: "developer",
3734
+ content: [{ type: "text", text: "Continue if you have next steps." }],
3735
+ timestamp: Date.now(),
3736
+ },
3737
+ "Continue if you have next steps.",
3738
+ { skipPostPromptRecoveryWait: true },
3739
+ );
3548
3740
  }
3549
3741
 
3550
3742
  if (willRetry) {
@@ -3554,15 +3746,15 @@ Be thorough - include exact file paths, function names, error messages, and tech
3554
3746
  this.agent.replaceMessages(messages.slice(0, -1));
3555
3747
  }
3556
3748
 
3557
- setTimeout(() => {
3558
- this.agent.continue().catch(() => {});
3559
- }, 100);
3749
+ this.#scheduleAgentContinue({ delayMs: 100, generation });
3560
3750
  } else if (this.agent.hasQueuedMessages()) {
3561
3751
  // Auto-compaction can complete while follow-up/steering/custom messages are waiting.
3562
3752
  // Kick the loop so queued messages are actually delivered.
3563
- setTimeout(() => {
3564
- this.agent.continue().catch(() => {});
3565
- }, 100);
3753
+ this.#scheduleAgentContinue({
3754
+ delayMs: 100,
3755
+ generation,
3756
+ shouldContinue: () => this.agent.hasQueuedMessages(),
3757
+ });
3566
3758
  }
3567
3759
  } catch (error) {
3568
3760
  if (this.#autoCompactionAbortController?.signal.aborted) {
@@ -3685,6 +3877,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
3685
3877
  const retrySettings = this.settings.getGroup("retry");
3686
3878
  if (!retrySettings.enabled) return false;
3687
3879
 
3880
+ const generation = this.#promptGeneration;
3688
3881
  this.#retryAttempt++;
3689
3882
 
3690
3883
  // Create retry promise on first attempt so waitForRetry() can await it
@@ -3764,12 +3957,8 @@ Be thorough - include exact file paths, function names, error messages, and tech
3764
3957
  }
3765
3958
  this.#retryAbortController = undefined;
3766
3959
 
3767
- // Retry via continue() - use setTimeout to break out of event handler chain
3768
- setTimeout(() => {
3769
- this.agent.continue().catch(() => {
3770
- // Retry failed - will be caught by next agent_end
3771
- });
3772
- }, 0);
3960
+ // Retry via continue() outside the agent_end event callback chain.
3961
+ this.#scheduleAgentContinue({ delayMs: 1, generation });
3773
3962
 
3774
3963
  return true;
3775
3964
  }
@@ -3783,16 +3972,6 @@ Be thorough - include exact file paths, function names, error messages, and tech
3783
3972
  this.#resolveRetry();
3784
3973
  }
3785
3974
 
3786
- /**
3787
- * Wait for any in-progress retry to complete.
3788
- * Returns immediately if no retry is in progress.
3789
- */
3790
- async #waitForRetry(): Promise<void> {
3791
- if (this.#retryPromise) {
3792
- await this.#retryPromise;
3793
- }
3794
- }
3795
-
3796
3975
  async #promptAgentWithIdleRetry(messages: AgentMessage[], options?: { toolChoice?: ToolChoice }): Promise<void> {
3797
3976
  const deadline = Date.now() + 30_000;
3798
3977
  for (;;) {
@@ -1070,6 +1070,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1070
1070
  });
1071
1071
 
1072
1072
  await session.prompt(task);
1073
+ await session.waitForIdle();
1073
1074
 
1074
1075
  const reminderToolChoice = buildSubmitResultToolChoice(session.model);
1075
1076
 
@@ -1083,6 +1084,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1083
1084
  });
1084
1085
 
1085
1086
  await session.prompt(reminder, reminderToolChoice ? { toolChoice: reminderToolChoice } : undefined);
1087
+ await session.waitForIdle();
1086
1088
  } catch (err) {
1087
1089
  logger.error("Subagent prompt failed", {
1088
1090
  error: err instanceof Error ? err.message : String(err),
@@ -1090,20 +1092,21 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1090
1092
  }
1091
1093
  }
1092
1094
 
1095
+ await session.waitForIdle();
1093
1096
  if (!submitResultCalled && !abortSignal.aborted) {
1094
1097
  aborted = true;
1095
1098
  exitCode = 1;
1096
1099
  error ??= SUBAGENT_WARNING_MISSING_SUBMIT_RESULT;
1097
1100
  }
1098
1101
 
1099
- const lastMessage = session.state.messages[session.state.messages.length - 1];
1100
- if (lastMessage?.role === "assistant") {
1101
- if (lastMessage.stopReason === "aborted") {
1102
+ const lastAssistant = session.getLastAssistantMessage();
1103
+ if (lastAssistant) {
1104
+ if (lastAssistant.stopReason === "aborted") {
1102
1105
  aborted = abortReason === "signal" || abortReason === undefined;
1103
1106
  exitCode = 1;
1104
- } else if (lastMessage.stopReason === "error") {
1107
+ } else if (lastAssistant.stopReason === "error") {
1105
1108
  exitCode = 1;
1106
- error ??= lastMessage.errorMessage || "Subagent failed";
1109
+ error ??= lastAssistant.errorMessage || "Subagent failed";
1107
1110
  }
1108
1111
  }
1109
1112
  } catch (err) {