@oh-my-pi/pi-coding-agent 15.12.4 → 15.13.0

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 (291) hide show
  1. package/CHANGELOG.md +304 -6
  2. package/dist/cli.js +1015 -881
  3. package/dist/types/async/job-manager.d.ts +15 -0
  4. package/dist/types/autolearn/controller.d.ts +25 -0
  5. package/dist/types/autolearn/managed-skills.d.ts +45 -0
  6. package/dist/types/autoresearch/state.d.ts +1 -1
  7. package/dist/types/autoresearch/types.d.ts +1 -1
  8. package/dist/types/cli/args.d.ts +19 -1
  9. package/dist/types/cli/session-picker.d.ts +1 -1
  10. package/dist/types/cli/setup-cli.d.ts +1 -1
  11. package/dist/types/cli/setup-model-picker.d.ts +14 -0
  12. package/dist/types/collab/protocol.d.ts +1 -1
  13. package/dist/types/commands/say.d.ts +24 -0
  14. package/dist/types/config/keybindings.d.ts +3 -3
  15. package/dist/types/config/model-registry.d.ts +10 -0
  16. package/dist/types/config/models-config-schema.d.ts +12 -0
  17. package/dist/types/config/models-config.d.ts +8 -2
  18. package/dist/types/config/settings-schema.d.ts +261 -58
  19. package/dist/types/export/html/index.d.ts +2 -1
  20. package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  22. package/dist/types/extensibility/extensions/types.d.ts +47 -1
  23. package/dist/types/extensibility/hooks/index.d.ts +2 -1
  24. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
  25. package/dist/types/extensibility/plugins/loader.d.ts +11 -0
  26. package/dist/types/extensibility/shared-events.d.ts +1 -1
  27. package/dist/types/extensibility/skills.d.ts +10 -0
  28. package/dist/types/goals/guided-setup.d.ts +18 -0
  29. package/dist/types/goals/state.d.ts +1 -1
  30. package/dist/types/hindsight/transcript.d.ts +1 -1
  31. package/dist/types/index.d.ts +5 -0
  32. package/dist/types/internal-urls/local-protocol.d.ts +4 -2
  33. package/dist/types/main.d.ts +4 -3
  34. package/dist/types/mcp/startup-events.d.ts +11 -0
  35. package/dist/types/memories/index.d.ts +7 -0
  36. package/dist/types/memory-backend/local-backend.d.ts +4 -3
  37. package/dist/types/mnemopi/config.d.ts +4 -4
  38. package/dist/types/modes/components/agent-hub.d.ts +6 -0
  39. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  40. package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
  41. package/dist/types/modes/components/custom-editor.d.ts +39 -1
  42. package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
  43. package/dist/types/modes/components/session-selector.d.ts +1 -1
  44. package/dist/types/modes/components/tool-execution.d.ts +26 -16
  45. package/dist/types/modes/components/transcript-container.d.ts +23 -2
  46. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  47. package/dist/types/modes/components/usage-row.d.ts +3 -0
  48. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  49. package/dist/types/modes/controllers/input-controller.d.ts +14 -0
  50. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  51. package/dist/types/modes/gradient-highlight.d.ts +9 -4
  52. package/dist/types/modes/image-references.d.ts +6 -0
  53. package/dist/types/modes/interactive-mode.d.ts +27 -3
  54. package/dist/types/modes/magic-keywords.d.ts +13 -1
  55. package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
  56. package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
  57. package/dist/types/modes/runtime-init.d.ts +4 -0
  58. package/dist/types/modes/theme/theme.d.ts +13 -2
  59. package/dist/types/modes/types.d.ts +8 -2
  60. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  61. package/dist/types/registry/agent-registry.d.ts +17 -0
  62. package/dist/types/secrets/obfuscator.d.ts +1 -1
  63. package/dist/types/session/agent-session.d.ts +14 -2
  64. package/dist/types/session/indexed-session-storage.d.ts +3 -4
  65. package/dist/types/session/session-context.d.ts +39 -0
  66. package/dist/types/session/session-entries.d.ts +159 -0
  67. package/dist/types/session/session-listing.d.ts +69 -0
  68. package/dist/types/session/session-loader.d.ts +16 -0
  69. package/dist/types/session/session-manager.d.ts +82 -474
  70. package/dist/types/session/session-migrations.d.ts +12 -0
  71. package/dist/types/session/session-paths.d.ts +25 -0
  72. package/dist/types/session/session-persistence.d.ts +8 -0
  73. package/dist/types/session/session-storage.d.ts +11 -12
  74. package/dist/types/session/snapcompact-inline.d.ts +12 -1
  75. package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
  76. package/dist/types/session/tool-choice-queue.d.ts +6 -6
  77. package/dist/types/stt/asr-client.d.ts +90 -0
  78. package/dist/types/stt/asr-protocol.d.ts +97 -0
  79. package/dist/types/stt/asr-worker.d.ts +2 -0
  80. package/dist/types/stt/downloader.d.ts +38 -0
  81. package/dist/types/stt/endpointer.d.ts +59 -0
  82. package/dist/types/stt/index.d.ts +5 -1
  83. package/dist/types/stt/models.d.ts +120 -0
  84. package/dist/types/stt/recorder.d.ts +17 -0
  85. package/dist/types/stt/stt-controller.d.ts +6 -0
  86. package/dist/types/stt/transcriber.d.ts +5 -7
  87. package/dist/types/stt/wav.d.ts +29 -0
  88. package/dist/types/system-prompt.d.ts +4 -0
  89. package/dist/types/task/executor.d.ts +2 -0
  90. package/dist/types/task/index.d.ts +9 -1
  91. package/dist/types/task/types.d.ts +36 -0
  92. package/dist/types/tools/bash.d.ts +2 -2
  93. package/dist/types/tools/eval-render.d.ts +1 -1
  94. package/dist/types/tools/index.d.ts +11 -1
  95. package/dist/types/tools/irc.d.ts +1 -0
  96. package/dist/types/tools/learn.d.ts +51 -0
  97. package/dist/types/tools/manage-skill.d.ts +40 -0
  98. package/dist/types/tools/plan-mode-guard.d.ts +10 -0
  99. package/dist/types/tools/renderers.d.ts +7 -11
  100. package/dist/types/tools/ssh.d.ts +1 -1
  101. package/dist/types/tools/todo.d.ts +1 -1
  102. package/dist/types/tools/tts.d.ts +25 -0
  103. package/dist/types/tools/write.d.ts +1 -1
  104. package/dist/types/tts/downloader.d.ts +20 -0
  105. package/dist/types/tts/index.d.ts +8 -0
  106. package/dist/types/tts/models.d.ts +82 -0
  107. package/dist/types/tts/player.d.ts +32 -0
  108. package/dist/types/tts/runtime.d.ts +6 -0
  109. package/dist/types/tts/streaming-player.d.ts +41 -0
  110. package/dist/types/tts/tts-client.d.ts +93 -0
  111. package/dist/types/tts/tts-protocol.d.ts +95 -0
  112. package/dist/types/tts/tts-worker.d.ts +2 -0
  113. package/dist/types/tts/vocalizer.d.ts +41 -0
  114. package/dist/types/tts/wav.d.ts +8 -0
  115. package/dist/types/utils/tool-choice.d.ts +8 -0
  116. package/dist/types/utils/tools-manager.d.ts +2 -1
  117. package/dist/types/utils/tools-manager.test.d.ts +1 -0
  118. package/dist/types/web/scrapers/github.d.ts +1 -1
  119. package/package.json +15 -14
  120. package/src/async/job-manager.ts +49 -0
  121. package/src/autolearn/controller.ts +139 -0
  122. package/src/autolearn/managed-skills.ts +257 -0
  123. package/src/autoresearch/state.ts +1 -1
  124. package/src/autoresearch/types.ts +1 -1
  125. package/src/cli/args.ts +56 -2
  126. package/src/cli/session-picker.ts +2 -1
  127. package/src/cli/setup-cli.ts +148 -47
  128. package/src/cli/setup-model-picker.ts +43 -0
  129. package/src/cli-commands.ts +1 -0
  130. package/src/cli.ts +45 -13
  131. package/src/collab/host.ts +1 -1
  132. package/src/collab/protocol.ts +1 -1
  133. package/src/commands/say.ts +102 -0
  134. package/src/commands/setup.ts +1 -1
  135. package/src/commit/agentic/tools/analyze-file.ts +3 -0
  136. package/src/config/keybindings.ts +2 -2
  137. package/src/config/model-discovery.ts +11 -5
  138. package/src/config/model-registry.ts +64 -9
  139. package/src/config/models-config-schema.ts +4 -1
  140. package/src/config/models-config.ts +2 -1
  141. package/src/config/settings-schema.ts +248 -32
  142. package/src/config/settings.ts +10 -0
  143. package/src/discovery/builtin.ts +23 -1
  144. package/src/discovery/claude-plugins.ts +44 -5
  145. package/src/discovery/helpers.ts +41 -1
  146. package/src/eval/__tests__/budget-bridge.test.ts +1 -1
  147. package/src/eval/js/shared/prelude.txt +69 -17
  148. package/src/export/html/index.ts +3 -6
  149. package/src/extensibility/extensions/model-api.ts +41 -0
  150. package/src/extensibility/extensions/runner.ts +4 -0
  151. package/src/extensibility/extensions/types.ts +52 -1
  152. package/src/extensibility/extensions/wrapper.ts +41 -5
  153. package/src/extensibility/hooks/index.ts +2 -1
  154. package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
  155. package/src/extensibility/plugins/loader.ts +30 -19
  156. package/src/extensibility/plugins/manager.ts +221 -90
  157. package/src/extensibility/shared-events.ts +1 -1
  158. package/src/extensibility/skills.ts +96 -15
  159. package/src/goals/guided-setup.ts +133 -0
  160. package/src/goals/state.ts +1 -1
  161. package/src/hindsight/transcript.ts +1 -1
  162. package/src/index.ts +5 -0
  163. package/src/internal-urls/docs-index.generated.ts +10 -10
  164. package/src/internal-urls/history-protocol.ts +1 -1
  165. package/src/internal-urls/local-protocol.ts +29 -7
  166. package/src/main.ts +27 -7
  167. package/src/mcp/startup-events.ts +21 -0
  168. package/src/mcp/transports/stdio.ts +2 -1
  169. package/src/memories/index.ts +146 -11
  170. package/src/memory-backend/local-backend.ts +11 -5
  171. package/src/mnemopi/backend.ts +1 -0
  172. package/src/mnemopi/config.ts +26 -10
  173. package/src/modes/acp/acp-agent.ts +3 -5
  174. package/src/modes/components/agent-hub.ts +49 -4
  175. package/src/modes/components/assistant-message.ts +4 -37
  176. package/src/modes/components/compaction-summary-message.ts +125 -26
  177. package/src/modes/components/custom-editor.test.ts +96 -0
  178. package/src/modes/components/custom-editor.ts +164 -8
  179. package/src/modes/components/session-selector.ts +1 -1
  180. package/src/modes/components/settings-defs.ts +7 -0
  181. package/src/modes/components/tool-execution.ts +82 -43
  182. package/src/modes/components/transcript-container.ts +70 -1
  183. package/src/modes/components/tree-selector.ts +1 -1
  184. package/src/modes/components/usage-row.ts +18 -0
  185. package/src/modes/components/user-message.ts +4 -2
  186. package/src/modes/controllers/command-controller.ts +14 -4
  187. package/src/modes/controllers/event-controller.ts +78 -11
  188. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  189. package/src/modes/controllers/input-controller.ts +258 -27
  190. package/src/modes/controllers/selector-controller.ts +12 -2
  191. package/src/modes/gradient-highlight.ts +21 -9
  192. package/src/modes/image-references.ts +20 -0
  193. package/src/modes/interactive-mode.ts +286 -40
  194. package/src/modes/magic-keywords.ts +27 -5
  195. package/src/modes/rpc/rpc-mode.ts +146 -14
  196. package/src/modes/rpc/rpc-subagents.ts +2 -2
  197. package/src/modes/rpc/rpc-types.ts +8 -2
  198. package/src/modes/runtime-init.ts +28 -3
  199. package/src/modes/theme/theme.ts +98 -50
  200. package/src/modes/types.ts +6 -2
  201. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  202. package/src/modes/utils/ui-helpers.ts +34 -6
  203. package/src/priority.json +5 -1
  204. package/src/prompts/agents/task.md +1 -0
  205. package/src/prompts/goals/guided-goal-interview.md +8 -0
  206. package/src/prompts/goals/guided-goal-system.md +12 -0
  207. package/src/prompts/memories/read-path.md +6 -0
  208. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  209. package/src/prompts/system/autolearn-guidance.md +7 -0
  210. package/src/prompts/system/autolearn-nudge.md +3 -0
  211. package/src/prompts/system/eager-task.md +7 -0
  212. package/src/prompts/system/eager-todo.md +11 -6
  213. package/src/prompts/system/subagent-system-prompt.md +4 -0
  214. package/src/prompts/system/system-prompt.md +10 -5
  215. package/src/prompts/system/title-marker-instruction.md +1 -0
  216. package/src/prompts/system/title-system-marker.md +16 -0
  217. package/src/prompts/tools/job.md +1 -0
  218. package/src/prompts/tools/learn.md +7 -0
  219. package/src/prompts/tools/manage-skill.md +9 -0
  220. package/src/prompts/tools/task.md +3 -0
  221. package/src/registry/agent-registry.ts +30 -0
  222. package/src/sdk.ts +88 -24
  223. package/src/secrets/obfuscator.ts +1 -1
  224. package/src/session/agent-session.ts +209 -87
  225. package/src/session/history-storage.ts +2 -2
  226. package/src/session/indexed-session-storage.ts +7 -17
  227. package/src/session/session-context.ts +352 -0
  228. package/src/session/session-entries.ts +194 -0
  229. package/src/session/session-listing.ts +588 -0
  230. package/src/session/session-loader.ts +106 -0
  231. package/src/session/session-manager.ts +933 -3145
  232. package/src/session/session-migrations.ts +78 -0
  233. package/src/session/session-paths.ts +193 -0
  234. package/src/session/session-persistence.ts +131 -0
  235. package/src/session/session-storage.ts +91 -50
  236. package/src/session/snapcompact-inline.ts +21 -1
  237. package/src/session/snapcompact-savings-journal.ts +113 -0
  238. package/src/session/tool-choice-queue.ts +23 -11
  239. package/src/slash-commands/builtin-registry.ts +25 -3
  240. package/src/stt/asr-client.ts +520 -0
  241. package/src/stt/asr-protocol.ts +65 -0
  242. package/src/stt/asr-worker.ts +790 -0
  243. package/src/stt/downloader.ts +107 -47
  244. package/src/stt/endpointer.ts +259 -0
  245. package/src/stt/index.ts +5 -1
  246. package/src/stt/models.ts +150 -0
  247. package/src/stt/recorder.ts +247 -60
  248. package/src/stt/stt-controller.ts +201 -22
  249. package/src/stt/transcriber.ts +37 -68
  250. package/src/stt/wav.ts +173 -0
  251. package/src/system-prompt.ts +8 -0
  252. package/src/task/agents.ts +1 -2
  253. package/src/task/executor.ts +49 -15
  254. package/src/task/index.ts +60 -6
  255. package/src/task/render.ts +83 -8
  256. package/src/task/types.ts +53 -0
  257. package/src/tools/ask.ts +8 -0
  258. package/src/tools/bash.ts +4 -3
  259. package/src/tools/eval-render.ts +4 -3
  260. package/src/tools/index.ts +40 -4
  261. package/src/tools/irc.ts +10 -2
  262. package/src/tools/job.ts +14 -2
  263. package/src/tools/learn.ts +144 -0
  264. package/src/tools/manage-skill.ts +104 -0
  265. package/src/tools/plan-mode-guard.ts +53 -19
  266. package/src/tools/renderers.ts +7 -11
  267. package/src/tools/ssh.ts +4 -3
  268. package/src/tools/todo.ts +1 -1
  269. package/src/tools/tts.ts +203 -92
  270. package/src/tools/write.ts +18 -2
  271. package/src/tts/downloader.ts +64 -0
  272. package/src/tts/index.ts +8 -0
  273. package/src/tts/models.ts +137 -0
  274. package/src/tts/player.ts +137 -0
  275. package/src/tts/runtime.ts +21 -0
  276. package/src/tts/streaming-player.ts +266 -0
  277. package/src/tts/tts-client.ts +647 -0
  278. package/src/tts/tts-protocol.ts +60 -0
  279. package/src/tts/tts-worker.ts +497 -0
  280. package/src/tts/vocalizer.ts +162 -0
  281. package/src/tts/wav.ts +58 -0
  282. package/src/utils/title-generator.ts +48 -5
  283. package/src/utils/tool-choice.ts +16 -0
  284. package/src/utils/tools-manager.test.ts +25 -0
  285. package/src/utils/tools-manager.ts +19 -1
  286. package/src/web/scrapers/github.ts +96 -0
  287. package/src/web/search/index.ts +13 -0
  288. package/src/web/search/providers/searxng.ts +13 -1
  289. package/dist/types/stt/setup.d.ts +0 -18
  290. package/src/stt/setup.ts +0 -52
  291. package/src/stt/transcribe.py +0 -70
