@oh-my-pi/pi-coding-agent 13.3.6 → 13.3.8

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 (68) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/package.json +9 -18
  3. package/scripts/format-prompts.ts +7 -172
  4. package/src/capability/mcp.ts +5 -0
  5. package/src/cli/args.ts +1 -0
  6. package/src/config/prompt-templates.ts +9 -55
  7. package/src/config/settings-schema.ts +24 -0
  8. package/src/discovery/builtin.ts +1 -0
  9. package/src/discovery/codex.ts +1 -2
  10. package/src/discovery/helpers.ts +0 -5
  11. package/src/discovery/mcp-json.ts +2 -0
  12. package/src/internal-urls/docs-index.generated.ts +1 -1
  13. package/src/lsp/client.ts +8 -0
  14. package/src/lsp/config.ts +2 -3
  15. package/src/lsp/index.ts +379 -99
  16. package/src/lsp/render.ts +21 -31
  17. package/src/lsp/types.ts +21 -8
  18. package/src/lsp/utils.ts +193 -1
  19. package/src/mcp/config-writer.ts +3 -0
  20. package/src/mcp/config.ts +1 -0
  21. package/src/mcp/oauth-flow.ts +3 -1
  22. package/src/mcp/types.ts +5 -0
  23. package/src/modes/components/settings-defs.ts +9 -0
  24. package/src/modes/components/status-line.ts +1 -1
  25. package/src/modes/controllers/mcp-command-controller.ts +6 -2
  26. package/src/modes/interactive-mode.ts +8 -1
  27. package/src/modes/theme/mermaid-cache.ts +4 -4
  28. package/src/modes/theme/theme.ts +33 -0
  29. package/src/prompts/system/custom-system-prompt.md +0 -10
  30. package/src/prompts/system/subagent-user-prompt.md +2 -0
  31. package/src/prompts/system/system-prompt.md +12 -9
  32. package/src/prompts/tools/ast-find.md +20 -0
  33. package/src/prompts/tools/ast-replace.md +21 -0
  34. package/src/prompts/tools/bash.md +2 -0
  35. package/src/prompts/tools/hashline.md +26 -8
  36. package/src/prompts/tools/lsp.md +22 -5
  37. package/src/prompts/tools/task.md +0 -1
  38. package/src/sdk.ts +11 -5
  39. package/src/session/agent-session.ts +293 -83
  40. package/src/system-prompt.ts +3 -34
  41. package/src/task/executor.ts +8 -7
  42. package/src/task/index.ts +8 -55
  43. package/src/task/template.ts +2 -4
  44. package/src/task/types.ts +0 -5
  45. package/src/task/worktree.ts +6 -2
  46. package/src/tools/ast-find.ts +316 -0
  47. package/src/tools/ast-replace.ts +294 -0
  48. package/src/tools/bash.ts +2 -1
  49. package/src/tools/browser.ts +2 -8
  50. package/src/tools/fetch.ts +55 -18
  51. package/src/tools/index.ts +8 -0
  52. package/src/tools/jtd-to-json-schema.ts +29 -13
  53. package/src/tools/path-utils.ts +34 -0
  54. package/src/tools/python.ts +2 -1
  55. package/src/tools/renderers.ts +4 -0
  56. package/src/tools/ssh.ts +2 -1
  57. package/src/tools/submit-result.ts +143 -44
  58. package/src/tools/todo-write.ts +34 -0
  59. package/src/tools/tool-timeouts.ts +29 -0
  60. package/src/utils/mime.ts +37 -14
  61. package/src/utils/prompt-format.ts +172 -0
  62. package/src/web/scrapers/arxiv.ts +12 -12
  63. package/src/web/scrapers/go-pkg.ts +2 -2
  64. package/src/web/scrapers/iacr.ts +17 -9
  65. package/src/web/scrapers/readthedocs.ts +3 -3
  66. package/src/web/scrapers/twitter.ts +11 -11
  67. package/src/web/scrapers/wikipedia.ts +4 -5
  68. package/src/utils/ignore-files.ts +0 -119
