@oh-my-pi/pi-coding-agent 13.19.0 → 14.0.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.
Files changed (205) hide show
  1. package/CHANGELOG.md +277 -2
  2. package/package.json +86 -20
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +91 -0
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +83 -125
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -5
  28. package/src/commit/agentic/index.ts +3 -4
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/validation.ts +1 -1
  31. package/src/commit/analysis/conventional.ts +4 -4
  32. package/src/commit/analysis/summary.ts +3 -3
  33. package/src/commit/changelog/generate.ts +4 -4
  34. package/src/commit/map-reduce/map-phase.ts +4 -4
  35. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  36. package/src/commit/pipeline.ts +3 -4
  37. package/src/config/model-registry.ts +17 -3
  38. package/src/config/prompt-templates.ts +44 -226
  39. package/src/config/resolve-config-value.ts +4 -2
  40. package/src/config/settings-schema.ts +54 -2
  41. package/src/config/settings.ts +25 -26
  42. package/src/dap/client.ts +674 -0
  43. package/src/dap/config.ts +150 -0
  44. package/src/dap/defaults.json +211 -0
  45. package/src/dap/index.ts +4 -0
  46. package/src/dap/session.ts +1255 -0
  47. package/src/dap/types.ts +600 -0
  48. package/src/debug/log-viewer.ts +3 -2
  49. package/src/discovery/builtin.ts +1 -2
  50. package/src/discovery/codex.ts +2 -2
  51. package/src/discovery/github.ts +2 -1
  52. package/src/discovery/helpers.ts +2 -2
  53. package/src/discovery/opencode.ts +2 -2
  54. package/src/edit/diff.ts +818 -0
  55. package/src/edit/index.ts +309 -0
  56. package/src/edit/line-hash.ts +67 -0
  57. package/src/edit/modes/chunk.ts +454 -0
  58. package/src/{patch → edit/modes}/hashline.ts +741 -361
  59. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  60. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  61. package/src/{patch → edit}/normalize.ts +97 -76
  62. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  63. package/src/exec/bash-executor.ts +4 -2
  64. package/src/exec/idle-timeout-watchdog.ts +126 -0
  65. package/src/exec/non-interactive-env.ts +5 -0
  66. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +2 -2
  67. package/src/extensibility/custom-commands/bundled/review/index.ts +36 -15
  68. package/src/extensibility/custom-commands/loader.ts +1 -2
  69. package/src/extensibility/custom-tools/loader.ts +34 -11
  70. package/src/extensibility/extensions/loader.ts +9 -4
  71. package/src/extensibility/extensions/runner.ts +24 -1
  72. package/src/extensibility/extensions/types.ts +1 -1
  73. package/src/extensibility/hooks/loader.ts +5 -6
  74. package/src/extensibility/hooks/types.ts +1 -1
  75. package/src/extensibility/plugins/doctor.ts +2 -1
  76. package/src/extensibility/slash-commands.ts +3 -7
  77. package/src/index.ts +2 -1
  78. package/src/internal-urls/docs-index.generated.ts +11 -11
  79. package/src/ipy/executor.ts +58 -17
  80. package/src/ipy/gateway-coordinator.ts +6 -4
  81. package/src/ipy/kernel.ts +45 -22
  82. package/src/ipy/runtime.ts +2 -2
  83. package/src/lsp/client.ts +7 -4
  84. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  85. package/src/lsp/config.ts +20 -4
  86. package/src/lsp/defaults.json +688 -154
  87. package/src/lsp/index.ts +234 -45
  88. package/src/lsp/lspmux.ts +2 -2
  89. package/src/lsp/startup-events.ts +13 -0
  90. package/src/lsp/types.ts +12 -1
  91. package/src/lsp/utils.ts +8 -1
  92. package/src/main.ts +102 -46
  93. package/src/memories/index.ts +4 -5
  94. package/src/modes/acp/acp-agent.ts +563 -163
  95. package/src/modes/acp/acp-event-mapper.ts +9 -1
  96. package/src/modes/acp/acp-mode.ts +4 -2
  97. package/src/modes/components/agent-dashboard.ts +3 -4
  98. package/src/modes/components/diff.ts +6 -7
  99. package/src/modes/components/read-tool-group.ts +6 -12
  100. package/src/modes/components/session-observer-overlay.ts +21 -12
  101. package/src/modes/components/settings-defs.ts +5 -0
  102. package/src/modes/components/tool-execution.ts +1 -1
  103. package/src/modes/components/welcome.ts +1 -1
  104. package/src/modes/controllers/btw-controller.ts +2 -2
  105. package/src/modes/controllers/command-controller.ts +3 -2
  106. package/src/modes/controllers/input-controller.ts +12 -8
  107. package/src/modes/index.ts +20 -2
  108. package/src/modes/interactive-mode.ts +94 -37
  109. package/src/modes/rpc/host-tools.ts +186 -0
  110. package/src/modes/rpc/rpc-client.ts +178 -13
  111. package/src/modes/rpc/rpc-mode.ts +73 -3
  112. package/src/modes/rpc/rpc-types.ts +53 -1
  113. package/src/modes/theme/theme.ts +80 -8
  114. package/src/modes/types.ts +2 -2
  115. package/src/prompts/review-request.md +6 -0
  116. package/src/prompts/system/system-prompt.md +2 -1
  117. package/src/prompts/tools/chunk-edit.md +223 -0
  118. package/src/prompts/tools/debug.md +43 -0
  119. package/src/prompts/tools/grep.md +3 -0
  120. package/src/prompts/tools/lsp.md +5 -5
  121. package/src/prompts/tools/read-chunk.md +17 -0
  122. package/src/prompts/tools/read.md +19 -5
  123. package/src/sdk.ts +190 -154
  124. package/src/secrets/obfuscator.ts +1 -1
  125. package/src/session/agent-session.ts +306 -256
  126. package/src/session/agent-storage.ts +12 -12
  127. package/src/session/compaction/branch-summarization.ts +3 -3
  128. package/src/session/compaction/compaction.ts +5 -6
  129. package/src/session/compaction/utils.ts +3 -3
  130. package/src/session/history-storage.ts +62 -19
  131. package/src/session/messages.ts +3 -3
  132. package/src/session/session-dump-format.ts +203 -0
  133. package/src/session/session-storage.ts +4 -2
  134. package/src/session/streaming-output.ts +1 -1
  135. package/src/session/tool-choice-queue.ts +213 -0
  136. package/src/slash-commands/builtin-registry.ts +56 -8
  137. package/src/ssh/connection-manager.ts +2 -2
  138. package/src/ssh/sshfs-mount.ts +5 -5
  139. package/src/stt/downloader.ts +4 -4
  140. package/src/stt/recorder.ts +4 -4
  141. package/src/stt/transcriber.ts +2 -2
  142. package/src/system-prompt.ts +21 -13
  143. package/src/task/agents.ts +5 -6
  144. package/src/task/commands.ts +2 -5
  145. package/src/task/executor.ts +4 -4
  146. package/src/task/index.ts +3 -4
  147. package/src/task/template.ts +2 -2
  148. package/src/task/worktree.ts +4 -4
  149. package/src/tools/ask.ts +2 -3
  150. package/src/tools/ast-edit.ts +7 -7
  151. package/src/tools/ast-grep.ts +7 -7
  152. package/src/tools/auto-generated-guard.ts +36 -41
  153. package/src/tools/await-tool.ts +2 -2
  154. package/src/tools/bash.ts +5 -23
  155. package/src/tools/browser.ts +4 -5
  156. package/src/tools/calculator.ts +2 -3
  157. package/src/tools/cancel-job.ts +2 -2
  158. package/src/tools/checkpoint.ts +3 -3
  159. package/src/tools/debug.ts +1007 -0
  160. package/src/tools/exit-plan-mode.ts +2 -3
  161. package/src/tools/fetch.ts +67 -3
  162. package/src/tools/find.ts +4 -5
  163. package/src/tools/fs-cache-invalidation.ts +5 -0
  164. package/src/tools/gemini-image.ts +13 -5
  165. package/src/tools/gh.ts +10 -11
  166. package/src/tools/grep.ts +57 -9
  167. package/src/tools/index.ts +44 -22
  168. package/src/tools/inspect-image.ts +4 -4
  169. package/src/tools/output-meta.ts +1 -1
  170. package/src/tools/python.ts +19 -6
  171. package/src/tools/read.ts +198 -67
  172. package/src/tools/render-mermaid.ts +2 -3
  173. package/src/tools/render-utils.ts +20 -6
  174. package/src/tools/renderers.ts +3 -1
  175. package/src/tools/report-tool-issue.ts +80 -0
  176. package/src/tools/resolve.ts +70 -39
  177. package/src/tools/search-tool-bm25.ts +2 -2
  178. package/src/tools/ssh.ts +2 -2
  179. package/src/tools/todo-write.ts +2 -2
  180. package/src/tools/tool-timeouts.ts +1 -0
  181. package/src/tools/write.ts +5 -6
  182. package/src/tui/tree-list.ts +3 -1
  183. package/src/utils/clipboard.ts +80 -0
  184. package/src/utils/commit-message-generator.ts +2 -3
  185. package/src/utils/edit-mode.ts +49 -0
  186. package/src/utils/file-display-mode.ts +6 -5
  187. package/src/utils/file-mentions.ts +8 -7
  188. package/src/utils/git.ts +4 -4
  189. package/src/utils/image-loading.ts +98 -0
  190. package/src/utils/title-generator.ts +2 -3
  191. package/src/utils/tools-manager.ts +6 -6
  192. package/src/web/scrapers/choosealicense.ts +1 -1
  193. package/src/web/search/index.ts +3 -3
  194. package/src/autoresearch/command-initialize.md +0 -34
  195. package/src/patch/diff.ts +0 -433
  196. package/src/patch/index.ts +0 -888
  197. package/src/patch/parser.ts +0 -532
  198. package/src/patch/types.ts +0 -292
  199. package/src/prompts/agents/oracle.md +0 -77
  200. package/src/tools/pending-action.ts +0 -49
  201. package/src/utils/child-process.ts +0 -88
  202. package/src/utils/frontmatter.ts +0 -117
  203. package/src/utils/image-input.ts +0 -274
  204. package/src/utils/mime.ts +0 -53
  205. package/src/utils/prompt-format.ts +0 -170
