@gajae-code/coding-agent 0.14.2 → 0.15.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 (345) hide show
  1. package/CHANGELOG.md +120 -1
  2. package/dist/types/async/job-manager.d.ts +4 -1
  3. package/dist/types/autoresearch/dashboard.d.ts +29 -4
  4. package/dist/types/autoresearch/data-context.d.ts +25 -0
  5. package/dist/types/autoresearch/git.d.ts +65 -7
  6. package/dist/types/autoresearch/harness.d.ts +58 -0
  7. package/dist/types/autoresearch/prompts.d.ts +51 -0
  8. package/dist/types/autoresearch/runs.d.ts +173 -0
  9. package/dist/types/autoresearch/types.d.ts +9 -139
  10. package/dist/types/capability/types.d.ts +20 -0
  11. package/dist/types/cli/accounts-cli.d.ts +1 -0
  12. package/dist/types/commands/{rlm.d.ts → autoresearch.d.ts} +2 -4
  13. package/dist/types/config/autorouting-contract.d.ts +146 -0
  14. package/dist/types/config/autorouting-generator.d.ts +40 -0
  15. package/dist/types/config/autorouting-tier-map.d.ts +16844 -0
  16. package/dist/types/config/autorouting.d.ts +35 -0
  17. package/dist/types/config/model-discovery-manager.d.ts +8 -1
  18. package/dist/types/config/model-registry.d.ts +30 -1
  19. package/dist/types/config/provider-selection-policy.d.ts +23 -0
  20. package/dist/types/config/settings-schema.d.ts +61 -3
  21. package/dist/types/config/settings.d.ts +3 -0
  22. package/dist/types/coordinator/contract.d.ts +1 -1
  23. package/dist/types/coordinator-mcp/durability.d.ts +29 -0
  24. package/dist/types/coordinator-mcp/event-webhook.d.ts +50 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +1 -1
  26. package/dist/types/coordinator-mcp/server.d.ts +14 -0
  27. package/dist/types/crash/index-store.d.ts +3 -0
  28. package/dist/types/crash/record-loader.d.ts +2 -0
  29. package/dist/types/crash/upstream/relay.d.ts +5 -3
  30. package/dist/types/cursor.d.ts +12 -0
  31. package/dist/types/defaults/gjc-defaults.d.ts +35 -1
  32. package/dist/types/defaults/gjc-skills.generated.d.ts +1 -1
  33. package/dist/types/discovery/helpers.d.ts +1 -1
  34. package/dist/types/edit/index.d.ts +2 -1
  35. package/dist/types/eval/py/executor.d.ts +2 -0
  36. package/dist/types/extensibility/extensions/loader.d.ts +32 -0
  37. package/dist/types/extensibility/gjc-plugins/types.d.ts +1 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +194 -0
  40. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  41. package/dist/types/gjc-runtime/psmux-detect.d.ts +1 -1
  42. package/dist/types/gjc-runtime/python-transcript.d.ts +19 -0
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +2 -8
  44. package/dist/types/gjc-runtime/session-layout.d.ts +6 -3
  45. package/dist/types/gjc-runtime/state-renderer.d.ts +0 -22
  46. package/dist/types/gjc-runtime/state-schema.d.ts +7 -7
  47. package/dist/types/gjc-runtime/tmux-common.d.ts +2 -2
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +38 -1
  49. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -24
  50. package/dist/types/gjc-runtime/ultragoal-validation-policy.d.ts +73 -0
  51. package/dist/types/gjc-runtime/workflow-recovery-projection.d.ts +100 -0
  52. package/dist/types/hooks/skill-keywords.d.ts +1 -1
  53. package/dist/types/main.d.ts +0 -10
  54. package/dist/types/modes/acp/acp-agent.d.ts +13 -6
  55. package/dist/types/modes/components/model-selector.d.ts +14 -0
  56. package/dist/types/modes/components/skill-hud/render.d.ts +6 -1
  57. package/dist/types/modes/components/smart-routing-panel.d.ts +82 -0
  58. package/dist/types/modes/controllers/selector-controller.d.ts +14 -0
  59. package/dist/types/modes/interactive-mode.d.ts +3 -0
  60. package/dist/types/modes/types.d.ts +9 -0
  61. package/dist/types/rlm/artifacts.d.ts +4 -4
  62. package/dist/types/runtime/optional-runtime-services.d.ts +2 -1
  63. package/dist/types/runtime/workspace-tree-service.d.ts +5 -1
  64. package/dist/types/runtime-mcp/config.d.ts +6 -0
  65. package/dist/types/runtime-mcp/manager.d.ts +10 -0
  66. package/dist/types/sdk/acp/adapter.d.ts +0 -4
  67. package/dist/types/sdk/broker/broker.d.ts +1 -1
  68. package/dist/types/sdk/broker/lifecycle.d.ts +57 -2
  69. package/dist/types/sdk/broker/session-index.d.ts +15 -0
  70. package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -0
  71. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
  72. package/dist/types/sdk/bus/index.d.ts +8 -0
  73. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +2 -0
  74. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -2
  75. package/dist/types/sdk/client/client.d.ts +39 -4
  76. package/dist/types/sdk/host/host.d.ts +1 -0
  77. package/dist/types/sdk/host/internal-autorouting-state.d.ts +3 -0
  78. package/dist/types/sdk/host/model-pin.d.ts +29 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +2 -1
  80. package/dist/types/sdk/lifecycle-session.d.ts +13 -0
  81. package/dist/types/sdk/router/session-router.d.ts +27 -1
  82. package/dist/types/sdk/session.d.ts +11 -1
  83. package/dist/types/session/agent-session.d.ts +36 -6
  84. package/dist/types/session/artifacts.d.ts +16 -1
  85. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  86. package/dist/types/session/session-manager.d.ts +84 -2
  87. package/dist/types/session/session-staging-paths.d.ts +3 -0
  88. package/dist/types/skill-state/canonical-skills.d.ts +1 -1
  89. package/dist/types/skill-state/workflow-hud.d.ts +12 -21
  90. package/dist/types/skill-state/workflow-mutation-guard.d.ts +1 -0
  91. package/dist/types/slash-commands/helpers/autorouting-status.d.ts +8 -0
  92. package/dist/types/system-prompt.d.ts +23 -0
  93. package/dist/types/task/executor.d.ts +35 -4
  94. package/dist/types/task/index.d.ts +17 -3
  95. package/dist/types/task/receipt.d.ts +3 -1
  96. package/dist/types/task/types.d.ts +144 -0
  97. package/dist/types/tools/atomic-file-write.d.ts +72 -0
  98. package/dist/types/tools/descriptors.d.ts +1 -0
  99. package/dist/types/tools/index.d.ts +36 -1
  100. package/dist/types/tools/move-session.d.ts +40 -0
  101. package/dist/types/tools/python.d.ts +12 -0
  102. package/dist/types/tools/resource-gc.d.ts +0 -3
  103. package/dist/types/tools/search.d.ts +8 -1
  104. package/dist/types/tools/skill.d.ts +9 -4
  105. package/dist/types/tools/subagent.d.ts +33 -3
  106. package/dist/types/utils/xml-escape.d.ts +2 -0
  107. package/dist/types/workflow/workflow-intent-diff.d.ts +2 -2
  108. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +1 -1
  109. package/package.json +11 -12
  110. package/scripts/build-sdk-package-smoke.ts +7 -5
  111. package/scripts/check-autorouting-tier-map.ts +183 -0
  112. package/scripts/generate-sdk-operation-inventory.ts +12 -0
  113. package/scripts/generate-tool-catalog.ts +40 -5
  114. package/src/async/job-manager.ts +5 -2
  115. package/src/autoresearch/dashboard.ts +134 -314
  116. package/src/autoresearch/data-context.ts +35 -0
  117. package/src/autoresearch/git.ts +186 -26
  118. package/src/autoresearch/{helpers.ts → harness.ts} +65 -40
  119. package/src/autoresearch/{prompt-setup.md → prompts/prompt-setup.md} +7 -7
  120. package/src/autoresearch/prompts/prompt.md +103 -0
  121. package/src/autoresearch/prompts.ts +126 -0
  122. package/src/autoresearch/runs.ts +708 -0
  123. package/src/autoresearch/types.ts +9 -150
  124. package/src/capability/index.ts +4 -4
  125. package/src/capability/types.ts +20 -0
  126. package/src/cli/accounts-cli.ts +21 -5
  127. package/src/cli/config-cli.ts +25 -1
  128. package/src/cli/fast-help.ts +1 -2
  129. package/src/cli/list-models.ts +5 -8
  130. package/src/cli/setup-cli.ts +8 -0
  131. package/src/cli.ts +2 -21
  132. package/src/commands/autoresearch.ts +33 -0
  133. package/src/commands/coordinator.ts +15 -0
  134. package/src/commands/launch.ts +2 -4
  135. package/src/commands/quick-lane.ts +1 -1
  136. package/src/commands/sdk.ts +24 -0
  137. package/src/commands/state.ts +1 -1
  138. package/src/config/autorouting-contract.ts +474 -0
  139. package/src/config/autorouting-generator.ts +238 -0
  140. package/src/config/autorouting-tier-map.ts +6108 -0
  141. package/src/config/autorouting.ts +120 -0
  142. package/src/config/model-discovery-manager.ts +42 -11
  143. package/src/config/model-registry.ts +348 -68
  144. package/src/config/provider-selection-policy.ts +60 -9
  145. package/src/config/settings-schema.ts +195 -6
  146. package/src/config/settings.ts +62 -1
  147. package/src/config.ts +2 -3
  148. package/src/coordinator/contract.ts +0 -1
  149. package/src/coordinator-mcp/codex-handoff.ts +68 -42
  150. package/src/coordinator-mcp/durability.ts +200 -0
  151. package/src/coordinator-mcp/event-webhook.ts +278 -0
  152. package/src/coordinator-mcp/question-state.ts +109 -46
  153. package/src/coordinator-mcp/server.ts +932 -504
  154. package/src/crash/index-store.ts +126 -22
  155. package/src/crash/record-loader.ts +7 -0
  156. package/src/crash/upstream/relay.ts +82 -34
  157. package/src/cursor.ts +84 -1
  158. package/src/debug/index.ts +26 -0
  159. package/src/defaults/gjc/skills/autoresearch/SKILL.md +116 -0
  160. package/src/defaults/gjc/skills/autoresearch/auto-critic.md +46 -0
  161. package/src/defaults/gjc/skills/autoresearch/auto-iterate.md +42 -0
  162. package/src/defaults/gjc/skills/deep-interview/SKILL.md +17 -37
  163. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -18
  164. package/src/defaults/gjc/skills/ultragoal/SKILL.md +6 -17
  165. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +2 -2
  166. package/src/defaults/gjc-defaults.ts +89 -2
  167. package/src/defaults/gjc-skills.generated.ts +24 -17
  168. package/src/discovery/builtin.ts +18 -5
  169. package/src/discovery/helpers.ts +6 -6
  170. package/src/edit/index.ts +5 -2
  171. package/src/eval/py/executor.ts +8 -0
  172. package/src/extensibility/extensions/loader.ts +135 -13
  173. package/src/extensibility/extensions/wrapper.ts +1 -2
  174. package/src/extensibility/gjc-plugins/schema.ts +1 -1
  175. package/src/extensibility/runtime-skill-discovery.ts +2 -2
  176. package/src/extensibility/skill-management.ts +2 -3
  177. package/src/extensibility/skills.ts +16 -8
  178. package/src/gjc-runtime/autoresearch-runtime.ts +1509 -0
  179. package/src/gjc-runtime/gc-render.ts +0 -1
  180. package/src/gjc-runtime/gc-runtime.ts +1 -11
  181. package/src/gjc-runtime/launch-tmux.ts +3 -3
  182. package/src/gjc-runtime/psmux-detect.ts +2 -2
  183. package/src/gjc-runtime/python-transcript.ts +58 -0
  184. package/src/gjc-runtime/ralplan-runtime.ts +6 -18
  185. package/src/gjc-runtime/session-layout.ts +18 -7
  186. package/src/gjc-runtime/session-state-sidecar.ts +58 -9
  187. package/src/gjc-runtime/state-renderer.ts +0 -38
  188. package/src/gjc-runtime/state-runtime.ts +43 -22
  189. package/src/gjc-runtime/state-schema.ts +1 -1
  190. package/src/gjc-runtime/tmux-common.ts +7 -7
  191. package/src/gjc-runtime/ultragoal-change-set.ts +232 -11
  192. package/src/gjc-runtime/ultragoal-runtime.ts +309 -114
  193. package/src/gjc-runtime/ultragoal-validation-policy.ts +247 -0
  194. package/src/gjc-runtime/workflow-command-ref.ts +15 -11
  195. package/src/gjc-runtime/workflow-manifest.generated.json +413 -532
  196. package/src/gjc-runtime/workflow-manifest.ts +43 -83
  197. package/src/gjc-runtime/workflow-recovery-projection.ts +616 -0
  198. package/src/hooks/skill-keywords.ts +3 -3
  199. package/src/hooks/skill-state.ts +2 -2
  200. package/src/internal-urls/docs-index.generated.ts +26 -24
  201. package/src/lsp/index.ts +25 -5
  202. package/src/main.ts +2 -27
  203. package/src/modes/acp/acp-agent.ts +200 -75
  204. package/src/modes/components/model-selector.ts +216 -13
  205. package/src/modes/components/oauth-selector.ts +5 -5
  206. package/src/modes/components/skill-hud/render.ts +173 -35
  207. package/src/modes/components/smart-routing-panel.ts +531 -0
  208. package/src/modes/components/tool-status-header.ts +6 -3
  209. package/src/modes/controllers/command-controller.ts +17 -7
  210. package/src/modes/controllers/input-controller.ts +44 -9
  211. package/src/modes/controllers/runtime-mcp-command-controller.ts +4 -0
  212. package/src/modes/controllers/selector-controller.ts +298 -65
  213. package/src/modes/interactive-mode.ts +8 -1
  214. package/src/modes/shared/agent-wire/workflow-gate-types.ts +1 -1
  215. package/src/modes/types.ts +9 -1
  216. package/src/prompts/agents/planner.md +1 -1
  217. package/src/prompts/system/system-prompt.md +1 -1
  218. package/src/prompts/system/volatile-project-context.md +2 -1
  219. package/src/prompts/tools/move-session.md +15 -0
  220. package/src/prompts/tools/python.md +12 -0
  221. package/src/prompts/tools/skill-discovery.md +1 -1
  222. package/src/prompts/tools/skill.md +2 -2
  223. package/src/prompts/tools/task-summary.md +1 -0
  224. package/src/prompts/tools/task.md +6 -0
  225. package/src/rlm/artifacts.ts +12 -10
  226. package/src/runtime/optional-runtime-services.ts +3 -2
  227. package/src/runtime/workspace-tree-service.ts +11 -3
  228. package/src/runtime-mcp/config.ts +8 -1
  229. package/src/runtime-mcp/manager.ts +33 -0
  230. package/src/sdk/acp/adapter.ts +48 -1
  231. package/src/sdk/broker/broker.ts +30 -2
  232. package/src/sdk/broker/discovery.ts +217 -2
  233. package/src/sdk/broker/lifecycle.ts +385 -59
  234. package/src/sdk/broker/session-index.ts +243 -48
  235. package/src/sdk/broker/transport.ts +1 -0
  236. package/src/sdk/bus/chat-daemon-control.ts +8 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +5 -0
  238. package/src/sdk/bus/index.ts +208 -11
  239. package/src/sdk/bus/kind-aware-reconciliation.ts +16 -0
  240. package/src/sdk/bus/reconciliation-store.ts +27 -5
  241. package/src/sdk/bus/telegram-daemon-contract.ts +12 -2
  242. package/src/sdk/client/client.ts +204 -22
  243. package/src/sdk/host/host.ts +16 -0
  244. package/src/sdk/host/internal-autorouting-state.ts +13 -0
  245. package/src/sdk/host/model-pin.ts +101 -0
  246. package/src/sdk/host/reverse-leases.ts +2 -1
  247. package/src/sdk/host/session-runtime.test.ts +224 -1
  248. package/src/sdk/host/session-runtime.ts +125 -37
  249. package/src/sdk/lifecycle/service.ts +3 -1
  250. package/src/sdk/lifecycle-session.ts +42 -1
  251. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  252. package/src/sdk/router/session-router.ts +375 -36
  253. package/src/sdk/session.ts +547 -47
  254. package/src/session/account-inventory.ts +1 -1
  255. package/src/session/agent-session.ts +765 -189
  256. package/src/session/artifacts.ts +234 -4
  257. package/src/session/internal/managed-session-storage.ts +91 -4
  258. package/src/session/provider-safety-stop-hint.ts +3 -2
  259. package/src/session/session-manager.ts +775 -16
  260. package/src/session/session-staging-paths.ts +8 -0
  261. package/src/setup/hermes/templates/operator-instructions.v1.md +0 -1
  262. package/src/setup/host-plugin-setup.ts +2 -2
  263. package/src/skill-state/active-state.ts +2 -2
  264. package/src/skill-state/canonical-skills.ts +1 -1
  265. package/src/skill-state/initial-phase.ts +1 -1
  266. package/src/skill-state/workflow-hud.ts +41 -41
  267. package/src/skill-state/workflow-mutation-guard.ts +26 -7
  268. package/src/slash-commands/builtin-registry.ts +77 -0
  269. package/src/slash-commands/helpers/autorouting-status.ts +55 -0
  270. package/src/system-prompt.ts +102 -5
  271. package/src/task/discovery.ts +2 -3
  272. package/src/task/executor.ts +617 -43
  273. package/src/task/index.ts +262 -15
  274. package/src/task/receipt.ts +30 -0
  275. package/src/task/render.ts +11 -1
  276. package/src/task/types.ts +316 -0
  277. package/src/tools/atomic-file-write.ts +594 -0
  278. package/src/tools/bisect.ts +3 -2
  279. package/src/tools/descriptors.test.ts +3 -0
  280. package/src/tools/descriptors.ts +24 -0
  281. package/src/tools/image-gen.ts +5 -1
  282. package/src/tools/index.ts +62 -5
  283. package/src/tools/move-session.ts +107 -0
  284. package/src/tools/python.ts +205 -0
  285. package/src/tools/read.ts +88 -15
  286. package/src/tools/renderers.ts +2 -0
  287. package/src/tools/resource-gc.ts +8 -177
  288. package/src/tools/search.ts +30 -6
  289. package/src/tools/skill-discovery.ts +1 -0
  290. package/src/tools/skill.ts +37 -7
  291. package/src/tools/subagent-render.ts +119 -13
  292. package/src/tools/subagent.ts +91 -74
  293. package/src/tools/tool-catalog.generated.ts +64 -2
  294. package/src/tools/write.ts +16 -12
  295. package/src/utils/xml-escape.ts +14 -0
  296. package/src/web/search/providers/codex.ts +2 -2
  297. package/src/workflow/workflow-intent-diff.ts +13 -12
  298. package/src/workflow/workflow-intent-report-consumer.ts +6 -6
  299. package/dist/types/autoresearch/helpers.d.ts +0 -24
  300. package/dist/types/autoresearch/index.d.ts +0 -2
  301. package/dist/types/autoresearch/state.d.ts +0 -17
  302. package/dist/types/autoresearch/storage.d.ts +0 -142
  303. package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
  304. package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
  305. package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
  306. package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
  307. package/dist/types/commands/team.d.ts +0 -24
  308. package/dist/types/gjc-runtime/team-gc.d.ts +0 -7
  309. package/dist/types/gjc-runtime/team-launch.d.ts +0 -61
  310. package/dist/types/gjc-runtime/team-notify.d.ts +0 -45
  311. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -336
  312. package/dist/types/gjc-runtime/team-store.d.ts +0 -168
  313. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +0 -43
  314. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +0 -92
  315. package/dist/types/gjc-runtime/team-workers.d.ts +0 -83
  316. package/dist/types/rlm/index.d.ts +0 -47
  317. package/dist/types/rlm/preset.d.ts +0 -23
  318. package/dist/types/rlm/python-tool.d.ts +0 -16
  319. package/src/autoresearch/command-resume.md +0 -14
  320. package/src/autoresearch/index.ts +0 -536
  321. package/src/autoresearch/prompt.md +0 -103
  322. package/src/autoresearch/resume-message.md +0 -10
  323. package/src/autoresearch/state.ts +0 -273
  324. package/src/autoresearch/storage.ts +0 -699
  325. package/src/autoresearch/tools/init-experiment.ts +0 -272
  326. package/src/autoresearch/tools/log-experiment.ts +0 -524
  327. package/src/autoresearch/tools/run-experiment.ts +0 -475
  328. package/src/autoresearch/tools/update-notes.ts +0 -109
  329. package/src/commands/rlm.ts +0 -19
  330. package/src/commands/team.ts +0 -238
  331. package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +0 -42
  332. package/src/defaults/gjc/skills/team/SKILL.md +0 -467
  333. package/src/gjc-runtime/team-gc.ts +0 -49
  334. package/src/gjc-runtime/team-launch.ts +0 -404
  335. package/src/gjc-runtime/team-notify.ts +0 -326
  336. package/src/gjc-runtime/team-runtime.ts +0 -5923
  337. package/src/gjc-runtime/team-store.ts +0 -878
  338. package/src/gjc-runtime/team-worker-heartbeat.ts +0 -149
  339. package/src/gjc-runtime/team-worker-memory-guard.ts +0 -363
  340. package/src/gjc-runtime/team-workers.ts +0 -1123
  341. package/src/prompts/system/rlm-report-command.md +0 -1
  342. package/src/prompts/system/rlm-research.md +0 -23
  343. package/src/rlm/index.ts +0 -365
  344. package/src/rlm/preset.ts +0 -76
  345. package/src/rlm/python-tool.ts +0 -68
