@aitne/daemon 0.1.9 → 0.1.11

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 (333) hide show
  1. package/dist/adapters/adapter-watchdog.d.ts +70 -0
  2. package/dist/adapters/adapter-watchdog.js +115 -0
  3. package/dist/adapters/discord.d.ts +17 -1
  4. package/dist/adapters/discord.js +33 -0
  5. package/dist/adapters/notification-manager.d.ts +27 -1
  6. package/dist/adapters/notification-manager.js +54 -39
  7. package/dist/adapters/slack-adapter.d.ts +26 -1
  8. package/dist/adapters/slack-adapter.js +41 -0
  9. package/dist/adapters/telegram-adapter.d.ts +18 -1
  10. package/dist/adapters/telegram-adapter.js +41 -2
  11. package/dist/adapters/types.d.ts +20 -0
  12. package/dist/adapters/whatsapp-adapter.d.ts +26 -7
  13. package/dist/adapters/whatsapp-adapter.js +74 -21
  14. package/dist/api/env-writer.d.ts +1 -0
  15. package/dist/api/env-writer.js +17 -7
  16. package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
  17. package/dist/api/helpers/agent-errors-registry.js +5 -5
  18. package/dist/api/routes/agent-schedule.js +5 -1
  19. package/dist/api/routes/agent.js +33 -12
  20. package/dist/api/routes/agents/index.js +75 -16
  21. package/dist/api/routes/agents/views.d.ts +37 -2
  22. package/dist/api/routes/agents/views.js +64 -2
  23. package/dist/api/routes/apple-calendar.js +4 -1
  24. package/dist/api/routes/background-task.d.ts +22 -0
  25. package/dist/api/routes/background-task.js +338 -0
  26. package/dist/api/routes/browser-history.js +9 -1
  27. package/dist/api/routes/calendar.js +12 -2
  28. package/dist/api/routes/context/path-resolve.js +6 -1
  29. package/dist/api/routes/context/permissions.js +12 -2
  30. package/dist/api/routes/context/snapshots.js +0 -3
  31. package/dist/api/routes/context/write.js +3 -17
  32. package/dist/api/routes/dashboard/config.js +58 -12
  33. package/dist/api/routes/dashboard/cost-approvals.js +66 -0
  34. package/dist/api/routes/dashboard/notifications.js +9 -9
  35. package/dist/api/routes/dashboard/oauth-google.js +5 -3
  36. package/dist/api/routes/feedback.d.ts +3 -0
  37. package/dist/api/routes/feedback.js +349 -0
  38. package/dist/api/routes/git.js +10 -3
  39. package/dist/api/routes/github.js +5 -1
  40. package/dist/api/routes/integrations/crud-patch.js +5 -1
  41. package/dist/api/routes/integrations-reconcile.js +2 -2
  42. package/dist/api/routes/mcp.js +65 -13
  43. package/dist/api/routes/notion.d.ts +1 -1
  44. package/dist/api/routes/observations.js +7 -7
  45. package/dist/api/routes/obsidian.d.ts +1 -1
  46. package/dist/api/routes/receipts.js +5 -1
  47. package/dist/api/routes/setup-migrate.js +1 -1
  48. package/dist/api/routes/setup.js +1 -1
  49. package/dist/api/routes/task-flows.d.ts +1 -1
  50. package/dist/api/routes/task-flows.js +1 -1
  51. package/dist/api/routes/tuning.d.ts +29 -0
  52. package/dist/api/routes/tuning.js +304 -0
  53. package/dist/api/server.d.ts +44 -16
  54. package/dist/api/server.js +12 -0
  55. package/dist/bootstrap/adapters.d.ts +19 -0
  56. package/dist/bootstrap/adapters.js +61 -0
  57. package/dist/bootstrap/api.d.ts +5 -3
  58. package/dist/bootstrap/api.js +45 -13
  59. package/dist/bootstrap/catchup.d.ts +1 -1
  60. package/dist/bootstrap/catchup.js +11 -11
  61. package/dist/bootstrap/event-pipeline.d.ts +11 -0
  62. package/dist/bootstrap/event-pipeline.js +246 -8
  63. package/dist/bootstrap/observers.js +9 -6
  64. package/dist/bootstrap/schedule-helpers.d.ts +104 -6
  65. package/dist/bootstrap/schedule-helpers.js +172 -19
  66. package/dist/config.js +32 -12
  67. package/dist/core/agent-core.d.ts +33 -1
  68. package/dist/core/agent-core.js +36 -1
  69. package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
  70. package/dist/core/agents/activity-scan-cadence.js +127 -0
  71. package/dist/core/agents/agent-route-override.d.ts +53 -0
  72. package/dist/core/agents/agent-route-override.js +69 -0
  73. package/dist/core/agents/builtin-registry.d.ts +51 -14
  74. package/dist/core/agents/builtin-registry.js +92 -15
  75. package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
  76. package/dist/core/agents/config-gate-reconcile.js +51 -0
  77. package/dist/core/agents/cron-substitute.d.ts +1 -1
  78. package/dist/core/agents/cron-substitute.js +1 -1
  79. package/dist/core/agents/custom-routine-migration.d.ts +60 -0
  80. package/dist/core/agents/custom-routine-migration.js +149 -0
  81. package/dist/core/agents/firing-blocked.d.ts +1 -1
  82. package/dist/core/agents/hourly-cadence.d.ts +102 -0
  83. package/dist/core/agents/hourly-cadence.js +126 -0
  84. package/dist/core/agents/loader-boot.js +23 -0
  85. package/dist/core/agents/loader.d.ts +19 -0
  86. package/dist/core/agents/loader.js +34 -2
  87. package/dist/core/agents/override-merge.d.ts +1 -1
  88. package/dist/core/agents/override-merge.js +9 -1
  89. package/dist/core/agents/recurrence-convert.d.ts +1 -1
  90. package/dist/core/agents/recurrence-convert.js +1 -1
  91. package/dist/core/agents/recurring-schedule-adapter.js +8 -0
  92. package/dist/core/alerts.js +6 -6
  93. package/dist/core/backends/auth-health-monitor.d.ts +2 -2
  94. package/dist/core/backends/auth-health-monitor.js +1 -1
  95. package/dist/core/backends/backend-router.d.ts +27 -1
  96. package/dist/core/backends/backend-router.js +165 -1
  97. package/dist/core/backends/claude-code-core.d.ts +71 -31
  98. package/dist/core/backends/claude-code-core.js +282 -54
  99. package/dist/core/backends/cli-quota-guards.d.ts +29 -1
  100. package/dist/core/backends/cli-quota-guards.js +40 -5
  101. package/dist/core/backends/codex-core.d.ts +6 -0
  102. package/dist/core/backends/codex-core.js +22 -6
  103. package/dist/core/backends/failure-spend.d.ts +58 -0
  104. package/dist/core/backends/failure-spend.js +137 -0
  105. package/dist/core/backends/gemini-cli-core.d.ts +6 -0
  106. package/dist/core/backends/gemini-cli-core.js +38 -6
  107. package/dist/core/backends/model-registry.d.ts +1 -1
  108. package/dist/core/backends/model-registry.js +4 -4
  109. package/dist/core/backends/opencode-core.d.ts +1 -1
  110. package/dist/core/backends/opencode-core.js +5 -5
  111. package/dist/core/backends/plan-presets.js +47 -18
  112. package/dist/core/bang-commands/commands-cost.js +3 -1
  113. package/dist/core/bang-commands/commands-report.js +4 -3
  114. package/dist/core/bang-commands/commands-research.js +4 -1
  115. package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
  116. package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
  117. package/dist/core/bang-commands/commands-stop-start.js +3 -3
  118. package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
  119. package/dist/core/bang-commands/commands-task-control.js +147 -0
  120. package/dist/core/bang-commands/commands-wiki.js +5 -5
  121. package/dist/core/bang-commands/index.d.ts +2 -0
  122. package/dist/core/bang-commands/index.js +12 -0
  123. package/dist/core/bang-commands/registry.d.ts +12 -0
  124. package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
  125. package/dist/core/browser-history/research-cluster-fanout.js +39 -16
  126. package/dist/core/channel-timeline.d.ts +5 -1
  127. package/dist/core/channel-timeline.js +13 -0
  128. package/dist/core/context/index-reconciler.js +5 -2
  129. package/dist/core/context/policy-index-reconciler.d.ts +6 -4
  130. package/dist/core/context/policy-index-runner.js +25 -6
  131. package/dist/core/context-builder-calendar.js +10 -2
  132. package/dist/core/context-builder-conversation.d.ts +8 -1
  133. package/dist/core/context-builder-conversation.js +41 -7
  134. package/dist/core/context-builder-yesterday.js +4 -3
  135. package/dist/core/context-builder.d.ts +7 -2
  136. package/dist/core/context-builder.js +193 -5
  137. package/dist/core/context-file-serializer.d.ts +1 -1
  138. package/dist/core/context-file-serializer.js +1 -1
  139. package/dist/core/context-health.js +2 -2
  140. package/dist/core/context-paths.d.ts +11 -1
  141. package/dist/core/context-paths.js +17 -1
  142. package/dist/core/context-validation/prepare-write.js +1 -1
  143. package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
  144. package/dist/core/context-vault-aliases.d.ts +0 -13
  145. package/dist/core/context-vault-aliases.js +37 -0
  146. package/dist/core/custom-routines.d.ts +99 -0
  147. package/dist/core/custom-routines.js +187 -0
  148. package/dist/core/daemon-api-cli.js +50 -1
  149. package/dist/core/day-boundary.d.ts +46 -0
  150. package/dist/core/day-boundary.js +40 -0
  151. package/dist/core/dispatcher-activity-scan.d.ts +221 -0
  152. package/dist/core/dispatcher-activity-scan.js +775 -0
  153. package/dist/core/dispatcher-error-handling.d.ts +6 -11
  154. package/dist/core/dispatcher-error-handling.js +38 -62
  155. package/dist/core/dispatcher-hourly-check.js +6 -1
  156. package/dist/core/dispatcher-message-handler.d.ts +10 -0
  157. package/dist/core/dispatcher-message-handler.js +24 -0
  158. package/dist/core/dispatcher-morning-routine.d.ts +6 -6
  159. package/dist/core/dispatcher-morning-routine.js +13 -13
  160. package/dist/core/dispatcher-result-processor.d.ts +33 -0
  161. package/dist/core/dispatcher-result-processor.js +167 -11
  162. package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
  163. package/dist/core/dispatcher-scheduled-background-task.js +89 -0
  164. package/dist/core/dispatcher-scheduled-tasks.d.ts +104 -1
  165. package/dist/core/dispatcher-scheduled-tasks.js +480 -8
  166. package/dist/core/dispatcher-task-delivery.d.ts +105 -0
  167. package/dist/core/dispatcher-task-delivery.js +555 -0
  168. package/dist/core/dispatcher-types.d.ts +48 -9
  169. package/dist/core/dispatcher-types.js +3 -3
  170. package/dist/core/dispatcher.d.ts +112 -31
  171. package/dist/core/dispatcher.js +297 -60
  172. package/dist/core/dm-freshness-metrics.d.ts +1 -1
  173. package/dist/core/drift-effects.js +2 -2
  174. package/dist/core/feedback/consolidation-prep.d.ts +94 -0
  175. package/dist/core/feedback/consolidation-prep.js +254 -0
  176. package/dist/core/feedback/eviction-scorer.d.ts +81 -0
  177. package/dist/core/feedback/eviction-scorer.js +136 -0
  178. package/dist/core/feedback/lesson-format.d.ts +79 -0
  179. package/dist/core/feedback/lesson-format.js +199 -0
  180. package/dist/core/feedback/lesson-injection.d.ts +98 -0
  181. package/dist/core/feedback/lesson-injection.js +174 -0
  182. package/dist/core/feedback/lesson-merge.d.ts +51 -0
  183. package/dist/core/feedback/lesson-merge.js +88 -0
  184. package/dist/core/feedback/lesson-store-overview.d.ts +46 -0
  185. package/dist/core/feedback/lesson-store-overview.js +42 -0
  186. package/dist/core/feedback/promotion-gate.d.ts +69 -0
  187. package/dist/core/feedback/promotion-gate.js +117 -0
  188. package/dist/core/feedback/regeneralization-prep.d.ts +87 -0
  189. package/dist/core/feedback/regeneralization-prep.js +152 -0
  190. package/dist/core/feedback/scope-parser.d.ts +86 -0
  191. package/dist/core/feedback/scope-parser.js +141 -0
  192. package/dist/core/feedback/self-performance-prep.d.ts +186 -0
  193. package/dist/core/feedback/self-performance-prep.js +541 -0
  194. package/dist/core/feedback/tuning-actuator.d.ts +198 -0
  195. package/dist/core/feedback/tuning-actuator.js +432 -0
  196. package/dist/core/feedback/tuning-recommender.d.ts +247 -0
  197. package/dist/core/feedback/tuning-recommender.js +580 -0
  198. package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
  199. package/dist/core/feedback/tuning-revert-monitor.js +213 -0
  200. package/dist/core/health-monitor.d.ts +6 -0
  201. package/dist/core/health-monitor.js +1 -1
  202. package/dist/core/injection-policy.d.ts +83 -1
  203. package/dist/core/injection-policy.js +61 -3
  204. package/dist/core/integration-main-backend.js +4 -0
  205. package/dist/core/management-md.d.ts +2 -2
  206. package/dist/core/management-md.js +51 -13
  207. package/dist/core/morning/orchestrator.d.ts +2 -2
  208. package/dist/core/morning/orchestrator.js +2 -2
  209. package/dist/core/notification-gate.d.ts +64 -0
  210. package/dist/core/notification-gate.js +51 -0
  211. package/dist/core/notification-rate-limit.d.ts +40 -0
  212. package/dist/core/notification-rate-limit.js +50 -0
  213. package/dist/core/policy-files.d.ts +1 -1
  214. package/dist/core/policy-files.js +2 -2
  215. package/dist/core/pre-pass-freshness.d.ts +4 -4
  216. package/dist/core/retention.d.ts +5 -0
  217. package/dist/core/retention.js +20 -4
  218. package/dist/core/review-context.d.ts +1 -1
  219. package/dist/core/review-context.js +10 -5
  220. package/dist/core/roadmap-write-lock.d.ts +2 -1
  221. package/dist/core/roadmap-write-lock.js +15 -10
  222. package/dist/core/routine-acquisition-plan.d.ts +47 -1
  223. package/dist/core/routine-acquisition-plan.js +78 -20
  224. package/dist/core/routine-fetch-window-retry.js +7 -4
  225. package/dist/core/routine-fetch-window-runner.d.ts +39 -3
  226. package/dist/core/routine-fetch-window-runner.js +264 -13
  227. package/dist/core/routine-windows.d.ts +2 -2
  228. package/dist/core/routine-windows.js +8 -5
  229. package/dist/core/scheduler.d.ts +175 -16
  230. package/dist/core/scheduler.js +559 -102
  231. package/dist/core/signal-detector.d.ts +51 -1
  232. package/dist/core/signal-detector.js +321 -24
  233. package/dist/core/skills-compiler-denied-tools.js +2 -2
  234. package/dist/core/skills-compiler-skill-index.d.ts +2 -2
  235. package/dist/core/skills-compiler-skill-index.js +2 -2
  236. package/dist/core/skills-compiler-variants.d.ts +1 -1
  237. package/dist/core/skills-compiler-variants.js +8 -0
  238. package/dist/core/skills-compiler.d.ts +29 -26
  239. package/dist/core/skills-compiler.js +117 -81
  240. package/dist/core/skills-manifest.d.ts +37 -0
  241. package/dist/core/skills-manifest.js +73 -2
  242. package/dist/core/sleep-inhibitor.d.ts +79 -0
  243. package/dist/core/sleep-inhibitor.js +132 -0
  244. package/dist/core/slim-system-prompt-loader.d.ts +77 -0
  245. package/dist/core/slim-system-prompt-loader.js +141 -0
  246. package/dist/core/spawn-gates.d.ts +126 -0
  247. package/dist/core/spawn-gates.js +180 -0
  248. package/dist/core/today-direct-writer.d.ts +60 -14
  249. package/dist/core/today-direct-writer.js +90 -13
  250. package/dist/core/today-write-lock.d.ts +4 -2
  251. package/dist/core/today-write-lock.js +30 -20
  252. package/dist/core/wake-detector.d.ts +55 -0
  253. package/dist/core/wake-detector.js +80 -0
  254. package/dist/core/wiki/compile-lock.d.ts +1 -1
  255. package/dist/core/wiki/compile-lock.js +1 -1
  256. package/dist/core/wiki/wiki-fts.js +13 -6
  257. package/dist/core/workdir.js +15 -6
  258. package/dist/db/activity-scan-signals.d.ts +77 -0
  259. package/dist/db/activity-scan-signals.js +378 -0
  260. package/dist/db/agents-store.d.ts +28 -0
  261. package/dist/db/agents-store.js +62 -0
  262. package/dist/db/background-task-clarifications-store.d.ts +81 -0
  263. package/dist/db/background-task-clarifications-store.js +152 -0
  264. package/dist/db/background-task-store.d.ts +207 -0
  265. package/dist/db/background-task-store.js +380 -0
  266. package/dist/db/browser-history-store.d.ts +39 -6
  267. package/dist/db/browser-history-store.js +51 -7
  268. package/dist/db/browser-task-clarifications-store.d.ts +12 -0
  269. package/dist/db/browser-task-clarifications-store.js +35 -5
  270. package/dist/db/browser-task-store.d.ts +3 -0
  271. package/dist/db/browser-task-store.js +29 -4
  272. package/dist/db/deferred-dm.d.ts +86 -0
  273. package/dist/db/deferred-dm.js +199 -0
  274. package/dist/db/feedback-signals-store.d.ts +77 -0
  275. package/dist/db/feedback-signals-store.js +144 -0
  276. package/dist/db/migrations.js +380 -0
  277. package/dist/db/observations.d.ts +2 -2
  278. package/dist/db/observations.js +3 -3
  279. package/dist/db/schema.js +260 -22
  280. package/dist/db/voice-transcripts-store.d.ts +1 -1
  281. package/dist/index.js +86 -29
  282. package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
  283. package/dist/messaging/browser-task-mcp-notifier.js +30 -151
  284. package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
  285. package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
  286. package/dist/observers/delegated-sync-worker.d.ts +6 -6
  287. package/dist/observers/delegated-sync-worker.js +10 -10
  288. package/dist/observers/git-delegated-cron.d.ts +1 -1
  289. package/dist/observers/git-delegated-cron.js +2 -2
  290. package/dist/observers/github-poller-classifier.d.ts +3 -3
  291. package/dist/observers/github-poller-classifier.js +3 -3
  292. package/dist/observers/imminent-event-scheduler.d.ts +1 -1
  293. package/dist/observers/imminent-event-scheduler.js +1 -1
  294. package/dist/observers/mail-poller.d.ts +1 -0
  295. package/dist/observers/mail-poller.js +42 -3
  296. package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
  297. package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
  298. package/dist/observers/observation-summarizer/worker.d.ts +2 -2
  299. package/dist/observers/observation-summarizer/worker.js +4 -4
  300. package/dist/observers/obsidian-watcher.d.ts +1 -1
  301. package/dist/observers/obsidian-watcher.js +1 -1
  302. package/dist/safety/agent-write-tracker.d.ts +4 -4
  303. package/dist/safety/agent-write-tracker.js +4 -4
  304. package/dist/safety/always-disallowed.d.ts +1 -1
  305. package/dist/safety/always-disallowed.js +39 -0
  306. package/dist/safety/audit.d.ts +43 -5
  307. package/dist/safety/audit.js +86 -18
  308. package/dist/safety/risk-classifier.d.ts +6 -0
  309. package/dist/safety/risk-classifier.js +97 -18
  310. package/dist/scheduler/activity-scan-gate.d.ts +86 -0
  311. package/dist/scheduler/activity-scan-gate.js +132 -0
  312. package/dist/services/background-task/background-task-budget.d.ts +80 -0
  313. package/dist/services/background-task/background-task-budget.js +91 -0
  314. package/dist/services/background-task/background-task-driver.d.ts +105 -0
  315. package/dist/services/background-task/background-task-driver.js +416 -0
  316. package/dist/services/background-task/background-task-runner.d.ts +96 -0
  317. package/dist/services/background-task/background-task-runner.js +673 -0
  318. package/dist/services/background-task/background-task-tools.d.ts +84 -0
  319. package/dist/services/background-task/background-task-tools.js +247 -0
  320. package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
  321. package/dist/services/background-task/background-task-transition-events.js +54 -0
  322. package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
  323. package/dist/services/browser-history/automation/egress-denylist.js +34 -8
  324. package/dist/services/browser-history/lifecycle/platform.js +44 -2
  325. package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
  326. package/dist/services/browser-task/browser-task-runner.js +53 -8
  327. package/dist/services/mcp/probe.js +30 -8
  328. package/dist/services/observations-batch.d.ts +1 -1
  329. package/dist/services/observations-batch.js +2 -2
  330. package/dist/settings/runtime-settings.d.ts +45 -12
  331. package/dist/settings/runtime-settings.js +215 -40
  332. package/dist/settings/settings-store.js +11 -3
  333. package/package.json +4 -4
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Background-task worker tools — BACKGROUND_TASK_RUNNER_DESIGN.md §4.1 / §4.3.
3
+ *
4
+ * The generic worker's tool envelope. Unlike browser-task's 11-tool
5
+ * Playwright plane, the background worker gets exactly three tools:
6
+ *
7
+ * - `read_memory(key)` — READ-ONLY access to an allowlisted set of
8
+ * owner memory / profile files so the worker can personalize results
9
+ * itself rather than the brief enumerating everything (§9 / §10.4).
10
+ * The worker MUST NOT write shared memory — results come back as the
11
+ * artifact and the DM agent persists anything memory-worthy.
12
+ * - `ask_user(question, contextSummary)` — write a clarification
13
+ * artifact, park the task (`awaiting_user`), and end the turn. The
14
+ * runner surfaces it through the gated delivery boundary.
15
+ * - `finish(result, draft, notify, significance?)` — WRITE THE ARTIFACT
16
+ * (verbatim `result` + plain `draft` summary + the `notify`
17
+ * disposition the worker evaluated against the spawn-time policy) and
18
+ * complete the task. The runner's reconcile hook reads the artifact
19
+ * and decides delivery.
20
+ *
21
+ * The tools do NOT send DMs or enqueue delivery — they only write to the
22
+ * task store + transition state. Delivery is the runner's job (it owns
23
+ * the `notify` gate + the `task.delivery` enqueue), keeping the
24
+ * disposition decision in one place. This module is store-write glue,
25
+ * excluded from the coverage gate; the pure decision (`notify` evaluation)
26
+ * is the worker's, and the budget arithmetic is covered separately.
27
+ */
28
+ import type Database from "better-sqlite3";
29
+ import { type McpSdkServerConfigWithInstance } from "@anthropic-ai/claude-agent-sdk";
30
+ import { z } from "zod";
31
+ import { type BackgroundTaskTransitionEmitter } from "./background-task-transition-events.js";
32
+ export declare const BACKGROUND_TASK_MCP_SERVER_NAME = "aitne-task";
33
+ export declare const BACKGROUND_TASK_TOOL_FQNS: readonly ["mcp__aitne-task__read_memory", "mcp__aitne-task__ask_user", "mcp__aitne-task__finish"];
34
+ export declare const MEMORY_KEYS: readonly string[];
35
+ export interface BackgroundTaskRuntime {
36
+ taskId: string;
37
+ db: Database.Database;
38
+ /** Vault root for `read_memory`. */
39
+ contextDir: string;
40
+ /** Clarification TTL in ms (from `backgroundTaskClarificationTtlMinutes`). */
41
+ clarificationTtlMs: number;
42
+ transitionEmitter: BackgroundTaskTransitionEmitter;
43
+ abortSignal: AbortSignal;
44
+ /** Set true once `ask_user` parks the task — read by the runner's
45
+ * post-execute hook to distinguish a clean park from a hang. */
46
+ yieldFlag: {
47
+ current: boolean;
48
+ };
49
+ /** Set true once `finish` writes the artifact — read by the runner to
50
+ * confirm a clean completion vs an SDK-side natural end. */
51
+ finishFlag: {
52
+ current: boolean;
53
+ };
54
+ nowFn?: () => number;
55
+ }
56
+ export declare function createBackgroundTaskRuntime(input: {
57
+ taskId: string;
58
+ db: Database.Database;
59
+ contextDir: string;
60
+ clarificationTtlMs: number;
61
+ abortSignal: AbortSignal;
62
+ transitionEmitter?: BackgroundTaskTransitionEmitter;
63
+ nowFn?: () => number;
64
+ }): BackgroundTaskRuntime;
65
+ /** The three worker tools, bound to a runtime. Exported so tests can
66
+ * invoke a tool's `handler` directly without standing up the MCP
67
+ * transport. */
68
+ export declare function createBackgroundTaskTools(runtime: BackgroundTaskRuntime): (import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
69
+ key: z.ZodEnum<{
70
+ [x: string]: string;
71
+ }>;
72
+ }> | import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
73
+ question: z.ZodString;
74
+ contextSummary: z.ZodOptional<z.ZodString>;
75
+ }> | import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
76
+ result: z.ZodString;
77
+ draft: z.ZodString;
78
+ notify: z.ZodBoolean;
79
+ significance: z.ZodOptional<z.ZodString>;
80
+ }>)[];
81
+ /** Construct the per-task MCP server. Returned config is passed verbatim
82
+ * into `query({ options: { mcpServers: { [BACKGROUND_TASK_MCP_SERVER_NAME]:
83
+ * <return value> } } })`. */
84
+ export declare function createBackgroundTaskMcpServer(runtime: BackgroundTaskRuntime): McpSdkServerConfigWithInstance;
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Background-task worker tools — BACKGROUND_TASK_RUNNER_DESIGN.md §4.1 / §4.3.
3
+ *
4
+ * The generic worker's tool envelope. Unlike browser-task's 11-tool
5
+ * Playwright plane, the background worker gets exactly three tools:
6
+ *
7
+ * - `read_memory(key)` — READ-ONLY access to an allowlisted set of
8
+ * owner memory / profile files so the worker can personalize results
9
+ * itself rather than the brief enumerating everything (§9 / §10.4).
10
+ * The worker MUST NOT write shared memory — results come back as the
11
+ * artifact and the DM agent persists anything memory-worthy.
12
+ * - `ask_user(question, contextSummary)` — write a clarification
13
+ * artifact, park the task (`awaiting_user`), and end the turn. The
14
+ * runner surfaces it through the gated delivery boundary.
15
+ * - `finish(result, draft, notify, significance?)` — WRITE THE ARTIFACT
16
+ * (verbatim `result` + plain `draft` summary + the `notify`
17
+ * disposition the worker evaluated against the spawn-time policy) and
18
+ * complete the task. The runner's reconcile hook reads the artifact
19
+ * and decides delivery.
20
+ *
21
+ * The tools do NOT send DMs or enqueue delivery — they only write to the
22
+ * task store + transition state. Delivery is the runner's job (it owns
23
+ * the `notify` gate + the `task.delivery` enqueue), keeping the
24
+ * disposition decision in one place. This module is store-write glue,
25
+ * excluded from the coverage gate; the pure decision (`notify` evaluation)
26
+ * is the worker's, and the budget arithmetic is covered separately.
27
+ */
28
+ import { readFile } from "node:fs/promises";
29
+ import { randomUUID } from "node:crypto";
30
+ import { createSdkMcpServer, tool, } from "@anthropic-ai/claude-agent-sdk";
31
+ import { z } from "zod";
32
+ import { createClarification } from "../../db/background-task-clarifications-store.js";
33
+ import { markAwaitingUser, markTerminal, } from "../../db/background-task-store.js";
34
+ import { CONTEXT_RELATIVE_PATHS, fullPath } from "../../core/context-paths.js";
35
+ import { noopBackgroundTaskTransitionEmitter, } from "./background-task-transition-events.js";
36
+ import { createLogger } from "../../logging.js";
37
+ const logger = createLogger("background-task-tools");
38
+ export const BACKGROUND_TASK_MCP_SERVER_NAME = "aitne-task";
39
+ export const BACKGROUND_TASK_TOOL_FQNS = [
40
+ `mcp__${BACKGROUND_TASK_MCP_SERVER_NAME}__read_memory`,
41
+ `mcp__${BACKGROUND_TASK_MCP_SERVER_NAME}__ask_user`,
42
+ `mcp__${BACKGROUND_TASK_MCP_SERVER_NAME}__finish`,
43
+ ];
44
+ /** Per-read output cap so a large memory file can't blow the worker's
45
+ * context window or budget in one tool call. */
46
+ const MEMORY_READ_CHAR_CAP = 8_000;
47
+ /**
48
+ * Allowlist of READ-ONLY memory keys → vault-relative paths. A fixed
49
+ * enum (no user-controlled path component) so there is no traversal
50
+ * surface. Single files only; the worker reads what it needs to
51
+ * personalize a result (the owner's profile, today's state, project
52
+ * context, the management policy).
53
+ */
54
+ const MEMORY_FILE_ALLOWLIST = {
55
+ today: CONTEXT_RELATIVE_PATHS.today,
56
+ profile: CONTEXT_RELATIVE_PATHS.user.profile,
57
+ people: CONTEXT_RELATIVE_PATHS.user.people,
58
+ work: CONTEXT_RELATIVE_PATHS.user.work,
59
+ goals: CONTEXT_RELATIVE_PATHS.user.goals,
60
+ projects: CONTEXT_RELATIVE_PATHS.projects.index,
61
+ management: CONTEXT_RELATIVE_PATHS.rules.management,
62
+ integrations: CONTEXT_RELATIVE_PATHS.integrations,
63
+ };
64
+ export const MEMORY_KEYS = Object.keys(MEMORY_FILE_ALLOWLIST);
65
+ // The SDK `tool()` helper takes a Zod RAW SHAPE (a `{ key: ZodType }`
66
+ // object), not a `z.object(...)` — mirroring browser-task's schemas.
67
+ const readMemoryArgsSchema = {
68
+ key: z
69
+ .enum(Object.keys(MEMORY_FILE_ALLOWLIST))
70
+ .describe("Which owner memory file to read. One of: "
71
+ + MEMORY_KEYS.join(", ")
72
+ + ". Read-only."),
73
+ };
74
+ const askUserArgsSchema = {
75
+ question: z
76
+ .string()
77
+ .min(1)
78
+ .max(2_000)
79
+ .describe("The clarification you need from the owner, phrased plainly. The DM agent weaves this into the conversation."),
80
+ contextSummary: z
81
+ .string()
82
+ .max(2_000)
83
+ .optional()
84
+ .describe("Optional one-paragraph recap of where the task is and why you're stuck, so the owner can answer without re-reading the whole brief."),
85
+ };
86
+ const finishArgsSchema = {
87
+ result: z
88
+ .string()
89
+ .min(1)
90
+ .max(100_000)
91
+ .describe("The FULL, verbatim outcome — every finding, number, URL, and id. Persisted unchanged as the fidelity anchor; precise follow-ups read this. Do NOT summarize here."),
92
+ draft: z
93
+ .string()
94
+ .min(1)
95
+ .max(4_000)
96
+ .describe("A plain, human-readable summary in the owner's language. NOT the final DM — the DM agent uses this as grounding / the idle-send body. 1-4 short paragraphs."),
97
+ notify: z
98
+ .boolean()
99
+ .describe("Your disposition vs the spawn-time notification policy. always ⇒ true (even for a '0 issues' result — the owner asked). if_significant ⇒ true ONLY if the brief's concrete criteria are met. silent ⇒ false. When unsure on always, prefer true."),
100
+ significance: z
101
+ .string()
102
+ .max(500)
103
+ .optional()
104
+ .describe("One line on why notify is true/false (e.g. '2 repos red' / 'no criteria met'). Used in the filed-results digest + audit."),
105
+ };
106
+ export function createBackgroundTaskRuntime(input) {
107
+ return {
108
+ taskId: input.taskId,
109
+ db: input.db,
110
+ contextDir: input.contextDir,
111
+ clarificationTtlMs: input.clarificationTtlMs,
112
+ abortSignal: input.abortSignal,
113
+ transitionEmitter: input.transitionEmitter ?? noopBackgroundTaskTransitionEmitter,
114
+ yieldFlag: { current: false },
115
+ finishFlag: { current: false },
116
+ nowFn: input.nowFn,
117
+ };
118
+ }
119
+ function textResult(payload, isError = false) {
120
+ return {
121
+ isError,
122
+ content: [{ type: "text", text: JSON.stringify(payload) }],
123
+ };
124
+ }
125
+ function makeReadMemoryTool(runtime) {
126
+ return tool("read_memory", "Read one owner memory / profile file (read-only) to personalize your result. Keys: "
127
+ + MEMORY_KEYS.join(", ")
128
+ + ". You cannot write memory — return everything memory-worthy in finish().", readMemoryArgsSchema, async (args) => {
129
+ const relative = MEMORY_FILE_ALLOWLIST[args.key];
130
+ if (!relative) {
131
+ return textResult({ ok: false, error: "unknown_key", detail: `key must be one of: ${MEMORY_KEYS.join(", ")}` }, true);
132
+ }
133
+ const path = fullPath(runtime.contextDir, relative);
134
+ try {
135
+ const raw = await readFile(path, "utf-8");
136
+ const truncated = raw.length > MEMORY_READ_CHAR_CAP;
137
+ const content = truncated ? raw.slice(0, MEMORY_READ_CHAR_CAP) : raw;
138
+ return textResult({
139
+ ok: true,
140
+ key: args.key,
141
+ truncated,
142
+ content: truncated
143
+ ? `${content}\n\n[... truncated at ${MEMORY_READ_CHAR_CAP} chars]`
144
+ : content,
145
+ });
146
+ }
147
+ catch {
148
+ // Missing file is normal (a fresh vault may not have every file).
149
+ return textResult({
150
+ ok: true,
151
+ key: args.key,
152
+ truncated: false,
153
+ content: "",
154
+ note: "file not present in the vault yet",
155
+ });
156
+ }
157
+ });
158
+ }
159
+ function makeAskUserTool(runtime) {
160
+ return tool("ask_user", "Pause for an owner clarification. Writes the question, parks your task, and ends the turn — the DM agent surfaces it and relays the answer back so you resume. Call this and then STOP; do not call further tools this turn.", askUserArgsSchema, async (args) => {
161
+ const now = (runtime.nowFn ?? (() => Date.now()))();
162
+ // running → awaiting_user CAS before writing the clarification row.
163
+ // On a CAS miss the task already transitioned (cancel-while-running)
164
+ // — bail without committing an orphan row the deadline tick would
165
+ // later process for a terminal task.
166
+ const parked = markAwaitingUser(runtime.db, runtime.taskId);
167
+ if (!parked) {
168
+ return textResult({
169
+ ok: false,
170
+ error: "task_not_running",
171
+ detail: "This task is no longer running (it may have been cancelled). Stop now.",
172
+ }, true);
173
+ }
174
+ const id = randomUUID();
175
+ const row = createClarification(runtime.db, {
176
+ id,
177
+ taskId: runtime.taskId,
178
+ question: args.question,
179
+ contextSummary: args.contextSummary ?? null,
180
+ askedAt: now,
181
+ ttlMs: runtime.clarificationTtlMs,
182
+ });
183
+ runtime.yieldFlag.current = true;
184
+ runtime.transitionEmitter.emitFromRow(parked, now);
185
+ return textResult({
186
+ ok: true,
187
+ status: "parked",
188
+ clarificationId: id,
189
+ deadlineAt: row.deadlineAt,
190
+ note: "Your task is parked. STOP now — the owner's answer will resume you.",
191
+ });
192
+ });
193
+ }
194
+ function makeFinishTool(runtime) {
195
+ return tool("finish", "Done. Writes your artifact (verbatim result + plain draft summary + the notify disposition) and completes the task. Do not call any tool after finish — your session ends here.", finishArgsSchema, async (args) => {
196
+ const now = (runtime.nowFn ?? (() => Date.now()))();
197
+ const terminal = markTerminal(runtime.db, {
198
+ id: runtime.taskId,
199
+ state: "completed",
200
+ outcomeDetail: null,
201
+ finishedAt: now,
202
+ report: args.result,
203
+ draft: args.draft,
204
+ notify: args.notify,
205
+ significance: args.significance ?? null,
206
+ });
207
+ if (!terminal) {
208
+ // CAS miss — the task was already cancelled / timed out. Surface
209
+ // it so the agent stops; the artifact is intentionally not forced
210
+ // onto a terminal row.
211
+ return textResult({
212
+ ok: false,
213
+ error: "task_not_active",
214
+ detail: "This task already reached a terminal state; the result was not stored. Stop now.",
215
+ }, true);
216
+ }
217
+ runtime.finishFlag.current = true;
218
+ runtime.transitionEmitter.emitFromRow(terminal, now);
219
+ return textResult({
220
+ ok: true,
221
+ completed: true,
222
+ notify: args.notify,
223
+ state: terminal.state,
224
+ });
225
+ });
226
+ }
227
+ /** The three worker tools, bound to a runtime. Exported so tests can
228
+ * invoke a tool's `handler` directly without standing up the MCP
229
+ * transport. */
230
+ export function createBackgroundTaskTools(runtime) {
231
+ return [
232
+ makeReadMemoryTool(runtime),
233
+ makeAskUserTool(runtime),
234
+ makeFinishTool(runtime),
235
+ ];
236
+ }
237
+ /** Construct the per-task MCP server. Returned config is passed verbatim
238
+ * into `query({ options: { mcpServers: { [BACKGROUND_TASK_MCP_SERVER_NAME]:
239
+ * <return value> } } })`. */
240
+ export function createBackgroundTaskMcpServer(runtime) {
241
+ return createSdkMcpServer({
242
+ name: BACKGROUND_TASK_MCP_SERVER_NAME,
243
+ version: "1.0.0",
244
+ tools: createBackgroundTaskTools(runtime),
245
+ });
246
+ }
247
+ void logger;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Background-task SSE transition emitter — BACKGROUND_TASK_RUNNER_DESIGN.md
3
+ * §15 (dashboard). Emits a `background_task` named event on the global
4
+ * `/api/events/stream` on every state transition so a future dashboard
5
+ * surface can invalidate its list/detail queries without per-id polling.
6
+ *
7
+ * Per `project_dashboard_testing`: `background_task` arrives on the
8
+ * default `event` stream with a `kind` field — here the named-event
9
+ * channel is `"background_task"`, payload bounded and ASCII-safe.
10
+ *
11
+ * Mirrors `browser-task-transition-events.ts`: a thin telemetry interface
12
+ * separate from the delivery path (which fans user-facing DMs). The
13
+ * payload never carries the full report — only an 80-char title/brief.
14
+ */
15
+ import type { BackgroundTaskRow } from "../../db/background-task-store.js";
16
+ export interface BackgroundTaskTransitionPayload {
17
+ taskId: string;
18
+ state: BackgroundTaskRow["state"];
19
+ /** Epoch ms — finishedAt for terminal rows, startedAt for running,
20
+ * createdAt for pending. Cache-busting timestamp only. */
21
+ transitionedAt: number;
22
+ /** First 80 chars of the title (or brief), control chars scrubbed. */
23
+ brief: string;
24
+ outcomeDetail: string | null;
25
+ /** Worker disposition once finished (null while in-flight). */
26
+ notify: boolean | null;
27
+ originatingChannel: string | null;
28
+ }
29
+ /** Minimal broadcaster surface — the impl lives in `api/routes/sse.ts`.
30
+ * Declared structurally so the daemon's `services/background-task/*`
31
+ * modules don't depend on the HTTP layer. */
32
+ export interface BroadcastSink {
33
+ broadcastNamedEvent(event: string, data: unknown): Promise<void> | void;
34
+ }
35
+ export interface BackgroundTaskTransitionEmitter {
36
+ emit(payload: BackgroundTaskTransitionPayload): void;
37
+ /** Extract fields from a row + transitionedAt and emit. Returns the
38
+ * payload (or null when row is null) for test chaining. */
39
+ emitFromRow(row: BackgroundTaskRow | null, transitionedAt: number): BackgroundTaskTransitionPayload | null;
40
+ }
41
+ export declare function briefPayload(row: BackgroundTaskRow, transitionedAt: number): BackgroundTaskTransitionPayload;
42
+ export declare const noopBackgroundTaskTransitionEmitter: BackgroundTaskTransitionEmitter;
43
+ export declare function createBackgroundTaskTransitionEmitter(sink: BroadcastSink | null | undefined): BackgroundTaskTransitionEmitter;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Background-task SSE transition emitter — BACKGROUND_TASK_RUNNER_DESIGN.md
3
+ * §15 (dashboard). Emits a `background_task` named event on the global
4
+ * `/api/events/stream` on every state transition so a future dashboard
5
+ * surface can invalidate its list/detail queries without per-id polling.
6
+ *
7
+ * Per `project_dashboard_testing`: `background_task` arrives on the
8
+ * default `event` stream with a `kind` field — here the named-event
9
+ * channel is `"background_task"`, payload bounded and ASCII-safe.
10
+ *
11
+ * Mirrors `browser-task-transition-events.ts`: a thin telemetry interface
12
+ * separate from the delivery path (which fans user-facing DMs). The
13
+ * payload never carries the full report — only an 80-char title/brief.
14
+ */
15
+ const CONTROL_CHAR_REGEX = new RegExp("[\\x00-\\x1f\\x7f]", "g");
16
+ export function briefPayload(row, transitionedAt) {
17
+ const source = row.title && row.title.length > 0 ? row.title : row.brief;
18
+ const brief = source.replace(CONTROL_CHAR_REGEX, " ").slice(0, 80);
19
+ return {
20
+ taskId: row.id,
21
+ state: row.state,
22
+ transitionedAt,
23
+ brief,
24
+ outcomeDetail: row.outcomeDetail,
25
+ notify: row.notify,
26
+ originatingChannel: row.originatingChannel,
27
+ };
28
+ }
29
+ export const noopBackgroundTaskTransitionEmitter = {
30
+ emit() {
31
+ /* no-op */
32
+ },
33
+ emitFromRow(row, transitionedAt) {
34
+ if (!row)
35
+ return null;
36
+ return briefPayload(row, transitionedAt);
37
+ },
38
+ };
39
+ export function createBackgroundTaskTransitionEmitter(sink) {
40
+ if (!sink)
41
+ return noopBackgroundTaskTransitionEmitter;
42
+ return {
43
+ emit(payload) {
44
+ void sink.broadcastNamedEvent("background_task", payload);
45
+ },
46
+ emitFromRow(row, transitionedAt) {
47
+ if (!row)
48
+ return null;
49
+ const payload = briefPayload(row, transitionedAt);
50
+ void sink.broadcastNamedEvent("background_task", payload);
51
+ return payload;
52
+ },
53
+ };
54
+ }
@@ -161,7 +161,7 @@ export declare function shouldDenyEgress(url: string, opts?: {
161
161
  hostnameDenylist?: ReadonlyArray<RegExp>;
162
162
  }): Promise<{
163
163
  denied: true;
164
- reason: "hostname" | "cidr" | "invalid_url";
164
+ reason: "hostname" | "cidr" | "invalid_url" | "resolve_error";
165
165
  } | {
166
166
  denied: false;
167
167
  }>;