@@ -166,6 +166,7 @@ import type {
166
166
  TurnEndEvent,
167
167
  TurnStartEvent,
168
168
  } from "../extensibility/extensions";
169
+ import { createExtensionModelQuery } from "../extensibility/extensions/model-api";
169
170
  import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
170
171
  import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
171
172
  import type { HookCommandContext } from "../extensibility/hooks/types";
@@ -187,6 +188,7 @@ import { containsWorkflow, WORKFLOW_NOTICE } from "../modes/workflow";
187
188
  import { createPlanReadMatcher } from "../plan-mode/plan-protection";
188
189
  import type { PlanModeState } from "../plan-mode/state";
189
190
  import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
191
+ import eagerTaskPrompt from "../prompts/system/eager-task.md" with { type: "text" };
190
192
  import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
191
193
  import emptyStopRetryTemplate from "../prompts/system/empty-stop-retry.md" with { type: "text" };
192
194
  import ircAutoReplyTemplate from "../prompts/system/irc-autoreply.md" with { type: "text" };
@@ -238,7 +240,7 @@ import { type EditMode, resolveEditMode } from "../utils/edit-mode";
238
240
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
239
241
  import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
240
242
  import { normalizeModelContextImages } from "../utils/image-loading";
241
- import { buildNamedToolChoice } from "../utils/tool-choice";
243
+ import { buildNamedToolChoice, isToolChoiceActive } from "../utils/tool-choice";
242
244
  import type { AuthStorage } from "./auth-storage";