@@ -78,6 +78,9 @@ export interface CrashSignatureEntry {
78
78
  relayedAt?: number;
79
79
  /** Occurrence record represented by the durable upstream watermark. */
80
80
  relayedRecordId?: string;
81
+ /** Refusal marker for the exact record and sanitizer contract. */
82
+ relayRefusedRecordId?: string;
83
+ relayRefusedVersion?: string;
81
84
  /** Issues this install already "+1"ed, so re-invocations cannot spam comments. */
82
85
  commentedIssues?: string[];
83
86
  }
@@ -151,6 +154,8 @@ const ENTRY_KEYS = new Set([
151
154
  "acknowledgedAt",
152
155
  "relayedAt",
153
156
  "relayedRecordId",
157
+ "relayRefusedRecordId",
158
+ "relayRefusedVersion",
154
159
  "commentedIssues",
155
160
  ]);
156
161
  const INDEX_KEYS = new Set([
@@ -240,6 +245,12 @@ function parseEntry(value: unknown, now: number): CrashSignatureEntry | undefine
240
245
  (typeof raw.lastAppendRecordId !== "string" || !/^[0-9a-f]{8,32}$/.test(raw.lastAppendRecordId))
241
246
  )
242
247
  return undefined;
248
+ if (
249
+ raw.relayRefusedRecordId !== undefined &&
250
+ (typeof raw.relayRefusedRecordId !== "string" || !/^[0-9a-f]{8,32}$/.test(raw.relayRefusedRecordId))
251
+ )
252
+ return undefined;
253
+ if (raw.relayRefusedVersion !== undefined && !isCleanString(raw.relayRefusedVersion, 64)) return undefined;
243
254
  if (raw.reportedAt !== undefined && !isTimestamp(raw.reportedAt, now)) return undefined;
244
255
  if (raw.acknowledgedAt !== undefined && !isTimestamp(raw.acknowledgedAt, now)) return undefined;
245
256
  if (raw.relayedAt !== undefined && !isTimestamp(raw.relayedAt, now)) return undefined;
@@ -262,21 +273,49 @@ function parseEntry(value: unknown, now: number): CrashSignatureEntry | undefine
262
273
  firstSeen: raw.firstSeen,
263
274
  lastSeen: raw.lastSeen,
264
275
  lastRecordId: raw.lastRecordId,
265
- lastAppendRecordId: typeof raw.lastAppendRecordId === "string" ? raw.lastAppendRecordId : raw.lastRecordId,
276
+ // Legacy entries do not carry append order. Keep it absent until a new
277
+ // journal occurrence supplies authoritative append-order evidence.
278
+ ...(typeof raw.lastAppendRecordId === "string" ? { lastAppendRecordId: raw.lastAppendRecordId } : {}),
266
279
  };
