@oh-my-pi/pi-coding-agent 13.18.0 → 14.0.2

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 (235) hide show
  1. package/CHANGELOG.md +316 -1
  2. package/package.json +86 -24
  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 +116 -30
  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 +123 -178
  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 -8
  28. package/src/commit/agentic/index.ts +22 -26
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/tools/git-file-diff.ts +3 -6
  31. package/src/commit/agentic/tools/git-hunk.ts +3 -3
  32. package/src/commit/agentic/tools/git-overview.ts +6 -9
  33. package/src/commit/agentic/tools/index.ts +6 -8
  34. package/src/commit/agentic/tools/propose-commit.ts +4 -7
  35. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  36. package/src/commit/agentic/tools/split-commit.ts +4 -4
  37. package/src/commit/agentic/validation.ts +1 -1
  38. package/src/commit/analysis/conventional.ts +4 -4
  39. package/src/commit/analysis/summary.ts +3 -3
  40. package/src/commit/changelog/generate.ts +4 -4
  41. package/src/commit/changelog/index.ts +5 -9
  42. package/src/commit/map-reduce/map-phase.ts +4 -4
  43. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  44. package/src/commit/pipeline.ts +13 -16
  45. package/src/config/keybindings.ts +7 -6
  46. package/src/config/prompt-templates.ts +44 -226
  47. package/src/config/resolve-config-value.ts +4 -2
  48. package/src/config/settings-schema.ts +98 -2
  49. package/src/config/settings.ts +25 -26
  50. package/src/dap/client.ts +674 -0
  51. package/src/dap/config.ts +150 -0
  52. package/src/dap/defaults.json +211 -0
  53. package/src/dap/index.ts +4 -0
  54. package/src/dap/session.ts +1255 -0
  55. package/src/dap/types.ts +600 -0
  56. package/src/debug/log-viewer.ts +3 -2
  57. package/src/discovery/builtin.ts +1 -2
  58. package/src/discovery/codex.ts +2 -2
  59. package/src/discovery/github.ts +2 -1
  60. package/src/discovery/helpers.ts +2 -2
  61. package/src/discovery/opencode.ts +2 -2
  62. package/src/edit/diff.ts +818 -0
  63. package/src/edit/index.ts +309 -0
  64. package/src/edit/line-hash.ts +67 -0
  65. package/src/edit/modes/chunk.ts +454 -0
  66. package/src/{patch → edit/modes}/hashline.ts +741 -361
  67. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  68. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  69. package/src/{patch → edit}/normalize.ts +97 -76
  70. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  71. package/src/exec/bash-executor.ts +4 -2
  72. package/src/exec/idle-timeout-watchdog.ts +126 -0
  73. package/src/exec/non-interactive-env.ts +5 -0
  74. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
  75. package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
  76. package/src/extensibility/custom-commands/loader.ts +1 -2
  77. package/src/extensibility/custom-tools/loader.ts +34 -11
  78. package/src/extensibility/custom-tools/types.ts +1 -1
  79. package/src/extensibility/extensions/loader.ts +9 -4
  80. package/src/extensibility/extensions/runner.ts +24 -1
  81. package/src/extensibility/extensions/types.ts +4 -2
  82. package/src/extensibility/hooks/loader.ts +5 -6
  83. package/src/extensibility/hooks/types.ts +2 -2
  84. package/src/extensibility/plugins/doctor.ts +2 -1
  85. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
  86. package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
  87. package/src/extensibility/slash-commands.ts +3 -7
  88. package/src/index.ts +3 -1
  89. package/src/internal-urls/docs-index.generated.ts +11 -11
  90. package/src/ipy/executor.ts +58 -17
  91. package/src/ipy/gateway-coordinator.ts +6 -4
  92. package/src/ipy/kernel.ts +45 -22
  93. package/src/ipy/runtime.ts +2 -2
  94. package/src/lsp/client.ts +7 -4
  95. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  96. package/src/lsp/config.ts +2 -2
  97. package/src/lsp/defaults.json +688 -154
  98. package/src/lsp/index.ts +234 -45
  99. package/src/lsp/lspmux.ts +2 -2
  100. package/src/lsp/startup-events.ts +13 -0
  101. package/src/lsp/types.ts +12 -1
  102. package/src/lsp/utils.ts +8 -1
  103. package/src/main.ts +125 -47
  104. package/src/memories/index.ts +4 -5
  105. package/src/modes/acp/acp-agent.ts +563 -163
  106. package/src/modes/acp/acp-event-mapper.ts +9 -1
  107. package/src/modes/acp/acp-mode.ts +4 -2
  108. package/src/modes/components/agent-dashboard.ts +3 -4
  109. package/src/modes/components/diff.ts +6 -7
  110. package/src/modes/components/footer.ts +9 -29
  111. package/src/modes/components/hook-editor.ts +3 -3
  112. package/src/modes/components/hook-selector.ts +6 -1
  113. package/src/modes/components/read-tool-group.ts +6 -12
  114. package/src/modes/components/session-observer-overlay.ts +472 -0
  115. package/src/modes/components/settings-defs.ts +24 -0
  116. package/src/modes/components/status-line.ts +15 -61
  117. package/src/modes/components/tool-execution.ts +1 -1
  118. package/src/modes/components/welcome.ts +1 -1
  119. package/src/modes/controllers/btw-controller.ts +2 -2
  120. package/src/modes/controllers/command-controller.ts +4 -2
  121. package/src/modes/controllers/event-controller.ts +59 -2
  122. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  123. package/src/modes/controllers/input-controller.ts +15 -8
  124. package/src/modes/controllers/selector-controller.ts +26 -0
  125. package/src/modes/index.ts +20 -2
  126. package/src/modes/interactive-mode.ts +278 -69
  127. package/src/modes/rpc/host-tools.ts +186 -0
  128. package/src/modes/rpc/rpc-client.ts +178 -13
  129. package/src/modes/rpc/rpc-mode.ts +73 -3
  130. package/src/modes/rpc/rpc-types.ts +53 -1
  131. package/src/modes/session-observer-registry.ts +146 -0
  132. package/src/modes/shared.ts +0 -42
  133. package/src/modes/theme/theme.ts +80 -8
  134. package/src/modes/types.ts +4 -2
  135. package/src/modes/utils/keybinding-matchers.ts +9 -0
  136. package/src/prompts/system/custom-system-prompt.md +5 -0
  137. package/src/prompts/system/system-prompt.md +8 -1
  138. package/src/prompts/tools/chunk-edit.md +219 -0
  139. package/src/prompts/tools/debug.md +43 -0
  140. package/src/prompts/tools/grep.md +3 -0
  141. package/src/prompts/tools/lsp.md +5 -5
  142. package/src/prompts/tools/read-chunk.md +17 -0
  143. package/src/prompts/tools/read.md +19 -5
  144. package/src/sdk.ts +216 -165
  145. package/src/secrets/index.ts +1 -1
  146. package/src/secrets/obfuscator.ts +25 -17
  147. package/src/session/agent-session.ts +381 -286
  148. package/src/session/agent-storage.ts +12 -12
  149. package/src/session/compaction/branch-summarization.ts +3 -3
  150. package/src/session/compaction/compaction.ts +5 -6
  151. package/src/session/compaction/utils.ts +3 -3
  152. package/src/session/history-storage.ts +62 -19
  153. package/src/session/messages.ts +3 -3
  154. package/src/session/session-dump-format.ts +203 -0
  155. package/src/session/session-manager.ts +15 -5
  156. package/src/session/session-storage.ts +4 -2
  157. package/src/session/streaming-output.ts +1 -1
  158. package/src/session/tool-choice-queue.ts +213 -0
  159. package/src/slash-commands/builtin-registry.ts +56 -8
  160. package/src/ssh/connection-manager.ts +2 -2
  161. package/src/ssh/sshfs-mount.ts +5 -5
  162. package/src/stt/downloader.ts +4 -4
  163. package/src/stt/recorder.ts +4 -4
  164. package/src/stt/transcriber.ts +2 -2
  165. package/src/system-prompt.ts +25 -13
  166. package/src/task/agents.ts +5 -6
  167. package/src/task/commands.ts +2 -5
  168. package/src/task/executor.ts +32 -4
  169. package/src/task/index.ts +91 -82
  170. package/src/task/template.ts +2 -2
  171. package/src/task/types.ts +25 -0
  172. package/src/task/worktree.ts +131 -149
  173. package/src/tools/ask.ts +2 -3
  174. package/src/tools/ast-edit.ts +7 -7
  175. package/src/tools/ast-grep.ts +7 -7
  176. package/src/tools/auto-generated-guard.ts +36 -41
  177. package/src/tools/await-tool.ts +2 -2
  178. package/src/tools/bash.ts +5 -23
  179. package/src/tools/browser.ts +4 -5
  180. package/src/tools/calculator.ts +2 -3
  181. package/src/tools/cancel-job.ts +2 -2
  182. package/src/tools/checkpoint.ts +3 -3
  183. package/src/tools/debug.ts +1007 -0
  184. package/src/tools/exit-plan-mode.ts +3 -3
  185. package/src/tools/fetch.ts +67 -3
  186. package/src/tools/find.ts +4 -5
  187. package/src/tools/fs-cache-invalidation.ts +5 -0
  188. package/src/tools/gemini-image.ts +13 -5
  189. package/src/tools/gh.ts +130 -308
  190. package/src/tools/grep.ts +57 -9
  191. package/src/tools/index.ts +44 -22
  192. package/src/tools/inspect-image.ts +4 -4
  193. package/src/tools/output-meta.ts +1 -1
  194. package/src/tools/python.ts +19 -6
  195. package/src/tools/read.ts +211 -146
  196. package/src/tools/render-mermaid.ts +2 -3
  197. package/src/tools/render-utils.ts +20 -6
  198. package/src/tools/renderers.ts +3 -1
  199. package/src/tools/report-tool-issue.ts +80 -0
  200. package/src/tools/resolve.ts +70 -39
  201. package/src/tools/search-tool-bm25.ts +2 -2
  202. package/src/tools/ssh.ts +2 -2
  203. package/src/tools/todo-write.ts +2 -2
  204. package/src/tools/tool-timeouts.ts +1 -0
  205. package/src/tools/write.ts +5 -6
  206. package/src/tui/tree-list.ts +3 -1
  207. package/src/utils/clipboard.ts +80 -0
  208. package/src/utils/commit-message-generator.ts +2 -3
  209. package/src/utils/edit-mode.ts +49 -0
  210. package/src/utils/external-editor.ts +11 -5
  211. package/src/utils/file-display-mode.ts +6 -5
  212. package/src/utils/file-mentions.ts +8 -7
  213. package/src/utils/git.ts +1400 -0
  214. package/src/utils/image-loading.ts +98 -0
  215. package/src/utils/title-generator.ts +2 -3
  216. package/src/utils/tools-manager.ts +6 -6
  217. package/src/web/scrapers/choosealicense.ts +1 -1
  218. package/src/web/search/index.ts +3 -3
  219. package/src/web/search/render.ts +6 -4
  220. package/src/autoresearch/command-initialize.md +0 -34
  221. package/src/commit/git/errors.ts +0 -9
  222. package/src/commit/git/index.ts +0 -210
  223. package/src/commit/git/operations.ts +0 -54
  224. package/src/patch/diff.ts +0 -433
  225. package/src/patch/index.ts +0 -888
  226. package/src/patch/parser.ts +0 -532
  227. package/src/patch/types.ts +0 -292
  228. package/src/prompts/agents/oracle.md +0 -77
  229. package/src/tools/gh-cli.ts +0 -125
  230. package/src/tools/pending-action.ts +0 -49
  231. package/src/utils/child-process.ts +0 -88
  232. package/src/utils/frontmatter.ts +0 -117
  233. package/src/utils/image-input.ts +0 -274
  234. package/src/utils/mime.ts +0 -53
  235. 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,