@@ -229,6 +229,10 @@ export const IP_DENYLIST_CIDRS = Object.freeze([
229
229
  "100.64.0.0/10",
230
230
  "224.0.0.0/4",
231
231
  "0.0.0.0/8",
232
+ // IETF protocol assignments (RFC 6890) — includes the DNS64 well-known
233
+ // host 192.0.0.171/.170 used to discover the NAT64 prefix. Not globally
234
+ // routable; no legitimate browse target lives here.
235
+ "192.0.0.0/24",
232
236
  // IPv6 equivalents
233
237
  "::1/128",
234
238
  "::/128", // IPv6 unspecified — mirrors IPv4 0.0.0.0/8; routes to ::1 as a connect target on common stacks
@@ -374,8 +378,20 @@ export function matchesCidrDenylist(ip) {
374
378
  const bits = ipv6ToBigInt(ip);
375
379
  if (bits !== null) {
376
380
  const mask96 = ((1n << 96n) - 1n) << 32n; // high 96 bits
377
- const v4MappedPrefix = 0xffffn << 32n; // ::ffff:0:0
378
- if ((bits & mask96) === v4MappedPrefix) {
381
+ const v4MappedPrefix = 0xffffn << 32n; // ::ffff:0:0/96
382
+ // NAT64 well-known prefix `64:ff9b::/96` (RFC 6052) — on NAT64/DNS64
383
+ // networks the entire IPv4 internet is reachable through this prefix,
384
+ // so `64:ff9b::a9fe:a9fe` routes to 169.254.169.254 and
385
+ // `64:ff9b::a00:1` to 10.0.0.1. We must NOT block the whole /96
386
+ // (that would break all IPv4 browsing on such networks) — instead
387
+ // decode the embedded IPv4 (low 32 bits, same position as the
388
+ // v4-mapped form) and run it through the IPv4 deny ranges, so only
389
+ // metadata/private destinations are blocked. Network-specific NAT64
390
+ // prefixes (operator-chosen /32../64) are out of scope — they aren't
391
+ // a guessable SSRF target the way the well-known prefix is.
392
+ const nat64WellKnownPrefix = 0x0064ff9bn << 96n; // 64:ff9b::/96
393
+ const prefix96 = bits & mask96;
394
+ if (prefix96 === v4MappedPrefix || prefix96 === nat64WellKnownPrefix) {
379
395
  const embedded = Number(bits & 0xffffffffn);
380
396
  const dotted = [
381
397
  (embedded >>> 24) & 255,
@@ -421,16 +437,26 @@ export async function shouldDenyEgress(url, opts) {
421
437
  return { denied: false };
422
438
  }
423
439
  if (opts?.resolveIps) {
424
- let resolved = [];
440
+ let resolved;
425
441
  try {
426
442
  resolved = await opts.resolveIps(hostname);
427
443
  }
428
444
  catch {
429
- // DNS failure let the request through; if the lookup is broken
430
- // here it will fail at the network layer too. Failing closed
431
- // (treating a DNS error as a block) would brick every workflow
432
- // during a temporary resolver outage.
433
- return { denied: false };
445
+ // Fail CLOSED. This CIDR gate is the primary defence against egress to
446
+ // private / link-local / cloud-metadata IPs (the browser sandbox shares
447
+ // the host network namespace, so there is no packet-layer block behind
448
+ // it). The guard and Chromium resolve the hostname independently — a
449
+ // name that Node's resolver fails on but Chromium's resolver succeeds on
450
+ // (SERVFAIL / timeout / search-domain / IDN differences) would otherwise
451
+ // reach an internal address completely unchecked. Blocking on resolve
452
+ // failure closes that differential-resolver gap; a host that is
453
+ // genuinely unresolvable would fail at the network layer regardless.
454
+ return { denied: true, reason: "resolve_error" };
455
+ }
456
+ if (resolved.length === 0) {
457
+ // No addresses to vet is as unverifiable as a thrown lookup — don't let
458
+ // the host through to Chromium's independent resolution.
459
+ return { denied: true, reason: "resolve_error" };
434
460
  }
435
461
  for (const ip of resolved) {
436
462
  if (matchesCidrDenylist(ip)) {
@@ -2,7 +2,7 @@ import { accessSync, constants, existsSync, readdirSync } from "node:fs";
2
2
  import { readlink } from "node:fs/promises";
3
3
  import { createRequire } from "node:module";
4
4
  import { homedir, release } from "node:os";
5
- import { dirname, join } from "node:path";
5
+ import { delimiter, dirname, join } from "node:path";
6
6
  import { execFile } from "node:child_process";
7
7
  import { promisify } from "node:util";
8
8
  const execFileAsync = promisify(execFile);
@@ -504,12 +504,54 @@ async function isUnixProcessRunning(binaryPath, userDataDir) {
504
504
  return false;
505
505
  }
506
506
  }
507
+ /**
508
+ * Resolve the Windows PowerShell executable defensively.
509
+ *
510
+ * This is the same `powershell.exe → pwsh.exe` probe every other PS call
511
+ * site in the codebase performs (secret-client-factory.ts:findExecutableInPath,
512
+ * doctor.mjs, scripts/lib/read-api-token.mjs). On Windows desktop SKUs the
513
+ * in-box Windows PowerShell 5.1 (`powershell.exe`) is present, but on
514
+ * minimal / headless / Server-Core / Nano hosts — or future builds where
515
+ * 5.1 has been removed — only PowerShell 7+ (`pwsh.exe`) exists, and a
516
+ * hardcoded `powershell.exe` exec throws ENOENT (which the caller's catch
517
+ * swallows, silently reporting the managed Chromium as not-running).
518
+ *
519
+ * Prefer 5.1, fall back to 7+, else keep the default so the first exec
520
+ * surfaces a clear ENOENT rather than masking a misconfigured host.
521
+ */
522
+ function resolveWindowsPowerShell() {
523
+ const pathValue = process.env.PATH ?? "";
524
+ const exts = process.env.PATHEXT?.split(";").filter(Boolean) ?? [".EXE"];
525
+ const probe = (name) => {
526
+ const hasExt = /\.[A-Za-z0-9]+$/.test(name);
527
+ for (const dir of pathValue.split(delimiter)) {
528
+ if (!dir)
529
+ continue;
530
+ const candidates = hasExt ? [name] : exts.map((e) => `${name}${e}`);
531
+ for (const c of candidates) {
532
+ try {
533
+ accessSync(join(dir, c), constants.X_OK);
534
+ return true;
535
+ }
536
+ catch {
537
+ // keep scanning
538
+ }
539
+ }
540
+ }
541
+ return false;
542
+ };
543
+ return probe("powershell.exe")
544
+ ? "powershell.exe"
545
+ : probe("pwsh.exe")
546
+ ? "pwsh.exe"
547
+ : "powershell.exe";
548
+ }
507
549
  async function isWindowsProcessRunning(binaryPath, userDataDir) {
508
550
  const escapedBinary = binaryPath.replace(/'/g, "''");
509
551
  const escapedDir = userDataDir.replace(/'/g, "''");
510
552
  const command = `Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*${escapedBinary}*' -and $_.CommandLine -like '*--user-data-dir=${escapedDir}*' } | Select-Object -First 1`;
511
553
  try {
512
- const { stdout } = await execFileAsync("powershell.exe", [
554
+ const { stdout } = await execFileAsync(resolveWindowsPowerShell(), [
513
555
  "-NoProfile",
514
556
  "-Command",
515
557
  command,
@@ -233,7 +233,6 @@ function loadWindowsHelper() {
233
233
  // a module-scope global — build one via `createRequire` to reach the
234
234
  // CommonJS loader. This branch only runs on win32.
235
235
  const req = createRequire(import.meta.url);
236
- // eslint-disable-next-line @typescript-eslint/no-require-imports
237
236
  const mod = req("../../../../native/win-appcontainer/loader.js");
238
237
  return mod.loadHelper();
239
238
  }
@@ -34,7 +34,7 @@ import { getBrowserTask, markRunning, markRunningFromParked, markTerminal, } fro
34
34
  import { resolveClarification } from "../../db/browser-task-clarifications-store.js";
35
35
  import { createLogger } from "../../logging.js";
36
36
  import { prepareDriverHandle, releaseDriverHandle, resumeDriver, runDriver, } from "./browser-task-driver.js";
37
- import { createInitialSlotState, decideAcquire, decidePark, decideRelease, decideUnpark, } from "./browser-task-slots.js";
37
+ import { createInitialSlotState, decideAcquire, decideCancel, decidePark, decideRelease, decideUnpark, } from "./browser-task-slots.js";
38
38
  import { noopBrowserTaskTransitionEmitter, } from "./browser-task-transition-events.js";
39
39
  const logger = createLogger("browser-task-runner");
40
40
  /**
@@ -52,6 +52,16 @@ const logger = createLogger("browser-task-runner");
52
52
  function slotSiteKeyForRow(row) {
53
53
  return row.siteKey ?? `__open__:${row.id}`;
54
54
  }
55
+ /** True when the slot manager already tracks `taskId` as an active
56
+ * occupant (acquired a slot), even if the DB row still reads `pending`
57
+ * in the narrow acquire→markRunning window. Mirrors the route helper. */
58
+ function slotManagerHasActive(state, taskId) {
59
+ for (const entry of state.active.values()) {
60
+ if (entry.taskId === taskId)
61
+ return true;
62
+ }
63
+ return false;
64
+ }
55
65
  export function createSlotStateRef(maxConcurrent) {
56
66
  return { state: createInitialSlotState(maxConcurrent) };
57
67
  }
@@ -494,15 +504,50 @@ export function createBrowserTaskRunner(deps) {
494
504
  // landing in that window is silently dropped and the SDK runs
495
505
  // anyway, burning turns / cost.
496
506
  //
497
- // Other non-terminal states are deliberately excluded: `pending`
498
- // is route-handled (decideCancel removes the FIFO entry) and the
499
- // parked states (`awaiting_user`/`final_confirm`) always have a
500
- // tracked handle, so they take the `handle` branch above. Recording
501
- // a pending-abort for a `pending` row would leak the map entry —
502
- // nothing ever consumes it (`runDriverFromPending` only drains it
503
- // after `prepareDriverHandle`, which a pending row never reaches).
507
+ // The parked states (`awaiting_user`/`final_confirm`) always have a
508
+ // tracked handle, so they take the `handle` branch above.
504
509
  pendingAborts.set(taskId, reason || "cancel");
505
510
  }
511
+ else if (row.state === "pending") {
512
+ // Queued behind the concurrency cap (or in the narrow acquire→
513
+ // markRunning window). The HTTP `/cancel` route handles `pending`
514
+ // itself, but `!stop <id>` (Phase 4) calls `cancel()` directly — so
515
+ // the runner must own this state too, or the bang path reports a
516
+ // false "Stopping…" while the task keeps running.
517
+ if (slotManagerHasActive(deps.slotStateRef.state, taskId)) {
518
+ // `tryAcquire` already promoted the task (slot active) but
519
+ // `markRunning` hasn't flipped the DB row yet — `decideCancel`
520
+ // would throw on an active occupant. Record the abort intent like
521
+ // the running case; the handle registered at `runDriverFromPending`
522
+ // fires it before the SDK loop begins (and drains the map entry,
523
+ // so it does not leak).
524
+ pendingAborts.set(taskId, reason || "cancel");
525
+ }
526
+ else {
527
+ // Genuinely queued — drop the FIFO entry and write the terminal
528
+ // directly (no slot was held, so no release cascade). Mirrors the
529
+ // route's `isPending` path and the background runner.
530
+ try {
531
+ deps.slotStateRef.state = decideCancel(deps.slotStateRef.state, taskId).state;
532
+ }
533
+ catch (err) {
534
+ /* c8 ignore start -- defensive: slot promoted between the check and here */
535
+ logger.warn({ err, taskId }, "decideCancel on pending row failed (continuing)");
536
+ /* c8 ignore stop */
537
+ }
538
+ const finishedAt = now();
539
+ const terminal = markTerminal(deps.db, {
540
+ id: taskId,
541
+ state: "cancelled",
542
+ outcomeDetail: `cancelled_in_queue:${reason}`,
543
+ report: null,
544
+ finishedAt,
545
+ });
546
+ emitter.emitFromRow(terminal, finishedAt);
547
+ logger.info({ taskId, reason }, "browser-task cancel (pending → cancelled)");
548
+ return true;
549
+ }
550
+ }
506
551
  if (handle) {
507
552
  // Cancel any pending lite-final-confirm tokens issued by this
508
553
  // task — otherwise a gate that was mid-flight leaves a token