267
- if (raw.reportedAt !== undefined) entry.reportedAt = raw.reportedAt;
268
- if (raw.reportedIssueUrl !== undefined) entry.reportedIssueUrl = raw.reportedIssueUrl;
269
- if (raw.acknowledgedAt !== undefined) entry.acknowledgedAt = raw.acknowledgedAt;
270
- if (raw.relayedAt !== undefined) entry.relayedAt = raw.relayedAt;
271
- if (raw.relayedRecordId !== undefined) entry.relayedRecordId = raw.relayedRecordId;
272
- if (raw.commentedIssues !== undefined) entry.commentedIssues = [...(raw.commentedIssues as string[])];
273
- if (Buffer.byteLength(JSON.stringify(entry), "utf8") > CRASH_INDEX_ENTRY_MAX_BYTES) {
274
- // A legacy entry may already occupy the complete serialized budget. The
275
- // derived append watermark is redundant for that format, because the old
276
- // lastRecordId remains the only known append-order id. Keep the entry
277
- // readable rather than quarantining an otherwise valid index during upgrade.
278
- if (raw.lastAppendRecordId === undefined) delete entry.lastAppendRecordId;
280
+ // Preserve idempotency and append-order watermarks before display metadata.
281
+ // A legacy entry may be exactly at the 1 KiB cap; dropping these fields would
282
+ // make a successful relay/comment look new on every subsequent invocation.
283
+ const optional: readonly [keyof CrashSignatureEntry, unknown][] = [
284
+ ["relayedAt", raw.relayedAt],
285
+ ["relayedRecordId", raw.relayedRecordId],
286
+ ["relayRefusedRecordId", raw.relayRefusedRecordId],
287
+ ["relayRefusedVersion", raw.relayRefusedVersion],
288
+ ["commentedIssues", raw.commentedIssues === undefined ? undefined : [...(raw.commentedIssues as string[])]],
289
+ ["reportedAt", raw.reportedAt],
290
+ ["reportedIssueUrl", raw.reportedIssueUrl],
291
+ ["acknowledgedAt", raw.acknowledgedAt],
292
+ ];
293
+ for (const [key, value] of optional) {
294
+ if (value === undefined) continue;
295
+ if (key === "commentedIssues" && Array.isArray(value)) {
296
+ // Keep the newest idempotency markers that fit. Silently dropping the
297
+ // whole list would make a capped entry comment the same issue again.
298
+ for (let start = 0; start < value.length; start++) {
299
+ const candidate = { ...entry, [key]: value.slice(start) };
300
+ if (Buffer.byteLength(JSON.stringify(candidate), "utf8") <= CRASH_INDEX_ENTRY_MAX_BYTES) {
301
+ (entry as unknown as Record<string, unknown>)[key] = value.slice(start);
302
+ break;
303
+ }
304
+ }
305
+ continue;
306
+ }
307
+ const candidate = { ...entry, [key]: value };
308
+ if (Buffer.byteLength(JSON.stringify(candidate), "utf8") <= CRASH_INDEX_ENTRY_MAX_BYTES)
309
+ (entry as unknown as Record<string, unknown>)[key] = value;
279
310
  }
311
+ if (raw.relayedRecordId !== undefined && entry.relayedRecordId !== raw.relayedRecordId) return undefined;
312
+ if (raw.relayRefusedRecordId !== undefined && entry.relayRefusedRecordId !== raw.relayRefusedRecordId)
313
+ return undefined;
314
+ if (raw.relayRefusedVersion !== undefined && entry.relayRefusedVersion !== raw.relayRefusedVersion) return undefined;
315
+ // A legacy entry may be exactly at the cap. Display/report metadata is
316
+ // bounded normalization fodder, so it is safe to drop when upgrading it;
317
+ // refusing the whole entry would lose the occurrence and make a later
318
+ // journal merge quarantine an otherwise valid index.
280
319
  if (Buffer.byteLength(JSON.stringify(entry), "utf8") > CRASH_INDEX_ENTRY_MAX_BYTES) return undefined;
281
320
  return entry;
282
321
  }
