@gajae-code/coding-agent 0.18.12 → 0.19.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 (305) hide show
  1. package/CHANGELOG.md +467 -0
  2. package/dist/types/capability/rule.d.ts +7 -2
  3. package/dist/types/cli/append-system-prompt.d.ts +6 -0
  4. package/dist/types/cli/args.d.ts +7 -2
  5. package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
  6. package/dist/types/cli/customize-doctor.d.ts +4 -3
  7. package/dist/types/cli/public-command-errors.d.ts +1 -0
  8. package/dist/types/cli/root-flags.d.ts +0 -12
  9. package/dist/types/cli/update-cli.d.ts +7 -2
  10. package/dist/types/cli-main.d.ts +0 -6
  11. package/dist/types/commands/launch.d.ts +0 -6
  12. package/dist/types/commands/migrate.d.ts +1 -1
  13. package/dist/types/commands/sdk.d.ts +8 -0
  14. package/dist/types/commit/map-reduce/index.d.ts +4 -4
  15. package/dist/types/config/autorouting-tier-map.d.ts +23 -1
  16. package/dist/types/config/config-hot-reload.d.ts +5 -0
  17. package/dist/types/config/file-lock.d.ts +44 -2
  18. package/dist/types/config/model-profile-activation.d.ts +19 -5
  19. package/dist/types/config/model-registry.d.ts +16 -3
  20. package/dist/types/config/model-resolver.d.ts +5 -4
  21. package/dist/types/config/settings-schema.d.ts +11 -1
  22. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  23. package/dist/types/customization/import.d.ts +2 -0
  24. package/dist/types/customization/types.d.ts +1 -1
  25. package/dist/types/dap/session.d.ts +11 -2
  26. package/dist/types/discovery/helpers.d.ts +7 -2
  27. package/dist/types/edit/index.d.ts +5 -0
  28. package/dist/types/edit/modes/patch.d.ts +9 -0
  29. package/dist/types/eval/py/runtime.d.ts +9 -0
  30. package/dist/types/export/ttsr.d.ts +19 -0
  31. package/dist/types/extensibility/extensions/types.d.ts +5 -1
  32. package/dist/types/extensibility/slash-commands.d.ts +1 -0
  33. package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
  34. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
  36. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
  37. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  38. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  39. package/dist/types/hindsight/content.d.ts +5 -6
  40. package/dist/types/internal-urls/types.d.ts +2 -0
  41. package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
  42. package/dist/types/main.d.ts +23 -18
  43. package/dist/types/migrate/action-planner.d.ts +10 -1
  44. package/dist/types/migrate/adapters/codex.d.ts +1 -1
  45. package/dist/types/migrate/adapters/index.d.ts +35 -3
  46. package/dist/types/migrate/hook-module.d.ts +23 -0
  47. package/dist/types/migrate/report.d.ts +2 -1
  48. package/dist/types/migrate/types.d.ts +69 -2
  49. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  50. package/dist/types/modes/components/model-selector.d.ts +43 -0
  51. package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
  52. package/dist/types/modes/utils/host-status.d.ts +15 -11
  53. package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
  54. package/dist/types/sdk/broker/broker-exit.d.ts +1 -1
  55. package/dist/types/sdk/broker/broker.d.ts +22 -1
  56. package/dist/types/sdk/broker/ensure.d.ts +5 -2
  57. package/dist/types/sdk/broker/hop.d.ts +9 -0
  58. package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
  59. package/dist/types/sdk/broker/session-index.d.ts +2 -0
  60. package/dist/types/sdk/broker/transport.d.ts +2 -0
  61. package/dist/types/sdk/bus/index.d.ts +21 -0
  62. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
  63. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  64. package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
  65. package/dist/types/sdk/host/control/operations.d.ts +5 -1
  66. package/dist/types/sdk/host/events.d.ts +4 -0
  67. package/dist/types/sdk/host/host.d.ts +2 -1
  68. package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
  69. package/dist/types/sdk/host/session-runtime.d.ts +8 -8
  70. package/dist/types/sdk/host/surface-policy.d.ts +2 -1
  71. package/dist/types/sdk/lifecycle/service.d.ts +12 -0
  72. package/dist/types/sdk/prompt-status.d.ts +11 -4
  73. package/dist/types/session/agent-session.d.ts +50 -8
  74. package/dist/types/session/session-manager.d.ts +16 -0
  75. package/dist/types/skill-state/workflow-mutation-guard.d.ts +13 -1
  76. package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
  77. package/dist/types/ssh/ssh-executor.d.ts +8 -1
  78. package/dist/types/system-prompt.d.ts +3 -2
  79. package/dist/types/task/role-body.d.ts +11 -0
  80. package/dist/types/tools/awaiting-input.d.ts +28 -0
  81. package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -0
  82. package/dist/types/tools/bash-skill-urls.d.ts +4 -0
  83. package/dist/types/tools/bash.d.ts +7 -0
  84. package/dist/types/tools/descriptors.d.ts +13 -2
  85. package/dist/types/tools/fetch.d.ts +6 -0
  86. package/dist/types/tools/irc.d.ts +5 -3
  87. package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
  88. package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
  89. package/dist/types/tools/write.d.ts +2 -0
  90. package/dist/types/utils/file-mentions.d.ts +5 -0
  91. package/dist/types/utils/image-limits.d.ts +7 -0
  92. package/dist/types/utils/image-loading.d.ts +0 -1
  93. package/dist/types/utils/open.d.ts +0 -1
  94. package/dist/types/utils/pasted-image-loading.d.ts +1 -5
  95. package/dist/types/utils/prompt-suggestion.d.ts +2 -2
  96. package/dist/types/utils/shell-snapshot.d.ts +2 -0
  97. package/dist/types/utils/title-generator.d.ts +13 -3
  98. package/dist/types/web/search/providers/codex.d.ts +6 -7
  99. package/package.json +9 -8
  100. package/scripts/acp-conformance-agent.ts +12 -2
  101. package/scripts/generate-sdk-operation-inventory.ts +16 -0
  102. package/src/capability/fs.ts +31 -3
  103. package/src/capability/rule.ts +11 -2
  104. package/src/cli/append-system-prompt.ts +12 -0
  105. package/src/cli/args.ts +11 -7
  106. package/src/cli/customize-doctor-precedence.ts +31 -0
  107. package/src/cli/customize-doctor.ts +49 -39
  108. package/src/cli/fast-help.ts +0 -2
  109. package/src/cli/migrate-cli.ts +14 -3
  110. package/src/cli/public-command-errors.ts +2 -0
  111. package/src/cli/root-flags.ts +0 -2
  112. package/src/cli/update-cli.ts +247 -6
  113. package/src/commands/harness.ts +25 -15
  114. package/src/commands/migrate.ts +3 -3
  115. package/src/commands/sdk.ts +81 -11
  116. package/src/commit/agentic/agent.ts +6 -6
  117. package/src/commit/analysis/conventional.ts +14 -5
  118. package/src/commit/map-reduce/index.ts +7 -7
  119. package/src/commit/pipeline.ts +12 -12
  120. package/src/config/autorouting-tier-map.ts +20 -1
  121. package/src/config/config-file.ts +106 -3
  122. package/src/config/config-hot-reload.ts +8 -0
  123. package/src/config/file-lock.ts +1431 -304
  124. package/src/config/keybindings.ts +154 -7
  125. package/src/config/model-profile-activation.ts +177 -39
  126. package/src/config/model-registry.ts +380 -55
  127. package/src/config/model-resolver.ts +49 -21
  128. package/src/config/settings-schema.ts +13 -1
  129. package/src/config/settings.ts +2 -0
  130. package/src/coordinator-mcp/server.ts +37 -2
  131. package/src/crash/sanitize.ts +3 -1
  132. package/src/customization/import.ts +276 -7
  133. package/src/customization/inventory.ts +24 -8
  134. package/src/customization/types.ts +1 -0
  135. package/src/dap/client.ts +3 -2
  136. package/src/dap/session.ts +161 -36
  137. package/src/debug/crash-diagnostics.ts +87 -5
  138. package/src/debug/report-bundle.ts +71 -2
  139. package/src/discovery/builtin.ts +4 -3
  140. package/src/discovery/helpers.ts +24 -14
  141. package/src/edit/index.ts +191 -5
  142. package/src/edit/modes/patch.ts +27 -0
  143. package/src/eval/py/runtime.ts +44 -4
  144. package/src/exec/non-interactive-env.ts +0 -1
  145. package/src/export/html/index.ts +60 -20
  146. package/src/export/ttsr.ts +176 -1
  147. package/src/extensibility/extensions/loader.ts +2 -0
  148. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
  149. package/src/extensibility/extensions/types.ts +5 -1
  150. package/src/extensibility/hooks/loader.ts +10 -2
  151. package/src/extensibility/plugins/marketplace/manager.ts +36 -10
  152. package/src/extensibility/slash-commands.ts +5 -0
  153. package/src/gjc-runtime/gc-runtime.ts +4 -1
  154. package/src/gjc-runtime/launch-tmux.ts +23 -13
  155. package/src/gjc-runtime/ralplan-current-final.ts +294 -0
  156. package/src/gjc-runtime/ralplan-runtime.ts +95 -1
  157. package/src/gjc-runtime/state-writer.ts +109 -5
  158. package/src/gjc-runtime/tmux-sessions.ts +93 -13
  159. package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
  160. package/src/gjc-runtime/ultragoal-guard.ts +9 -1
  161. package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
  162. package/src/harness-control-plane/finalize.ts +11 -0
  163. package/src/harness-control-plane/receipt-spool.ts +105 -28
  164. package/src/harness-control-plane/storage.ts +124 -6
  165. package/src/hindsight/content.ts +116 -16
  166. package/src/hindsight/mental-models.ts +22 -10
  167. package/src/hindsight/state.ts +7 -4
  168. package/src/hooks/native-skill-hook.ts +3 -1
  169. package/src/internal-urls/artifact-protocol.ts +3 -1
  170. package/src/internal-urls/docs-index.generated.ts +14 -14
  171. package/src/internal-urls/local-protocol.ts +28 -3
  172. package/src/internal-urls/types.ts +2 -0
  173. package/src/lsp/client.ts +2 -1
  174. package/src/lsp/clients/biome-client.ts +2 -0
  175. package/src/lsp/clients/swiftlint-client.ts +2 -0
  176. package/src/main.ts +330 -92
  177. package/src/memories/index.ts +25 -2
  178. package/src/migrate/action-planner.ts +517 -2
  179. package/src/migrate/adapters/claude-code.ts +141 -3
  180. package/src/migrate/adapters/codex.ts +57 -5
  181. package/src/migrate/adapters/index.ts +240 -5
  182. package/src/migrate/adapters/opencode.ts +4 -0
  183. package/src/migrate/executor.ts +141 -3
  184. package/src/migrate/hook-module.ts +193 -0
  185. package/src/migrate/report.ts +45 -1
  186. package/src/migrate/types.ts +73 -3
  187. package/src/modes/acp/acp-agent.ts +10 -1
  188. package/src/modes/components/agent-dashboard.ts +20 -4
  189. package/src/modes/components/customization/customization-dashboard.ts +7 -0
  190. package/src/modes/components/customization/import-wizard.ts +29 -3
  191. package/src/modes/components/model-selector.ts +157 -103
  192. package/src/modes/components/welcome.ts +3 -1
  193. package/src/modes/controllers/command-controller.ts +271 -16
  194. package/src/modes/controllers/event-controller.ts +1 -1
  195. package/src/modes/controllers/extension-ui-controller.ts +31 -1
  196. package/src/modes/controllers/goal-mode-controller.ts +79 -10
  197. package/src/modes/controllers/input-controller.ts +2 -0
  198. package/src/modes/controllers/plan-mode-controller.ts +49 -4
  199. package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
  200. package/src/modes/controllers/selector-controller.ts +14 -1
  201. package/src/modes/interactive-mode.ts +55 -4
  202. package/src/modes/prompt-suggestion-controller.ts +1 -1
  203. package/src/modes/runtime-init.ts +5 -1
  204. package/src/modes/utils/host-status.ts +21 -10
  205. package/src/prompts/agent-fragments/browser-aside.md +2 -2
  206. package/src/prompts/system/irc-incoming.md +3 -1
  207. package/src/prompts/tools/irc.md +1 -1
  208. package/src/runtime-mcp/manager.ts +8 -3
  209. package/src/runtime-mcp/project-host-values.ts +12 -8
  210. package/src/runtime-mcp/tool-bridge.ts +68 -4
  211. package/src/runtime-mcp/transports/stdio.ts +2 -1
  212. package/src/sdk/broker/broker-exit.ts +2 -0
  213. package/src/sdk/broker/broker.ts +143 -4
  214. package/src/sdk/broker/ensure.ts +74 -6
  215. package/src/sdk/broker/hop.ts +13 -1
  216. package/src/sdk/broker/lifecycle-ledger.ts +3 -0
  217. package/src/sdk/broker/lifecycle.ts +102 -6
  218. package/src/sdk/broker/session-index.ts +29 -4
  219. package/src/sdk/broker/transport.ts +17 -1
  220. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  221. package/src/sdk/bus/index.ts +408 -14
  222. package/src/sdk/bus/session-control.ts +2 -0
  223. package/src/sdk/bus/slack-daemon.ts +105 -9
  224. package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
  225. package/src/sdk/bus/telegram-daemon.ts +10 -2
  226. package/src/sdk/bus/threaded-render.ts +20 -1
  227. package/src/sdk/cli/session-cli.ts +2 -0
  228. package/src/sdk/host/control/dispatch.ts +13 -1
  229. package/src/sdk/host/control/operations.ts +10 -1
  230. package/src/sdk/host/events.ts +67 -4
  231. package/src/sdk/host/host.ts +8 -2
  232. package/src/sdk/host/prompt-image-upload.ts +438 -0
  233. package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
  234. package/src/sdk/host/session-runtime.test.ts +1537 -24
  235. package/src/sdk/host/session-runtime.ts +332 -36
  236. package/src/sdk/host/surface-policy.ts +4 -0
  237. package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
  238. package/src/sdk/host/websocket-transport.ts +19 -7
  239. package/src/sdk/lifecycle/service.ts +14 -1
  240. package/src/sdk/prompt-status.ts +6 -4
  241. package/src/sdk/protocol/adapter-validation.ts +28 -1
  242. package/src/sdk/protocol/operation-inventory.generated.json +160 -0
  243. package/src/sdk/protocol/operation-registry.ts +33 -3
  244. package/src/sdk/session.ts +123 -36
  245. package/src/session/agent-session.ts +1614 -329
  246. package/src/session/history-storage.ts +182 -12
  247. package/src/session/internal/managed-session-scope.ts +47 -18
  248. package/src/session/internal/managed-session-storage.ts +71 -1
  249. package/src/session/session-manager.ts +90 -5
  250. package/src/session-import/redact.ts +53 -2
  251. package/src/skill-state/workflow-mutation-guard.ts +1498 -18
  252. package/src/slash-commands/builtin-registry.ts +6 -3
  253. package/src/slash-commands/helpers/mcp.ts +11 -3
  254. package/src/ssh/ssh-executor.ts +9 -2
  255. package/src/stt/recorder.ts +26 -9
  256. package/src/system-prompt.ts +61 -16
  257. package/src/task/executor.ts +12 -8
  258. package/src/task/role-body.ts +84 -0
  259. package/src/task/token-log-mode.test.ts +162 -0
  260. package/src/task/token-log.ts +91 -2
  261. package/src/tools/archive-reader.ts +650 -37
  262. package/src/tools/ask.ts +1 -0
  263. package/src/tools/ast-grep.ts +7 -2
  264. package/src/tools/auto-generated-guard.ts +12 -3
  265. package/src/tools/awaiting-input.ts +114 -0
  266. package/src/tools/bash-allowed-prefixes.ts +61 -1
  267. package/src/tools/bash-skill-urls.ts +58 -5
  268. package/src/tools/bash.ts +99 -26
  269. package/src/tools/browser/tab-worker.ts +39 -13
  270. package/src/tools/browser.ts +17 -7
  271. package/src/tools/cron.ts +10 -1
  272. package/src/tools/debug.ts +9 -2
  273. package/src/tools/descriptors.test.ts +4 -4
  274. package/src/tools/descriptors.ts +16 -4
  275. package/src/tools/eval.ts +15 -0
  276. package/src/tools/fetch.ts +90 -2
  277. package/src/tools/gh.ts +27 -12
  278. package/src/tools/image-gen.ts +20 -6
  279. package/src/tools/irc.ts +5 -3
  280. package/src/tools/monitor.ts +9 -1
  281. package/src/tools/output-meta.ts +51 -3
  282. package/src/tools/read.ts +9 -2
  283. package/src/tools/recipe/runner.ts +23 -12
  284. package/src/tools/render-utils.ts +2 -2
  285. package/src/tools/search.ts +3 -2
  286. package/src/tools/ssh.ts +274 -8
  287. package/src/tools/subagent.ts +10 -4
  288. package/src/tools/tmux-self-injection-guard.ts +3301 -122
  289. package/src/tools/tool-catalog.generated.ts +1 -1
  290. package/src/tools/ultragoal-ask-guard.ts +13 -1
  291. package/src/tools/write.ts +7 -0
  292. package/src/tui/tree-list.ts +15 -1
  293. package/src/utils/commit-message-generator.ts +3 -2
  294. package/src/utils/external-editor.ts +21 -7
  295. package/src/utils/file-mentions.ts +46 -6
  296. package/src/utils/git.ts +38 -7
  297. package/src/utils/image-limits.ts +8 -0
  298. package/src/utils/image-loading.ts +1 -1
  299. package/src/utils/open.ts +73 -4
  300. package/src/utils/pasted-image-loading.ts +9 -8
  301. package/src/utils/prompt-suggestion.ts +5 -4
  302. package/src/utils/shell-snapshot.ts +9 -1
  303. package/src/utils/title-generator.ts +65 -20
  304. package/src/web/search/index.ts +2 -1
  305. package/src/web/search/providers/codex.ts +29 -32