243
245
  import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
244
246
  import {
@@ -258,15 +260,12 @@ import {
258
260
  SKILL_PROMPT_MESSAGE_TYPE,
259
261
  stripImagesFromMessage,
260
262
  } from "./messages";
263
+ import type { SessionContext } from "./session-context";
264
+ import { getLatestCompactionEntry, getRestorableSessionModels } from "./session-context";
261
265
  import { formatSessionDumpText } from "./session-dump-format";
262
- import type {
263
- BranchSummaryEntry,
264
- CompactionEntry,
265
- NewSessionOptions,
266
- SessionContext,
267
- SessionManager,
268
- } from "./session-manager";
269
- import { EPHEMERAL_MODEL_CHANGE_ROLE, getLatestCompactionEntry, getRestorableSessionModels } from "./session-manager";
266
+ import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions } from "./session-entries";
267
+ import { EPHEMERAL_MODEL_CHANGE_ROLE } from "./session-entries";
268
+ import type { SessionManager } from "./session-manager";
270
269
  import type { ShakeMode, ShakeResult } from "./shake-types";
271
270
  import { ToolChoiceQueue } from "./tool-choice-queue";
272
271
  import { YieldQueue } from "./yield-queue";
@@ -441,6 +440,10 @@ export interface AgentSessionConfig {
441
440
  asyncJobManager?: AsyncJobManager;
442
441
  /** Agent identity (registry id like "Main" or "Alice") used for IRC routing. */
443
442
  agentId?: string;
443
+ /** Whether this session is the top-level agent or a subagent. Drives eager-task
444
+ * prelude gating so a top-level session created with a custom `agentId` still
445
+ * receives the always-mode reminder. Defaults to "main". */
446
+ agentKind?: "main" | "sub";
444
447
  /**
445
448
  * Override the provider-facing session ID for all API requests from this session.
446
449
  * When absent, `sessionManager.getSessionId()` is used. Needed when benchmark or
@@ -931,6 +934,7 @@ export class AgentSession {
931
934
  /** Messages queued to be included with the next user prompt as context ("asides"). */
932
935
  #pendingNextTurnMessages: CustomMessage[] = [];
933
936
  #scheduledHiddenNextTurnGeneration: number | undefined = undefined;
937
+ #queuedMessageDrainScheduled = false;
934
938
  #planModeState: PlanModeState | undefined;
935
939
  #goalModeState: GoalModeState | undefined;
936
940
  #goalRuntime: GoalRuntime;
@@ -994,6 +998,7 @@ export class AgentSession {
994
998
  #pendingIrcAsides: CustomMessage[] = [];
995
999
  // Agent identity (registry id) used for IRC routing and job ownership.
996
1000
  #agentId: string | undefined;
1001
+ #agentKind: "main" | "sub" = "main";
997
1002
  #providerSessionId: string | undefined;
998
1003
  #freshProviderSessionId: string | undefined;
999
1004
  #isDisposed = false;
@@ -1085,6 +1090,7 @@ export class AgentSession {
1085
1090
 
1086
1091
  #streamingEditFileCache = new Map<string, string>();
1087
1092
  #promptInFlightCount = 0;
1093
+ #abortInProgress = false;
1088
1094
  // Wire-level agent_end emission deferred until #promptInFlightCount drops to 0.
1089
1095
  // Internal extension hooks and post-emit work (auto-retry, auto-compaction, todo
1090
1096
  // checks in #handleAgentEvent) still fire on the original schedule — only the
@@ -1154,24 +1160,21 @@ export class AgentSession {
1154
1160
  }
1155
1161
  }
1156
1162
 
1157
- /** A steer/follow-up can land after the agent loop's final queue poll but
1158
- * before the prompt unwinds: #promptInFlightCount keeps isStreaming true
1159
- * through post-prompt recovery, so senders (collab guests, skills) still
1160
- * queue via agent.steer()/followUp() instead of starting a fresh prompt.
1161
- * Without a drain those messages strand invisibly until the next manual
1162
- * prompt. Runs when the session settles; the guard makes it a no-op when
1163
- * the queue was consumed normally or a new turn already started. */
1163
+ /** A steer/follow-up can land after the agent loop's final queue poll, or
1164
+ * after an abort stops an auto-continued queued turn. In both cases the
1165
+ * agent-core queue still owns the message, but no loop is left to poll it.
1166
+ * Runs whenever the session settles; the guard makes it a no-op when the
1167
+ * queue was consumed normally or a new turn already started. */
1164
1168
  #drainStrandedQueuedMessages(): void {
1165
- if (!this.agent.hasQueuedMessages()) return;
1166
- this.#scheduleAgentContinue({
1167
- shouldContinue: () => this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages(),
1168
- });
1169
+ if (this.#abortInProgress) return;
1170
+ this.#scheduleQueuedMessageDrain();
1169
1171
  }