@@ -83,7 +83,13 @@ import {
83
83
  } from "./mcp/discoverable-tool-metadata";
84
84
  import { buildMemoryToolDeveloperInstructions, getMemoryRoot, startMemoryStartupTask } from "./memories";
85
85
  import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
86
- import { collectEnvSecrets, loadSecrets, obfuscateMessages, SecretObfuscator } from "./secrets";
86
+ import {
87
+ collectEnvSecrets,
88
+ deobfuscateSessionContext,
89
+ loadSecrets,
90
+ obfuscateMessages,
91
+ SecretObfuscator,
92
+ } from "./secrets";
87
93
  import { AgentSession } from "./session/agent-session";
88
94
  import { AuthStorage } from "./session/auth-storage";
89
95
  import { convertToLlm } from "./session/messages";
@@ -101,12 +107,14 @@ import {
101
107
  BashTool,
102
108
  BUILTIN_TOOLS,
103
109
  createTools,
110
+ discoverStartupLspServers,
104
111
  EditTool,
105
112
  FindTool,
106
113
  GrepTool,
107
114
  getSearchTools,
108
115
  HIDDEN_TOOLS,
109
116
  isSearchProviderPreference,
117
+ type LspStartupServerInfo,
110
118
  loadSshTool,
111
119
  PythonTool,
112
120
  ReadTool,
@@ -122,8 +130,9 @@ import {
122
130
  import { ToolContextStore } from "./tools/context";
123
131
  import { getGeminiImageTools } from "./tools/gemini-image";
124
132
  import { wrapToolWithMetaNotice } from "./tools/output-meta";
125
- import { PendingActionStore } from "./tools/pending-action";
133
+ import { queueResolveHandler } from "./tools/resolve";
126
134
  import { EventBus } from "./utils/event-bus";
135
+ import { buildNamedToolChoice } from "./utils/tool-choice";
127
136
 
128
137
  // Types
129
138
  export interface CreateAgentSessionOptions {
@@ -227,8 +236,10 @@ export interface CreateAgentSessionResult {
227
236
  mcpManager?: MCPManager;
228
237
  /** Warning if session was restored with a different model than saved */
229
238
  modelFallbackMessage?: string;
230
- /** LSP servers that were warmed up at startup */
231
- 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[];
241
+ /** Shared event bus for tool/extension communication */
242
+ eventBus: EventBus;
232
243
  }
233
244
 
234
245
  // Re-exports
@@ -638,17 +649,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
638
649
  registerPythonCleanup();
639
650
 
640
651
  // Use provided or create AuthStorage and ModelRegistry
641
- const { authStorage, modelRegistry } = await logger.timeAsync("discoverModels", async () => {
642
- const authStorage = options.authStorage ?? (await discoverAuthStorage(agentDir));
643
- const modelRegistry = options.modelRegistry ?? new ModelRegistry(authStorage);
644
- return { authStorage, modelRegistry };
645
- });
652
+ const authStorage = options.authStorage ?? (await logger.time("discoverModels", discoverAuthStorage, agentDir));
653
+ const modelRegistry = options.modelRegistry ?? new ModelRegistry(authStorage);
646
654
 
647
- const settings = await logger.timeAsync(
648
- "settings",
649
- async () => options.settings ?? (await Settings.init({ cwd, agentDir })),
650
- );
651
- 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);
652
658
  if (!options.modelRegistry) {
653
659
  modelRegistry.refreshInBackground();
654
660
  }
@@ -691,9 +697,24 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
691
697
  return hasKey;
692
698
  };
693
699
 
700
+ // Load and create secret obfuscator early so resumed session state and prompt warnings
701
+ // reflect actual loaded secrets, not just the setting toggle.
702
+ let obfuscator: SecretObfuscator | undefined;
703
+ if (settings.get("secrets.enabled")) {
704
+ const fileEntries = await logger.time("loadSecrets", loadSecrets, cwd, agentDir);
705
+ const envEntries = collectEnvSecrets();
706
+ const allEntries = [...envEntries, ...fileEntries];
707
+ if (allEntries.length > 0) {
708
+ obfuscator = new SecretObfuscator(allEntries);
709
+ }
710
+ }
711
+ const secretsEnabled = obfuscator?.hasSecrets() === true;
712
+
694
713
  // Check if session has existing data to restore
695
- const existingSession = logger.time("loadSession", () => sessionManager.buildSessionContext());
696
- const existingBranch = sessionManager.getBranch();
714
+ const existingSession = logger.time("loadSessionContext", () =>
715
+ deobfuscateSessionContext(sessionManager.buildSessionContext(), obfuscator),
716
+ );
717
+ const existingBranch = logger.time("getSessionBranch", () => sessionManager.getBranch());
697
718
  const hasExistingSession = existingBranch.length > 0;
698
719
  const hasThinkingEntry = existingBranch.some(entry => entry.type === "thinking_level_change");
699
720
  const hasServiceTierEntry = existingBranch.some(entry => entry.type === "service_tier_change");
@@ -702,34 +723,41 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
702
723
  const modelMatchPreferences = {
703
724
  usageOrder: settings.getStorage()?.getModelUsageOrder(),
704
725
  };
705
- const defaultRoleSpec = resolveModelRoleValue(settings.getModelRole("default"), modelRegistry.getAvailable(), {
706
- settings,
707
- matchPreferences: modelMatchPreferences,
708
- });
726
+ const defaultRoleSpec = logger.time("resolveDefaultModelRole", () =>
727
+ resolveModelRoleValue(settings.getModelRole("default"), modelRegistry.getAvailable(), {
728
+ settings,
729
+ matchPreferences: modelMatchPreferences,
730
+ }),
731
+ );
709
732
  let model = options.model;
710
733
  let modelFallbackMessage: string | undefined;
711
734
  // If session has data, try to restore model from it.
712
735
  // Skip restore when an explicit model was requested.
713
736
  const defaultModelStr = existingSession.models.default;
714
737
  if (!hasExplicitModel && !model && hasExistingSession && defaultModelStr) {
715
- const parsedModel = parseModelString(defaultModelStr);
716
- if (parsedModel) {
717
- const restoredModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
718
- if (restoredModel && (await hasModelApiKey(restoredModel))) {
719
- 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
+ }
720
745
  }
721
- }
722
- if (!model) {
723
- modelFallbackMessage = `Could not restore model ${defaultModelStr}`;
724
- }
746
+ if (!model) {
747
+ modelFallbackMessage = `Could not restore model ${defaultModelStr}`;
748
+ }
749
+ });
725
750
  }