@@ -1,6 +1,5 @@
1
1
  import { Database, type Statement } from "bun:sqlite";
2
2
  import * as fs from "node:fs";
3
- import * as fsPromises from "node:fs/promises";
4
3
  import * as path from "node:path";
5
4
  import { getHistoryDbPath, logger } from "@gajae-code/utils";
6
5
 
@@ -26,6 +25,155 @@ function escapeLikePattern(text: string): string {
26
25
  return text.replace(/[\\%_]/g, "\\$&");
27
26
  }
28
27
 
28
+ const HISTORY_SUFFIXES = ["", "-wal", "-shm"] as const;
29
+ let openingHistory: Database | undefined;
30
+
31
+ function secureHistoryPath(pathname: string, kind: "directory" | "file"): void {
32
+ if (process.platform !== "win32") return;
33
+ const native = require("@gajae-code/natives") as typeof import("@gajae-code/natives");
34
+ const applied = native.applyOwnerOnlyPathSecurity(pathname, kind);
35
+ const verified = native.verifyOwnerOnlyPathSecurity(pathname, kind);
36
+ if (applied?.ok !== true || verified?.ok !== true) {
37
+ throw new Error("history database owner-only protection could not be established");
38
+ }
39
+ }
40
+
41
+ function historyErrno(error: unknown): string | undefined {
42
+ return (error as NodeJS.ErrnoException | undefined)?.code;
43
+ }
44
+
45
+ function historyLeaf(dbPath: string, suffix: string): fs.BigIntStats | undefined {
46
+ try {
47
+ return fs.lstatSync(`${dbPath}${suffix}`, { bigint: true });
48
+ } catch (error) {
49
+ if (historyErrno(error) !== "ENOENT") throw error;
50
+ return undefined;
51
+ }
52
+ }
53
+
54
+ function assertRegularHistoryLeaf(existing: fs.BigIntStats | undefined): void {
55
+ if (existing && (existing.isSymbolicLink() || !existing.isFile())) {
56
+ throw new Error("history database path is not a regular file");
57
+ }
58
+ }
59
+
60
+ function assertHistoryLeaves(dbPath: string): Set<string> {
61
+ const present = new Set<string>();
62
+ for (const suffix of HISTORY_SUFFIXES) {
63
+ const existing = historyLeaf(dbPath, suffix);
64
+ assertRegularHistoryLeaf(existing);
65
+ if (existing) present.add(suffix);
66
+ }
67
+ return present;
68
+ }
69
+
70
+ function removeNewHistoryLeaves(): void {
71
+ // A WAL reader can stay live while BEGIN EXCLUSIVE succeeds, so failure
72
+ // cleanup must not unlink a database another opener has adopted.
73
+ try {
74
+ openingHistory?.close();
75
+ } catch {
76
+ // The caller reports the original open failure.
77
+ }
78
+ openingHistory = undefined;
79
+ }
80
+
81
+ function readHistoryDirectory(dir: string): fs.Stats | undefined {
82
+ let existing: fs.Stats;
83
+ try {
84
+ existing = fs.lstatSync(dir);
85
+ } catch (error) {
86
+ if (historyErrno(error) !== "ENOENT") throw error;
87
+ return undefined;
88
+ }
89
+ if (existing.isSymbolicLink() || !existing.isDirectory()) {
90
+ throw new Error("history database directory is not a real directory");
91
+ }
92
+ return existing;
93
+ }
94
+
95
+ function tightenHistoryDirectory(dir: string): void {
96
+ let existing = readHistoryDirectory(dir);
97
+ if (!existing) {
98
+ try {
99
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
100
+ } catch (error) {
101
+ if (historyErrno(error) !== "EEXIST") throw error;
102
+ }
103
+ existing = readHistoryDirectory(dir);
104
+ if (!existing) throw new Error("history database directory is not a real directory");
105
+ }
106
+ secureHistoryPath(dir, "directory");
107
+ if (process.platform === "win32") return;
108
+ // 0300/0700 are already owner-only. Sticky directories are shared and stay as they are.
109
+ // chmod(2) needs no read bit, so mode 0333 becomes 0700. A symlink swap is
110
+ // followed by chmod; the recheck then refuses and no database file is created.
111
+ if ((existing.mode & 0o077) === 0 || (existing.mode & 0o1000) !== 0) return;
112
+ try {
113
+ fs.chmodSync(dir, 0o700);
114
+ } catch (error) {
115
+ if (historyErrno(error) === "ELOOP") throw new Error("history database directory is not a real directory");
116
+ throw error;
117
+ }
118
+ const after = readHistoryDirectory(dir);
119
+ if (!after || (after.mode & 0o077) !== 0) {
120
+ throw new Error("history database directory could not be tightened to mode 0700");
121
+ }
122
+ }
123
+
124
+ function sameHistoryFile(left: fs.BigIntStats, right: fs.BigIntStats): boolean {
125
+ return left.dev === right.dev && left.ino === right.ino && left.nlink === right.nlink;
126
+ }
127
+
128
+ function restrictHistoryFiles(dbPath: string, createMissingDb: boolean): void {
129
+ const noFollow = process.platform === "win32" ? 0 : (fs.constants.O_NOFOLLOW ?? 0);
130
+ for (const suffix of HISTORY_SUFFIXES) {
131
+ const target = `${dbPath}${suffix}`;
132
+ let existing = historyLeaf(dbPath, suffix);
133
+ assertRegularHistoryLeaf(existing);
134
+ if (!existing && suffix === "" && createMissingDb) {
135
+ let created: number | undefined;
136
+ try {
137
+ created = fs.openSync(
138
+ target,
139
+ fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow,
140
+ 0o600,
141
+ );
142
+ fs.fchmodSync(created, 0o600);
143
+ } catch (error) {
144
+ if (historyErrno(error) === "ELOOP") throw new Error("history database path is not a regular file");
145
+ if (historyErrno(error) !== "EEXIST") throw error;
146
+ } finally {
147
+ if (created !== undefined) fs.closeSync(created);
148
+ }
149
+ existing = historyLeaf(dbPath, suffix);
150
+ assertRegularHistoryLeaf(existing);
151
+ if (!existing) throw new Error("history database path is not a regular file");
152
+ }
153
+ if (!existing) continue;
154
+ secureHistoryPath(target, "file");
155
+ if (process.platform === "win32" || (Number(existing.mode) & 0o077) === 0) continue;
156
+ let fd: number | undefined;
157
+ try {
158
+ fd = fs.openSync(target, fs.constants.O_RDONLY | noFollow);
159
+ const opened = fs.fstatSync(fd, { bigint: true });
160
+ const again = historyLeaf(dbPath, suffix);
161
+ if (!again || !opened.isFile() || !sameHistoryFile(existing, opened) || !sameHistoryFile(opened, again)) {
162
+ throw new Error("history database path is not a regular file");
163
+ }
164
+ fs.fchmodSync(fd, 0o600);
165
+ if ((Number(fs.fstatSync(fd, { bigint: true }).mode) & 0o777) !== 0o600) {
166
+ throw new Error("history database path could not be tightened to mode 0600");
167
+ }
168
+ } catch (error) {
169
+ if (historyErrno(error) === "ELOOP") throw new Error("history database path is not a regular file");
170
+ throw error;
171
+ } finally {
172
+ if (fd !== undefined) fs.closeSync(fd);
173
+ }
174
+ }
175
+ }
176
+
29
177
  class AsyncDrain<T> {
30
178
  #queue?: T[];
31
179
  #promise = Promise.resolve();
@@ -83,6 +231,7 @@ export class HistoryStorage {
83
231
 
84
232
  private constructor(dbPath: string) {
85
233
  this.#db = new Database(dbPath);
234
+ openingHistory = this.#db;
86
235
 
87
236
  const hasFts = this.#db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='history_fts'").get();
88
237
 
@@ -137,12 +286,32 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
137
286
 
138
287
  const last = this.#lastPromptStmt.get() as { prompt?: string } | undefined;
139
288
  this.#lastPromptCache = last?.prompt ?? null;
289
+ try {
290
+ restrictHistoryFiles(dbPath, false);
291
+ } catch (error) {
292
+ this.#db.close();
293
+ throw error;
294
+ }
295
+ openingHistory = undefined;
296
+ }
297
+
298
+ static #openNew(dbPath: string): HistoryStorage {
299
+ openingHistory = undefined;
300
+ assertHistoryLeaves(dbPath);
301
+ try {
302
+ tightenHistoryDirectory(path.dirname(dbPath));
303
+ assertHistoryLeaves(dbPath);
304
+ restrictHistoryFiles(dbPath, true);
305
+ return new HistoryStorage(dbPath);
306
+ } catch (error) {
307
+ removeNewHistoryLeaves();
308
+ throw error;
309
+ }
140
310
  }
141
311
 
142
312
  static open(dbPath: string = getHistoryDbPath()): HistoryStorage {
143
313
  if (!HistoryStorage.#instance) {
144
- fs.mkdirSync(path.dirname(dbPath), { recursive: true });
145
- HistoryStorage.#instance = new HistoryStorage(dbPath);
314
+ HistoryStorage.#instance = HistoryStorage.#openNew(dbPath);
146
315
  }
147
316
  return HistoryStorage.#instance;
148
317
  }
@@ -150,16 +319,17 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
150
319
  static async openAsync(dbPath: string = getHistoryDbPath()): Promise<HistoryStorage> {
151
320
  if (HistoryStorage.#instance) return HistoryStorage.#instance;
152
321
  if (!HistoryStorage.#opening) {
153
- HistoryStorage.#opening = (async () => {
154
- await fsPromises.mkdir(path.dirname(dbPath), { recursive: true });
155
- const storage = new HistoryStorage(dbPath);
156
- HistoryStorage.#instance ??= storage;
157
- return HistoryStorage.#instance;
158
- })().finally(() => {
159
- HistoryStorage.#opening = undefined;
160
- });
322
+ HistoryStorage.#opening = Promise.resolve()
323
+ .then(() => {
324
+ const storage = HistoryStorage.#openNew(dbPath);
325
+ HistoryStorage.#instance ??= storage;
326
+ return HistoryStorage.#instance;
327
+ })
328
+ .finally(() => {
329
+ HistoryStorage.#opening = undefined;
330
+ });
161
331
  }
162
- return await HistoryStorage.#opening;
332
+ return HistoryStorage.#opening;
163
333
  }
