@gajae-code/coding-agent 0.16.7 → 0.17.1

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 (321) hide show
  1. package/CHANGELOG.md +180 -1
  2. package/dist/types/capability/fs.d.ts +37 -4
  3. package/dist/types/capability/index.d.ts +19 -0
  4. package/dist/types/capability/types.d.ts +9 -0
  5. package/dist/types/cli/doctor/args.d.ts +36 -0
  6. package/dist/types/cli/doctor/checks.d.ts +18 -0
  7. package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
  8. package/dist/types/cli/doctor/context.d.ts +89 -0
  9. package/dist/types/cli/doctor/files.d.ts +27 -0
  10. package/dist/types/cli/doctor/ids.d.ts +20 -0
  11. package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
  12. package/dist/types/cli/doctor/journal.d.ts +43 -0
  13. package/dist/types/cli/doctor/managed-link.d.ts +39 -0
  14. package/dist/types/cli/doctor/native-probe.d.ts +2 -0
  15. package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
  16. package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
  17. package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
  18. package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
  19. package/dist/types/cli/doctor/probe-client.d.ts +9 -0
  20. package/dist/types/cli/doctor/probe-types.d.ts +18 -0
  21. package/dist/types/cli/doctor/probe.d.ts +3 -0
  22. package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
  23. package/dist/types/cli/doctor/repairs.d.ts +28 -0
  24. package/dist/types/cli/doctor/report.d.ts +27 -0
  25. package/dist/types/cli/doctor/runner.d.ts +4 -0
  26. package/dist/types/cli/doctor/safety.d.ts +10 -0
  27. package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
  28. package/dist/types/cli/doctor/service-targets.d.ts +41 -0
  29. package/dist/types/cli/doctor/types.d.ts +145 -0
  30. package/dist/types/cli/doctor-cli.d.ts +7 -0
  31. package/dist/types/cli/doctor-supervisor.d.ts +121 -0
  32. package/dist/types/cli/doctor-worker.d.ts +29 -0
  33. package/dist/types/cli/install-activation.d.ts +158 -0
  34. package/dist/types/cli/macos-community-app.d.ts +41 -0
  35. package/dist/types/cli/mcp-cli.d.ts +0 -11
  36. package/dist/types/cli/update-cli.d.ts +41 -4
  37. package/dist/types/cli-main.d.ts +174 -0
  38. package/dist/types/cli.d.ts +1 -171
  39. package/dist/types/commands/doctor.d.ts +7 -0
  40. package/dist/types/commands/sdk.d.ts +17 -8
  41. package/dist/types/config/autorouting-tier-map.d.ts +19 -0
  42. package/dist/types/config/file-lock.d.ts +27 -3
  43. package/dist/types/config/model-preset-registry.d.ts +1 -0
  44. package/dist/types/config/model-registry.d.ts +12 -5
  45. package/dist/types/config/model-resolver.d.ts +16 -3
  46. package/dist/types/config/models-config-schema.d.ts +2 -0
  47. package/dist/types/config/settings-schema.d.ts +1 -1
  48. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
  49. package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
  50. package/dist/types/cursor.d.ts +9 -9
  51. package/dist/types/discovery/agents-md.d.ts +6 -1
  52. package/dist/types/discovery/helpers.d.ts +36 -7
  53. package/dist/types/discovery/index.d.ts +1 -1
  54. package/dist/types/discovery/mcp-json.d.ts +2 -0
  55. package/dist/types/extensibility/extensions/loader.d.ts +22 -1
  56. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
  57. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
  58. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  59. package/dist/types/extensibility/plugins/manager.d.ts +33 -4
  60. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
  61. package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
  62. package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
  63. package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
  64. package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
  65. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
  66. package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
  67. package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
  68. package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
  69. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  70. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
  71. package/dist/types/main.d.ts +19 -1
  72. package/dist/types/modes/acp/acp-agent.d.ts +40 -0
  73. package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
  74. package/dist/types/modes/components/model-selector.d.ts +2 -0
  75. package/dist/types/modes/interactive-mode.d.ts +1 -1
  76. package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
  77. package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
  78. package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
  79. package/dist/types/runtime-mcp/types.d.ts +2 -2
  80. package/dist/types/sdk/acp/adapter.d.ts +5 -1
  81. package/dist/types/sdk/broker/broker.d.ts +51 -0
  82. package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
  83. package/dist/types/sdk/broker/discovery.d.ts +38 -0
  84. package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
  85. package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
  86. package/dist/types/sdk/broker/ensure.d.ts +14 -0
  87. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  88. package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
  89. package/dist/types/sdk/broker/runtime.d.ts +26 -0
  90. package/dist/types/sdk/broker/session-index.d.ts +13 -4
  91. package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
  92. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
  93. package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
  94. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
  95. package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
  96. package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
  97. package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
  98. package/dist/types/sdk/bus/index.d.ts +2 -0
  99. package/dist/types/sdk/bus/notification-service.d.ts +3 -0
  100. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
  101. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  102. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
  103. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
  104. package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
  105. package/dist/types/sdk/cli/session-cli.d.ts +1 -0
  106. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  107. package/dist/types/sdk/host/surface-policy.d.ts +3 -1
  108. package/dist/types/sdk/lifecycle/service.d.ts +44 -10
  109. package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
  110. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
  111. package/dist/types/sdk/prompt-failure.d.ts +47 -0
  112. package/dist/types/sdk/prompt-status.d.ts +19 -0
  113. package/dist/types/sdk/router/session-router.d.ts +3 -4
  114. package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
  115. package/dist/types/sdk/session-authority.d.ts +9 -0
  116. package/dist/types/sdk/session.d.ts +3 -1
  117. package/dist/types/session/agent-session.d.ts +1 -1
  118. package/dist/types/session/blob-store.d.ts +1 -1
  119. package/dist/types/session/fallback-chain-controller.d.ts +3 -6
  120. package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
  121. package/dist/types/session/primary-control-surface.d.ts +4 -0
  122. package/dist/types/session/session-manager.d.ts +6 -0
  123. package/dist/types/system-prompt.d.ts +6 -0
  124. package/dist/types/tools/ask.d.ts +25 -0
  125. package/dist/types/tools/image-gen.d.ts +0 -4
  126. package/dist/types/tools/skill-discovery.d.ts +7 -5
  127. package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
  128. package/dist/types/utils/changelog.d.ts +0 -9
  129. package/dist/types/utils/mupdf-wasm.d.ts +1 -0
  130. package/package.json +9 -8
  131. package/scripts/compile-args.ts +8 -4
  132. package/src/capability/fs.ts +395 -17
  133. package/src/capability/index.ts +273 -10
  134. package/src/capability/types.ts +9 -0
  135. package/src/cli/customize-doctor.ts +18 -12
  136. package/src/cli/doctor/args.ts +236 -0
  137. package/src/cli/doctor/checks.ts +431 -0
  138. package/src/cli/doctor/config-repairs.ts +517 -0
  139. package/src/cli/doctor/context.ts +149 -0
  140. package/src/cli/doctor/files.ts +149 -0
  141. package/src/cli/doctor/ids.ts +115 -0
  142. package/src/cli/doctor/install-repairs.ts +335 -0
  143. package/src/cli/doctor/journal.ts +423 -0
  144. package/src/cli/doctor/managed-link.ts +514 -0
  145. package/src/cli/doctor/native-probe.ts +13 -0
  146. package/src/cli/doctor/permission-repairs.ts +242 -0
  147. package/src/cli/doctor/plugin-quarantine.ts +377 -0
  148. package/src/cli/doctor/plugin-restore.ts +59 -0
  149. package/src/cli/doctor/plugin-targets.ts +373 -0
  150. package/src/cli/doctor/probe-client.ts +195 -0
  151. package/src/cli/doctor/probe-types.ts +21 -0
  152. package/src/cli/doctor/probe.ts +42 -0
  153. package/src/cli/doctor/projection-probe.ts +249 -0
  154. package/src/cli/doctor/repairs.ts +1157 -0
  155. package/src/cli/doctor/report.ts +176 -0
  156. package/src/cli/doctor/runner.ts +342 -0
  157. package/src/cli/doctor/safety.ts +40 -0
  158. package/src/cli/doctor/service-repairs.ts +452 -0
  159. package/src/cli/doctor/service-targets.ts +274 -0
  160. package/src/cli/doctor/types.ts +164 -0
  161. package/src/cli/doctor-cli.ts +62 -0
  162. package/src/cli/doctor-supervisor.ts +571 -0
  163. package/src/cli/doctor-worker.ts +181 -0
  164. package/src/cli/install-activation.ts +666 -0
  165. package/src/cli/macos-community-app.ts +1449 -0
  166. package/src/cli/mcp-cli.ts +11 -21
  167. package/src/cli/nofile-limit.ts +5 -1
  168. package/src/cli/plugin-cli.ts +6 -1
  169. package/src/cli/update-cli.ts +362 -264
  170. package/src/cli-main.ts +517 -0
  171. package/src/cli.ts +64 -433
  172. package/src/commands/autoresearch.ts +11 -5
  173. package/src/commands/completion.ts +1 -1
  174. package/src/commands/doctor.ts +13 -0
  175. package/src/commands/sdk.ts +58 -10
  176. package/src/commands/ultragoal.ts +11 -5
  177. package/src/config/autorouting-tier-map.ts +7 -0
  178. package/src/config/file-lock.ts +736 -71
  179. package/src/config/mcp-schema.json +3 -3
  180. package/src/config/model-preset-registry.ts +1 -0
  181. package/src/config/model-registry.ts +80 -27
  182. package/src/config/model-resolver.ts +33 -4
  183. package/src/config/models-config-schema.ts +2 -0
  184. package/src/config/settings-schema.ts +6 -2
  185. package/src/coordinator-mcp/question-gate-codec.ts +1 -0
  186. package/src/coordinator-mcp/server.ts +534 -51
  187. package/src/coordinator-mcp/session-reaper.ts +64 -8
  188. package/src/cursor.ts +223 -53
  189. package/src/discovery/agents-md.ts +74 -8
  190. package/src/discovery/agents.ts +32 -7
  191. package/src/discovery/builtin.ts +184 -75
  192. package/src/discovery/claude-plugins.ts +85 -17
  193. package/src/discovery/cline.ts +38 -10
  194. package/src/discovery/cursor.ts +19 -6
  195. package/src/discovery/gemini.ts +62 -22
  196. package/src/discovery/github.ts +13 -3
  197. package/src/discovery/helpers.ts +483 -89
  198. package/src/discovery/index.ts +1 -0
  199. package/src/discovery/mcp-json.ts +36 -10
  200. package/src/discovery/opencode.ts +36 -19
  201. package/src/discovery/ssh.ts +11 -8
  202. package/src/discovery/windsurf.ts +12 -3
  203. package/src/exec/bash-executor.ts +10 -3
  204. package/src/exec/isolated-shell.ts +15 -2
  205. package/src/extensibility/extensions/loader.ts +88 -26
  206. package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
  207. package/src/extensibility/gjc-plugins/paths.ts +4 -1
  208. package/src/extensibility/gjc-plugins/types.ts +27 -0
  209. package/src/extensibility/plugins/manager.ts +200 -22
  210. package/src/extensibility/plugins/marketplace/cache.ts +55 -2
  211. package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
  212. package/src/extensibility/plugins/marketplace/registry.ts +153 -0
  213. package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
  214. package/src/extensibility/runtime-skill-discovery.ts +61 -9
  215. package/src/extensibility/skill-management.ts +7 -1
  216. package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
  217. package/src/gjc-runtime/launch-worktree.test.ts +248 -0
  218. package/src/gjc-runtime/launch-worktree.ts +275 -38
  219. package/src/gjc-runtime/repository-binding.ts +26 -1
  220. package/src/gjc-runtime/session-layout.ts +10 -0
  221. package/src/gjc-runtime/session-state-lock.ts +330 -23
  222. package/src/gjc-runtime/session-state-sidecar.ts +328 -80
  223. package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
  224. package/src/harness-control-plane/session-lease.ts +2 -2
  225. package/src/internal-urls/docs-index.generated.ts +28 -26
  226. package/src/main.ts +150 -29
  227. package/src/memories/index.ts +24 -2
  228. package/src/modes/acp/acp-agent.ts +522 -68
  229. package/src/modes/components/agent-dashboard.ts +3 -1
  230. package/src/modes/components/model-selector.ts +28 -19
  231. package/src/modes/components/plugin-settings.ts +2 -1
  232. package/src/modes/controllers/selector-controller.ts +15 -9
  233. package/src/modes/interactive-mode.ts +21 -3
  234. package/src/prompts/system/system-prompt.md +5 -1
  235. package/src/prompts/tools/browser.md +1 -1
  236. package/src/prompts/tools/move-session.md +4 -0
  237. package/src/prompts/tools/read.md +2 -1
  238. package/src/prompts/tools/skill-discovery.md +6 -5
  239. package/src/prompts/tools/task.md +1 -0
  240. package/src/runtime-mcp/config-writer.ts +228 -0
  241. package/src/runtime-mcp/oauth-discovery.ts +114 -14
  242. package/src/runtime-mcp/policy-json-edit.ts +129 -0
  243. package/src/runtime-mcp/startup-policy.ts +14 -0
  244. package/src/runtime-mcp/types.ts +2 -2
  245. package/src/sdk/acp/adapter.ts +27 -12
  246. package/src/sdk/broker/broker.ts +273 -44
  247. package/src/sdk/broker/daemon-entry.ts +119 -0
  248. package/src/sdk/broker/discovery.ts +173 -20
  249. package/src/sdk/broker/doctor-restart.ts +206 -0
  250. package/src/sdk/broker/endpoint-authority.ts +36 -0
  251. package/src/sdk/broker/ensure.ts +49 -14
  252. package/src/sdk/broker/lifecycle.ts +174 -42
  253. package/src/sdk/broker/process-incarnation.ts +86 -2
  254. package/src/sdk/broker/runtime.ts +145 -21
  255. package/src/sdk/broker/session-index.ts +136 -22
  256. package/src/sdk/broker/spawn-substrate.ts +4 -1
  257. package/src/sdk/broker/transport.ts +5 -0
  258. package/src/sdk/broker/worktree-occupancy.ts +21 -1
  259. package/src/sdk/bus/chat-daemon-cli.ts +63 -0
  260. package/src/sdk/bus/chat-daemon-control.ts +260 -64
  261. package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
  262. package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
  263. package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
  264. package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
  265. package/src/sdk/bus/index.ts +252 -51
  266. package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
  267. package/src/sdk/bus/notification-service.ts +21 -3
  268. package/src/sdk/bus/reconciliation-store.ts +17 -11
  269. package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
  270. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  271. package/src/sdk/bus/telegram-daemon-control.ts +47 -0
  272. package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
  273. package/src/sdk/bus/telegram-daemon.ts +178 -13
  274. package/src/sdk/cli/session-cli.ts +100 -12
  275. package/src/sdk/host/host.ts +2 -7
  276. package/src/sdk/host/session-runtime.test.ts +329 -8
  277. package/src/sdk/host/session-runtime.ts +202 -50
  278. package/src/sdk/host/surface-policy.ts +8 -1
  279. package/src/sdk/lifecycle/service.ts +199 -22
  280. package/src/sdk/mcp/server.ts +1 -1
  281. package/src/sdk/prompt-deadline-lease.ts +12 -4
  282. package/src/sdk/prompt-deadline-manager.ts +5 -7
  283. package/src/sdk/prompt-failure.ts +148 -0
  284. package/src/sdk/prompt-status.ts +30 -1
  285. package/src/sdk/prompt-watchdog.ts +11 -3
  286. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  287. package/src/sdk/protocol/operation-registry.ts +20 -8
  288. package/src/sdk/router/session-router.ts +8 -3
  289. package/src/sdk/service-artifact-paths.ts +26 -0
  290. package/src/sdk/session-authority.ts +10 -0
  291. package/src/sdk/session.ts +361 -194
  292. package/src/session/agent-session.ts +398 -93
  293. package/src/session/blob-store.ts +9 -9
  294. package/src/session/fallback-chain-controller.ts +3 -7
  295. package/src/session/internal/managed-session-scope.ts +445 -29
  296. package/src/session/internal/managed-session-storage.ts +38 -6
  297. package/src/session/primary-control-surface.ts +12 -0
  298. package/src/session/session-manager.ts +11 -3
  299. package/src/session/session-storage.ts +1 -1
  300. package/src/session-import/codex.ts +6 -1
  301. package/src/session-import/redact.ts +7 -0
  302. package/src/skill-state/workflow-mutation-guard.ts +645 -39
  303. package/src/slash-commands/builtin-registry.ts +7 -3
  304. package/src/system-prompt.ts +8 -0
  305. package/src/task/executor.ts +36 -3
  306. package/src/task/types.ts +27 -5
  307. package/src/tools/ask.ts +114 -34
  308. package/src/tools/browser/actions.ts +2 -2
  309. package/src/tools/browser/tab-worker.ts +11 -8
  310. package/src/tools/descriptors.ts +6 -1
  311. package/src/tools/fetch.ts +7 -2
  312. package/src/tools/image-gen.ts +33 -34
  313. package/src/tools/read.ts +78 -32
  314. package/src/tools/skill-discovery.ts +17 -7
  315. package/src/tools/tool-catalog.generated.ts +8 -8
  316. package/src/tools/ultragoal-ask-guard.ts +16 -26
  317. package/src/utils/changelog.ts +7 -1
  318. package/src/utils/git.ts +3 -1
  319. package/src/utils/markit.ts +24 -2
  320. package/src/utils/mupdf-wasm.ts +26 -0
  321. package/src/web/search/providers/openai-compatible.ts +191 -14