726
751
 
727
752
  // If still no model, try settings default.
728
753
  // Skip settings fallback when an explicit model was requested.
729
754
  if (!hasExplicitModel && !model && defaultRoleSpec.model) {
730
- if (await hasModelApiKey(defaultRoleSpec.model)) {
731
- model = defaultRoleSpec.model;
732
- }
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
+ });
733
761
  }
734
762
 
735
763
  const taskDepth = options.taskDepth ?? 0;
@@ -750,7 +778,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
750
778
  thinkingLevel = settings.get("defaultThinkingLevel");
751
779
  }
752
780
  if (model) {
753
- thinkingLevel = resolveThinkingLevelForModel(model, thinkingLevel);
781
+ const resolvedModel = model;
782
+ thinkingLevel = logger.time("resolveThinkingLevelForModel", () =>
783
+ resolveThinkingLevelForModel(resolvedModel, thinkingLevel),
784
+ );
754
785
  }
755
786
 
756
787
  let skills: Skill[];
@@ -759,55 +790,44 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
759
790
  skills = options.skills;
760
791
  skillWarnings = [];
761
792
  } else {
762
- const discovered = await logger.timeAsync("discoverSkills", async () =>
763
- discoveredSkillsPromise ? await discoveredSkillsPromise : { skills: [], warnings: [] },
793
+ const discovered = await logger.time(
794
+ "discoverSkills",
795
+ () => discoveredSkillsPromise ?? Promise.resolve({ skills: [], warnings: [] }),
764
796
  );
765
797
  skills = discovered.skills;
766
798
  skillWarnings = discovered.warnings;
767
799
  }
768
800
 
769
- // Discover rules
770
- const { ttsrManager, rulesResult, registeredTtsrRuleNames } = await logger.timeAsync(
771
- "discoverTtsrRules",
772
- async () => {
773
- const ttsrSettings = settings.getGroup("ttsr");
774
- const ttsrManager = new TtsrManager(ttsrSettings);
775
- const rulesResult =
776
- options.rules !== undefined
777
- ? { items: options.rules, warnings: undefined }
778
- : await loadCapability<Rule>(ruleCapability.id, { cwd });
779
- const registeredTtsrRuleNames = new Set<string>();
780
- for (const rule of rulesResult.items) {
781
- if (rule.condition && rule.condition.length > 0) {
782
- if (ttsrManager.addRule(rule)) {
783
- registeredTtsrRuleNames.add(rule.name);
784
- }
785
- }
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;
786
815
  }
787
- if (existingSession.injectedTtsrRules.length > 0) {
788
- ttsrManager.restoreInjected(existingSession.injectedTtsrRules);
816
+ if (rule.alwaysApply === true) {
817
+ alwaysApplyRules.push(rule);
818
+ continue;
789
819
  }
790
- return { ttsrManager, rulesResult, registeredTtsrRuleNames };
791
- },
792
- );
793
-
794
- // Filter rules for the rulebook (non-TTSR, non-alwaysApply, with descriptions)
795
- const rulebookRules = logger.time("filterRulebookRules", () =>
796
- rulesResult.items.filter((rule: Rule) => {
797
- if (registeredTtsrRuleNames.has(rule.name)) return false;
798
- if (rule.alwaysApply) return false;
799
- if (!rule.description) return false;
800
- return true;
801
- }),
802
- );
803
-
804
- // collect alwaysApply rules — full content injected into system prompt
805
- const alwaysApplyRules = rulesResult.items.filter((rule: Rule) => {
806
- if (registeredTtsrRuleNames.has(rule.name)) return false;
807
- 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 };
808
828
  });
809
829
 
810
- const contextFiles = await logger.timeAsync(
830
+ const contextFiles = await logger.time(
811
831
  "discoverContextFiles",
812
832
  async () => options.contextFiles ?? (await discoverContextFiles(cwd, agentDir)),
813
833
  );
@@ -846,7 +866,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
846
866
  onJobComplete: async (jobId, result, job) => {
847
867
  if (!session) return;
848
868
  const formattedResult = await formatAsyncResultForFollowUp(result);
849
- const message = renderPromptTemplate(asyncResultTemplate, { jobId, result: formattedResult });
869
+ const message = prompt.render(asyncResultTemplate, { jobId, result: formattedResult });
850
870
  const durationMs = job ? Math.max(0, Date.now() - job.startTime) : undefined;
851
871
  await session.sendCustomMessage(
852
872
  {
@@ -868,7 +888,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
868
888
  : undefined;
869
889
 
870
890
  const searchDb = options.searchDb ?? new SearchDb(getSearchDbDir(agentDir));
871
- const pendingActionStore = new PendingActionStore();
872
891
  const toolSession: ToolSession = {
873
892
  cwd,
874
893
  hasUI: options.hasUI ?? false,
@@ -905,6 +924,22 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
905
924
  activateDiscoveredMCPTools: toolNames => session.activateDiscoveredMCPTools(toolNames),
906
925
  getCheckpointState: () => session.getCheckpointState(),
907
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(),
908
943
  allocateOutputArtifact: async toolType => {
909
944
  try {
910
945
  return await sessionManager.allocateArtifactPath(toolType);
@@ -916,7 +951,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
916
951
  authStorage,
917
952
  modelRegistry,
918
953
  asyncJobManager,
919
- pendingActionStore,
920
954
  searchDb,
921
955
  };
922
956
 
@@ -957,29 +991,27 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
957
991
  );
958
992
 
959
993
  // Create built-in tools (already wrapped with meta notice formatting)
960
- const builtinTools = await logger.timeAsync("createAllTools", () => createTools(toolSession, options.toolNames));
994
+ const builtinTools = await logger.time("createAllTools", createTools, toolSession, options.toolNames);
961
995
 
962
996
  // Discover MCP tools from .mcp.json files
963
997
  let mcpManager: MCPManager | undefined;
964
998
  const enableMCP = options.enableMCP ?? true;
965
999
  const customTools: CustomTool[] = [];
966
1000
  if (enableMCP) {
967
- const mcpResult = await logger.timeAsync("discoverAndLoadMCPTools", () =>
968
- discoverAndLoadMCPTools(cwd, {
969
- onConnecting: serverNames => {
970
- if (options.hasUI && serverNames.length > 0) {
971
- process.stderr.write(`${chalk.gray(`Connecting to MCP servers: ${serverNames.join(", ")}…`)}\n`);
972
- }
973
- },
974
- enableProjectConfig: settings.get("mcp.enableProjectConfig") ?? true,
975
- // Always filter Exa - we have native integration
976
- filterExa: true,
977
- // Filter browser MCP servers when builtin browser tool is active
978
- filterBrowser: settings.get("browser.enabled") ?? false,
979
- cacheStorage: settings.getStorage(),
980
- authStorage,
981
- }),
982
- );
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
+ });
983
1015
  mcpManager = mcpResult.manager;
984
1016
  toolSession.mcpManager = mcpManager;
985
1017
 
@@ -1003,7 +1035,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1003
1035
  }
1004
1036
 
1005
1037
  // Add Gemini image tools if GEMINI_API_KEY (or GOOGLE_API_KEY) is available
1006
- const geminiImageTools = await logger.timeAsync("getGeminiImageTools", getGeminiImageTools);
1038
+ const geminiImageTools = await logger.time("getGeminiImageTools", getGeminiImageTools);
1007
1039
  if (geminiImageTools.length > 0) {
1008
1040
  customTools.push(...(geminiImageTools as unknown as CustomTool[]));
1009
1041
  }
@@ -1015,13 +1047,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1015
1047
 
1016
1048
  // Discover and load custom tools from .omp/tools/, .claude/tools/, etc.
1017
1049
  const builtInToolNames = builtinTools.map(t => t.name);
1018
- const discoveredCustomTools = await logger.timeAsync(
1050
+ const discoveredCustomTools = await logger.time(
1019
1051
  "discoverAndLoadCustomTools",
1020
1052
  discoverAndLoadCustomTools,
1021
1053
  [],
1022
1054
  cwd,
1023
1055
  builtInToolNames,
1024
- pendingActionStore,
1056
+ action => queueResolveHandler(toolSession, action),
1025
1057
  );
1026
1058
  for (const { path, error } of discoveredCustomTools.errors) {
1027
1059
  logger.error("Custom tool load failed", { path, error });
@@ -1040,7 +1072,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1040
1072
  let extensionsResult: LoadExtensionsResult;
1041
1073
  if (options.disableExtensionDiscovery) {
1042
1074
  const configuredPaths = options.additionalExtensionPaths ?? [];
1043
- extensionsResult = await logger.timeAsync("loadExtensions", loadExtensions, configuredPaths, cwd, eventBus);
1075
+ extensionsResult = await logger.time("loadExtensions", loadExtensions, configuredPaths, cwd, eventBus);
1044
1076
  for (const { path, error } of extensionsResult.errors) {
1045
1077
  logger.error("Failed to load extension", { path, error });
1046
1078
  }
@@ -1050,7 +1082,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1050
1082
  // Merge CLI extension paths with settings extension paths
1051
1083
  const configuredPaths = [...(options.additionalExtensionPaths ?? []), ...(settings.get("extensions") ?? [])];
1052
1084
  const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
1053
- extensionsResult = await logger.timeAsync(
1085
+ extensionsResult = await logger.time(
1054
1086
  "discoverAndLoadExtensions",
1055
1087
  discoverAndLoadExtensions,
1056
1088
  configuredPaths,
@@ -1131,7 +1163,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1131
1163
  // Discover custom commands (TypeScript slash commands)
1132
1164
  const customCommandsResult: CustomCommandsLoadResult = options.disableExtensionDiscovery
1133
1165
  ? { commands: [], errors: [] }
1134
- : await logger.timeAsync("discoverCustomCommands", loadCustomCommandsInternal, { cwd, agentDir });
1166
+ : await logger.time("discoverCustomCommands", loadCustomCommandsInternal, { cwd, agentDir });
1135
1167
  if (!options.disableExtensionDiscovery) {
1136
1168
  for (const { path, error } of customCommandsResult.errors) {
1137
1169
  logger.error("Failed to load custom command", { path, error });
@@ -1214,7 +1246,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1214
1246
  if (!hasDeferrableTools) {
1215
1247
  toolRegistry.delete("resolve");
1216
1248
  } else if (!toolRegistry.has("resolve")) {
1217
- 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);
1218
1250
  if (resolveTool) {
1219
1251
  toolRegistry.set(resolveTool.name, wrapToolWithMetaNotice(resolveTool));
1220
1252
  }
@@ -1276,6 +1308,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1276
1308
  mcpDiscoveryMode: hasDiscoverableMCPTools,
1277
1309
  mcpDiscoveryServerSummaries: discoverableMCPSummary.servers.map(formatDiscoverableMCPToolServerSummary),
1278
1310
  eagerTasks,
1311
+ secretsEnabled,
1279
1312
  });
1280
1313
 
1281
1314
  if (options.systemPrompt === undefined) {
@@ -1298,6 +1331,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1298
1331
  mcpDiscoveryMode: hasDiscoverableMCPTools,
1299
1332
  mcpDiscoveryServerSummaries: discoverableMCPSummary.servers.map(formatDiscoverableMCPToolServerSummary),
1300
1333
  eagerTasks,
1334
+ secretsEnabled,
1301
1335
  });
1302
1336
  }
1303
1337
  return options.systemPrompt(defaultPrompt);
@@ -1362,20 +1396,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1362
1396
  }
1363
1397
  }
1364
1398
 
1365
- const systemPrompt = await logger.timeAsync(
1366
- "buildSystemPrompt",
1367
- rebuildSystemPrompt,
1368
- initialToolNames,
1369
- toolRegistry,
1370
- );
1399
+ const systemPrompt = await logger.time("buildSystemPrompt", rebuildSystemPrompt, initialToolNames, toolRegistry);
1371
1400
 
1372
1401
  const promptTemplates =
1373
- options.promptTemplates ??
1374
- (await logger.timeAsync("discoverPromptTemplates", discoverPromptTemplates, cwd, agentDir));
1402
+ options.promptTemplates ?? (await logger.time("discoverPromptTemplates", discoverPromptTemplates, cwd, agentDir));
1375
1403
  toolSession.promptTemplates = promptTemplates;
1376
1404
 
1377
1405
  const slashCommands =
1378
- options.slashCommands ?? (await logger.timeAsync("discoverSlashCommands", discoverSlashCommands, cwd));
1406
+ options.slashCommands ?? (await logger.time("discoverSlashCommands", discoverSlashCommands, cwd));
1379
1407
 
1380
1408
  // Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)
1381
1409
  const convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {
@@ -1407,17 +1435,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1407
1435
  });
1408
1436
  };
1409
1437
 
1410
- // Load and create secret obfuscator if secrets are enabled
1411
- let obfuscator: SecretObfuscator | undefined;
1412
- if (settings.get("secrets.enabled")) {
1413
- const fileEntries = await logger.timeAsync("loadSecrets", loadSecrets, cwd, agentDir);
1414
- const envEntries = collectEnvSecrets();
1415
- const allEntries = [...envEntries, ...fileEntries];
1416
- if (allEntries.length > 0) {
1417
- obfuscator = new SecretObfuscator(allEntries);
1418
- }
1419
- }
1420
-
1421
1438
  // Final convertToLlm: chain block-images filter with secret obfuscation
1422
1439
  const convertToLlmFinal = (messages: AgentMessage[]): Message[] => {
1423
1440
  const converted = convertToLlmWithBlockImages(messages);
@@ -1501,13 +1518,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1501
1518
  return result;
1502
1519
  },
1503
1520
  intentTracing: !!intentField,
1504
- getToolChoice: () => {
1505
- if (pendingActionStore.hasPending) {
1506
- return { type: "function", name: "resolve" };
1507
- }
1508
- return session?.consumeNextToolChoiceOverride();
1509
- },
1521
+ getToolChoice: () => session?.nextToolChoice(),
1510
1522
  });
1523
+
1511
1524
  cursorEventEmitter = event => agent.emitExternalEvent(event);
1512
1525
 
1513
1526
  // Restore messages if session has existing data
@@ -1548,51 +1561,87 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1548
1561
  ttsrManager,
1549
1562
  obfuscator,
1550
1563
  asyncJobManager,
1551
- pendingActionStore,
1552
1564
  searchDb,
1553
1565
  });