@@ -474,6 +513,51 @@ function evictOne(index: CrashIndex): boolean {
474
513
  return true;
475
514
  }
476
515
 
516
+ /**
517
+ * Add a refusal watermark without allowing optional presentation/report data
518
+ * to make the entry unpersistable at the hard 1 KiB boundary. The marker is
519
+ * the durable safety property; older display metadata is deliberately the
520
+ * first thing sacrificed. Build the candidate first so callers never see a
521
+ * partially-mutated entry when even the mandatory fields cannot fit.
522
+ */
523
+ function normalizeEntryForPersistence(candidate: CrashSignatureEntry): CrashSignatureEntry | undefined {
524
+ const fits = (value: CrashSignatureEntry): boolean =>
525
+ Buffer.byteLength(JSON.stringify(value), "utf8") <= CRASH_INDEX_ENTRY_MAX_BYTES;
526
+ if (fits(candidate)) return candidate;
527
+ if (candidate.commentedIssues !== undefined) {
528
+ for (let start = 0; start < candidate.commentedIssues.length; start++) {
529
+ const trimmed = { ...candidate, commentedIssues: candidate.commentedIssues.slice(start) };
530
+ if (fits(trimmed)) return trimmed;
531
+ }
532
+ }
533
+
534
+ // Report/display metadata is advisory and can be reconstructed from the
535
+ // journal or re-entered by the user. Relay watermarks are intentionally not
536
+ // in this list: dropping one would make a successful/refused batch repeat.
537
+ for (const key of ["commentedIssues", "reportedIssueUrl", "reportedAt", "acknowledgedAt"] as const) {
538
+ delete candidate[key];
539
+ if (fits(candidate)) return candidate;
540
+ }
541
+ // Keep the error identity while shrinking only its human-facing preview.
542
+ if (candidate.messageClass) {
543
+ candidate.messageClass = boundMessageClass(candidate.messageClass.slice(0, 128));
544
+ if (fits(candidate)) return candidate;
545
+ }
546
+ return undefined;
547
+ }
548
+
549
+ function withRefusalWatermark(
550
+ existing: CrashSignatureEntry,
551
+ recordId: string,
552
+ contractVersion: string,
553
+ ): CrashSignatureEntry | undefined {
554
+ return normalizeEntryForPersistence({
555
+ ...existing,
556
+ relayRefusedRecordId: recordId,
557
+ relayRefusedVersion: contractVersion,
558
+ });
559
+ }
560
+
477
561
  /** Apply one journal event to the in-memory index. Returns whether it changed anything. */