@@ -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 (;;) {
@@ -3844,6 +4023,22 @@ Be thorough - include exact file paths, function names, error messages, and tech
3844
4023
  onChunk?: (chunk: string) => void,
3845
4024
  options?: { excludeFromContext?: boolean },
3846
4025
  ): Promise<BashResult> {
4026
+ const excludeFromContext = options?.excludeFromContext === true;
4027
+ const cwd = this.sessionManager.getCwd();
4028
+
4029
+ if (this.#extensionRunner?.hasHandlers("user_bash")) {
4030
+ const hookResult = await this.#extensionRunner.emitUserBash({
4031
+ type: "user_bash",
4032
+ command,
4033
+ excludeFromContext,
4034
+ cwd,
4035
+ });
4036
+ if (hookResult?.result) {
4037
+ this.recordBashResult(command, hookResult.result, options);
4038
+ return hookResult.result;
4039
+ }
4040
+ }
4041
+
3847
4042
  this.#bashAbortController = new AbortController();
3848
4043
 
3849
4044
  try {
@@ -3942,12 +4137,27 @@ Be thorough - include exact file paths, function names, error messages, and tech
3942
4137
  onChunk?: (chunk: string) => void,
3943
4138
  options?: { excludeFromContext?: boolean },
3944
4139
  ): Promise<PythonResult> {
4140
+ const excludeFromContext = options?.excludeFromContext === true;
4141
+ const cwd = this.sessionManager.getCwd();
4142
+
4143
+ if (this.#extensionRunner?.hasHandlers("user_python")) {
4144
+ const hookResult = await this.#extensionRunner.emitUserPython({
4145
+ type: "user_python",
4146
+ code,
4147
+ excludeFromContext,
4148
+ cwd,
4149
+ });
4150
+ if (hookResult?.result) {
4151
+ this.recordPythonResult(code, hookResult.result, options);
4152
+ return hookResult.result;
4153
+ }
4154
+ }
4155
+
3945
4156
  this.#pythonAbortController = new AbortController();
3946
4157
 