1554
1566
 
1555
1567
  if (model?.api === "openai-codex-responses") {
1556
- try {
1557
- await logger.timeAsync("prewarmCodexWebsocket", prewarmOpenAICodexResponses, model, {
1558
- apiKey: await modelRegistry.getApiKey(model, providerSessionId),
1559
- sessionId: providerSessionId,
1560
- preferWebsockets: preferOpenAICodexWebsockets,
1561
- providerSessionState: session.providerSessionState,
1562
- });
1563
- } catch (error) {
1564
- logger.debug("Codex websocket prewarm failed", {
1565
- error: error instanceof Error ? error.message : String(error),
1566
- provider: model.provider,
1567
- model: model.id,
1568
- });
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
+ })();
1569
1595
  }
1570
1596
  }
1571
1597
 
1572
- // Warm up LSP servers (connects to detected servers)
1598
+ // Start LSP warmup in the background so startup does not block on language server initialization.
1573
1599
  let lspServers: CreateAgentSessionResult["lspServers"];
1574
1600
  if (enableLsp && settings.get("lsp.diagnosticsOnWrite")) {
1575
- try {
1576
- const result = await logger.timeAsync("warmupLspServers", warmupLspServers, cwd, {
1577
- onConnecting: serverNames => {
1578
- if (options.hasUI && serverNames.length > 0) {
1579
- 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;
1580
1613
  }
1581
- },
1582
- });
1583
- lspServers = result.servers;
1584
- } catch (error) {
1585
- 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
+ })();
1586
1633
  }