@@ -3,12 +3,13 @@ import { logger } from "@gajae-code/utils";
3
3
  /**
4
4
  * Idle reaper for coordinator-managed GJC worker sessions.
5
5
  *
6
- * Every `gjc_delegate_*` call that omits `session_id` starts a fresh tmux worker
7
- * session. Nothing in the coordinator ever tore those down, so completed/crashed
8
- * sessions accumulated (RAM + worktrees) until something killed them by hand.
6
+ * New coordinator-owned sessions created by `gjc_coordinator_start_session` or by a
7
+ * `gjc_delegate_*` call that omits `session_id` are ephemeral. Nothing in the
8
+ * coordinator ever tore those down, so completed/crashed sessions accumulated
9
+ * (RAM + worktrees) until something killed them by hand.
9
10
  *
10
11
  * This is the automatic backstop (defense-in-depth): a periodic sweep force-closes
11
- * sessions that are (a) ephemeral — coordinator delegate-created, never a user's
12
+ * sessions that are (a) ephemeral — coordinator-created, never a user's
12
13
  * registered resident session, (b) not mid-turn, and (c) idle past a TTL.
13
14
  *
14
15
  * The controller is pure + fully injectable (clock / list / reap side-effects) so
@@ -21,11 +22,13 @@ export const DEFAULT_SESSION_IDLE_TTL_MS = 30 * 60_000; // 30 min idle → reap
21
22
  export const DEFAULT_SESSION_SWEEP_INTERVAL_MS = 5 * 60_000; // sweep every 5 min
22
23
  export const MIN_SESSION_IDLE_TTL_MS = 60_000; // never reap a <1min-idle session
23
24
  export const MIN_SESSION_SWEEP_INTERVAL_MS = 30_000;
25
+ /** After this many consecutive reap failures the session is force-evicted from the index. */
26
+ export const MAX_REAP_FAILURES = 3;
24
27
 