3947
4158
  try {
3948
4159
  // Use the same session ID as the Python tool for kernel sharing
3949
4160
  const sessionFile = this.sessionManager.getSessionFile();
3950
- const cwd = this.sessionManager.getCwd();
3951
4161
  const sessionId = sessionFile ? `session:${sessionFile}:cwd:${cwd}` : `cwd:${cwd}`;
3952
4162
 
3953
4163
  const result = await executePythonCommand(code, {
@@ -16,24 +16,6 @@ import { loadSkills, type Skill } from "./extensibility/skills";
16
16
  import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
17
17
  import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
18
18
 
19
- type PreloadedSkill = { name: string; content: string };
20
-
21
- async function loadPreloadedSkillContents(preloadedSkills: Skill[]): Promise<PreloadedSkill[]> {
22
- const contents = await Promise.all(
23
- preloadedSkills.map(async skill => {
24
- try {
25
- const content = await Bun.file(skill.filePath).text();
26
- return { name: skill.name, content };
27
- } catch (err) {
28
- const message = err instanceof Error ? err.message : String(err);
29
- throw new Error(`Failed to load skill "${skill.name}" from ${skill.filePath}: ${message}`);
30
- }
31
- }),
32
- );
33
-
34
- return contents;
35
- }
36
-
37
19
  function firstNonEmpty(...values: (string | undefined | null)[]): string | null {
38
20
  for (const value of values) {
39
21
  const trimmed = value?.trim();
@@ -350,11 +332,9 @@ export interface BuildSystemPromptOptions {
350
332
  cwd?: string;
351
333
  /** Pre-loaded context files (skips discovery if provided). */
352
334
  contextFiles?: Array<{ path: string; content: string; depth?: number }>;
353
- /** Pre-loaded skills (skips discovery if provided). */
335
+ /** Skills provided directly to system prompt construction. */
354
336
  skills?: Skill[];
355
- /** Skills to inline into the system prompt instead of listing available skills. */
356
- preloadedSkills?: Skill[];
357
- /** Pre-loaded rulebook rules (rules with descriptions, excluding TTSR and always-apply). */
337
+ /** Pre-loaded rulebook rules (descriptions, excluding TTSR and always-apply). */
358
338
  rules?: Array<{ name: string; description?: string; path: string; globs?: string[] }>;
359
339
  /** Intent field name injected into every tool schema. If set, explains the field in the prompt. */
360
340
  intentField?: string;
@@ -378,13 +358,11 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
378
358
  cwd,
379
359
  contextFiles: providedContextFiles,
380
360
  skills: providedSkills,
381
- preloadedSkills: providedPreloadedSkills,
382
361
  rules,
383
362
  intentField,
384
363
  eagerTasks = false,
385
364
  } = options;
386
365
  const resolvedCwd = cwd ?? getProjectDir();
387
- const preloadedSkills = providedPreloadedSkills;
388
366
 
389
367
  const prepPromise = (() => {
390
368
  const systemPromptCustomizationPromise = logger.timeAsync("loadSystemPromptFiles", loadSystemPromptFiles, {
@@ -400,9 +378,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
400
378
  : skillsSettings?.enabled !== false
401
379
  ? loadSkills({ ...skillsSettings, cwd: resolvedCwd }).then(result => result.skills)
402
380
  : Promise.resolve([]);
403
- const preloadedSkillContentsPromise = preloadedSkills
404
- ? logger.timeAsync("loadPreloadedSkills", loadPreloadedSkillContents, preloadedSkills)
405
- : [];
406
381
 
407
382
  return Promise.all([
408
383
  resolvePromptInput(customPrompt, "system prompt"),
@@ -411,7 +386,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
411
386
  contextFilesPromise,
412
387
  agentsMdSearchPromise,
413
388
  skillsPromise,
414
- preloadedSkillContentsPromise,
415
389
  ]).then(
416
390
  ([
417
391
  resolvedCustomPrompt,
@@ -420,7 +394,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
420
394
  contextFiles,
421
395
  agentsMdSearch,
422
396
  skills,
423
- preloadedSkillContents,
424
397
  ]) => ({
425
398
  resolvedCustomPrompt,
426
399
  resolvedAppendPrompt,
@@ -428,7 +401,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
428
401
  contextFiles,
429
402
  agentsMdSearch,
430
403
  skills,
431
- preloadedSkillContents,
432
404
  }),
433
405
  );
434
406
  })();
@@ -451,7 +423,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
451
423
  files: [],
452
424
  };
453
425
  let skills: Skill[] = providedSkills ?? [];
454
- let preloadedSkillContents: PreloadedSkill[] = [];
455
426
 
456
427
  if (prepResult.type === "timeout") {
457
428
  logger.warn("System prompt preparation timed out; using minimal startup context", {
@@ -474,7 +445,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
474
445
  contextFiles = prepResult.value.contextFiles;
475
446
  agentsMdSearch = prepResult.value.agentsMdSearch;
476
447
  skills = prepResult.value.skills;
477
- preloadedSkillContents = prepResult.value.preloadedSkillContents;
478
448
  }
479
449
 
480
450
  const now = new Date();
@@ -517,7 +487,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
517
487
 
518
488
  // Filter skills to only include those with read tool
519
489
  const hasRead = tools?.has("read");
520
- const filteredSkills = preloadedSkills === undefined && hasRead ? skills : [];
490
+ const filteredSkills = hasRead ? skills : [];
521
491
 
522
492
  const environment = await logger.timeAsync("getEnvironmentInfo", getEnvironmentInfo);
523
493
  const data = {
@@ -531,7 +501,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
531
501
  contextFiles,
532
502
  agentsMdSearch,
533
503
  skills: filteredSkills,
534
- preloadedSkills: preloadedSkillContents,
535
504
  rules: rules ?? [],
536
505
  date,
537
506
  dateTime,