478
562
  export function applyCrashEvent(index: CrashIndex, event: CrashEvent, now: number): boolean {
479
563
  if (event.at > now + MAX_FUTURE_SKEW_MS || event.at < MIN_TIMESTAMP_MS) return false;
@@ -511,6 +595,16 @@ export function applyCrashEvent(index: CrashIndex, event: CrashEvent, now: numbe
511
595
  }
512
596
  return true;
513
597
  }
598
+ if (event.kind === "refused") {
599
+ if (!existing || existing.fpv !== event.fpv) return false;
600
+ if (existing.relayRefusedRecordId === event.recordId && existing.relayRefusedVersion === event.contractVersion)
601
+ return false;
602
+ const updated = withRefusalWatermark(existing, event.recordId, event.contractVersion);
603
+ if (!updated) return false;
604
+ for (const key of Object.keys(existing)) delete (existing as unknown as Record<string, unknown>)[key];
605
+ Object.assign(existing, updated);
606
+ return true;
607
+ }
514
608
  if (event.kind === "acknowledged") {
515
609
  if (!existing) return false;
516
610
  if (existing.acknowledgedAt !== undefined && existing.acknowledgedAt >= event.at) return false;
@@ -527,21 +621,28 @@ export function applyCrashEvent(index: CrashIndex, event: CrashEvent, now: numbe
527
621
  return false;
528
622
  }
529
623
  if (index.recentEventIds.includes(event.recordId)) return false;
530
- index.recentEventIds.push(event.recordId);
531
- if (index.recentEventIds.length > RECENT_EVENT_ID_LIMIT)
532
- index.recentEventIds.splice(0, index.recentEventIds.length - RECENT_EVENT_ID_LIMIT);
533
624
  // A journal occurrence is newer authority than a prior eviction tombstone.
534
625
  // Clear it before capacity eviction so a full retirement ledger cannot block
535
626
  // the signature's explicit revival.
536
627
  if (existing) {
537
- existing.lifetimeCount += 1;
538
- existing.firstSeen = Math.min(existing.firstSeen, event.at);
539
- existing.lastAppendRecordId = event.recordId;
628
+ const updated: CrashSignatureEntry = {
629
+ ...existing,
630
+ lifetimeCount: existing.lifetimeCount + 1,
631
+ firstSeen: Math.min(existing.firstSeen, event.at),
632
+ lastAppendRecordId: event.recordId,
633
+ };
540
634
  if (event.at >= existing.lastSeen) {
541
- existing.lastSeen = event.at;
542
- existing.lastRecordId = event.recordId;
543
- if (event.messageClass) existing.messageClass = boundMessageClass(event.messageClass);
635
+ updated.lastSeen = event.at;
636
+ updated.lastRecordId = event.recordId;
637
+ if (event.messageClass) updated.messageClass = boundMessageClass(event.messageClass);
544
638
  }
639
+ const normalized = normalizeEntryForPersistence(updated);
640
+ if (!normalized) return false;
641
+ for (const key of Object.keys(existing)) delete (existing as unknown as Record<string, unknown>)[key];
642
+ Object.assign(existing, normalized);
643
+ index.recentEventIds.push(event.recordId);
644
+ if (index.recentEventIds.length > RECENT_EVENT_ID_LIMIT)
645
+ index.recentEventIds.splice(0, index.recentEventIds.length - RECENT_EVENT_ID_LIMIT);
545
646
  return true;
546
647
  }
547
648
  if (Object.keys(index.signatures).length >= CRASH_INDEX_MAX_SIGNATURES && !evictOne(index)) {
@@ -561,6 +662,9 @@ export function applyCrashEvent(index: CrashIndex, event: CrashEvent, now: numbe
561
662
  lastRecordId: event.recordId,
562
663
  lastAppendRecordId: event.recordId,
563
664
  };
665
+ index.recentEventIds.push(event.recordId);
666
+ if (index.recentEventIds.length > RECENT_EVENT_ID_LIMIT)
667
+ index.recentEventIds.splice(0, index.recentEventIds.length - RECENT_EVENT_ID_LIMIT);
564
668
  return true;
565
669
  }
566
670
 
@@ -147,3 +147,10 @@ export function findLatestRecord(contents: string, fingerprint: string): LoadedC
147
147
  const matches = parseCrashRecords(contents).filter(record => record.fingerprint === fingerprint);
148
148
  return matches.at(-1);
149
149
  }