package/src/sdk.ts CHANGED
@@ -7,8 +7,10 @@ import {
7
7
  type ThinkingLevel,
8
8
  } from "@oh-my-pi/pi-agent-core";
9
9
  import type { Message, Model } from "@oh-my-pi/pi-ai";
10
-
11
- import { prewarmOpenAICodexResponses } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
10
+ import {
11
+ getOpenAICodexTransportDetails,
12
+ prewarmOpenAICodexResponses,
13
+ } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
12
14
  import { SearchDb } from "@oh-my-pi/pi-natives";
13
15
  import type { Component } from "@oh-my-pi/pi-tui";
14
16
  import {
@@ -19,6 +21,7 @@ import {
19
21
  getSearchDbDir,
20
22
  logger,
21
23
  postmortem,
24
+ prompt,
22
25
  } from "@oh-my-pi/pi-utils";
23
26
  import chalk from "chalk";
24
27
  import { AsyncJobManager } from "./async";
@@ -27,11 +30,7 @@ import { loadCapability } from "./capability";
27
30
  import { type Rule, ruleCapability } from "./capability/rule";
28
31
  import { ModelRegistry } from "./config/model-registry";
29
32
  import { formatModelString, parseModelPattern, parseModelString, resolveModelRoleValue } from "./config/model-resolver";
30
- import {
31
- loadPromptTemplates as loadPromptTemplatesInternal,
32
- type PromptTemplate,
33
- renderPromptTemplate,
34
- } from "./config/prompt-templates";
33
+ import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "./config/prompt-templates";
35
34
  import { Settings, type SkillsSettings } from "./config/settings";
36
35
  import { CursorExecHandlers } from "./cursor";
37
36
  import "./discovery";
@@ -74,6 +73,7 @@ import {
74
73
  SkillProtocolHandler,
75
74
  } from "./internal-urls";
76
75
  import { disposeAllKernelSessions } from "./ipy/executor";
76
+ import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
77
77
  import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp";
78
78
  import {
79
79
  collectDiscoverableMCPTools,
@@ -107,12 +107,14 @@ import {
107
107
  BashTool,
108
108
  BUILTIN_TOOLS,
109
109
  createTools,
110
+ discoverStartupLspServers,
110
111
  EditTool,
111
112
  FindTool,
112
113
  GrepTool,
113
114
  getSearchTools,
114
115
  HIDDEN_TOOLS,
115
116
  isSearchProviderPreference,
117
+ type LspStartupServerInfo,
116
118
  loadSshTool,
117
119
  PythonTool,
118
120
  ReadTool,
@@ -128,8 +130,9 @@ import {
128
130
  import { ToolContextStore } from "./tools/context";
129
131
  import { getGeminiImageTools } from "./tools/gemini-image";
130
132
  import { wrapToolWithMetaNotice } from "./tools/output-meta";
131
- import { PendingActionStore } from "./tools/pending-action";
133
+ import { queueResolveHandler } from "./tools/resolve";
132
134
  import { EventBus } from "./utils/event-bus";
135
+ import { buildNamedToolChoice } from "./utils/tool-choice";
133
136
 
134
137
  // Types
135
138
  export interface CreateAgentSessionOptions {
@@ -233,8 +236,8 @@ export interface CreateAgentSessionResult {
233
236
  mcpManager?: MCPManager;
234
237
  /** Warning if session was restored with a different model than saved */
235
238
  modelFallbackMessage?: string;
236
- /** LSP servers that were warmed up at startup */
237
- lspServers?: Array<{ name: string; status: "ready" | "error"; fileTypes: string[]; error?: string }>;
239
+ /** LSP servers detected for startup; warmup may continue in the background */
240
+ lspServers?: LspStartupServerInfo[];
238
241
  /** Shared event bus for tool/extension communication */
239
242
  eventBus: EventBus;
240
243
  }
@@ -646,17 +649,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
646
649
  registerPythonCleanup();
647
650
 
648
651
  // Use provided or create AuthStorage and ModelRegistry
649
- const { authStorage, modelRegistry } = await logger.timeAsync("discoverModels", async () => {
650
- const authStorage = options.authStorage ?? (await discoverAuthStorage(agentDir));
651
- const modelRegistry = options.modelRegistry ?? new ModelRegistry(authStorage);
652
- return { authStorage, modelRegistry };
653
- });
652
+ const authStorage = options.authStorage ?? (await logger.time("discoverModels", discoverAuthStorage, agentDir));
653
+ const modelRegistry = options.modelRegistry ?? new ModelRegistry(authStorage);
654
654
 
655
- const settings = await logger.timeAsync(
656
- "settings",
657
- async () => options.settings ?? (await Settings.init({ cwd, agentDir })),
658
- );
659
- logger.time("initializeWithSettings", initializeWithSettings, settings);
655
+ const settings = options.settings ?? (await logger.time("settings", Settings.init, { cwd, agentDir }));
656
+ logger.time("initializeWithSettings");
657
+ initializeWithSettings(settings);
660
658
  if (!options.modelRegistry) {
661
659
  modelRegistry.refreshInBackground();
662
660
  }
@@ -703,7 +701,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
703
701
  // reflect actual loaded secrets, not just the setting toggle.
704
702
  let obfuscator: SecretObfuscator | undefined;
705
703
  if (settings.get("secrets.enabled")) {
706
- const fileEntries = await logger.timeAsync("loadSecrets", loadSecrets, cwd, agentDir);
704
+ const fileEntries = await logger.time("loadSecrets", loadSecrets, cwd, agentDir);
707
705
  const envEntries = collectEnvSecrets();
708
706
  const allEntries = [...envEntries, ...fileEntries];
709
707
  if (allEntries.length > 0) {
@@ -713,10 +711,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
713
711
  const secretsEnabled = obfuscator?.hasSecrets() === true;
714
712
 
715
713
  // Check if session has existing data to restore
716
- const existingSession = logger.time("loadSession", () =>
714
+ const existingSession = logger.time("loadSessionContext", () =>
717
715
  deobfuscateSessionContext(sessionManager.buildSessionContext(), obfuscator),
718
716
  );
719
- const existingBranch = sessionManager.getBranch();
717
+ const existingBranch = logger.time("getSessionBranch", () => sessionManager.getBranch());
720
718
  const hasExistingSession = existingBranch.length > 0;
721
719
  const hasThinkingEntry = existingBranch.some(entry => entry.type === "thinking_level_change");
722
720
  const hasServiceTierEntry = existingBranch.some(entry => entry.type === "service_tier_change");
@@ -725,34 +723,41 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
725
723
  const modelMatchPreferences = {
726
724
  usageOrder: settings.getStorage()?.getModelUsageOrder(),
727
725
  };
728
- const defaultRoleSpec = resolveModelRoleValue(settings.getModelRole("default"), modelRegistry.getAvailable(), {
729
- settings,
730
- matchPreferences: modelMatchPreferences,
731
- });
726
+ const defaultRoleSpec = logger.time("resolveDefaultModelRole", () =>
727
+ resolveModelRoleValue(settings.getModelRole("default"), modelRegistry.getAvailable(), {
728
+ settings,
729
+ matchPreferences: modelMatchPreferences,
730
+ }),
731
+ );
732
732
  let model = options.model;
733
733
  let modelFallbackMessage: string | undefined;
734
734
  // If session has data, try to restore model from it.
735
735
  // Skip restore when an explicit model was requested.
736
736
  const defaultModelStr = existingSession.models.default;
737
737
  if (!hasExplicitModel && !model && hasExistingSession && defaultModelStr) {
738
- const parsedModel = parseModelString(defaultModelStr);
739
- if (parsedModel) {
740
- const restoredModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
741
- if (restoredModel && (await hasModelApiKey(restoredModel))) {
742
- model = restoredModel;
738
+ await logger.time("restoreSessionModel", async () => {
739
+ const parsedModel = parseModelString(defaultModelStr);
740
+ if (parsedModel) {
741
+ const restoredModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
742
+ if (restoredModel && (await hasModelApiKey(restoredModel))) {
743
+ model = restoredModel;
744
+ }
743
745
  }
744
- }
745
- if (!model) {
746
- modelFallbackMessage = `Could not restore model ${defaultModelStr}`;
747
- }
746
+ if (!model) {
747
+ modelFallbackMessage = `Could not restore model ${defaultModelStr}`;
748
+ }
749
+ });
748
750
  }
749
751
 
750
752
  // If still no model, try settings default.
751
753
  // Skip settings fallback when an explicit model was requested.
752
754
  if (!hasExplicitModel && !model && defaultRoleSpec.model) {
753
- if (await hasModelApiKey(defaultRoleSpec.model)) {
754
- model = defaultRoleSpec.model;
755
- }
755
+ const settingsDefaultModel = defaultRoleSpec.model;
756
+ logger.time("resolveSettingsDefaultModel", () => {
757
+ // defaultRoleSpec.model already comes from modelRegistry.getAvailable(),
758
+ // so re-validating auth here just repeats the expensive lookup path.
759
+ model = settingsDefaultModel;
760
+ });
756
761
  }
757
762
 
758
763
  const taskDepth = options.taskDepth ?? 0;
@@ -773,7 +778,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
773
778
  thinkingLevel = settings.get("defaultThinkingLevel");
774
779
  }
775
780
  if (model) {
776
- thinkingLevel = resolveThinkingLevelForModel(model, thinkingLevel);
781
+ const resolvedModel = model;
782
+ thinkingLevel = logger.time("resolveThinkingLevelForModel", () =>
783
+ resolveThinkingLevelForModel(resolvedModel, thinkingLevel),
784
+ );
777
785
  }
778
786
 
779
787
  let skills: Skill[];
@@ -782,55 +790,44 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
782
790
  skills = options.skills;
783
791
  skillWarnings = [];
784
792
  } else {
785
- const discovered = await logger.timeAsync("discoverSkills", async () =>
786
- discoveredSkillsPromise ? await discoveredSkillsPromise : { skills: [], warnings: [] },
793
+ const discovered = await logger.time(
794
+ "discoverSkills",
795
+ () => discoveredSkillsPromise ?? Promise.resolve({ skills: [], warnings: [] }),
787
796
  );
788
797
  skills = discovered.skills;
789
798
  skillWarnings = discovered.warnings;
790
799
  }
791
800
 
792
- // Discover rules
793
- const { ttsrManager, rulesResult, registeredTtsrRuleNames } = await logger.timeAsync(
794
- "discoverTtsrRules",
795
- async () => {
796
- const ttsrSettings = settings.getGroup("ttsr");
797
- const ttsrManager = new TtsrManager(ttsrSettings);
798
- const rulesResult =
799
- options.rules !== undefined
800
- ? { items: options.rules, warnings: undefined }
801
- : await loadCapability<Rule>(ruleCapability.id, { cwd });
802
- const registeredTtsrRuleNames = new Set<string>();
803
- for (const rule of rulesResult.items) {
804
- if (rule.condition && rule.condition.length > 0) {
805
- if (ttsrManager.addRule(rule)) {
806
- registeredTtsrRuleNames.add(rule.name);
807
- }
808
- }
801
+ // Discover rules and bucket them in one pass to avoid repeated scans over large rule sets.
802
+ const { ttsrManager, rulebookRules, alwaysApplyRules } = await logger.time("discoverTtsrRules", async () => {
803
+ const ttsrSettings = settings.getGroup("ttsr");
804
+ const ttsrManager = new TtsrManager(ttsrSettings);
805
+ const rulesResult =
806
+ options.rules !== undefined
807
+ ? { items: options.rules, warnings: undefined }
808
+ : await loadCapability<Rule>(ruleCapability.id, { cwd });
809
+ const rulebookRules: Rule[] = [];
810
+ const alwaysApplyRules: Rule[] = [];
811
+ for (const rule of rulesResult.items) {
812
+ const isTtsrRule = rule.condition && rule.condition.length > 0 ? ttsrManager.addRule(rule) : false;
813
+ if (isTtsrRule) {
814
+ continue;
809
815
  }
810
- if (existingSession.injectedTtsrRules.length > 0) {
811
- ttsrManager.restoreInjected(existingSession.injectedTtsrRules);
816
+ if (rule.alwaysApply === true) {
817
+ alwaysApplyRules.push(rule);
818
+ continue;
812
819
  }
813
- return { ttsrManager, rulesResult, registeredTtsrRuleNames };
814
- },
815
- );
816
-
817
- // Filter rules for the rulebook (non-TTSR, non-alwaysApply, with descriptions)
818
- const rulebookRules = logger.time("filterRulebookRules", () =>
819
- rulesResult.items.filter((rule: Rule) => {
820
- if (registeredTtsrRuleNames.has(rule.name)) return false;
821
- if (rule.alwaysApply) return false;
822
- if (!rule.description) return false;
823
- return true;
824
- }),
825
- );
826
-
827
- // collect alwaysApply rules — full content injected into system prompt
828
- const alwaysApplyRules = rulesResult.items.filter((rule: Rule) => {
829
- if (registeredTtsrRuleNames.has(rule.name)) return false;
830
- return rule.alwaysApply === true;
820
+ if (rule.description) {
821
+ rulebookRules.push(rule);
822
+ }
823
+ }
824
+ if (existingSession.injectedTtsrRules.length > 0) {
825
+ ttsrManager.restoreInjected(existingSession.injectedTtsrRules);
826
+ }
827
+ return { ttsrManager, rulebookRules, alwaysApplyRules };
831
828
  });
832
829
 
833
- const contextFiles = await logger.timeAsync(
830
+ const contextFiles = await logger.time(
834
831
  "discoverContextFiles",
835
832
  async () => options.contextFiles ?? (await discoverContextFiles(cwd, agentDir)),
836
833
  );
@@ -869,7 +866,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
869
866
  onJobComplete: async (jobId, result, job) => {
870
867
  if (!session) return;
871
868
  const formattedResult = await formatAsyncResultForFollowUp(result);
872
- const message = renderPromptTemplate(asyncResultTemplate, { jobId, result: formattedResult });
869
+ const message = prompt.render(asyncResultTemplate, { jobId, result: formattedResult });
873
870
  const durationMs = job ? Math.max(0, Date.now() - job.startTime) : undefined;
874
871
  await session.sendCustomMessage(
875
872
  {
@@ -891,7 +888,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
891
888
  : undefined;
892
889
 
893
890
  const searchDb = options.searchDb ?? new SearchDb(getSearchDbDir(agentDir));
894
- const pendingActionStore = new PendingActionStore();
895
891
  const toolSession: ToolSession = {
896
892
  cwd,
897
893
  hasUI: options.hasUI ?? false,
@@ -928,6 +924,22 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
928
924
  activateDiscoveredMCPTools: toolNames => session.activateDiscoveredMCPTools(toolNames),
929
925
  getCheckpointState: () => session.getCheckpointState(),
930
926
  setCheckpointState: state => session.setCheckpointState(state ?? undefined),
927
+ getToolChoiceQueue: () => session.toolChoiceQueue,
928
+ buildToolChoice: name => {
929
+ const m = session.model;
930
+ return m ? buildNamedToolChoice(name, m) : undefined;
931
+ },
932
+ steer: msg =>
933
+ session.agent.steer({
934
+ role: "custom",
935
+ customType: msg.customType,
936
+ content: msg.content,
937
+ display: false,
938
+ details: msg.details,
939
+ attribution: "agent",
940
+ timestamp: Date.now(),
941
+ }),
942
+ peekQueueInvoker: () => session.peekQueueInvoker(),
931
943
  allocateOutputArtifact: async toolType => {
932
944
  try {
933
945
  return await sessionManager.allocateArtifactPath(toolType);
@@ -939,7 +951,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
939
951
  authStorage,
940
952
  modelRegistry,
941
953
  asyncJobManager,
942
- pendingActionStore,
943
954
  searchDb,
944
955
  };
945
956
 
@@ -980,29 +991,27 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
980
991
  );
981
992
 
982
993
  // Create built-in tools (already wrapped with meta notice formatting)
983
- const builtinTools = await logger.timeAsync("createAllTools", () => createTools(toolSession, options.toolNames));
994
+ const builtinTools = await logger.time("createAllTools", createTools, toolSession, options.toolNames);
984
995
 
985
996
  // Discover MCP tools from .mcp.json files
986
997
  let mcpManager: MCPManager | undefined;
987
998
  const enableMCP = options.enableMCP ?? true;
988
999
  const customTools: CustomTool[] = [];
989
1000
  if (enableMCP) {
990
- const mcpResult = await logger.timeAsync("discoverAndLoadMCPTools", () =>
991
- discoverAndLoadMCPTools(cwd, {
992
- onConnecting: serverNames => {
993
- if (options.hasUI && serverNames.length > 0) {
994
- process.stderr.write(`${chalk.gray(`Connecting to MCP servers: ${serverNames.join(", ")}…`)}\n`);
995
- }
996
- },
997
- enableProjectConfig: settings.get("mcp.enableProjectConfig") ?? true,
998
- // Always filter Exa - we have native integration
999
- filterExa: true,
1000
- // Filter browser MCP servers when builtin browser tool is active
1001
- filterBrowser: settings.get("browser.enabled") ?? false,
1002
- cacheStorage: settings.getStorage(),
1003
- authStorage,
1004
- }),
1005
- );
1001
+ const mcpResult = await logger.time("discoverAndLoadMCPTools", discoverAndLoadMCPTools, cwd, {
1002
+ onConnecting: serverNames => {
1003
+ if (options.hasUI && serverNames.length > 0) {
1004
+ process.stderr.write(`${chalk.gray(`Connecting to MCP servers: ${serverNames.join(", ")}…`)}\n`);
1005
+ }
1006
+ },
1007
+ enableProjectConfig: settings.get("mcp.enableProjectConfig") ?? true,
1008
+ // Always filter Exa - we have native integration
1009
+ filterExa: true,
1010
+ // Filter browser MCP servers when builtin browser tool is active
1011
+ filterBrowser: settings.get("browser.enabled") ?? false,
1012
+ cacheStorage: settings.getStorage(),
1013
+ authStorage,
1014
+ });
1006
1015
  mcpManager = mcpResult.manager;
1007
1016
  toolSession.mcpManager = mcpManager;
1008
1017
 
@@ -1026,7 +1035,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1026
1035
  }
1027
1036
 
1028
1037
  // Add Gemini image tools if GEMINI_API_KEY (or GOOGLE_API_KEY) is available
1029
- const geminiImageTools = await logger.timeAsync("getGeminiImageTools", getGeminiImageTools);
1038
+ const geminiImageTools = await logger.time("getGeminiImageTools", getGeminiImageTools);
1030
1039
  if (geminiImageTools.length > 0) {
1031
1040
  customTools.push(...(geminiImageTools as unknown as CustomTool[]));
1032
1041
  }
@@ -1038,13 +1047,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1038
1047
 
1039
1048
  // Discover and load custom tools from .omp/tools/, .claude/tools/, etc.
1040
1049
  const builtInToolNames = builtinTools.map(t => t.name);
1041
- const discoveredCustomTools = await logger.timeAsync(
1050
+ const discoveredCustomTools = await logger.time(
1042
1051
  "discoverAndLoadCustomTools",
1043
1052
  discoverAndLoadCustomTools,
1044
1053
  [],
1045
1054
  cwd,
1046
1055
  builtInToolNames,
1047
- pendingActionStore,
1056
+ action => queueResolveHandler(toolSession, action),
1048
1057
  );
1049
1058
  for (const { path, error } of discoveredCustomTools.errors) {
1050
1059
  logger.error("Custom tool load failed", { path, error });
@@ -1063,7 +1072,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1063
1072
  let extensionsResult: LoadExtensionsResult;
1064
1073
  if (options.disableExtensionDiscovery) {
1065
1074
  const configuredPaths = options.additionalExtensionPaths ?? [];
1066
- extensionsResult = await logger.timeAsync("loadExtensions", loadExtensions, configuredPaths, cwd, eventBus);
1075
+ extensionsResult = await logger.time("loadExtensions", loadExtensions, configuredPaths, cwd, eventBus);
1067
1076
  for (const { path, error } of extensionsResult.errors) {
1068
1077
  logger.error("Failed to load extension", { path, error });
1069
1078
  }
@@ -1073,7 +1082,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1073
1082
  // Merge CLI extension paths with settings extension paths
1074
1083
  const configuredPaths = [...(options.additionalExtensionPaths ?? []), ...(settings.get("extensions") ?? [])];
1075
1084
  const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
1076
- extensionsResult = await logger.timeAsync(
1085
+ extensionsResult = await logger.time(
1077
1086
  "discoverAndLoadExtensions",
1078
1087
  discoverAndLoadExtensions,
1079
1088
  configuredPaths,
@@ -1154,7 +1163,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1154
1163
  // Discover custom commands (TypeScript slash commands)
1155
1164
  const customCommandsResult: CustomCommandsLoadResult = options.disableExtensionDiscovery
1156
1165
  ? { commands: [], errors: [] }
1157
- : await logger.timeAsync("discoverCustomCommands", loadCustomCommandsInternal, { cwd, agentDir });
1166
+ : await logger.time("discoverCustomCommands", loadCustomCommandsInternal, { cwd, agentDir });
1158
1167
  if (!options.disableExtensionDiscovery) {
1159
1168
  for (const { path, error } of customCommandsResult.errors) {
1160
1169
  logger.error("Failed to load custom command", { path, error });
@@ -1237,7 +1246,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1237
1246
  if (!hasDeferrableTools) {
1238
1247
  toolRegistry.delete("resolve");
1239
1248
  } else if (!toolRegistry.has("resolve")) {
1240
- const resolveTool = await logger.timeAsync("createTools:resolve:session", HIDDEN_TOOLS.resolve, toolSession);
1249
+ const resolveTool = await logger.time("createTools:resolve:session", HIDDEN_TOOLS.resolve, toolSession);
1241
1250
  if (resolveTool) {
1242
1251
  toolRegistry.set(resolveTool.name, wrapToolWithMetaNotice(resolveTool));
1243
1252
  }
@@ -1387,20 +1396,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1387
1396
  }
1388
1397
  }
1389
1398
 
1390
- const systemPrompt = await logger.timeAsync(
1391
- "buildSystemPrompt",
1392
- rebuildSystemPrompt,
1393
- initialToolNames,
1394
- toolRegistry,
1395
- );
1399
+ const systemPrompt = await logger.time("buildSystemPrompt", rebuildSystemPrompt, initialToolNames, toolRegistry);
1396
1400
 
1397
1401
  const promptTemplates =
1398
- options.promptTemplates ??
1399
- (await logger.timeAsync("discoverPromptTemplates", discoverPromptTemplates, cwd, agentDir));
1402
+ options.promptTemplates ?? (await logger.time("discoverPromptTemplates", discoverPromptTemplates, cwd, agentDir));
1400
1403
  toolSession.promptTemplates = promptTemplates;
1401
1404
 
1402
1405
  const slashCommands =
1403
- options.slashCommands ?? (await logger.timeAsync("discoverSlashCommands", discoverSlashCommands, cwd));
1406
+ options.slashCommands ?? (await logger.time("discoverSlashCommands", discoverSlashCommands, cwd));
1404
1407
 
1405
1408
  // Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)
1406
1409
  const convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {
@@ -1515,13 +1518,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1515
1518
  return result;
1516
1519
  },
1517
1520
  intentTracing: !!intentField,
1518
- getToolChoice: () => {
1519
- if (pendingActionStore.hasPending) {
1520
- return { type: "function", name: "resolve" };
1521
- }
1522
- return session?.consumeNextToolChoiceOverride();
1523
- },
1521
+ getToolChoice: () => session?.nextToolChoice(),
1524
1522
  });
1523
+
1525
1524
  cursorEventEmitter = event => agent.emitExternalEvent(event);
1526
1525
 
1527
1526
  // Restore messages if session has existing data
@@ -1562,51 +1561,87 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1562
1561
  ttsrManager,
1563
1562
  obfuscator,
1564
1563
  asyncJobManager,
1565
- pendingActionStore,
1566
1564
  searchDb,
1567
1565
  });
1568
1566
 
1569
1567
  if (model?.api === "openai-codex-responses") {
1570
- try {
1571
- await logger.timeAsync("prewarmCodexWebsocket", prewarmOpenAICodexResponses, model, {
1572
- apiKey: await modelRegistry.getApiKey(model, providerSessionId),
1573
- sessionId: providerSessionId,
1574
- preferWebsockets: preferOpenAICodexWebsockets,
1575
- providerSessionState: session.providerSessionState,
1576
- });
1577
- } catch (error) {
1578
- logger.debug("Codex websocket prewarm failed", {
1579
- error: error instanceof Error ? error.message : String(error),
1580
- provider: model.provider,
1581
- model: model.id,
1582
- });
1568
+ const codexModel = model;
1569
+ const codexTransport = getOpenAICodexTransportDetails(codexModel, {
1570
+ sessionId: providerSessionId,
1571
+ baseUrl: codexModel.baseUrl,
1572
+ preferWebsockets: preferOpenAICodexWebsockets,
1573
+ providerSessionState: session.providerSessionState,
1574
+ });
1575
+ if (codexTransport.websocketPreferred) {
1576
+ void (async () => {
1577
+ try {
1578
+ const codexPrewarmApiKey = await modelRegistry.getApiKey(codexModel, providerSessionId);
1579
+ if (!codexPrewarmApiKey) return;
1580
+ await logger.time("prewarmOpenAICodexResponses", prewarmOpenAICodexResponses, codexModel, {
1581
+ apiKey: codexPrewarmApiKey,
1582
+ sessionId: providerSessionId,
1583
+ preferWebsockets: preferOpenAICodexWebsockets,
1584
+ providerSessionState: session.providerSessionState,
1585
+ });
1586
+ } catch (error) {
1587
+ const errorMessage = error instanceof Error ? error.message : String(error);
1588
+ logger.debug("Codex websocket prewarm failed", {
1589
+ error: errorMessage,
1590
+ provider: codexModel.provider,
1591
+ model: codexModel.id,
1592
+ });
1593
+ }
1594
+ })();
1583
1595
  }
1584
1596
  }
1585
1597
 
1586
- // Warm up LSP servers (connects to detected servers)
1598
+ // Start LSP warmup in the background so startup does not block on language server initialization.
1587
1599
  let lspServers: CreateAgentSessionResult["lspServers"];
1588
1600
  if (enableLsp && settings.get("lsp.diagnosticsOnWrite")) {
1589
- try {
1590
- const result = await logger.timeAsync("warmupLspServers", warmupLspServers, cwd, {
1591
- onConnecting: serverNames => {
1592
- if (options.hasUI && serverNames.length > 0) {
1593
- process.stderr.write(chalk.gray(`Starting LSP servers: ${serverNames.join(", ")}…\n`));
1601
+ lspServers = discoverStartupLspServers(cwd);
1602
+ if (lspServers.length > 0) {
1603
+ void (async () => {
1604
+ try {
1605
+ const result = await logger.time("warmupLspServers", warmupLspServers, cwd);
1606
+ const serversByName = new Map(result.servers.map(server => [server.name, server] as const));
1607
+ for (const server of lspServers ?? []) {
1608
+ const next = serversByName.get(server.name);
1609
+ if (!next) continue;
1610
+ server.status = next.status;
1611
+ server.fileTypes = next.fileTypes;
1612
+ server.error = next.error;
1594
1613
  }
1595
- },
1596
- });
1597
- lspServers = result.servers;
1598
- } catch (error) {
1599
- logger.warn("LSP server warmup failed", { cwd, error: String(error) });
1614
+ const event: LspStartupEvent = {
1615
+ type: "completed",
1616
+ servers: result.servers,
1617
+ };
1618
+ eventBus.emit(LSP_STARTUP_EVENT_CHANNEL, event);
1619
+ } catch (error) {
1620
+ const errorMessage = error instanceof Error ? error.message : String(error);
1621
+ logger.warn("LSP server warmup failed", { cwd, error: errorMessage });
1622
+ for (const server of lspServers ?? []) {
1623
+ server.status = "error";
1624
+ server.error = errorMessage;
1625
+ }
1626
+ const event: LspStartupEvent = {
1627
+ type: "failed",
1628
+ error: errorMessage,
1629
+ };
1630
+ eventBus.emit(LSP_STARTUP_EVENT_CHANNEL, event);
1631
+ }
1632
+ })();
1600
1633
  }
1601
1634
  }
1602
1635
 
1603
- startMemoryStartupTask({
1604
- session,
1605
- settings,
1606
- modelRegistry,
1607
- agentDir,
1608
- taskDepth,
1609
- });
1636
+ logger.time("startMemoryStartupTask", () =>
1637
+ startMemoryStartupTask({
1638
+ session,
1639
+ settings,
1640
+ modelRegistry,
1641
+ agentDir,
1642
+ taskDepth,
1643
+ }),
1644
+ );
1610
1645
 
1611
1646
  // Wire MCP manager callbacks to session for reactive tool updates
1612
1647
  if (mcpManager) {
@@ -1646,6 +1681,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1646
1681
  });
1647
1682
  }
1648
1683
 
1684
+ logger.time("createAgentSession:return");
1649
1685
  return {
1650
1686
  session,
1651
1687
  extensionsResult,
@@ -172,7 +172,7 @@ export class SecretObfuscator {
172
172
 
173
173
  /** Deobfuscate obfuscate-mode placeholders back to original secrets. Replace-mode is NOT reversed. */
174
174
  deobfuscate(text: string): string {
175
- if (!this.#hasAny) return text;
175
+ if (!this.#hasAny || !text.includes("#")) return text;
176
176
  return text.replace(PLACEHOLDER_RE, match => {
177
177
  return this.#deobfuscateMap.get(match) ?? match;
178
178
  });