25
28
  /** Minimal projection of a coordinator session the reaper needs to decide. */
26
29
  export interface ReapableSession {
27
30
  sessionId: string;
28
- /** True only for coordinator delegate-created sessions. User-registered resident sessions are false and never reaped. */
31
+ /** True for newly coordinator-created sessions. User-registered resident sessions are false and never reaped. */
29
32
  ephemeral: boolean;
30
33
  /** Epoch ms of last observed activity (turn update / session-state write / session creation). */
31
34
  lastActivityMs: number;
@@ -51,10 +54,29 @@ export function selectReapableSessions(
51
54
  return sessions.filter(s => s.ephemeral && !s.hasActiveTurn && now - s.lastActivityMs >= ttl);
52
55
  }
53
56
 
57
+ /**
58
+ * Only a stale/absent endpoint makes a session permanently unreapable, so it is
59
+ * the sole failure that advances the force-eviction counter. Every other reap
60
+ * failure (close_failed, broker unavailability, filesystem errors) is transient
61
+ * and must keep retrying without ever escalating to force eviction. The server
62
+ * wires `reapSession` to throw `new Error(reason)`, so the reason code is the
63
+ * error message here.
64
+ */
65
+ function isEndpointStaleReapError(err: unknown): boolean {
66
+ return (err instanceof Error ? err.message : String(err)) === "endpoint_stale";
67
+ }
68
+
54
69
  /** Injectable side-effects so the controller runs in tests without tmux/fs/real time. */
55
70
  export interface SessionReaperDeps {
56
71
  listSessions: () => Promise<ReapableSession[]>;
57
72
  reapSession: (sessionId: string) => Promise<void>;
73
+ /**
74
+ * Force-evict a session from the coordinator index when it has exceeded the
75
+ * consecutive-failure limit and cannot be reaped normally (e.g. endpoint_stale).
76
+ * Called exactly once per session-id at the eviction boundary; the session must
77
+ * not appear in future listSessions() results after this resolves.
78
+ */
79
+ markSessionDead: (sessionId: string) => Promise<void>;
58
80
  now: () => number;
59
81
  }
60
82
 
@@ -72,6 +94,8 @@ export function createSessionReaper(deps: SessionReaperDeps, policy: SessionReap
72
94
  let timer: ReturnType<typeof setTimeout> | null = null;
73
95
  let generation = 0;
74
96
  let inProgress = false;
97
+ /** Consecutive reap-failure count per session-id. Cleared on success or eviction. */
98
+ const failureCounts = new Map<string, number>();
75
99
 
76
100
  async function sweepOnce(): Promise<number> {
77
101
  if (inProgress) return 0; // never overlap sweeps
@@ -83,12 +107,44 @@ export function createSessionReaper(deps: SessionReaperDeps, policy: SessionReap
83
107
  for (const session of targets) {
84
108
  try {
85
109
  await deps.reapSession(session.sessionId);
110
+ // Success — clear any accumulated failure count.
111
+ failureCounts.delete(session.sessionId);
86
112
  reaped += 1;
87
113
  } catch (err) {
88
114
  // One wedged session must not abort the rest of the sweep.
89
- logger.warn(
90
- `session-reaper: failed to reap ${session.sessionId}: ${err instanceof Error ? err.message : String(err)}`,
91
- );
115
+ const msg = err instanceof Error ? err.message : String(err);
116
+ if (!isEndpointStaleReapError(err)) {
117
+ // Transient failure (close_failed, broker/filesystem error): a non-stale
118
+ // outcome breaks the consecutive-stale streak, so clear the counter and
119
+ // retry on the next sweep. Only genuinely consecutive endpoint_stale
120
+ // failures may accumulate toward force eviction.
121
+ logger.warn(`session-reaper: failed to reap ${session.sessionId}: ${msg}`);
122
+ failureCounts.delete(session.sessionId);
123
+ continue;
124
+ }
125
+ const prev = failureCounts.get(session.sessionId) ?? 0;
126
+ const count = prev + 1;
127
+ if (count < MAX_REAP_FAILURES) {
128
+ // First failure(s): log at warn and keep retrying next sweep.
129
+ logger.warn(`session-reaper: failed to reap ${session.sessionId}: ${msg}`);
130
+ failureCounts.set(session.sessionId, count);
131
+ } else {
132
+ // Hit the limit — evict unconditionally and silence future attempts.
133
+ logger.warn(
134
+ `session-reaper: session ${session.sessionId} evicted after ${count} consecutive failures (${msg})`,
135
+ );
136
+ failureCounts.delete(session.sessionId);
137
+ try {
138
+ await deps.markSessionDead(session.sessionId);
139
+ } catch (evictErr) {
140
+ // Eviction failure is non-fatal; the session will be retried next
141
+ // sweep and the counter has been cleared, so it will get MAX_REAP_FAILURES
142
+ // fresh chances before the next eviction attempt.
143
+ logger.warn(
144
+ `session-reaper: markSessionDead failed for ${session.sessionId}: ${evictErr instanceof Error ? evictErr.message : String(evictErr)}`,
145
+ );
146
+ }
147
+ }
92
148
  }
93
149
  }
94
150
  return reaped;
package/src/cursor.ts CHANGED
@@ -71,19 +71,30 @@ function buildToolErrorResult(message: string): AgentToolResult<unknown> {
71
71
  };
72
72
  }
73
73
 
74
+ /** Admission markers are control flow, not user-facing tool failures. */
75
+ function isCursorExecAdmissionClosedError(error: unknown): boolean {
76
+ return (
77
+ typeof error === "object" &&
78
+ error !== null &&
79
+ (error as { name?: unknown }).name === "CursorExecAdmissionClosedError"
80
+ );
81
+ }
82
+
74
83
  async function executeTool(
75
84
  options: CursorExecBridgeOptions,
76
85
  toolName: string,
77
86
  toolCallId: string,
78
87
  args: Record<string, unknown>,
79
88
  overrideTool?: AgentTool,
89
+ execSignal?: AbortSignal,
90
+ markNonAbortable?: () => void,
80
91
  ): Promise<ToolResultMessage> {
92
+ if (execSignal?.aborted) throw cursorAbortError(execSignal);
81
93
  const tool = overrideTool ?? options.tools.get(toolName);
82
94
  if (!tool) {
83
95
  const result = buildToolErrorResult(`Tool "${toolName}" not available`);
84
96
  return createToolResultMessage(toolCallId, toolName, result, true);
85
97
  }
86
-
87
98
  // `tool` is the object this call will run; pass it so the start event carries proven
88
99
  // provenance instead of a name a consumer would have to re-resolve later.
89
100
  options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args }, tool);
@@ -108,14 +119,23 @@ async function executeTool(
108
119
  : undefined;
109
120
 
110
121
  try {
122
+ if (execSignal?.aborted) throw cursorAbortError(execSignal);
123
+ const toolContext = options.getToolContext?.();
124
+ if (execSignal?.aborted) throw cursorAbortError(execSignal);
125
+ if (tool.nonAbortable) markNonAbortable?.();
111
126
  result = await tool.execute(
112
127
  toolCallId,
113
128
  args as Record<string, unknown>,
114
- undefined,
129
+ // Non-abortable tools receive NO signal, matching agent-loop.ts: their
130
+ // returned promise must represent actual mutation settlement, so an
131
+ // untilAborted(signal, ...) rejection cannot make the settlement fence
132
+ // publish the terminal while the mutation is still running.
133
+ tool.nonAbortable ? undefined : (execSignal ?? undefined),
115
134
  onUpdate,
116
- options.getToolContext?.(),
135
+ toolContext,
117
136
  );
118
137
  } catch (error) {
138
+ if (isCursorExecAdmissionClosedError(error)) throw error;
119
139
  const message = error instanceof Error ? error.message : String(error);
120
140
  result = buildToolErrorResult(message);
121
141
  isError = true;
@@ -131,9 +151,23 @@ async function executeTool(
131
151
  return createToolResultMessage(toolCallId, toolName, result, isError);
132
152
  }
133
153
 
134
- async function executeDelete(options: CursorExecBridgeOptions, pathArg: string, toolCallId: string) {
154
+ async function executeDelete(
155
+ options: CursorExecBridgeOptions,
156
+ pathArg: string,
157
+ toolCallId: string,
158
+ execSignal?: AbortSignal,
159
+ markNonAbortable?: () => void,
160
+ ) {
135
161
  const toolName = "delete";
162
+ if (execSignal?.aborted) throw cursorAbortError(execSignal);
163
+ // Register the non-abortable mutation before notifying observers: a synchronous
164
+ // tool_execution_start subscriber may abort the run.
165
+ markNonAbortable?.();
136
166
  options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args: { path: pathArg } });
167
+ if (execSignal?.aborted) throw cursorAbortError(execSignal);
168
+ // Delete is a filesystem mutation with no cooperative cancellation point:
169
+ // mark it non-abortable so the settlement fence publishes the exec terminal
170
+ // only once the unlink is final, matching write/edit dispatch.
137
171
 
138
172
  const absolutePath = resolveToCwd(pathArg, options.cwd);
139
173
  let isError = false;
@@ -201,6 +235,11 @@ function shellTimeoutSeconds(timeout: number | undefined): number | undefined {
201
235
  return Math.max(1, Math.ceil(timeout / 1000));
202
236
  }
203
237
 
238
+ function cursorAbortError(signal: AbortSignal): Error {
239
+ const reason = signal.reason;
240
+ return reason instanceof Error ? reason : new Error("Request was aborted");
241
+ }
242
+
204
243
  export class CursorExecHandlers implements ICursorExecHandlers {
205
244
  constructor(private options: CursorExecBridgeOptions) {
206
245
  // Bind every native handler so methods stay instance-safe when invoked
@@ -242,20 +281,44 @@ export class CursorExecHandlers implements ICursorExecHandlers {
242
281
  };
243
282
  }
244
283
 
245
- async read(args: Parameters<NonNullable<ICursorExecHandlers["read"]>>[0]) {
284
+ async read(
285
+ args: Parameters<NonNullable<ICursorExecHandlers["read"]>>[0],
286
+ signal?: AbortSignal,
287
+ markNonAbortable?: () => void,
288
+ ) {
246
289
  const toolCallId = decodeToolCallId(args.toolCallId);
247
- const toolResultMessage = await executeTool(this.#optionsForCall(), "read", toolCallId, { path: args.path });
290
+ const toolResultMessage = await executeTool(
291
+ this.#optionsForCall(),
292
+ "read",
293
+ toolCallId,
294
+ { path: args.path },
295
+ undefined,
296
+ signal,
297
+ markNonAbortable,
298
+ );
248
299
  return toolResultMessage;
249
300
  }
250
301
 
251
- async ls(args: Parameters<NonNullable<ICursorExecHandlers["ls"]>>[0]) {
302
+ async ls(
303
+ args: Parameters<NonNullable<ICursorExecHandlers["ls"]>>[0],
304
+ signal?: AbortSignal,
305
+ markNonAbortable?: () => void,
306
+ ) {
252
307
  const toolCallId = decodeToolCallId(args.toolCallId);
253
308
  // Redirect ls to read tool, which handles directories
254
- const toolResultMessage = await executeTool(this.#optionsForCall(), "read", toolCallId, { path: args.path });
309
+ const toolResultMessage = await executeTool(
310
+ this.#optionsForCall(),
311
+ "read",
312
+ toolCallId,
313
+ { path: args.path },
314
+ undefined,
315
+ signal,
316
+ markNonAbortable,
317
+ );
255
318
  return toolResultMessage;
256
319
  }
257
320
 
258
- async grep(args: Parameters<NonNullable<ICursorExecHandlers["grep"]>>[0]) {
321
+ async grep(args: Parameters<NonNullable<ICursorExecHandlers["grep"]>>[0], signal?: AbortSignal) {
259
322
  const toolCallId = decodeToolCallId(args.toolCallId);
260
323
  // Cursor's native Glob tool arrives as a grep exec with a glob but no content
261
324
  // pattern. The search tool requires a non-empty pattern, so an empty pattern
@@ -265,7 +328,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
265
328
  if (pattern.trim().length === 0) {
266
329
  if (args.glob) {
267
330
  const globPath = `${args.path || "."}/${args.glob}`;
268
- return executeTool(this.#optionsForCall(), "find", toolCallId, { paths: [globPath] });
331
+ return executeTool(this.#optionsForCall(), "find", toolCallId, { paths: [globPath] }, undefined, signal);
269
332
  }
270
333
  const result = buildToolErrorResult(
271
334
  "Cursor grep request rejected: pattern must not be empty. Provide a non-empty search pattern.",
@@ -273,45 +336,90 @@ export class CursorExecHandlers implements ICursorExecHandlers {
273
336
  return createToolResultMessage(toolCallId, "search", result, true);
274
337
  }
275
338
  const searchPath = args.glob ? `${args.path || "."}/${args.glob}` : args.path || ".";
276
- const toolResultMessage = await executeTool(this.#optionsForCall(), "search", toolCallId, {
277
- pattern,
278
- paths: [searchPath],
279
- i: args.caseInsensitive || undefined,
280
- });
339
+ const toolResultMessage = await executeTool(
340
+ this.#optionsForCall(),
341
+ "search",
342
+ toolCallId,
343
+ {
344
+ pattern,
345
+ paths: [searchPath],
346
+ i: args.caseInsensitive || undefined,
347
+ },
348
+ undefined,
349
+ signal,
350
+ );
281
351
  return toolResultMessage;
282
352
  }
283
353
 
284
- async write(args: Parameters<NonNullable<ICursorExecHandlers["write"]>>[0]) {
354
+ async write(
355
+ args: Parameters<NonNullable<ICursorExecHandlers["write"]>>[0],
356
+ signal?: AbortSignal,
357
+ markNonAbortable?: () => void,
358
+ ) {
285
359
  const toolCallId = decodeToolCallId(args.toolCallId);
286
360
  const content = args.fileText ?? new TextDecoder().decode(args.fileBytes ?? new Uint8Array());
287
- const toolResultMessage = await executeTool(this.#optionsForCall(), "write", toolCallId, {
288
- path: args.path,
289
- content,
290
- });
361
+ const toolResultMessage = await executeTool(
362
+ this.#optionsForCall(),
363
+ "write",
364
+ toolCallId,
365
+ {
366
+ path: args.path,
367
+ content,
368
+ },
369
+ undefined,
370
+ signal,
371
+ markNonAbortable,
372
+ );
291
373
  return toolResultMessage;
292
374
  }
293
375
 
294
- async delete(args: Parameters<NonNullable<ICursorExecHandlers["delete"]>>[0]) {
376
+ async delete(
377
+ args: Parameters<NonNullable<ICursorExecHandlers["delete"]>>[0],
378
+ signal?: AbortSignal,
379
+ markNonAbortable?: () => void,
380
+ ) {
381
+ if (signal?.aborted) throw cursorAbortError(signal);
295
382
  const toolCallId = decodeToolCallId(args.toolCallId);
296
- const toolResultMessage = await executeDelete(this.#optionsForCall(), args.path, toolCallId);
383
+ const toolResultMessage = await executeDelete(
384
+ this.#optionsForCall(),
385
+ args.path,
386
+ toolCallId,
387
+ signal,
388
+ markNonAbortable,
389
+ );
297
390
  return toolResultMessage;
298
391
  }
299
392
 
300
- async shell(args: Parameters<NonNullable<ICursorExecHandlers["shell"]>>[0]) {
393
+ async shell(
394
+ args: Parameters<NonNullable<ICursorExecHandlers["shell"]>>[0],
395
+ signal?: AbortSignal,
396
+ markNonAbortable?: () => void,
397
+ ) {
301
398
  const toolCallId = decodeToolCallId(args.toolCallId);
302
399
  const timeoutSeconds = shellTimeoutSeconds(args.timeout);
303
- const toolResultMessage = await executeTool(this.#optionsForCall(), "bash", toolCallId, {
304
- command: args.command,
305
- cwd: args.workingDirectory || undefined,
306
- timeout: timeoutSeconds,
307
- });
400
+ const toolResultMessage = await executeTool(
401
+ this.#optionsForCall(),
402
+ "bash",
403
+ toolCallId,
404
+ {
405
+ command: args.command,
406
+ cwd: args.workingDirectory || undefined,
407
+ timeout: timeoutSeconds,
408
+ },
409
+ undefined,
410
+ signal,
411
+ markNonAbortable,
412
+ );
308
413
  return toolResultMessage;
309
414
  }
310
415
 
311
416
  async shellStream(
312
417
  args: Parameters<NonNullable<ICursorExecHandlers["shellStream"]>>[0],
313
418
  callbacks: CursorShellStreamCallbacks,
419
+ signal?: AbortSignal,
420
+ markNonAbortable?: () => void,
314
421
  ) {
422
+ if (signal?.aborted) throw cursorAbortError(signal);
315
423
  const options = this.#optionsForCall();
316
424
  const toolCallId = decodeToolCallId(args.toolCallId);
317
425
  const toolName = "bash";
@@ -329,6 +437,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
329
437
  };
330
438
 
331
439
  options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args: toolArgs }, tool);
440
+ if (signal?.aborted) throw cursorAbortError(signal);
332
441
 
333
442
  let result: AgentToolResult<unknown>;
334
443
  let isError = false;
@@ -373,8 +482,13 @@ export class CursorExecHandlers implements ICursorExecHandlers {
373
482
  };
374
483
 
375
484
  try {
376
- result = await tool.execute(toolCallId, toolArgs, undefined, onUpdate, options.getToolContext?.());
485
+ const nonAbortable = tool.nonAbortable === true;
486
+ const toolContext = options.getToolContext?.();
487
+ if (signal?.aborted) throw cursorAbortError(signal);
488
+ if (nonAbortable) markNonAbortable?.();
489
+ result = await tool.execute(toolCallId, toolArgs, nonAbortable ? undefined : signal, onUpdate, toolContext);
377
490
  } catch (error) {
491
+ if (isCursorExecAdmissionClosedError(error)) throw error;
378
492
  const message = error instanceof Error ? error.message : String(error);
379
493
  result = buildToolErrorResult(message);
380
494
  isError = true;
@@ -414,14 +528,30 @@ export class CursorExecHandlers implements ICursorExecHandlers {
414
528
  if (composed === null) {
415
529
  return createToolResultMessage(call.toolCallId, "read", { content: [{ type: "text", text: "" }] }, false);
416
530
  }
417
- return executeTool(this.#optionsForCall(), "read", call.toolCallId, { path: composed });
531
+ return executeTool(
532
+ this.#optionsForCall(),
533
+ "read",
534
+ call.toolCallId,
535
+ { path: composed },
536
+ undefined,
537
+ call.signal,
538
+ call.markNonAbortable,
539
+ );
418
540
  }
419
541
 
420
542
  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
- });
543
+ return executeTool(
544
+ this.#optionsForCall(),
545
+ "bash",
546
+ call.toolCallId,
547
+ {
548
+ command: call.args.command,
549
+ timeout: piTimeout(call.args.timeout),
550
+ },
551
+ undefined,
552
+ call.signal,
553
+ call.markNonAbortable,
554
+ );
425
555
  }
426
556
 
427
557
  async piEdit(call: Parameters<NonNullable<ICursorExecHandlers["piEdit"]>>[0]) {
@@ -435,14 +565,24 @@ export class CursorExecHandlers implements ICursorExecHandlers {
435
565
  call.toolCallId,
436
566
  { path: call.args.path, edits },
437
567
  this.options.getEditReplaceTool?.(),
568
+ call.signal,
569
+ call.markNonAbortable,
438
570
  );
439
571
  }
440
572
 
441
573
  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
- });
574
+ return executeTool(
575
+ this.#optionsForCall(),
576
+ "write",
577
+ call.toolCallId,
578
+ {
579
+ path: call.args.path,
580
+ content: call.args.content,
581
+ },
582
+ undefined,
583
+ call.signal,
584
+ call.markNonAbortable,
585
+ );
446
586
  }
447
587
 
448
588
  async piGrep(call: Parameters<NonNullable<ICursorExecHandlers["piGrep"]>>[0]) {
@@ -456,33 +596,55 @@ export class CursorExecHandlers implements ICursorExecHandlers {
456
596
  paths: [glob ? piJoinPath(path, glob) : path || "."],
457
597
  };
458
598
  if (ignoreCase === true) args.i = true;
459
- return executeTool(this.#optionsForCall(), "search", call.toolCallId, args, perCallSearch);
599
+ return executeTool(this.#optionsForCall(), "search", call.toolCallId, args, perCallSearch, call.signal);
460
600
  }
461
601
 
462
602
  async piFind(call: Parameters<NonNullable<ICursorExecHandlers["piFind"]>>[0]) {
463
603
  const { pattern, path, limit } = call.args;
464
- return executeTool(this.#optionsForCall(), "find", call.toolCallId, {
465
- paths: [piJoinPath(path, pattern)],
466
- limit: piLimit(limit),
467
- });
604
+ return executeTool(
605
+ this.#optionsForCall(),
606
+ "find",
607
+ call.toolCallId,
608
+ {
609
+ paths: [piJoinPath(path, pattern)],
610
+ limit: piLimit(limit),
611
+ },
612
+ undefined,
613
+ call.signal,
614
+ );
468
615
  }
469
616
 
470
617
  async piLs(call: Parameters<NonNullable<ICursorExecHandlers["piLs"]>>[0]) {
471
- return executeTool(this.#optionsForCall(), "read", call.toolCallId, {
472
- path: piLsPath(call.args.path),
473
- });
618
+ return executeTool(
619
+ this.#optionsForCall(),
620
+ "read",
621
+ call.toolCallId,
622
+ {
623
+ path: piLsPath(call.args.path),
624
+ },
625
+ undefined,
626
+ call.signal,
627
+ call.markNonAbortable,
628
+ );
474
629
  }
475
630
 
476
- async diagnostics(args: Parameters<NonNullable<ICursorExecHandlers["diagnostics"]>>[0]) {
631
+ async diagnostics(args: Parameters<NonNullable<ICursorExecHandlers["diagnostics"]>>[0], signal?: AbortSignal) {
477
632
  const toolCallId = decodeToolCallId(args.toolCallId);
478
- const toolResultMessage = await executeTool(this.#optionsForCall(), "lsp", toolCallId, {
479
- action: "diagnostics",
480
- file: args.path,
481
- });
633
+ const toolResultMessage = await executeTool(
634
+ this.#optionsForCall(),
635
+ "lsp",
636
+ toolCallId,
637
+ {
638
+ action: "diagnostics",
639
+ file: args.path,
640
+ },
641
+ undefined,
642
+ signal,
643
+ );
482
644
  return toolResultMessage;
483
645
  }
484
646
 
485
- async mcp(call: CursorMcpCall) {
647
+ async mcp(call: CursorMcpCall, signal?: AbortSignal, markNonAbortable?: () => void) {
486
648
  const options = this.#optionsForCall();
487
649
  const toolName = call.toolName || call.name;
488
650
  const toolCallId = decodeToolCallId(call.toolCallId);
@@ -495,7 +657,15 @@ export class CursorExecHandlers implements ICursorExecHandlers {
495
657
  }
496
658
 
497
659
  const args = Object.keys(call.args ?? {}).length > 0 ? call.args : decodeMcpArgs(call.rawArgs ?? {});
498
- const toolResultMessage = await executeTool(options, toolName, toolCallId, args);
660
+ const toolResultMessage = await executeTool(
661
+ options,
662
+ toolName,
663
+ toolCallId,
664
+ args,
665
+ undefined,
666
+ signal,
667
+ markNonAbortable,
668
+ );
499
669
  return toolResultMessage;
500
670
  }
501
671
  }