150
+
151
+ /** Identity-bearing record bound to the indexed append-order identity. */
152
+ export function findRecordById(contents: string, fingerprint: string, recordId: string): LoadedCrashRecord | undefined {
153
+ return parseRecoverableCrashRecords(contents).find(
154
+ record => record.fingerprint === fingerprint && record.recordId === recordId,
155
+ );
156
+ }
@@ -22,18 +22,7 @@
22
22
  import { createHash } from "node:crypto";
23
23
  import * as fs from "node:fs/promises";
24
24
  import * as path from "node:path";
25
- import {
26
- getCrashEventsPath,
27
- getCrashIndexPath,
28
- getCrashLogPath,
29
- getHandledErrorEventsPath,
30
- getHandledErrorIndexPath,
31
- getHandledErrorLogPath,
32
- getTrustedAgentFile,
33
- isProjectEnvDeclaration,
34
- normalizeCrashFrames,
35
- VERSION,
36
- } from "@gajae-code/utils";
25
+ import { getTrustedAgentFile, normalizeCrashFrames, VERSION } from "@gajae-code/utils";
37
26
  import { $credentialEnv } from "@gajae-code/utils/env";
38
27
  import {
39
28
  type CrashSignatureView,
@@ -42,10 +31,13 @@ import {
42
31
  listCrashSignatures,
43
32
  recordCrashStateEvent,
44
33
  } from "../index-store";
45
- import { findLatestRecord } from "../record-loader";
34
+ import { findRecordById } from "../record-loader";
46
35
  import { parseSentryDsn, type SentryDsn } from "./dsn";
47
36
  import { buildCrashEnvelope, type CrashEventFrame, sentryAuthHeader } from "./envelope";
48
37
 
38
+ /** Version of the complete sanitizer/egress contract persisted in refusals. */
39
+ export const SANITIZER_EGRESS_CONTRACT_VERSION = "sanitize-external-crash-v1";
40
+
49
41
  /** Trusted environment variable form of the DSN, for CI and one-off runs. */
50
42
  export const CRASH_UPSTREAM_DSN_ENV = "GJC_CRASH_SENTRY_DSN";
51
43
 
@@ -56,6 +48,25 @@ export const CRASH_UPSTREAM_DSN_ENV = "GJC_CRASH_SENTRY_DSN";
56
48
  const MAX_RELAY_PER_RUN = 8;
57
49
  const RELAY_TIMEOUT_MS = 10_000;
58
50
  const RELAY_CLAIM_TTL_MS = RELAY_TIMEOUT_MS * 2;
51
+ const CRASH_LOG_READ_MAX_BYTES = 1024 * 1024;
52
+ const NOFOLLOW = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
53
+
54
+ async function readCrashLogNoFollow(filePath: string): Promise<string | undefined> {
55
+ let handle: fs.FileHandle | undefined;
56
+ try {
57
+ handle = await fs.open(filePath, fs.constants.O_RDONLY | NOFOLLOW);
58
+ const stat = await handle.stat();
59
+ if (!stat.isFile()) return undefined;
60
+ const length = Math.min(stat.size, CRASH_LOG_READ_MAX_BYTES);
61
+ const buffer = Buffer.allocUnsafe(length);
62
+ await handle.read(buffer, 0, length, Math.max(0, stat.size - length));
63
+ return buffer.toString("utf8");
64
+ } catch {
65
+ return undefined;
66
+ } finally {
67
+ await handle?.close().catch(() => {});
68
+ }
69
+ }
59
70
 
60
71
  export interface CrashRelayConfig {
61
72
  readonly upstream: "off" | "sentry";
@@ -170,6 +181,18 @@ export function resolveRelayDsn(
170
181
  */
171
182
  export function isRelayDue(signature: CrashSignatureView): boolean {
172
183
  const appendId = signature.lastAppendRecordId ?? signature.lastRecordId;
184
+ if (
185
+ signature.relayRefusedRecordId === appendId &&
186
+ signature.relayRefusedVersion === SANITIZER_EGRESS_CONTRACT_VERSION
187
+ )
188
+ return false;
189
+ // Modern entries with a durable relay record id use exact identity. Legacy
190
+ // entries upgraded from a wall-clock-only relay have no relayedRecordId, so
191
+ // retain their timestamp coverage compatibility until a new append exists.
192
+ if (signature.lastAppendRecordId !== undefined) {
193
+ if (signature.relayedRecordId !== undefined) return signature.relayedRecordId !== appendId;
194
+ return true;
195
+ }
173
196
  if (signature.relayedRecordId !== undefined) {
174
197
  if (signature.relayedRecordId === appendId) return false;
175
198
  if (
@@ -195,26 +218,51 @@ function relayEventId(signature: CrashSignatureView): string {
195
218
  .slice(0, 32);
196
219
  }
197
220
 
221
+ async function persistRefusal(
222
+ paths: CrashStatePaths,
223
+ signature: CrashSignatureView,
224
+ now: () => number,
225
+ ): Promise<boolean> {
226
+ try {
227
+ const index = await recordCrashStateEvent(
228
+ {
229
+ kind: "refused",
230
+ fingerprint: signature.fingerprint,
231
+ fpv: signature.fpv,
232
+ recordId: relayAppendRecordId(signature),
233
+ contractVersion: SANITIZER_EGRESS_CONTRACT_VERSION,
234
+ at: signature.lastSeen,
235
+ },
236
+ { paths, now: now() },
237
+ );
238
+ const entry = index.signatures[signature.fingerprint];
239
+ return (
240
+ entry?.relayRefusedRecordId === relayAppendRecordId(signature) &&
241
+ entry.relayRefusedVersion === SANITIZER_EGRESS_CONTRACT_VERSION
242
+ );
243
+ } catch {
244
+ return false;
245
+ }
246
+ }
247
+
198
248
  /**
199
- * Automatic relay stores use the normal XDG state location only when the XDG
200
- * value came from a trusted environment. `dirs.ts` rejects checkout `.env`
201
- * declarations before resolving these paths, including dotenv-expanded values.
249
+ * Automatic relay always reads the trusted agent stores. XDG-aware paths remain
250
+ * available to ordinary crash state operations, but never select automatic-egress
251
+ * input files.
202
252
  */
203
253
  export function resolveTrustedRelayStatePaths(): CrashStatePaths {
204
- const useTrustedAgent = isProjectEnvDeclaration("XDG_STATE_HOME");
205
254
  return {
206
- index: useTrustedAgent ? getTrustedAgentFile("gjc-crash-index.json") : getCrashIndexPath(),
207
- events: useTrustedAgent ? getTrustedAgentFile("gjc-crash-events.jsonl") : getCrashEventsPath(),
208
- crashLog: useTrustedAgent ? getTrustedAgentFile("gjc-crash.log") : getCrashLogPath(),
255
+ index: getTrustedAgentFile("gjc-crash-index.json"),
256
+ events: getTrustedAgentFile("gjc-crash-events.jsonl"),
257
+ crashLog: getTrustedAgentFile("gjc-crash.log"),
209
258
  };
210
259
  }
211
260
 
212
261
  export function resolveTrustedHandledRelayStatePaths(): CrashStatePaths {
213
- const useTrustedAgent = isProjectEnvDeclaration("XDG_STATE_HOME");
214
262
  return {
215
- index: useTrustedAgent ? getTrustedAgentFile("gjc-error-index.json") : getHandledErrorIndexPath(),
216
- events: useTrustedAgent ? getTrustedAgentFile("gjc-error-events.jsonl") : getHandledErrorEventsPath(),
217
- crashLog: useTrustedAgent ? getTrustedAgentFile("gjc-error.log") : getHandledErrorLogPath(),
263
+ index: getTrustedAgentFile("gjc-error-index.json"),
264
+ events: getTrustedAgentFile("gjc-error-events.jsonl"),
265
+ crashLog: getTrustedAgentFile("gjc-error.log"),
218
266
  };
219
267
  }
220
268
 
@@ -292,10 +340,8 @@ export async function relayCrashSignatures(options: CrashRelayOptions): Promise<
292
340
  .slice(0, limit);
293
341
  if (signatures.length === 0) return { status: "skipped", reason: "nothing-to-relay" };
294
342
 
295
- let crashLog = "";
296
- try {
297
- crashLog = await fs.readFile(paths.crashLog, "utf8");
298
- } catch {
343
+ const crashLog = await readCrashLogNoFollow(paths.crashLog);
344
+ if (crashLog === undefined) {
299
345
  // A missing or unreadable log means no stack to attach; the signature
300
346
  // metadata alone is not worth a send.
301
347
  return { status: "skipped", reason: "nothing-to-relay" };
@@ -314,9 +360,10 @@ export async function relayCrashSignatures(options: CrashRelayOptions): Promise<
314
360
  if (claim.status === "failed") failed++;
315
361
  continue;
316
362
  }
317
- const record = findLatestRecord(crashLog, signature.fingerprint);
318
- if (!record) {
319
- refused++;
363
+ const record = findRecordById(crashLog, signature.fingerprint, relayAppendRecordId(signature));
364
+ if (!record || record.fpv !== signature.fpv) {
365
+ if (await persistRefusal(paths, signature, now)) refused++;
366
+ else failed++;
320
367
  await claim.release().catch(() => {});
321
368
  continue;
322
369
  }
@@ -324,8 +371,8 @@ export async function relayCrashSignatures(options: CrashRelayOptions): Promise<
324
371
  const envelope = buildCrashEnvelope({
325
372
  eventId,
326
373
  fingerprint: signature.fingerprint,
327
- errorName: signature.errorName,
328
- messageClass: signature.messageClass,
374
+ errorName: record.errorName,
375
+ messageClass: record.messageClass,
329
376
  frames: toEventFrames(record.body),
330
377
  firstSeen: signature.firstSeen,
331
378
  lastSeen: signature.lastSeen,
@@ -338,7 +385,8 @@ export async function relayCrashSignatures(options: CrashRelayOptions): Promise<
338
385
  });
339
386
  if (!envelope.ok) {
340
387
  // Fail closed. Deliberately no retry with a reduced payload.
341
- refused++;
388
+ if (await persistRefusal(paths, signature, now)) refused++;
389
+ else failed++;
342
390
  await claim.release().catch(() => {});
343
391
  continue;
344
392
  }
package/src/cursor.ts CHANGED
@@ -14,6 +14,14 @@ import type {
14
14
  CursorExecHandlers as ICursorExecHandlers,
15
15
  ToolResultMessage,
16
16
  } from "@gajae-code/ai/core";
17
+ import {
18
+ piEscapeRegexLiteral,
19
+ piJoinPath,
20
+ piLimit,
21
+ piLsPath,
22
+ piReadPath,
23
+ piTimeout,
24
+ } from "@gajae-code/ai/providers/cursor/exec-modern";
17
25
  import { sanitizeText } from "@gajae-code/utils";
18
26
  import { resolveToCwd } from "./tools/path-utils";
19
27
 
@@ -32,6 +40,8 @@ type CursorExecEventEmitter = (event: AgentEvent, dispatchedTool?: AgentTool) =>
32
40
  interface CursorExecBridgeOptions {
33
41
  cwd: string;
34
42
  tools: Map<string, AgentTool>;
43
+ getEditReplaceTool?: () => AgentTool | undefined;
44
+ createSearchTool?: (options: { context?: number; totalMatchLimit?: number }) => AgentTool | undefined;
35
45
  getToolContext?: () => AgentToolContext | undefined;
36
46
  emitEvent?: CursorExecEventEmitter;
37
47
  createEventEmitter?: () => ((event: AgentEvent) => void) | undefined;
@@ -66,8 +76,9 @@ async function executeTool(
66
76
  toolName: string,
67
77
  toolCallId: string,
68
78
  args: Record<string, unknown>,
79
+ overrideTool?: AgentTool,
69
80
  ): Promise<ToolResultMessage> {
70
- const tool = options.tools.get(toolName);
81
+ const tool = overrideTool ?? options.tools.get(toolName);
71
82
  if (!tool) {
72
83
  const result = buildToolErrorResult(`Tool "${toolName}" not available`);
73
84
  return createToolResultMessage(toolCallId, toolName, result, true);
@@ -109,6 +120,7 @@ async function executeTool(
109
120
  result = buildToolErrorResult(message);
110
121
  isError = true;
111
122
  }
123
+ isError ||= result.isError === true;
112
124
 
113
125
  const sanitizedFinalResult: AgentToolResult<unknown> = {
114
126
  content: result.content.map(c => (c.type === "text" ? { ...c, text: sanitizeText(c.text) } : c)),
@@ -203,6 +215,13 @@ export class CursorExecHandlers implements ICursorExecHandlers {
203
215
  this.shellStream = this.shellStream.bind(this);
204
216
  this.diagnostics = this.diagnostics.bind(this);
205
217
  this.mcp = this.mcp.bind(this);
218
+ this.piRead = this.piRead.bind(this);
219
+ this.piBash = this.piBash.bind(this);
220
+ this.piEdit = this.piEdit.bind(this);
221
+ this.piWrite = this.piWrite.bind(this);
222
+ this.piGrep = this.piGrep.bind(this);
223
+ this.piFind = this.piFind.bind(this);
224
+ this.piLs = this.piLs.bind(this);
206
225
  }
207
226
 
208
227
  #optionsForCall(): CursorExecBridgeOptions {
@@ -390,6 +409,70 @@ export class CursorExecHandlers implements ICursorExecHandlers {
390
409
  return createToolResultMessage(toolCallId, toolName, result, isError);
391
410
  }
392
411
 
412
+ async piRead(call: Parameters<NonNullable<ICursorExecHandlers["piRead"]>>[0]) {
413
+ const composed = piReadPath(call.args.path, call.args.offset, call.args.limit);
414
+ if (composed === null) {
415
+ return createToolResultMessage(call.toolCallId, "read", { content: [{ type: "text", text: "" }] }, false);
416
+ }
417
+ return executeTool(this.#optionsForCall(), "read", call.toolCallId, { path: composed });
418
+ }
419
+
420
+ async piBash(call: Parameters<NonNullable<ICursorExecHandlers["piBash"]>>[0]) {
421
+ return executeTool(this.#optionsForCall(), "bash", call.toolCallId, {
422
+ command: call.args.command,
423
+ timeout: piTimeout(call.args.timeout),
424
+ });
425
+ }
426
+
427
+ async piEdit(call: Parameters<NonNullable<ICursorExecHandlers["piEdit"]>>[0]) {
428
+ const edits = call.args.edits.map(edit => ({
429
+ old_text: edit.oldText,
430
+ new_text: edit.newText,
431
+ }));
432
+ return executeTool(
433
+ this.#optionsForCall(),
434
+ "edit",
435
+ call.toolCallId,
436
+ { path: call.args.path, edits },
437
+ this.options.getEditReplaceTool?.(),
438
+ );
439
+ }
440
+
441
+ async piWrite(call: Parameters<NonNullable<ICursorExecHandlers["piWrite"]>>[0]) {
442
+ return executeTool(this.#optionsForCall(), "write", call.toolCallId, {
443
+ path: call.args.path,
444
+ content: call.args.content,
445
+ });
446
+ }
447
+
448
+ async piGrep(call: Parameters<NonNullable<ICursorExecHandlers["piGrep"]>>[0]) {
449
+ const { pattern, path, glob, ignoreCase, literal, context, limit } = call.args;
450
+ const perCallSearch = this.options.createSearchTool?.({
451
+ context: context === undefined ? undefined : Math.max(0, Math.floor(context)),
452
+ totalMatchLimit: piLimit(limit),
453
+ });
454
+ const args: Record<string, unknown> = {
455
+ pattern: literal === true ? piEscapeRegexLiteral(pattern) : pattern,
456
+ paths: [glob ? piJoinPath(path, glob) : path || "."],
457
+ };
458
+ if (ignoreCase === true) args.i = true;
459
+ return executeTool(this.#optionsForCall(), "search", call.toolCallId, args, perCallSearch);
460
+ }
461
+
462
+ async piFind(call: Parameters<NonNullable<ICursorExecHandlers["piFind"]>>[0]) {
463
+ const { pattern, path, limit } = call.args;
464
+ return executeTool(this.#optionsForCall(), "find", call.toolCallId, {
465
+ paths: [piJoinPath(path, pattern)],
466
+ limit: piLimit(limit),
467
+ });
468
+ }
469
+
470
+ async piLs(call: Parameters<NonNullable<ICursorExecHandlers["piLs"]>>[0]) {
471
+ return executeTool(this.#optionsForCall(), "read", call.toolCallId, {
472
+ path: piLsPath(call.args.path),
473
+ });
474
+ }
475
+
393
476
  async diagnostics(args: Parameters<NonNullable<ICursorExecHandlers["diagnostics"]>>[0]) {
394
477
  const toolCallId = decodeToolCallId(args.toolCallId);
395
478
  const toolResultMessage = await executeTool(this.#optionsForCall(), "lsp", toolCallId, {
@@ -327,9 +327,27 @@ export class DebugSelectorComponent extends Container {
327
327
  }
328
328
 
329
329
  async #handleViewLogs(): Promise<void> {
330
+ // The selector restores the reusable composer synchronously before this
331
+ // async operation starts. Capture both the exact mounted child and the
332
+ // container revision so a stale completion cannot mistake a newer
333
+ // non-Container owner (RawSSE/Input) for the composer, or reclaim the
334
+ // same composer after a newer overlay has already opened and closed.
335
+ const expectedComposerMount =
336
+ this.ctx.editorContainer.children.length === 1 ? this.ctx.editorContainer.children[0] : undefined;
337
+ const expectedMountRevision = this.ctx.editorContainer.getRenderRevision();
330
338
  try {
331
339
  const logSource = await createDebugLogSource();
332
340
  const logs = await logSource.getInitialText();
341
+ // The awaits above leave a window where a newer overlay can take over
342
+ // the editor container (or the UI can stop). Mount only when the exact
343
+ // child and the mount revision captured before the awaits still match.
344
+ if (
345
+ expectedComposerMount === undefined ||
346
+ this.ctx.editorContainer.getRenderRevision() !== expectedMountRevision ||
347
+ this.ctx.editorContainer.children.length !== 1 ||
348
+ this.ctx.editorContainer.children[0] !== expectedComposerMount
349
+ )
350
+ return;
333
351
  if (!logs && !logSource.hasOlderLogs()) {
334
352
  this.ctx.showWarning("No log entries found for today.");
335
353
  return;
@@ -345,6 +363,10 @@ export class DebugSelectorComponent extends Container {
345
363
  logSource,
346
364
  });
347
365
 
366
+ // The composer is reusable across overlays; detach it before clearing
367
+ // so clear() disposes only the transient viewer, not the editor's
368
+ // tab-width listener (disposal is terminal).
369
+ this.ctx.editorContainer.detachChild(this.ctx.editor);
348
370
  this.ctx.editorContainer.clear();
349
371
  this.ctx.editorContainer.addChild(viewer);
350
372
  this.ctx.ui.setFocus(viewer);
@@ -364,6 +386,10 @@ export class DebugSelectorComponent extends Container {
364
386
  onUpdate: () => this.ctx.ui.requestRender(),
365
387
  });
366
388
 
389
+ // The composer is reusable across overlays; detach it before clearing
390
+ // so clear() disposes only the transient viewer, not the editor's
391
+ // tab-width listener (disposal is terminal).
392
+ this.ctx.editorContainer.detachChild(this.ctx.editor);
367
393
  this.ctx.editorContainer.clear();
368
394
  this.ctx.editorContainer.addChild(viewer);
369
395
  this.ctx.ui.setFocus(viewer);