1170
1172
 
1171
1173
  #resetInFlight(): void {
1172
1174
  this.#promptInFlightCount = 0;
1173
1175
  this.#releasePowerAssertion();
1174
1176
  this.#flushPendingAgentEnd();
1177
+ this.#drainStrandedQueuedMessages();
1175
1178
  }
1176
1179
 
1177
1180
  #flushPendingAgentEnd(): void {
@@ -1298,6 +1301,7 @@ export class AgentSession {
1298
1301
  this.#ttsrManager = config.ttsrManager;
1299
1302
  this.#obfuscator = config.obfuscator;
1300
1303
  this.#agentId = config.agentId;
1304
+ this.#agentKind = config.agentKind ?? "main";
1301
1305
  this.#providerSessionId = config.providerSessionId;
1302
1306
  this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
1303
1307
  const event: AgentEvent = {
@@ -1374,7 +1378,12 @@ export class AgentSession {
1374
1378
 
1375
1379
  /** Advance the tool-choice queue and return the next directive for the upcoming LLM call. */
1376
1380
  nextToolChoice(): ToolChoice | undefined {
1377
- return this.#toolChoiceQueue.nextToolChoice();
1381
+ const choice = this.#toolChoiceQueue.nextToolChoice();
1382
+ if (isToolChoiceActive(choice, this.agent.state.tools)) {
1383
+ return choice;
1384
+ }
1385
+ this.#toolChoiceQueue.reject("unavailable");
1386
+ return undefined;
1378
1387
  }
1379
1388
 
1380
1389
  /**
@@ -1912,6 +1921,11 @@ export class AgentSession {
1912
1921
  return;
1913
1922
  }
1914
1923
 
1924
+ // A deliberate abort should settle the current turn, not trigger queued continuations.
1925
+ if (msg.stopReason === "aborted") {
1926
+ this.#resolveRetry();
1927
+ return;
1928
+ }
1915
1929
  // Check for retryable errors first (overloaded, rate limit, server errors)
1916
1930
  if (this.#isRetryableError(msg)) {
1917
1931
  const didRetry = await this.#handleRetryableError(msg);
@@ -1934,7 +1948,7 @@ export class AgentSession {
1934
1948
  if (compactionDeferredHandoff) {
1935
1949
  return;
1936
1950
  }
1937
- if (msg.stopReason !== "error" && msg.stopReason !== "aborted") {
1951
+ if (msg.stopReason !== "error") {
1938
1952
  if (this.#enforceRewindBeforeYield()) {
1939
1953
  return;
1940
1954
  }
@@ -2030,13 +2044,13 @@ export class AgentSession {
2030
2044
  onError?: () => void;
2031
2045
  }): void {
2032
2046
  this.#schedulePostPromptTask(
2033
- async () => {
2047
+ async signal => {
2034
2048
  // Defense in depth: if compaction/handoff slipped onto the post-prompt queue
2035
2049
  // alongside us (e.g. via a scheduler we don't own), refuse to start a fresh
2036
2050
  // streaming turn — agent.continue() here would race the handoff's session
2037
2051
  // reset. The first-class fix is in #checkCompaction/the agent_end handler,
2038
2052
  // but this guard catches anything that bypasses that path.
2039
- if (this.isCompacting || this.isGeneratingHandoff) {
2053
+ if (signal.aborted || this.#isDisposed || this.isCompacting || this.isGeneratingHandoff) {
2040
2054
  options?.onSkip?.();
2041
2055
  return;
2042
2056
  }
@@ -2044,14 +2058,21 @@ export class AgentSession {
2044
2058
  options?.onSkip?.();
2045
2059
  return;
2046
2060
  }
2061
+ this.#beginInFlight();
2047
2062
  try {
2048
2063
  await this.#maybeRestoreRetryFallbackPrimary();
2064
+ if (signal.aborted || this.#isDisposed) {
2065
+ options?.onSkip?.();
2066
+ return;
2067
+ }
2049
2068
  await this.agent.continue();
2050
2069
  } catch (error) {
2051
2070
  logger.warn("agent.continue failed after scheduling", {
2052
2071
  error: error instanceof Error ? error.message : String(error),
2053
2072
  });
2054
2073
  options?.onError?.();
2074
+ } finally {
2075
+ this.#endInFlight();
2055
2076
  }
2056
2077
  },
2057
2078
  {
@@ -2107,8 +2128,13 @@ export class AgentSession {
2107
2128
  * and fire-and-forget `agent.continue()` may still be streaming after
2108
2129
  * the TTSR resume gate resolves.
2109
2130
  */
2110
- async #waitForPostPromptRecovery(): Promise<void> {
2131
+ async #waitForPostPromptRecovery(generation?: number): Promise<void> {
2111
2132
  while (true) {
2133
+ // An abort bumps #promptGeneration. When this wait runs on behalf of a
2134
+ // specific prompt turn, stop as soon as that turn has been superseded:
2135
+ // its promise must resolve on the abort, not block on a queued
2136
+ // steer/follow-up that the post-abort drain starts as a fresh turn.
2137
+ if (generation !== undefined && this.#promptGeneration !== generation) return;
2112
2138
  if (this.#retryPromise) {
2113
2139
  await this.#retryPromise;
2114
2140
  continue;
@@ -3155,8 +3181,9 @@ export class AgentSession {
3155
3181
  // session's dispose.
3156
3182
  this.abortRetry();
3157
3183
  this.abortCompaction();
3184
+ const postPromptDrain = this.#cancelPostPromptTasks();
3158
3185
  this.agent.abort();
3159
- await this.#cancelPostPromptTasks();
3186
+ await postPromptDrain;
3160
3187
  // Cancel jobs this agent registered so a subagent's teardown doesn't
3161
3188
  // leak its background bash/task work into the parent's manager. Only
3162
3189
  // the session that owns the manager goes on to dispose it (which itself
@@ -4603,10 +4630,12 @@ export class AgentSession {
4603
4630
  return true;
4604
4631
  }
4605
4632
 
4606
- // Skip eager todo prelude when the user has already queued a directive
4633
+ // Skip eager preludes when the user has already queued a directive
4607
4634
  const hasPendingUserDirective = this.#toolChoiceQueue.inspect().includes("user-force");
4608
4635
  const eagerTodoPrelude =
4609
4636
  !options?.synthetic && !hasPendingUserDirective ? this.#createEagerTodoPrelude(expandedText) : undefined;
4637
+ const eagerTaskPrelude =
4638
+ !options?.synthetic && !hasPendingUserDirective ? this.#createEagerTaskPrelude(expandedText) : undefined;
4610
4639
  const normalizedImages = await this.#normalizeImagesForModel(options?.images);
4611
4640
 
4612
4641
  const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
@@ -4619,17 +4648,24 @@ export class AgentSession {
4619
4648
  ? { role: "developer" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() }
4620
4649
  : { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
4621
4650
 
4651
+ const preludeMessages: AgentMessage[] = [];
4622
4652
  if (eagerTodoPrelude) {
4623
- this.#toolChoiceQueue.pushOnce(eagerTodoPrelude.toolChoice, {
4624
- label: "eager-todo",
4625
- });
4653
+ if (eagerTodoPrelude.toolChoice) {
4654
+ this.#toolChoiceQueue.pushOnce(eagerTodoPrelude.toolChoice, {
4655
+ label: "eager-todo",
4656
+ });
4657
+ }
4658
+ preludeMessages.push(eagerTodoPrelude.message);
4659
+ }
4660
+ if (eagerTaskPrelude) {
4661
+ preludeMessages.push(eagerTaskPrelude);
4626
4662
  }
4627
4663
 
4628
4664
  try {
4629
4665
  await this.#promptWithMessage(message, expandedText, {
4630
4666
  ...options,
4631
4667
  images: normalizedImages,
4632
- prependMessages: eagerTodoPrelude ? [eagerTodoPrelude.message] : undefined,
4668
+ prependMessages: preludeMessages.length > 0 ? preludeMessages : undefined,
4633
4669
  appendMessages: keywordNotices.length > 0 ? keywordNotices : undefined,
4634
4670
  });
4635
4671
  } finally {
@@ -4857,7 +4893,7 @@ export class AgentSession {
4857
4893
  const agentPromptOptions = options?.toolChoice ? { toolChoice: options.toolChoice } : undefined;
4858
4894
  await this.#promptAgentWithIdleRetry(messages, agentPromptOptions);
4859
4895
  if (!options?.skipPostPromptRecoveryWait) {
4860
- await this.#waitForPostPromptRecovery();
4896
+ await this.#waitForPostPromptRecovery(generation);
4861
4897
  }
4862
4898
  } finally {
4863
4899
  this.#endInFlight();
@@ -4907,6 +4943,7 @@ export class AgentSession {
4907
4943
  sessionManager: this.sessionManager,
4908
4944
  modelRegistry: this.#modelRegistry,
4909
4945
  model: this.model ?? undefined,
4946
+ models: createExtensionModelQuery(this.#modelRegistry, this.settings, () => this.model ?? undefined),
4910
4947
  isIdle: () => !this.isStreaming,
4911
4948
  abort: () => {
4912
4949
  void this.abort();
@@ -5054,9 +5091,25 @@ export class AgentSession {
5054
5091
  }
5055
5092
 
5056
5093
  #scheduleIdleQueueDrain(): void {
5057
- if (!this.#canAutoContinueForFollowUp()) return;
5094
+ this.#scheduleQueuedMessageDrain();
5095
+ }
5096
+
5097
+ #scheduleQueuedMessageDrain(): void {
5098
+ if (this.#queuedMessageDrainScheduled || !this.#canAutoContinueForFollowUp() || !this.agent.hasQueuedMessages()) {
5099
+ return;
5100
+ }
5101
+ this.#queuedMessageDrainScheduled = true;
5058
5102
  this.#scheduleAgentContinue({
5059
- shouldContinue: () => this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages(),
5103
+ shouldContinue: () => {
5104
+ this.#queuedMessageDrainScheduled = false;
5105
+ return this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages();
5106
+ },
5107
+ onSkip: () => {
5108
+ this.#queuedMessageDrainScheduled = false;
5109
+ },
5110
+ onError: () => {
5111
+ this.#queuedMessageDrainScheduled = false;
5112
+ },
5060
5113
  });
5061
5114
  }
5062
5115
 
@@ -5068,7 +5121,11 @@ export class AgentSession {
5068
5121
  if (this.isRetrying) return false;
5069
5122
  const messages = this.agent.state.messages;
5070
5123
  const last = messages[messages.length - 1];
5071
- return last?.role === "assistant";
5124
+ // A user interrupt during tool execution can leave the transcript ending
5125
+ // with the emitted tool result, not the aborted assistant message. Continuing
5126
+ // from that state is still resumable: Agent.continue() first polls queued
5127
+ // steering before making the next model call.
5128
+ return last?.role === "assistant" || last?.role === "toolResult";
5072
5129
  }
5073
5130
 
5074
5131
  queueDeferredMessage(message: CustomMessage): void {
@@ -5167,11 +5224,17 @@ export class AgentSession {
5167
5224
  * - Streaming: queue as steer/follow-up or store for next turn
5168
5225
  * - Not streaming + triggerTurn: appends to state/session, starts new turn unless the client cannot own it
5169
5226
  * - Not streaming + no trigger: appends to state/session, no turn
5227
+ *
5228
+ * @returns true iff this call synchronously started a new turn (awaited
5229
+ * `agent.prompt`); false when the message was queued/appended without a turn
5230
+ * — including when `triggerTurn` is downgraded because the client defers
5231
+ * agent-initiated turns. Callers that must mirror the resulting `agent_end`
5232
+ * use this to avoid acting on a turn that never ran.
5170
5233
  */
5171
5234
  async sendCustomMessage<T = unknown>(
5172
5235
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
5173
5236
  options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn"; queueChipText?: string },
5174
- ): Promise<void> {
5237
+ ): Promise<boolean> {
5175
5238
  const details =
5176
5239
  options?.queueChipText && options.deliverAs !== "nextTurn"
5177
5240
  ? ({
@@ -5195,7 +5258,7 @@ export class AgentSession {
5195
5258
  if (this.isStreaming) {
5196
5259
  if (options?.deliverAs === "nextTurn") {
5197
5260
  this.#queueHiddenNextTurnMessage(normalizedAppMessage, options?.triggerTurn ?? false);
5198
- return;
5261
+ return false;
5199
5262
  }
5200
5263
 
5201
5264
  if (options?.deliverAs === "followUp") {
@@ -5204,17 +5267,17 @@ export class AgentSession {
5204
5267
  this.agent.steer(normalizedAppMessage);
5205
5268
  }
5206
5269
  this.#scheduleIdleQueueDrain();
5207
- return;
5270
+ return false;
5208
5271
  }
5209
5272
 
5210
5273
  if (options?.deliverAs === "nextTurn") {
5211
5274
  if (options?.triggerTurn) {
5212
5275
  if (this.#clientBridge?.deferAgentInitiatedTurns && !this.#allowAcpAgentInitiatedTurns) {
5213
5276
  this.#queueHiddenNextTurnMessage(normalizedAppMessage, false);
5214
- return;
5277
+ return false;
5215
5278
  }
5216
5279
  await this.agent.prompt(normalizedAppMessage);
5217
- return;
5280
+ return true;
5218
5281
  }
5219
5282
  this.agent.appendMessage(normalizedAppMessage);
5220
5283
  this.sessionManager.appendCustomMessageEntry(
@@ -5224,16 +5287,16 @@ export class AgentSession {
5224
5287
  message.details,
5225
5288
  message.attribution ?? "agent",
5226
5289
  );
5227
- return;
5290
+ return false;
5228
5291
  }
5229
5292
 
5230
5293
  if (options?.triggerTurn) {
5231
5294
  if (this.#clientBridge?.deferAgentInitiatedTurns && !this.#allowAcpAgentInitiatedTurns) {
5232
5295
  this.#queueHiddenNextTurnMessage(normalizedAppMessage, false);
5233
- return;
5296
+ return false;
5234
5297
  }
5235
5298
  await this.agent.prompt(normalizedAppMessage);
5236
- return;
5299
+ return true;
5237
5300
  }
5238
5301
 
5239
5302
  this.agent.appendMessage(normalizedAppMessage);
@@ -5244,6 +5307,7 @@ export class AgentSession {
5244
5307
  message.details,
5245
5308
  message.attribution ?? "agent",
5246
5309
  );
5310
+ return false;
5247
5311
  }
5248
5312
 
5249
5313
  /**
@@ -5380,28 +5444,37 @@ export class AgentSession {
5380
5444
  * abort. Omit it for internal/lifecycle aborts.
5381
5445
  */
5382
5446
  async abort(options?: { goalReason?: "interrupted" | "internal"; reason?: string }): Promise<void> {
5383
- this.abortRetry();
5384
- this.#promptGeneration++;
5385
- this.#scheduledHiddenNextTurnGeneration = undefined;
5386
- this.abortCompaction();
5387
- this.abortHandoff();
5388
- this.abortBash();
5389
- this.abortEval();
5390
- const postPromptDrain = this.#cancelPostPromptTasks();
5391
- this.agent.abort(options?.reason);
5392
- await postPromptDrain;
5393
- await this.agent.waitForIdle();
5394
- await this.#goalRuntime.onTaskAborted({ reason: options?.goalReason ?? "interrupted" });
5395
- // Clear prompt-in-flight state: waitForIdle resolves when the agent loop's finally
5396
- // block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
5397
- // a subsequent prompt() can incorrectly observe the session as busy after an abort.
5398
- this.#resetInFlight();
5399
- // Safety net: if the agent loop aborted without producing an assistant
5400
- // message (e.g. failed before the first stream), the in-flight yield was
5401
- // never resolved or rejected by the normal message_end path. Reject it now
5402
- // so any requeue callback still fires and the queue stays consistent.
5403
- if (this.#toolChoiceQueue.hasInFlight) {
5404
- this.#toolChoiceQueue.reject("aborted");
5447
+ // Session switch/compact paths disconnect first; explicit aborts should
5448
+ // leave any queued steer/follow-up visible for the user rather than
5449
+ // auto-starting a fresh turn during cleanup.
5450
+ this.#abortInProgress = true;
5451
+ try {
5452
+ this.abortRetry();
5453
+ this.#promptGeneration++;
5454
+ this.#scheduledHiddenNextTurnGeneration = undefined;
5455
+ this.abortCompaction();
5456
+ this.abortHandoff();
5457
+ this.abortBash();
5458
+ this.abortEval();
5459
+ const postPromptDrain = this.#cancelPostPromptTasks();
5460
+ this.agent.abort(options?.reason);
5461
+ await postPromptDrain;
5462
+ await this.agent.waitForIdle();
5463
+ await this.#goalRuntime.onTaskAborted({ reason: options?.goalReason ?? "interrupted" });
5464
+ // Clear prompt-in-flight state: waitForIdle resolves when the agent loop's finally
5465
+ // block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
5466
+ // a subsequent prompt() can incorrectly observe the session as busy after an abort.
5467
+ this.#resetInFlight();
5468
+ // Safety net: if the agent loop aborted without producing an assistant
5469
+ // message (e.g. failed before the first stream), the in-flight yield was
5470
+ // never resolved or rejected by the normal message_end path. Reject it now
5471
+ // so any requeue callback still fires and the queue stays consistent.
5472
+ if (this.#toolChoiceQueue.hasInFlight) {
5473
+ this.#toolChoiceQueue.reject("aborted");
5474
+ }
5475
+ } finally {
5476
+ this.#abortInProgress = false;
5477
+ this.#drainStrandedQueuedMessages();
5405
5478
  }
5406
5479
  }
5407
5480
 
@@ -6007,7 +6080,12 @@ export class AgentSession {
6007
6080
  // Already on under any scope — keep the user's scoped value.
6008
6081
  return;
6009
6082
  }
6010
- this.setServiceTier(enabled ? "priority" : undefined);
6083
+ if (!enabled) {
6084
+ this.setServiceTier(undefined);
6085
+ return;
6086
+ }
6087
+ const scope = this.settings.get("fastModeScope");
6088
+ this.setServiceTier(scope === "openai" ? "openai-only" : scope === "claude" ? "claude-only" : "priority");
6011
6089
  }
6012
6090
 
6013
6091
  toggleFastMode(): boolean {
@@ -7025,10 +7103,28 @@ export class AgentSession {
7025
7103
  });
7026
7104
  }
7027
7105
 
7028
- #createEagerTodoPrelude(promptText: string): { message: AgentMessage; toolChoice: ToolChoice } | undefined {
7029
- const eagerTodosEnabled = this.settings.get("todo.eager");
7106
+ /**
7107
+ * Render context shared by the eager todo/task preludes. `toolRefs` resolves each
7108
+ * tool's wire name (matching `buildSystemPrompt`'s `toolRefs`) so the reminder names
7109
+ * the tool the model actually sees when an extension renames it; `taskBatch` gates
7110
+ * batch-call guidance that would steer toward a failing call shape when `task.batch`
7111
+ * is off (the flat single-spawn schema rejects `tasks`/`context`).
7112
+ */
7113
+ #buildEagerPreludeContext(): { toolRefs: Record<string, string>; taskBatch: boolean } {
7114
+ const wireName = (name: string): string => {
7115
+ const tool = this.#toolRegistry.get(name);
7116
+ return typeof tool?.customWireName === "string" ? tool.customWireName : name;
7117
+ };
7118
+ return {
7119
+ toolRefs: { task: wireName("task"), todo: wireName("todo") },
7120
+ taskBatch: this.settings.get("task.batch"),
7121
+ };
7122
+ }
7123
+
7124
+ #createEagerTodoPrelude(promptText: string): { message: AgentMessage; toolChoice?: ToolChoice } | undefined {
7125
+ const mode = this.settings.get("todo.eager");
7030
7126
  const todosEnabled = this.settings.get("todo.enabled");
7031
- if (!eagerTodosEnabled || !todosEnabled) {
7127
+ if (mode === "default" || !todosEnabled) {
7032
7128
  return undefined;
7033
7129
  }
7034
7130
 
@@ -7063,27 +7159,53 @@ export class AgentSession {
7063
7159
  return undefined;
7064
7160
  }
7065
7161
 
7162
+ const message: AgentMessage = {
7163
+ role: "custom",
7164
+ customType: "eager-todo-prelude",
7165
+ content: prompt.render(eagerTodoPrompt, { ...this.#buildEagerPreludeContext(), forced: mode === "always" }),
7166
+ display: false,
7167
+ attribution: "agent",
7168
+ timestamp: Date.now(),
7169
+ };
7170
+ // `preferred` suggests a todo list (reminder only); `always` also forces the
7171
+ // `todo` tool on the first turn — the previous boolean-on behavior.
7172
+ if (mode === "preferred") {
7173
+ return { message };
7174
+ }
7066
7175
  const todoToolChoice = buildNamedToolChoice("todo", this.model);
7067
7176
  if (!todoToolChoice) {
7068
- logger.warn("Eager todo enforcement skipped because the current model does not support forcing todo", {
7069
- modelApi: this.model?.api,
7070
- modelId: this.model?.id,
7071
- });
7072
- return undefined;
7073
- }
7074
-
7075
- const eagerTodoReminder = prompt.render(eagerTodoPrompt);
7076
-
7177
+ // `always` on a model that can't be forced degrades to reminder-only (no
7178
+ // tool_choice). For `todo.eager: true` users migrated to `always`, such
7179
+ // models now receive the first-turn reminder where they previously got
7180
+ // nothing (see the CHANGELOG entry); `always ⊇ preferred` is preserved.
7181
+ logger.warn(
7182
+ "Eager todo proceeding with the reminder only because the current model does not support a forced todo tool_choice",
7183
+ { modelApi: this.model?.api, modelId: this.model?.id },
7184
+ );
7185
+ return { message };
7186
+ }
7187
+ return { message, toolChoice: todoToolChoice };
7188
+ }
7189
+
7190
+ #createEagerTaskPrelude(promptText: string): AgentMessage | undefined {
7191
+ if (this.settings.get("task.eager") !== "always") return undefined;
7192
+ // Main agent only: subagents keep `task` active (the parent only filters `todo`),
7193
+ // so a salient delegate-reminder there would amplify nested fan-out. Gate on the
7194
+ // resolved agent kind, not the id, so a top-level session with a custom `agentId`
7195
+ // still gets the reminder.
7196
+ if (this.#agentKind === "sub") return undefined;
7197
+ if (this.#planModeState?.enabled) return undefined;
7198
+ if (this.agent.state.messages.some(m => m.role === "user")) return undefined;
7199
+ const trimmed = promptText.trimEnd();
7200
+ if (trimmed.endsWith("?") || trimmed.endsWith("!")) return undefined;
7201
+ if (!this.getActiveToolNames().includes("task")) return undefined;
7077
7202
  return {
7078
- message: {
7079
- role: "custom",
7080
- customType: "eager-todo-prelude",
7081
- content: eagerTodoReminder,
7082
- display: false,
7083
- attribution: "agent",
7084
- timestamp: Date.now(),
7085
- },
7086
- toolChoice: todoToolChoice,
7203
+ role: "custom",
7204
+ customType: "eager-task-prelude",
7205
+ content: prompt.render(eagerTaskPrompt, this.#buildEagerPreludeContext()),
7206
+ display: false,
7207
+ attribution: "agent",
7208
+ timestamp: Date.now(),
7087
7209
  };
7088
7210
  }
7089
7211
  /**
@@ -84,10 +84,10 @@ export class HistoryStorage {
84
84
 
85
85
  this.#db = new Database(dbPath);
86
86
 
87
- const hasFts = this.#db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='history_fts'").get();
88
-
89
87
  // Install the busy handler BEFORE any lock-taking statement. See #2421.
90
88
  this.#db.run("PRAGMA busy_timeout = 5000");
89
+
90
+ const hasFts = this.#db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='history_fts'").get();
91
91
  this.#db.run(`
92
92
  PRAGMA journal_mode=WAL;
93
93
  PRAGMA synchronous=NORMAL;
@@ -173,6 +173,10 @@ export class IndexedSessionStorage implements SessionStorage {
173
173
  }
174
174
  }
175
175
 
176
+ writeTextAtomic(path: string, content: string): Promise<void> {
177
+ return this.writeText(path, content);
178
+ }
179
+
176
180
  async rename(src: string, dst: string): Promise<void> {
177
181
  await this.#awaitPath(src);
178
182
  await this.#awaitPath(dst);
@@ -390,14 +394,7 @@ class IndexedSessionStorageWriter implements SessionStorageWriter {
390
394
  return next;
391
395
  }
392
396
 
393
- writeLineSync(line: string): void {
394
- if (this.#closed) throw new Error("Writer closed");
395
- if (this.#error) throw this.#error;
396
- const mtimeMs = this.#storage._appendForWriter(this.#path, line);
397
- this.#trackPromise(this.#storage._queueAppend(this.#path, line, mtimeMs, () => this.#error));
398
- }
399
-
400
- async writeLine(line: string): Promise<void> {
397
+ async append(line: string): Promise<void> {
401
398
  if (this.#closed) throw new Error("Writer closed");
402
399
  if (this.#error) throw this.#error;
403
400
  const mtimeMs = this.#storage._appendForWriter(this.#path, line);
@@ -410,15 +407,8 @@ class IndexedSessionStorageWriter implements SessionStorageWriter {
410
407
  if (this.#error) throw this.#error;
411
408
  }
412
409
 
413
- async fsync(): Promise<void> {
414
- await this.flush();
415
- }
416
-
417
- fsyncSync(): void {
418
- // Indexed storage has no real fd to fsync; drain the pending chain
419
- // synchronously is not possible, so this is a no-op. The async flush()
420
- // above already ensures durability for the indexed backend.
421
- if (this.#error) throw this.#error;
410
+ isOpen(): boolean {
411
+ return !this.#closed;
422
412
  }
423
413
 
424
414
  async close(): Promise<void> {