1587
1634
  }
1588
1635
 
1589
- startMemoryStartupTask({
1590
- session,
1591
- settings,
1592
- modelRegistry,
1593
- agentDir,
1594
- taskDepth,
1595
- });
1636
+ logger.time("startMemoryStartupTask", () =>
1637
+ startMemoryStartupTask({
1638
+ session,
1639
+ settings,
1640
+ modelRegistry,
1641
+ agentDir,
1642
+ taskDepth,
1643
+ }),
1644
+ );
1596
1645
 
1597
1646
  // Wire MCP manager callbacks to session for reactive tool updates
1598
1647
  if (mcpManager) {
@@ -1632,6 +1681,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1632
1681
  });
1633
1682
  }
1634
1683
 
1684
+ logger.time("createAgentSession:return");
1635
1685
  return {
1636
1686
  session,
1637
1687
  extensionsResult,
@@ -1639,5 +1689,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1639
1689
  mcpManager,
1640
1690
  modelFallbackMessage,
1641
1691
  lspServers,
1692
+ eventBus,
1642
1693
  };
1643
1694
  }
@@ -4,7 +4,7 @@ import { YAML } from "bun";
4
4
  import type { SecretEntry } from "./obfuscator";
5
5
  import { compileSecretRegex } from "./regex";
6
6
 
7
- export { obfuscateMessages, type SecretEntry, SecretObfuscator } from "./obfuscator";
7
+ export { deobfuscateSessionContext, obfuscateMessages, type SecretEntry, SecretObfuscator } from "./obfuscator";
8
8
 
9
9
  /**
10
10
  * Load secrets from project-local and global secrets.yml files.