164
334
 
165
335
  /** @internal Reset the singleton — test-only. */
@@ -228,14 +228,22 @@ function bindManagedWriteAuthority(scope: ManagedScope, authority: ManagedCandid
228
228
  function assertRetainedManagedDirectoryIdentity(scope: ManagedScope): void {
229
229
  const expected = managedDirectoryIdentities.get(scope);
230
230
  if (!expected) return;
231
- const current = fs.lstatSync(scope.directoryPath, { bigint: true });
232
- if (
233
- !current.isDirectory() ||
234
- current.isSymbolicLink() ||
235
- current.dev !== expected.dev ||
236
- current.ino !== expected.ino
237
- )
238
- throw new Error("Managed session directory changed");
231
+ assertSameManagedDirectoryIdentity(fs.lstatSync(scope.directoryPath, { bigint: true }), expected);
232
+ }
233
+
234
+ /**
235
+ * Fence a managed directory to the identity it was bound with. The decision is
236
+ * the same for every mismatch; only the reported reason differs. The same
237
+ * inode on a different `dev` is the shape a remount, drive-letter change or
238
+ * volume-serial change leaves behind (#6750). It is still refused, because an
239
+ * inode is only unique within one volume, but it is named so an operator is
240
+ * not sent looking for a corrupted binding.
241
+ */
242
+ function assertSameManagedDirectoryIdentity(current: fs.BigIntStats, expected: { dev: bigint; ino: bigint }): void {
243
+ if (!current.isDirectory() || current.isSymbolicLink()) throw new Error("Managed session directory changed");
244
+ if (current.dev === expected.dev && current.ino === expected.ino) return;
245
+ if (current.ino === expected.ino) throw new Error("volume_identity_changed");
246
+ throw new Error("Managed session directory changed");
239
247
  }
240
248
 
241
249
  export function managedDirectoryAuthorityForScope(scope: ManagedScope): RecoveryFsRoot | undefined {
@@ -319,7 +327,22 @@ export type ManagedScopeResolution =
319
327
  * propagating them keeps startup redaction intact while telling the operator
320
328
  * which invariant actually failed instead of a blanket `binding_invalid`.
321
329
  */
322
- const managedScopeIdentityFailureCodes = new Set(["identity_mismatch", "reparse_point", "not_directory", "not_file"]);
330
+ const managedScopeIdentityFailureCodes = new Set([
331
+ "identity_mismatch",
332
+ "volume_identity_changed",
333
+ "reparse_point",
334
+ "not_directory",
335
+ "not_file",
336
+ ]);
337
+
338
+ /**
339
+ * Fixed, path-free tokens from the replacement-cleanup receipt preflight that
340
+ * runs before every no-replace publish. A stale receipt the preflight refuses
341
+ * (for example one whose filename encodes a device number that changed after a
342
+ * reboot, #6820) blocks an otherwise healthy binding publish; reporting it as
343
+ * `binding_invalid` sends operators to the binding instead of the receipt.
344
+ */
345
+ const managedScopeReceiptFailureCodes = new Set(["managed_replace_cleanup_receipt_invalid"]);
323
346
 
324
347
  /**
325
348
  * Fixed, path-free errno tokens. A managed scope that fails to prepare because
@@ -376,6 +399,7 @@ function isManagedScopeCapacityMessage(message: string): boolean {
376
399
  function managedScopeResidualClassification(error: unknown): string | undefined {
377
400
  if (!(error instanceof Error)) return undefined;
378
401
  if (managedScopeIdentityFailureCodes.has(error.message)) return error.message;
402
+ if (managedScopeReceiptFailureCodes.has(error.message)) return error.message;
379
403
  const errno = (error as NodeJS.ErrnoException).code;
380
404
  return errno !== undefined && managedScopeErrnoFailureCodes.has(errno) ? errno : undefined;
381
405
  }
@@ -508,6 +532,7 @@ type NativeIdentity =
508
532
  type CanonicalNativeIdentity = Extract<NativeIdentity, { ok: true }>;
509
533
 
510
534
  type NativeIdentityFailureCode =
535
+ | "access_denied"
511
536
  | "not_found"
512
537
  | "not_directory"
513
538
  | "not_utf8"
@@ -618,6 +643,12 @@ function nativeFailure(code: NativeIdentityFailureCode): ManagedScopeResolution
618
643
  return { kind: "error", code: "cwd_missing", message: "The workspace directory does not exist." };
619
644
  if (code === "not_directory")
620
645
  return { kind: "error", code: "cwd_not_directory", message: "The workspace path is not a directory." };
646
+ if (code === "access_denied")
647
+ return {
648
+ kind: "error",
649
+ code: "permission_denied",
650
+ message: "Permission was denied while identifying the workspace directory.",
651
+ };
621
652
  if (code === "network_unsupported") {
622
653
  return {
623
654
  kind: "error",
@@ -628,6 +659,10 @@ function nativeFailure(code: NativeIdentityFailureCode): ManagedScopeResolution
628
659
  return { kind: "error", code: "identity_unavailable", message: "Workspace directory identity is unavailable." };
629
660
  }
630
661
 
662
+ function nativeIdentityDiagnosticCode(code: NativeIdentityFailureCode): string {
663
+ return code === "access_denied" ? "permission_denied" : `cwd_${code}`;
664
+ }
665
+
631
666
  function bindingFor(scope: ManagedScope): Binding {
632
667
  return {
633
668
  schemaVersion: 1,
@@ -1107,7 +1142,7 @@ function inspectCandidate(
1107
1142
  if (header.type !== "session" || typeof header.id !== "string" || typeof header.cwd !== "string")
1108
1143
  return { code: "invalid_header" };
1109
1144
  const cwdIdentity = identityFor(header.cwd);
1110
- if (!cwdIdentity.ok) return { code: `cwd_${cwdIdentity.code}` };
1145
+ if (!cwdIdentity.ok) return { code: nativeIdentityDiagnosticCode(cwdIdentity.code) };
1111
1146
  const named = fs.lstatSync(filePath, { bigint: true });
1112
1147
  if (
1113
1148
  !named.isFile() ||
@@ -1872,13 +1907,7 @@ export function prepareManagedSessionScopeForWriteSync(
1872
1907
  managedDirectoryAuthorities.set(scope, retainedAuthority);
1873
1908
  stage = "scope_revalidate";
1874
1909
  const directoryStat = fs.lstatSync(scope.directoryPath, { bigint: true });
1875
- if (
1876
- !directoryStat.isDirectory() ||
1877
- directoryStat.isSymbolicLink() ||
1878
- directoryStat.dev !== preparedDirectory.dev ||
1879
- directoryStat.ino !== preparedDirectory.ino
1880
- )
1881
- throw new Error("Managed session directory changed");
1910
+ assertSameManagedDirectoryIdentity(directoryStat, preparedDirectory);
1882
1911
  managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
1883
1912
  if (validated) {
1884
1913
  if (validated.kind !== "error") throw new Error("Unexpected managed scope binding result");
@@ -1954,7 +1983,7 @@ export function listManagedCandidates(scope: ManagedScope): ManagedCandidateList
1954
1983
  const candidateIdentity = identityFor(candidate.cwd);
1955
1984
  if (!candidateIdentity.ok) {
1956
1985
  invalid.push({
1957
- code: `cwd_${candidateIdentity.code}`,
1986
+ code: nativeIdentityDiagnosticCode(candidateIdentity.code),
1958
1987
  ...(safeManagedCandidateSessionId(candidate.sessionId) === undefined
1959
1988
  ? {}
1960
1989
  : { sessionId: candidate.sessionId }),
@@ -170,7 +170,11 @@ function exactUnlinkCompleted(result: NativeExactUnlinkResult): boolean {
170
170
  }
171
171
 
172
172
  function isRetryableReplacementReceiptCleanupError(error: unknown): boolean {
173
- return error instanceof Error && error.message === "managed_replace_receipt_cleanup_pending:io_error";
173
+ return (
174
+ error instanceof Error &&
175
+ (error.message === "managed_replace_receipt_cleanup_pending:io_error" ||
176
+ error.message === "managed_replace_receipt_cleanup_pending:access_denied")
177
+ );
174
178
  }
175
179
 
176
180
  /**
@@ -450,6 +454,17 @@ function replacementReceiptIsOwned(bytes: Uint8Array): boolean {
450
454
  return observed.status !== "present" || observed.incarnation === publisher.incarnation;
451
455
  }
452
456
 
457
+ /** Only positive local process-incarnation evidence proves a mismatched receipt's publisher is stale. */
458
+ function replacementReceiptPublisherIsVerifiedDead(bytes: Uint8Array): boolean {
459
+ const publisher = replacementReceiptPublisher(bytes);
460
+ if (!publisher || publisher.host !== os.hostname()) return false;
461
+ if (publisher.pid === process.pid && publisher.ownerId === replacementPublisherOwnerId) return false;
462
+ const observed = observeProcessIncarnation(publisher.pid);
463
+ return (
464
+ observed.status === "absent" || (observed.status === "present" && observed.incarnation !== publisher.incarnation)
465
+ );
466
+ }
467
+
453
468
  type LegacyReplacementCleanupIdentity = {
454
469
  dev: bigint;
455
470
  ino: bigint;
@@ -1100,6 +1115,7 @@ const GENERAL_FAILURE_CODES = new Set([
1100
1115
  "identity_unavailable",
1101
1116
  "identity_mismatch",
1102
1117
  "owner_mismatch",
1118
+ "access_denied",
1103
1119
  "mode_mismatch",
1104
1120
  "io_error",
1105
1121
  ]);
@@ -1964,6 +1980,59 @@ export class ManagedSessionDescendantStore {
1964
1980
  return true;
1965
1981
  }
1966
1982
 
1983
+ #recoverRenumberedReplacementCleanupReceipt(
1984
+ receiptPath: string,
1985
+ binding: { predecessor: { dev: bigint; ino: bigint }; receipt: { dev: bigint; ino: bigint } },
1986
+ receipt: ManagedFileSnapshot,
1987
+ ): boolean {
1988
+ const parsed = parseReplacementCleanupReceipt(receipt.bytes);
1989
+ const predecessor = parsed ? parseReplacementIdentity(parsed.predecessor) : undefined;
1990
+ if (
1991
+ receipt.identity.dev === binding.receipt.dev ||
1992
+ receipt.identity.ino !== binding.receipt.ino ||
1993
+ !parsed ||
1994
+ !predecessor ||
1995
+ predecessor.dev !== binding.predecessor.dev ||
1996
+ predecessor.ino !== binding.predecessor.ino ||
1997
+ !replacementReceiptPublisherIsVerifiedDead(receipt.bytes)
1998
+ )
1999
+ return false;
2000
+
2001
+ // The filename's device is the only identity component allowed to drift.
2002
+ // The no-follow snapshot supplies the exact current size and digest, and
2003
+ // detachOnly preserves those bytes under a non-canonical quarantine name.
2004
+ const quarantineName = replacementReceiptRetirementName(receipt.identity, binding.predecessor);
2005
+ const detached = nativeSessionStorage().exactUnlink(receiptPath, {
2006
+ dev: receipt.identity.dev,
2007
+ ino: receipt.identity.ino,
2008
+ nlink: receipt.identity.nlink,
2009
+ parentDev: this.#subtreeRoot.dev,
2010
+ parentIno: this.#subtreeRoot.ino,
2011
+ size: BigInt(receipt.identity.size),
2012
+ mtimeNs: receipt.identity.mtimeNs,
2013
+ sha256: receipt.identity.sha256,
2014
+ detachOnly: true,
2015
+ quarantineName,
2016
+ });
2017
+ if (isPeerOwnedReceiptRetirement(detached)) return true;
2018
+ if (
2019
+ !detached.ok &&
2020
+ detached.code === "not_found" &&
2021
+ detached.retainedSuccessorPath === undefined &&
2022
+ detached.retainedUnknownPath === undefined
2023
+ )
2024
+ return true;
2025
+ if (
2026
+ (!detached.ok && detached.code !== "cleanup_pending") ||
2027
+ detached.detachedPath !== path.join(this.#baseDir, quarantineName) ||
2028
+ detached.retainedSuccessorPath ||
2029
+ detached.retainedUnknownPath
2030
+ )
2031
+ throw new Error(`managed_replace_receipt_cleanup_pending:${detached.code ?? "unknown"}`);
2032
+ fsyncDirectory(this.#baseDir);
2033
+ return true;
2034
+ }
2035
+
1967
2036
  #detachReplacementCleanupReceipt(receiptPath: string): void {
1968
2037
  const binding = replacementCleanupReceiptBinding(path.basename(receiptPath));
1969
2038
  if (!binding) throw new Error("managed_replace_cleanup_receipt_invalid");
@@ -1975,6 +2044,7 @@ export class ManagedSessionDescendantStore {
1975
2044
  throw error;
1976
2045
  }
1977
2046
  if (receipt.identity.dev !== binding.receipt.dev || receipt.identity.ino !== binding.receipt.ino) {
2047
+ if (this.#recoverRenumberedReplacementCleanupReceipt(receiptPath, binding, receipt)) return;
1978
2048
  if (this.#recoverReplacementCleanupPlaceholder(receiptPath, binding, receipt)) return;
1979
2049
  throw new Error("managed_replace_cleanup_receipt_invalid");
1980
2050
  }
@@ -540,6 +540,76 @@ function isUnwritableResidentCacheFailure(error: unknown): boolean {
540
540
  );
541
541
  }
542
542
 
543
+ /** Nearest real directory at or above `start`. A file or missing path is not a root. */
544
+ function nearestExistingDirectory(start: string): string | null {
545
+ let current = path.resolve(start);
546
+ for (let depth = 0; depth < 64; depth++) {
547
+ try {
548
+ if (fs.lstatSync(current).isDirectory()) return current;
549
+ } catch {
550
+ // Missing components are not worktree roots.
551
+ }
552
+ const parent = path.dirname(current);
553
+ if (parent === current) return null;
554
+ current = parent;
555
+ }
556
+ return null;
557
+ }
558
+
559
+ /**
560
+ * Canonical path of `start`, following a symlink ancestor.
561
+ * The unmatched suffix is appended so a not-yet-created cache root classifies
562
+ * the directory `openVerifiedResidentCacheInstanceDir` would create it in.
563
+ */
564
+ function canonicalCachePath(start: string): string | null {
565
+ let current = path.resolve(start);
566
+ const pending: string[] = [];
567
+ for (let depth = 0; depth < 64; depth++) {
568
+ try {
569
+ const stat = fs.lstatSync(current);
570
+ if (stat.isDirectory() || stat.isSymbolicLink()) {
571
+ const real = fs.realpathSync(current);
572
+ return pending.length === 0 ? real : path.join(real, ...pending.reverse());
573
+ }
574
+ } catch {
575
+ // Missing components are resolved from the nearest existing ancestor.
576
+ }
577
+ const parent = path.dirname(current);
578
+ if (parent === current) return null;
579
+ pending.push(path.basename(current));
580
+ current = parent;
581
+ }
582
+ return null;
583
+ }
584
+
585
+ function cacheDirectoryInsideGitWorktree(directory: string): boolean {
586
+ return git.repo.resolveSync(directory) !== null;
587
+ }
588
+
589
+ /**
590
+ * Resident text is a second plaintext copy of assistant and tool output.
591
+ * A cache root inside a Git worktree can be staged by a later `git add`, so
592
+ * that root stays in memory. Both the lexical directory and the canonical
593
+ * directory are classified: a symlink outside the worktree must not hide a
594
+ * target inside it, and a lexical worktree path stays refused when its
595
+ * canonical target is outside. A missing or non-directory root still reaches
596
+ * the resident-cache trust check when both views are outside a worktree.
597
+ * No existing directory within 64 ancestors, or a resolver error, stays in memory.
598
+ */
599
+ export function residentTextCacheRootAllowsDisk(cacheRoot: string): boolean {
600
+ try {
601
+ const lexical = nearestExistingDirectory(cacheRoot);
602
+ if (lexical === null || cacheDirectoryInsideGitWorktree(lexical)) return false;
603
+ const canonical = canonicalCachePath(cacheRoot);
604
+ if (canonical === null) return false;
605
+ const canonicalDirectory = nearestExistingDirectory(canonical);
606
+ if (canonicalDirectory === null || cacheDirectoryInsideGitWorktree(canonicalDirectory)) return false;
607
+ return true;
608
+ } catch {
609
+ return false;
610
+ }
611
+ }
612
+
543
613
  export interface SessionEntryBase {
544
614
  type: string;
545
615
  id: string;
@@ -7311,6 +7381,17 @@ function sessionMatchesResumeArg(session: SessionInfo, sessionArg: string): bool
7311
7381
  return fileSessionId.startsWith(normalizedArg);
7312
7382
  }
7313
7383
 
7384
+ /**
7385
+ * Equal session id first, the same `id ===` check managed resume already uses.
7386
+ * Prefix matching stays for every other argument, in list order.
7387
+ */
7388
+ export function pickResumableSession(sessions: readonly SessionInfo[], sessionArg: string): SessionInfo | undefined {
7389
+ return (
7390
+ sessions.find(session => session.id === sessionArg) ??
7391
+ sessions.find(session => sessionMatchesResumeArg(session, sessionArg))
7392
+ );
7393
+ }
7394
+
7314
7395
  export async function resolveResumableSession(
7315
7396
  sessionArg: string,
7316
7397
  cwd: string,
@@ -7321,7 +7402,7 @@ export async function resolveResumableSession(
7321
7402
  const localSessions = sessionDir
7322
7403
  ? await SessionManager.list(cwd, sessionDir, storage)
7323
7404
  : await SessionManager.listManagedForResumePickerReadOnly(cwd, managedAgentDir, storage);
7324
- const localMatch = localSessions.find(session => sessionMatchesResumeArg(session, sessionArg));
7405
+ const localMatch = pickResumableSession(localSessions, sessionArg);
7325
7406
  if (localMatch) {
7326
7407
  return { session: localMatch, scope: "local" };
7327
7408
  }
@@ -7331,7 +7412,7 @@ export async function resolveResumableSession(
7331
7412
  }
7332
7413
 
7333
7414
  const globalSessions = await SessionManager.listAll(storage, managedAgentDir);
7334
- const globalMatch = globalSessions.find(session => sessionMatchesResumeArg(session, sessionArg));
7415
+ const globalMatch = pickResumableSession(globalSessions, sessionArg);
7335
7416
  if (!globalMatch) {
7336
7417
  return undefined;
7337
7418
  }
@@ -8092,9 +8173,13 @@ export class SessionManager {
8092
8173
  this.#residentCacheWin32FallbackCount++;
8093
8174
  return { store: this.#newCanonicalResidentTextStore(), ownsStore: false };
8094
8175
  }
8095
- const instanceDir = openVerifiedResidentCacheInstanceDir(
8096
- getResidentCacheRootDir(this.#residentCacheProfileAgentDir()),
8097
- );
8176
+ const cacheRoot = getResidentCacheRootDir(this.#residentCacheProfileAgentDir());
8177
+ // Not a provisioning failure: retain-and-throw must still commit, with no file to stage.
8178
+ if (!residentTextCacheRootAllowsDisk(cacheRoot)) {
8179
+ this.#residentCacheAdoptFallbackCount++;
8180
+ return { store: this.#newCanonicalResidentTextStore(), ownsStore: false };
8181
+ }
8182
+ const instanceDir = openVerifiedResidentCacheInstanceDir(cacheRoot);
8098
8183
  return { store: EphemeralBlobStore.adoptVerifiedDir(instanceDir), ownsStore: true };
8099
8184
  }
8100
8185
 
@@ -11,7 +11,6 @@ export const IMPORT_REDACTED_PLACEHOLDER = "[REDACTED]";
11
11
  /** Bumped when patterns change; persisted in import provenance. */
12
12
  export const IMPORT_SANITIZER_VERSION = 2;
13
13
 
14
- const ANSI_ESCAPE = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))/gu;
15
14
  const UNSAFE_CONTROL =
16
15
  /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2060\u2066-\u2069\ufeff]/gu;
17
16
  const HEADER_CREDENTIAL = /(\b(?:authorization|cookie|set-cookie)\b[ \t]*:[ \t]*)([^\r\n]*)/giu;
@@ -80,6 +79,58 @@ const REDACTION_RULES: readonly RedactionRule[] = [
80
79
  },
81
80
  ];
82
81
 
82
+ /**
83
+ * Linear CSI/OSC strip. An unterminated `ESC ]` used to make the global
84
+ * replace rescan every later suffix, which is quadratic in the prefix count.
85
+ */
86
+ function stripImportedAnsiEscapes(input: string): string {
87
+ if (!input.includes("\u001b")) return input;
88
+ const parts: string[] = [];
89
+ let from = 0;
90
+ let i = 0;
91
+ let oscLive = true;
92
+ const n = input.length;
93
+ while (i < n) {
94
+ const esc = input.indexOf("\u001b", i);
95
+ if (esc < 0 || esc + 1 >= n) break;
96
+ const kind = input.charCodeAt(esc + 1);
97
+ let end = -1;
98
+ if (kind === 0x5b) end = csiSequenceEnd(input, esc + 2);
99
+ else if (kind === 0x5d && oscLive) end = oscSequenceEnd(input, esc + 2);
100
+ // No BEL and no ST anywhere after this prefix, so later OSC cannot terminate.
101
+ if (kind === 0x5d && end < 0) oscLive = false;
102
+ if (end < 0) {
103
+ i = esc + 1;
104
+ continue;
105
+ }
106
+ if (esc > from) parts.push(input.slice(from, esc));
107
+ from = end;
108
+ i = end;
109
+ }
110
+ if (from === 0) return input;
111
+ if (from < n) parts.push(input.slice(from));
112
+ return parts.join("");
113
+ }
114
+
115
+ function csiSequenceEnd(input: string, j: number): number {
116
+ const n = input.length;
117
+ while (j < n && input.charCodeAt(j) >= 0x30 && input.charCodeAt(j) <= 0x3f) j++;
118
+ while (j < n && input.charCodeAt(j) >= 0x20 && input.charCodeAt(j) <= 0x2f) j++;
119
+ const finalByte = input.charCodeAt(j);
120
+ return finalByte >= 0x40 && finalByte <= 0x7e ? j + 1 : -1;
121
+ }
122
+
123
+ function oscSequenceEnd(input: string, index: number): number {
124
+ // One forward scan. A second search to the end of the suffix is quadratic for repeated OSC sequences.
125
+ const n = input.length;
126
+ for (let j = index; j < n; j++) {
127
+ const code = input.charCodeAt(j);
128
+ if (code === 0x07) return j + 1;
129
+ if (code === 0x1b && j + 1 < n && input.charCodeAt(j + 1) === 0x5c) return j + 2;
130
+ }
131
+ return -1;
132
+ }
133
+
83
134
  export interface ImportRedactionResult {
84
135
  value: string;
85
136
  redacted: number;
@@ -91,7 +142,7 @@ export interface ImportRedactionResult {
91
142
  * is used for all kinds so a partial pattern overlap cannot leak fragments.
92
143
  */
93
144
  export function redactImportedText(input: string): ImportRedactionResult {
94
- let value = input.replace(ANSI_ESCAPE, "").replace(UNSAFE_CONTROL, "");
145
+ let value = stripImportedAnsiEscapes(input).replace(UNSAFE_CONTROL, "");
95
146
  let redacted = 0;
96
147
  const kinds = new Set<string>();
97
148
  value = value.replace(HEADER_CREDENTIAL, (_match, prefix: string, raw: string) => {