@aitne/daemon 0.1.10 → 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 (305) 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.js +8 -5
  15. package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
  16. package/dist/api/helpers/agent-errors-registry.js +5 -5
  17. package/dist/api/routes/agent.js +33 -12
  18. package/dist/api/routes/agents/index.js +75 -16
  19. package/dist/api/routes/agents/views.d.ts +37 -2
  20. package/dist/api/routes/agents/views.js +64 -2
  21. package/dist/api/routes/background-task.d.ts +22 -0
  22. package/dist/api/routes/background-task.js +338 -0
  23. package/dist/api/routes/browser-history.js +9 -1
  24. package/dist/api/routes/context/permissions.js +3 -2
  25. package/dist/api/routes/context/snapshots.js +0 -3
  26. package/dist/api/routes/context/write.js +3 -17
  27. package/dist/api/routes/dashboard/config.js +48 -12
  28. package/dist/api/routes/dashboard/cost-approvals.js +66 -0
  29. package/dist/api/routes/dashboard/notifications.js +9 -9
  30. package/dist/api/routes/integrations/crud-patch.js +5 -1
  31. package/dist/api/routes/integrations-reconcile.js +2 -2
  32. package/dist/api/routes/notion.d.ts +1 -1
  33. package/dist/api/routes/observations.js +7 -7
  34. package/dist/api/routes/obsidian.d.ts +1 -1
  35. package/dist/api/routes/receipts.js +5 -1
  36. package/dist/api/routes/setup-migrate.js +1 -1
  37. package/dist/api/routes/setup.js +1 -1
  38. package/dist/api/routes/task-flows.d.ts +1 -1
  39. package/dist/api/routes/task-flows.js +1 -1
  40. package/dist/api/routes/tuning.d.ts +29 -0
  41. package/dist/api/routes/tuning.js +304 -0
  42. package/dist/api/server.d.ts +44 -16
  43. package/dist/api/server.js +9 -0
  44. package/dist/bootstrap/adapters.d.ts +19 -0
  45. package/dist/bootstrap/adapters.js +61 -0
  46. package/dist/bootstrap/api.d.ts +5 -3
  47. package/dist/bootstrap/api.js +45 -13
  48. package/dist/bootstrap/catchup.d.ts +1 -1
  49. package/dist/bootstrap/catchup.js +11 -11
  50. package/dist/bootstrap/event-pipeline.d.ts +11 -0
  51. package/dist/bootstrap/event-pipeline.js +245 -7
  52. package/dist/bootstrap/observers.js +9 -6
  53. package/dist/bootstrap/schedule-helpers.d.ts +104 -6
  54. package/dist/bootstrap/schedule-helpers.js +172 -19
  55. package/dist/config.js +26 -12
  56. package/dist/core/agent-core.d.ts +33 -1
  57. package/dist/core/agent-core.js +36 -1
  58. package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
  59. package/dist/core/agents/activity-scan-cadence.js +127 -0
  60. package/dist/core/agents/agent-route-override.d.ts +53 -0
  61. package/dist/core/agents/agent-route-override.js +69 -0
  62. package/dist/core/agents/builtin-registry.d.ts +51 -14
  63. package/dist/core/agents/builtin-registry.js +92 -15
  64. package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
  65. package/dist/core/agents/config-gate-reconcile.js +51 -0
  66. package/dist/core/agents/cron-substitute.d.ts +1 -1
  67. package/dist/core/agents/cron-substitute.js +1 -1
  68. package/dist/core/agents/custom-routine-migration.d.ts +60 -0
  69. package/dist/core/agents/custom-routine-migration.js +149 -0
  70. package/dist/core/agents/firing-blocked.d.ts +1 -1
  71. package/dist/core/agents/hourly-cadence.d.ts +102 -0
  72. package/dist/core/agents/hourly-cadence.js +126 -0
  73. package/dist/core/agents/loader-boot.js +23 -0
  74. package/dist/core/agents/loader.d.ts +19 -0
  75. package/dist/core/agents/loader.js +34 -2
  76. package/dist/core/agents/override-merge.d.ts +1 -1
  77. package/dist/core/agents/override-merge.js +9 -1
  78. package/dist/core/agents/recurrence-convert.d.ts +1 -1
  79. package/dist/core/agents/recurrence-convert.js +1 -1
  80. package/dist/core/agents/recurring-schedule-adapter.js +8 -0
  81. package/dist/core/alerts.js +6 -6
  82. package/dist/core/backends/auth-health-monitor.d.ts +2 -2
  83. package/dist/core/backends/auth-health-monitor.js +1 -1
  84. package/dist/core/backends/backend-router.d.ts +27 -1
  85. package/dist/core/backends/backend-router.js +165 -1
  86. package/dist/core/backends/claude-code-core.d.ts +71 -31
  87. package/dist/core/backends/claude-code-core.js +282 -54
  88. package/dist/core/backends/cli-quota-guards.d.ts +29 -1
  89. package/dist/core/backends/cli-quota-guards.js +40 -5
  90. package/dist/core/backends/codex-core.d.ts +6 -0
  91. package/dist/core/backends/codex-core.js +22 -6
  92. package/dist/core/backends/failure-spend.d.ts +58 -0
  93. package/dist/core/backends/failure-spend.js +137 -0
  94. package/dist/core/backends/gemini-cli-core.d.ts +6 -0
  95. package/dist/core/backends/gemini-cli-core.js +25 -6
  96. package/dist/core/backends/model-registry.d.ts +1 -1
  97. package/dist/core/backends/model-registry.js +4 -4
  98. package/dist/core/backends/opencode-core.d.ts +1 -1
  99. package/dist/core/backends/opencode-core.js +5 -5
  100. package/dist/core/backends/plan-presets.js +39 -15
  101. package/dist/core/bang-commands/commands-cost.js +3 -1
  102. package/dist/core/bang-commands/commands-report.js +4 -3
  103. package/dist/core/bang-commands/commands-research.js +4 -1
  104. package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
  105. package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
  106. package/dist/core/bang-commands/commands-stop-start.js +3 -3
  107. package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
  108. package/dist/core/bang-commands/commands-task-control.js +147 -0
  109. package/dist/core/bang-commands/commands-wiki.js +5 -5
  110. package/dist/core/bang-commands/index.d.ts +2 -0
  111. package/dist/core/bang-commands/index.js +12 -0
  112. package/dist/core/bang-commands/registry.d.ts +12 -0
  113. package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
  114. package/dist/core/browser-history/research-cluster-fanout.js +39 -16
  115. package/dist/core/channel-timeline.d.ts +5 -1
  116. package/dist/core/channel-timeline.js +13 -0
  117. package/dist/core/context/index-reconciler.js +5 -2
  118. package/dist/core/context/policy-index-reconciler.d.ts +6 -4
  119. package/dist/core/context/policy-index-runner.js +25 -6
  120. package/dist/core/context-builder-calendar.js +10 -2
  121. package/dist/core/context-builder-conversation.d.ts +8 -1
  122. package/dist/core/context-builder-conversation.js +41 -7
  123. package/dist/core/context-builder-yesterday.js +4 -3
  124. package/dist/core/context-builder.d.ts +7 -2
  125. package/dist/core/context-builder.js +62 -20
  126. package/dist/core/context-file-serializer.d.ts +1 -1
  127. package/dist/core/context-file-serializer.js +1 -1
  128. package/dist/core/context-health.js +2 -2
  129. package/dist/core/context-paths.d.ts +1 -1
  130. package/dist/core/context-paths.js +1 -1
  131. package/dist/core/context-validation/prepare-write.js +1 -1
  132. package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
  133. package/dist/core/context-vault-aliases.d.ts +0 -13
  134. package/dist/core/context-vault-aliases.js +37 -0
  135. package/dist/core/custom-routines.d.ts +99 -0
  136. package/dist/core/custom-routines.js +187 -0
  137. package/dist/core/daemon-api-cli.js +49 -0
  138. package/dist/core/day-boundary.d.ts +46 -0
  139. package/dist/core/day-boundary.js +40 -0
  140. package/dist/core/dispatcher-activity-scan.d.ts +221 -0
  141. package/dist/core/dispatcher-activity-scan.js +775 -0
  142. package/dist/core/dispatcher-error-handling.d.ts +6 -11
  143. package/dist/core/dispatcher-error-handling.js +38 -62
  144. package/dist/core/dispatcher-hourly-check.js +6 -1
  145. package/dist/core/dispatcher-message-handler.d.ts +10 -0
  146. package/dist/core/dispatcher-message-handler.js +17 -0
  147. package/dist/core/dispatcher-morning-routine.d.ts +6 -6
  148. package/dist/core/dispatcher-morning-routine.js +13 -13
  149. package/dist/core/dispatcher-result-processor.d.ts +33 -0
  150. package/dist/core/dispatcher-result-processor.js +167 -11
  151. package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
  152. package/dist/core/dispatcher-scheduled-background-task.js +89 -0
  153. package/dist/core/dispatcher-scheduled-tasks.d.ts +63 -1
  154. package/dist/core/dispatcher-scheduled-tasks.js +213 -6
  155. package/dist/core/dispatcher-task-delivery.d.ts +105 -0
  156. package/dist/core/dispatcher-task-delivery.js +555 -0
  157. package/dist/core/dispatcher-types.d.ts +48 -9
  158. package/dist/core/dispatcher-types.js +3 -3
  159. package/dist/core/dispatcher.d.ts +112 -31
  160. package/dist/core/dispatcher.js +284 -59
  161. package/dist/core/dm-freshness-metrics.d.ts +1 -1
  162. package/dist/core/drift-effects.js +2 -2
  163. package/dist/core/feedback/consolidation-prep.js +17 -5
  164. package/dist/core/feedback/eviction-scorer.js +6 -2
  165. package/dist/core/feedback/lesson-format.js +9 -4
  166. package/dist/core/feedback/lesson-injection.d.ts +1 -1
  167. package/dist/core/feedback/lesson-injection.js +17 -2
  168. package/dist/core/feedback/lesson-store-overview.d.ts +8 -4
  169. package/dist/core/feedback/lesson-store-overview.js +8 -4
  170. package/dist/core/feedback/regeneralization-prep.js +29 -16
  171. package/dist/core/feedback/self-performance-prep.d.ts +186 -0
  172. package/dist/core/feedback/self-performance-prep.js +541 -0
  173. package/dist/core/feedback/tuning-actuator.d.ts +198 -0
  174. package/dist/core/feedback/tuning-actuator.js +432 -0
  175. package/dist/core/feedback/tuning-recommender.d.ts +247 -0
  176. package/dist/core/feedback/tuning-recommender.js +580 -0
  177. package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
  178. package/dist/core/feedback/tuning-revert-monitor.js +213 -0
  179. package/dist/core/health-monitor.d.ts +6 -0
  180. package/dist/core/health-monitor.js +1 -1
  181. package/dist/core/injection-policy.d.ts +4 -4
  182. package/dist/core/injection-policy.js +4 -4
  183. package/dist/core/integration-main-backend.js +4 -0
  184. package/dist/core/management-md.d.ts +2 -2
  185. package/dist/core/management-md.js +51 -13
  186. package/dist/core/morning/orchestrator.d.ts +2 -2
  187. package/dist/core/morning/orchestrator.js +2 -2
  188. package/dist/core/notification-gate.d.ts +64 -0
  189. package/dist/core/notification-gate.js +51 -0
  190. package/dist/core/notification-rate-limit.d.ts +40 -0
  191. package/dist/core/notification-rate-limit.js +50 -0
  192. package/dist/core/policy-files.d.ts +1 -1
  193. package/dist/core/policy-files.js +2 -2
  194. package/dist/core/pre-pass-freshness.d.ts +4 -4
  195. package/dist/core/retention.d.ts +5 -0
  196. package/dist/core/retention.js +20 -4
  197. package/dist/core/review-context.d.ts +1 -1
  198. package/dist/core/review-context.js +10 -5
  199. package/dist/core/roadmap-write-lock.d.ts +2 -1
  200. package/dist/core/roadmap-write-lock.js +15 -10
  201. package/dist/core/routine-acquisition-plan.d.ts +47 -1
  202. package/dist/core/routine-acquisition-plan.js +78 -20
  203. package/dist/core/routine-fetch-window-retry.js +7 -4
  204. package/dist/core/routine-fetch-window-runner.d.ts +39 -3
  205. package/dist/core/routine-fetch-window-runner.js +264 -13
  206. package/dist/core/routine-windows.d.ts +2 -2
  207. package/dist/core/routine-windows.js +8 -5
  208. package/dist/core/scheduler.d.ts +175 -16
  209. package/dist/core/scheduler.js +559 -102
  210. package/dist/core/signal-detector.d.ts +12 -0
  211. package/dist/core/signal-detector.js +53 -9
  212. package/dist/core/skills-compiler-denied-tools.js +2 -2
  213. package/dist/core/skills-compiler-skill-index.d.ts +2 -2
  214. package/dist/core/skills-compiler-skill-index.js +2 -2
  215. package/dist/core/skills-compiler-variants.d.ts +1 -1
  216. package/dist/core/skills-compiler-variants.js +8 -0
  217. package/dist/core/skills-compiler.d.ts +29 -26
  218. package/dist/core/skills-compiler.js +117 -81
  219. package/dist/core/skills-manifest.d.ts +37 -0
  220. package/dist/core/skills-manifest.js +73 -2
  221. package/dist/core/sleep-inhibitor.d.ts +79 -0
  222. package/dist/core/sleep-inhibitor.js +132 -0
  223. package/dist/core/slim-system-prompt-loader.d.ts +77 -0
  224. package/dist/core/slim-system-prompt-loader.js +141 -0
  225. package/dist/core/spawn-gates.d.ts +126 -0
  226. package/dist/core/spawn-gates.js +180 -0
  227. package/dist/core/today-direct-writer.d.ts +2 -2
  228. package/dist/core/today-direct-writer.js +1 -1
  229. package/dist/core/today-write-lock.d.ts +4 -2
  230. package/dist/core/today-write-lock.js +30 -20
  231. package/dist/core/wake-detector.d.ts +55 -0
  232. package/dist/core/wake-detector.js +80 -0
  233. package/dist/core/wiki/compile-lock.d.ts +1 -1
  234. package/dist/core/wiki/compile-lock.js +1 -1
  235. package/dist/core/workdir.js +15 -6
  236. package/dist/db/activity-scan-signals.d.ts +77 -0
  237. package/dist/db/activity-scan-signals.js +378 -0
  238. package/dist/db/agents-store.d.ts +28 -0
  239. package/dist/db/agents-store.js +62 -0
  240. package/dist/db/background-task-clarifications-store.d.ts +81 -0
  241. package/dist/db/background-task-clarifications-store.js +152 -0
  242. package/dist/db/background-task-store.d.ts +207 -0
  243. package/dist/db/background-task-store.js +380 -0
  244. package/dist/db/browser-history-store.d.ts +39 -6
  245. package/dist/db/browser-history-store.js +51 -7
  246. package/dist/db/browser-task-clarifications-store.d.ts +12 -0
  247. package/dist/db/browser-task-clarifications-store.js +35 -5
  248. package/dist/db/browser-task-store.d.ts +3 -0
  249. package/dist/db/browser-task-store.js +29 -4
  250. package/dist/db/deferred-dm.d.ts +86 -0
  251. package/dist/db/deferred-dm.js +199 -0
  252. package/dist/db/migrations.js +330 -0
  253. package/dist/db/observations.d.ts +2 -2
  254. package/dist/db/observations.js +3 -3
  255. package/dist/db/schema.js +217 -16
  256. package/dist/db/voice-transcripts-store.d.ts +1 -1
  257. package/dist/index.js +86 -29
  258. package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
  259. package/dist/messaging/browser-task-mcp-notifier.js +30 -151
  260. package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
  261. package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
  262. package/dist/observers/delegated-sync-worker.d.ts +6 -6
  263. package/dist/observers/delegated-sync-worker.js +10 -10
  264. package/dist/observers/git-delegated-cron.d.ts +1 -1
  265. package/dist/observers/git-delegated-cron.js +2 -2
  266. package/dist/observers/github-poller-classifier.d.ts +3 -3
  267. package/dist/observers/github-poller-classifier.js +3 -3
  268. package/dist/observers/imminent-event-scheduler.d.ts +1 -1
  269. package/dist/observers/imminent-event-scheduler.js +1 -1
  270. package/dist/observers/mail-poller.d.ts +1 -0
  271. package/dist/observers/mail-poller.js +42 -3
  272. package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
  273. package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
  274. package/dist/observers/observation-summarizer/worker.d.ts +2 -2
  275. package/dist/observers/observation-summarizer/worker.js +4 -4
  276. package/dist/observers/obsidian-watcher.d.ts +1 -1
  277. package/dist/observers/obsidian-watcher.js +1 -1
  278. package/dist/safety/agent-write-tracker.d.ts +4 -4
  279. package/dist/safety/agent-write-tracker.js +4 -4
  280. package/dist/safety/audit.d.ts +43 -5
  281. package/dist/safety/audit.js +86 -18
  282. package/dist/safety/risk-classifier.d.ts +6 -0
  283. package/dist/safety/risk-classifier.js +75 -11
  284. package/dist/scheduler/activity-scan-gate.d.ts +86 -0
  285. package/dist/scheduler/activity-scan-gate.js +132 -0
  286. package/dist/services/background-task/background-task-budget.d.ts +80 -0
  287. package/dist/services/background-task/background-task-budget.js +91 -0
  288. package/dist/services/background-task/background-task-driver.d.ts +105 -0
  289. package/dist/services/background-task/background-task-driver.js +416 -0
  290. package/dist/services/background-task/background-task-runner.d.ts +96 -0
  291. package/dist/services/background-task/background-task-runner.js +673 -0
  292. package/dist/services/background-task/background-task-tools.d.ts +84 -0
  293. package/dist/services/background-task/background-task-tools.js +247 -0
  294. package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
  295. package/dist/services/background-task/background-task-transition-events.js +54 -0
  296. package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
  297. package/dist/services/browser-history/automation/egress-denylist.js +16 -6
  298. package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
  299. package/dist/services/browser-task/browser-task-runner.js +53 -8
  300. package/dist/services/observations-batch.d.ts +1 -1
  301. package/dist/services/observations-batch.js +2 -2
  302. package/dist/settings/runtime-settings.d.ts +38 -11
  303. package/dist/settings/runtime-settings.js +203 -40
  304. package/dist/settings/settings-store.js +11 -3
  305. package/package.json +4 -4
@@ -0,0 +1,673 @@
1
+ /**
2
+ * Background-task runner — BACKGROUND_TASK_RUNNER_DESIGN.md §4.1.
3
+ *
4
+ * The browser-task runner's lifecycle skeleton, generalized: it owns the
5
+ * per-task lifecycle from POST → terminal state, REUSING the pure slot
6
+ * manager (`browser-task-slots.ts`) with a synthetic per-task slot key so
7
+ * background tasks contend ONLY on the global concurrency cap (never on a
8
+ * per-key queue). The Playwright plane is gone; the worker is a generic
9
+ * SDK session (`background-task-driver.ts`).
10
+ *
11
+ * What this owns beyond browser-task's runner:
12
+ * - DELIVERY ENQUEUE — the runner's `reconcileDriverOutcome` reads the
13
+ * finished artifact and decides delivery in ONE place (the design's
14
+ * "post-transition hook"): completed + notify=true ⇒ enqueue result;
15
+ * parked ⇒ enqueue the open clarification; failed/timeout/no_finish ⇒
16
+ * FAIL-LOUD synthesize a failure artifact (notify=true) + enqueue;
17
+ * cancelled ⇒ no delivery (the owner cancelled). notify=false ⇒ file
18
+ * only. The periodic recovery sweep re-enqueues any lost delivery.
19
+ *
20
+ * I/O-shaped. Excluded from the 100% coverage gate; the pure logic lives
21
+ * in the reused slot manager + the budget envelope.
22
+ */
23
+ import { appendResolvedClarificationToBrief, getBackgroundTask, markRunning, markRunningFromParked, markTerminal, resetSingleForBootRedispatch, } from "../../db/background-task-store.js";
24
+ import { getClarification, getOpenClarificationForTask, } from "../../db/background-task-clarifications-store.js";
25
+ import { createLogger } from "../../logging.js";
26
+ import { prepareDriverHandle, releaseDriverHandle, resumeDriver, resumeFromBootDriver, runDriver, } from "./background-task-driver.js";
27
+ import { createInitialSlotState, decideAcquire, decideCancel, decidePark, decideRelease, decideUnpark, } from "../browser-task/browser-task-slots.js";
28
+ import { noopBackgroundTaskTransitionEmitter, } from "./background-task-transition-events.js";
29
+ const logger = createLogger("background-task-runner");
30
+ /** Synthetic per-task slot key — every background task gets a unique key
31
+ * so the reused per-siteKey slot manager only ever contends them against
32
+ * the global `maxConcurrent` cap, never against each other on a shared
33
+ * queue. `bg:` prefix is a grep-friendly debugging affordance. */
34
+ function slotKeyForTask(id) {
35
+ return `bg:${id}`;
36
+ }
37
+ export function createBackgroundTaskSlotStateRef(maxConcurrent) {
38
+ return { state: createInitialSlotState(maxConcurrent) };
39
+ }
40
+ export function createBackgroundTaskRunner(deps) {
41
+ const now = deps.nowFn ?? (() => Date.now());
42
+ const emitter = deps.transitionEmitter ?? noopBackgroundTaskTransitionEmitter;
43
+ const resumeAcrossRestart = deps.resumeAcrossRestart ?? false;
44
+ const parkedHandles = new Map();
45
+ const liveHandles = new Map();
46
+ const pendingAborts = new Map();
47
+ function tryAcquire(taskId) {
48
+ const row = getBackgroundTask(deps.db, taskId);
49
+ if (!row)
50
+ return { promoted: false, blocked: 0 };
51
+ const entry = {
52
+ taskId: row.id,
53
+ siteKey: slotKeyForTask(row.id),
54
+ enqueuedAt: row.createdAt,
55
+ };
56
+ try {
57
+ const { state, effect } = decideAcquire(deps.slotStateRef.state, entry, now());
58
+ deps.slotStateRef.state = state;
59
+ if (effect.kind === "promoted")
60
+ return { promoted: true, blocked: 0 };
61
+ return { promoted: false, blocked: effect.globalPos };
62
+ }
63
+ catch (err) {
64
+ logger.warn({ err, taskId }, "background-task tryAcquire: slot manager already tracks this task — treating as no-op");
65
+ return { promoted: false, blocked: 0, alreadyTracked: true };
66
+ }
67
+ }
68
+ function releaseAndPromote(taskId) {
69
+ const result = decideRelease(deps.slotStateRef.state, taskId, now());
70
+ deps.slotStateRef.state = result.state;
71
+ void emitReleaseEffects(result.effects);
72
+ }
73
+ function parkSlot(taskId) {
74
+ try {
75
+ deps.slotStateRef.state = decidePark(deps.slotStateRef.state, taskId);
76
+ }
77
+ catch (err) {
78
+ logger.warn({ err, taskId }, "decidePark failed (slot telemetry)");
79
+ }
80
+ }
81
+ function unparkSlot(taskId) {
82
+ try {
83
+ deps.slotStateRef.state = decideUnpark(deps.slotStateRef.state, taskId);
84
+ }
85
+ catch (err) {
86
+ logger.warn({ err, taskId }, "decideUnpark failed (slot telemetry)");
87
+ }
88
+ }
89
+ async function emitReleaseEffects(effects) {
90
+ for (const e of effects) {
91
+ if (e.kind !== "promoted")
92
+ continue;
93
+ const startedAt = now();
94
+ const runningRow = markRunning(deps.db, e.taskId, startedAt);
95
+ if (!runningRow) {
96
+ pendingAborts.delete(e.taskId);
97
+ releaseAndPromote(e.taskId);
98
+ continue;
99
+ }
100
+ emitter.emitFromRow(runningRow, startedAt);
101
+ void runDriverFromPending(e.taskId).catch((err) => {
102
+ logger.error({ err, taskId: e.taskId }, "background-task promoted-drive failed");
103
+ });
104
+ }
105
+ }
106
+ /** Synthesize a fail-loud artifact for a worker that died before
107
+ * `finish`. The owner asked for this task; silence on a requested task
108
+ * is the worse failure (§4.3). notify=true regardless of policy. */
109
+ function failLoudArtifact(row, outcomeDetail) {
110
+ const title = row.title ?? row.brief.slice(0, 80);
111
+ return {
112
+ draft: `That task ("${title}") couldn't finish: ${outcomeDetail}.`,
113
+ report: `Background task ${row.id} ("${title}") ended without a result.\n`
114
+ + `Outcome: ${outcomeDetail}.\n`
115
+ + `The worker did not call finish(), so no verbatim result was captured.`,
116
+ };
117
+ }
118
+ async function reconcileDriverOutcome(input) {
119
+ const { taskId, handle, result } = input;
120
+ // PARK — keep the handle alive for /clarify. The ask_user tool
121
+ // already moved the row to `awaiting_user`; enqueue the clarification
122
+ // delivery so the owner sees the question.
123
+ if (result.outcome === "yielded_for_clarification") {
124
+ parkedHandles.set(taskId, handle);
125
+ liveHandles.delete(taskId);
126
+ parkSlot(taskId);
127
+ await enqueueClarificationDelivery(taskId);
128
+ logger.info({ taskId }, "background-task parked — awaiting clarification");
129
+ return { ok: true, reason: "parked_awaiting_user", state: "awaiting_user" };
130
+ }
131
+ // COMPLETED — the finish tool wrote the artifact + terminal. Read it
132
+ // and enqueue the result delivery iff the worker set notify=true.
133
+ if (result.outcome === "completed") {
134
+ cleanupHandle(taskId);
135
+ if (deps.driver) {
136
+ await releaseDriverHandle(deps.driver, handle).catch((err) => {
137
+ logger.warn({ err, taskId }, "release driver handle failed (continuing)");
138
+ });
139
+ }
140
+ releaseAndPromote(taskId);
141
+ const row = getBackgroundTask(deps.db, taskId);
142
+ emitter.emitFromRow(row, now());
143
+ if (row && row.notify === true) {
144
+ await enqueueResultDelivery(row);
145
+ }
146
+ else {
147
+ logger.info({ taskId, notify: row?.notify }, "background-task completed with notify=false — filed, no push (§10.5)");
148
+ }
149
+ return { ok: true, reason: "completed", state: row?.state ?? "completed" };
150
+ }
151
+ // CANCELLED — the owner cancelled; no fail-loud delivery.
152
+ if (result.outcome === "cancelled") {
153
+ const finishedAt = now();
154
+ const terminal = markTerminal(deps.db, {
155
+ id: taskId,
156
+ state: "cancelled",
157
+ outcomeDetail: result.detail ?? "cancelled",
158
+ finishedAt,
159
+ });
160
+ emitter.emitFromRow(terminal, finishedAt);
161
+ cleanupHandle(taskId);
162
+ if (deps.driver) {
163
+ await releaseDriverHandle(deps.driver, handle).catch(() => { });
164
+ }
165
+ releaseAndPromote(taskId);
166
+ return { ok: false, reason: "cancelled", state: terminal?.state ?? "cancelled" };
167
+ }
168
+ // FAIL-LOUD terminals — worker died/timed out/exceeded budget without
169
+ // finish(). Synthesize the artifact (notify=true) so the owner always
170
+ // hears back on a requested task, then enqueue delivery.
171
+ const isTimeout = result.outcome === "timeout";
172
+ const terminalState = isTimeout ? "timeout" : "failed";
173
+ const outcomeDetail = result.detail ?? result.outcome;
174
+ const finishedAt = now();
175
+ const rowBefore = getBackgroundTask(deps.db, taskId);
176
+ const synthesized = rowBefore
177
+ ? failLoudArtifact(rowBefore, outcomeDetail)
178
+ : { report: null, draft: null };
179
+ const terminal = markTerminal(deps.db, {
180
+ id: taskId,
181
+ state: terminalState,
182
+ outcomeDetail,
183
+ finishedAt,
184
+ report: synthesized.report,
185
+ draft: synthesized.draft,
186
+ notify: true,
187
+ significance: `task failed (${outcomeDetail})`,
188
+ });
189
+ emitter.emitFromRow(terminal, finishedAt);
190
+ cleanupHandle(taskId);
191
+ if (deps.driver) {
192
+ await releaseDriverHandle(deps.driver, handle).catch((err) => {
193
+ logger.warn({ err, taskId }, "release driver handle failed (continuing)");
194
+ });
195
+ }
196
+ releaseAndPromote(taskId);
197
+ if (terminal)
198
+ await enqueueResultDelivery(terminal);
199
+ return { ok: false, reason: terminalState, state: terminal?.state ?? terminalState };
200
+ }
201
+ async function enqueueResultDelivery(row) {
202
+ if (!deps.deliveryEnqueuer)
203
+ return;
204
+ if (!row.draft)
205
+ return;
206
+ try {
207
+ await deps.deliveryEnqueuer.enqueueResult({
208
+ taskId: row.id,
209
+ originatingChannel: row.originatingChannel,
210
+ title: row.title ?? row.brief.slice(0, 80),
211
+ draft: row.draft,
212
+ report: row.report ?? row.draft,
213
+ });
214
+ }
215
+ catch (err) {
216
+ // Best-effort — the recovery sweep re-enqueues (notify=1 &
217
+ // delivered_at IS NULL) so a lost enqueue is not a lost result.
218
+ logger.warn({ err, taskId: row.id }, "background-task result delivery enqueue failed (recovery sweep will retry)");
219
+ }
220
+ }
221
+ async function enqueueClarificationDelivery(taskId) {
222
+ if (!deps.deliveryEnqueuer)
223
+ return;
224
+ const clar = getOpenClarificationForTask(deps.db, taskId);
225
+ if (!clar)
226
+ return;
227
+ const row = getBackgroundTask(deps.db, taskId);
228
+ try {
229
+ await deps.deliveryEnqueuer.enqueueClarification({
230
+ taskId,
231
+ originatingChannel: row?.originatingChannel ?? null,
232
+ title: row?.title ?? row?.brief.slice(0, 80) ?? `Task ${taskId}`,
233
+ clarificationId: clar.id,
234
+ question: clar.question,
235
+ contextSummary: clar.contextSummary,
236
+ });
237
+ }
238
+ catch (err) {
239
+ logger.warn({ err, taskId }, "background-task clarification delivery enqueue failed (recovery sweep will retry)");
240
+ }
241
+ }
242
+ function cleanupHandle(taskId) {
243
+ parkedHandles.delete(taskId);
244
+ liveHandles.delete(taskId);
245
+ pendingAborts.delete(taskId);
246
+ }
247
+ async function runDriverFromPending(taskId) {
248
+ if (!deps.driver) {
249
+ const finishedAt = now();
250
+ const terminal = markTerminal(deps.db, {
251
+ id: taskId,
252
+ state: "failed",
253
+ outcomeDetail: "runner_unavailable",
254
+ finishedAt,
255
+ report: "The background-task runner has no worker driver wired.",
256
+ draft: "That task couldn't start — the worker runtime is unavailable.",
257
+ notify: true,
258
+ });
259
+ emitter.emitFromRow(terminal, finishedAt);
260
+ pendingAborts.delete(taskId);
261
+ releaseAndPromote(taskId);
262
+ if (terminal)
263
+ await enqueueResultDelivery(terminal);
264
+ return { ok: false, reason: "no_driver", state: terminal?.state ?? "failed" };
265
+ }
266
+ const row = getBackgroundTask(deps.db, taskId);
267
+ if (!row) {
268
+ pendingAborts.delete(taskId);
269
+ releaseAndPromote(taskId);
270
+ return { ok: false, reason: "task_missing", state: null };
271
+ }
272
+ const prepared = await prepareDriverHandle({ deps: deps.driver, row });
273
+ if (!prepared.ok) {
274
+ const finishedAt = now();
275
+ const synthesized = failLoudArtifact(row, prepared.detail ?? prepared.reason);
276
+ const terminal = markTerminal(deps.db, {
277
+ id: taskId,
278
+ state: "failed",
279
+ outcomeDetail: prepared.detail ?? prepared.reason,
280
+ finishedAt,
281
+ report: synthesized.report,
282
+ draft: synthesized.draft,
283
+ notify: true,
284
+ });
285
+ emitter.emitFromRow(terminal, finishedAt);
286
+ pendingAborts.delete(taskId);
287
+ releaseAndPromote(taskId);
288
+ if (terminal)
289
+ await enqueueResultDelivery(terminal);
290
+ return { ok: false, reason: "failed", state: terminal?.state ?? "failed" };
291
+ }
292
+ const handle = prepared.handle;
293
+ liveHandles.set(taskId, handle);
294
+ const pendingAbort = pendingAborts.get(taskId);
295
+ if (pendingAbort !== undefined) {
296
+ pendingAborts.delete(taskId);
297
+ try {
298
+ handle.abortController.abort(new Error(pendingAbort));
299
+ }
300
+ catch (err) {
301
+ /* c8 ignore next 2 -- defensive */
302
+ logger.warn({ err, taskId, reason: pendingAbort }, "forwarding pending-cancel abort failed");
303
+ }
304
+ }
305
+ let result;
306
+ try {
307
+ result = await runDriver(deps.driver, row, handle);
308
+ }
309
+ catch (err) {
310
+ logger.error({ err, taskId }, "background-task driver threw");
311
+ result = {
312
+ outcome: "sdk_error",
313
+ sdkSessionId: handle.sdkSessionId,
314
+ detail: err instanceof Error ? err.message : String(err),
315
+ costUsd: 0,
316
+ numTurns: 0,
317
+ durationMs: 0,
318
+ };
319
+ }
320
+ return reconcileDriverOutcome({ taskId, handle, result });
321
+ }
322
+ async function runOnce(taskId) {
323
+ const row = getBackgroundTask(deps.db, taskId);
324
+ if (!row)
325
+ return { ok: false, reason: "task_missing", state: null };
326
+ if (row.state !== "pending") {
327
+ return { ok: false, reason: "already_terminal", state: row.state };
328
+ }
329
+ const { promoted, blocked, alreadyTracked } = tryAcquire(taskId);
330
+ if (alreadyTracked) {
331
+ return { ok: false, reason: "already_terminal", state: row.state };
332
+ }
333
+ if (!promoted) {
334
+ logger.info({ taskId, blocked }, "background-task queued — waiting for slot");
335
+ return { ok: true, reason: "queued", state: "pending" };
336
+ }
337
+ const startedAt = now();
338
+ const runningRow = markRunning(deps.db, taskId, startedAt);
339
+ if (!runningRow) {
340
+ pendingAborts.delete(taskId);
341
+ releaseAndPromote(taskId);
342
+ const afterRow = getBackgroundTask(deps.db, taskId);
343
+ return { ok: false, reason: "already_terminal", state: afterRow?.state ?? null };
344
+ }
345
+ emitter.emitFromRow(runningRow, startedAt);
346
+ return runDriverFromPending(taskId);
347
+ }
348
+ async function runFromPost(taskId) {
349
+ return runOnce(taskId);
350
+ }
351
+ async function runFromScheduleRow(taskId) {
352
+ return runOnce(taskId);
353
+ }
354
+ async function cancel(taskId, reason) {
355
+ const row = getBackgroundTask(deps.db, taskId);
356
+ if (!row)
357
+ return false;
358
+ const live = liveHandles.get(taskId);
359
+ const parked = parkedHandles.get(taskId);
360
+ const handle = live ?? parked;
361
+ if (handle) {
362
+ try {
363
+ handle.abortController.abort(new Error(reason || "cancel"));
364
+ }
365
+ catch (err) {
366
+ /* c8 ignore next 2 -- defensive */
367
+ logger.warn({ err, taskId }, "abort signal failed");
368
+ }
369
+ }
370
+ else if (row.state === "running") {
371
+ pendingAborts.set(taskId, reason || "cancel");
372
+ }
373
+ else if (row.state === "pending") {
374
+ // Queued behind the concurrency cap, not yet running — remove it
375
+ // from the slot FIFO and write the terminal directly. Without this
376
+ // the row stays `pending` forever and the FIFO entry leaks a slot
377
+ // reservation. `decideCancel` throws only if the task is the active
378
+ // occupant — by construction a `pending` DB row never is (markRunning
379
+ // runs synchronously after acquire), so the catch is defensive.
380
+ try {
381
+ deps.slotStateRef.state = decideCancel(deps.slotStateRef.state, taskId).state;
382
+ }
383
+ catch (err) {
384
+ logger.warn({ err, taskId }, "decideCancel on pending row failed (continuing)");
385
+ }
386
+ const finishedAt = now();
387
+ const terminal = markTerminal(deps.db, {
388
+ id: taskId,
389
+ state: "cancelled",
390
+ outcomeDetail: `cancelled_in_queue:${reason}`,
391
+ finishedAt,
392
+ });
393
+ emitter.emitFromRow(terminal, finishedAt);
394
+ logger.info({ taskId, reason }, "background-task cancel (pending → cancelled)");
395
+ return true;
396
+ }
397
+ // Parked tasks aren't iterating the SDK, so abort alone won't unwind —
398
+ // walk the terminal path manually (no fail-loud delivery on cancel).
399
+ if (parked && !live) {
400
+ parkedHandles.delete(taskId);
401
+ if (deps.driver) {
402
+ await releaseDriverHandle(deps.driver, parked).catch(() => { });
403
+ }
404
+ const finishedAt = now();
405
+ const terminal = markTerminal(deps.db, {
406
+ id: taskId,
407
+ state: "cancelled",
408
+ outcomeDetail: reason,
409
+ finishedAt,
410
+ });
411
+ emitter.emitFromRow(terminal, finishedAt);
412
+ pendingAborts.delete(taskId);
413
+ releaseAndPromote(taskId);
414
+ }
415
+ logger.info({ taskId, reason, currentState: row.state, hadLive: !!live, hadParked: !!parked }, "background-task cancel");
416
+ return true;
417
+ }
418
+ async function resumeAfterClarification(input) {
419
+ const row = getBackgroundTask(deps.db, input.taskId);
420
+ const parked = parkedHandles.get(input.taskId);
421
+ // The task vanished (retention prune / manual delete) — drop any stray
422
+ // handle and free its slot.
423
+ if (!row) {
424
+ if (parked) {
425
+ parkedHandles.delete(input.taskId);
426
+ if (deps.driver)
427
+ await releaseDriverHandle(deps.driver, parked).catch(() => { });
428
+ }
429
+ releaseAndPromote(input.taskId);
430
+ return { ok: false, reason: "task_missing", state: null };
431
+ }
432
+ // (1) Warm in-memory handle (no restart since the park) — resume the
433
+ // live SDK session. The slot was held across the park, so unpark it.
434
+ if (parked && deps.driver) {
435
+ parkedHandles.delete(input.taskId);
436
+ liveHandles.set(input.taskId, parked);
437
+ unparkSlot(input.taskId);
438
+ return driveResumeOrFallback(input, row, parked, deps.driver);
439
+ }
440
+ // A parked handle with no driver to drive it is pathological — drop it
441
+ // and fall through to the re-dispatch floor.
442
+ if (parked)
443
+ parkedHandles.delete(input.taskId);
444
+ // (2) Cross-restart: the in-memory handle was lost with the prior
445
+ // process. Reconstruct + resume the warm SDK session (§10.2) ONLY
446
+ // when resume is enabled, a session id was persisted, AND a slot is
447
+ // immediately free. Every other case — and any resume that can't
448
+ // load the session (§driveResumeOrFallback) — degrades to the
449
+ // zero-regression floor: re-dispatch from the brief with the owner's
450
+ // answer folded in (so the cold re-run doesn't re-ask). This mirrors
451
+ // `resumeFromBoot`; a clarify-after-restart therefore never
452
+ // fail-louds a recoverable task, over-commits a slot, or loses the
453
+ // owner's already-consumed answer.
454
+ if (resumeAcrossRestart && deps.driver && row.backendSessionId) {
455
+ const { promoted } = tryAcquire(input.taskId);
456
+ if (promoted) {
457
+ const prepared = await prepareDriverHandle({ deps: deps.driver, row });
458
+ if (prepared.ok) {
459
+ liveHandles.set(input.taskId, prepared.handle);
460
+ logger.info({ taskId: input.taskId }, "background-task clarify-after-restart — reconstructed handle from persisted session id");
461
+ return driveResumeOrFallback(input, row, prepared.handle, deps.driver);
462
+ }
463
+ // Reconstruction failed — release the slot we just took, then
464
+ // re-dispatch.
465
+ releaseAndPromote(input.taskId);
466
+ logger.warn({ taskId: input.taskId, reason: prepared.reason }, "background-task clarify-after-restart — handle reconstruction failed; re-dispatching from brief");
467
+ }
468
+ else {
469
+ // Concurrency cap full — tryAcquire queued the task; re-dispatch
470
+ // resets the row to pending so that queued FIFO entry drives it
471
+ // fresh (the same pattern resumeFromBoot uses).
472
+ logger.info({ taskId: input.taskId }, "background-task clarify-after-restart — concurrency cap full; re-dispatching from brief");
473
+ }
474
+ }
475
+ return redispatchAfterClarification(input);
476
+ }
477
+ /** Resume a parked/reconstructed worker with the owner's answer; if the
478
+ * warm SDK session can't load (`resume_unavailable`), degrade to a cold
479
+ * re-dispatch-with-answer rather than fail-louding (§10.2). */
480
+ async function driveResumeOrFallback(input, row, handle, driver) {
481
+ const resumedAt = now();
482
+ const resumed = markRunningFromParked(deps.db, input.taskId);
483
+ if (resumed)
484
+ emitter.emitFromRow(resumed, resumedAt);
485
+ let result;
486
+ try {
487
+ result = await resumeDriver(driver, row, handle, input.answer);
488
+ }
489
+ catch (err) {
490
+ logger.error({ err, taskId: input.taskId }, "background-task resume threw");
491
+ result = {
492
+ outcome: "sdk_error",
493
+ sdkSessionId: handle.sdkSessionId,
494
+ detail: err instanceof Error ? err.message : String(err),
495
+ costUsd: 0,
496
+ numTurns: 0,
497
+ durationMs: 0,
498
+ };
499
+ }
500
+ if (result.outcome === "resume_unavailable") {
501
+ logger.warn({ taskId: input.taskId, detail: result.detail }, "background-task clarify resume unavailable — re-dispatching from brief with the answer");
502
+ cleanupHandle(input.taskId);
503
+ await releaseDriverHandle(driver, handle).catch(() => { });
504
+ releaseAndPromote(input.taskId);
505
+ return redispatchAfterClarification(input);
506
+ }
507
+ return reconcileDriverOutcome({ taskId: input.taskId, handle, result });
508
+ }
509
+ /** Zero-regression fallback for a clarify that can't reuse the warm SDK
510
+ * session: fold the owner's just-answered clarification into the brief
511
+ * (the route already CAS-resolved it) and re-dispatch from the brief, so
512
+ * the cold re-run has the answer and doesn't re-ask. */
513
+ async function redispatchAfterClarification(input) {
514
+ const clar = getClarification(deps.db, input.clarificationId);
515
+ appendResolvedClarificationToBrief(deps.db, input.taskId, clar?.question ?? null, input.answer);
516
+ return redispatchFromBrief(input.taskId);
517
+ }
518
+ /**
519
+ * Phase 4 (§10.2) — boot recovery for ONE non-terminal task. A `running`
520
+ * task that captured an SDK session id is resumed via `query({resume})`
521
+ * (warm transcript + prompt cache survive the restart); everything else,
522
+ * and any resume that can't load the session, re-dispatches from the
523
+ * self-contained brief. Resume is a pure optimization — every failure
524
+ * path degrades to the proven v1 re-dispatch behaviour.
525
+ */
526
+ async function resumeFromBoot(taskId) {
527
+ const row = getBackgroundTask(deps.db, taskId);
528
+ if (!row)
529
+ return { ok: false, reason: "task_missing", state: null };
530
+ if (!resumeAcrossRestart
531
+ || !deps.driver
532
+ || !row.backendSessionId
533
+ || row.state !== "running") {
534
+ return redispatchFromBrief(taskId);
535
+ }
536
+ const { promoted, alreadyTracked } = tryAcquire(taskId);
537
+ if (alreadyTracked || !promoted) {
538
+ // Concurrency cap is full right now — re-dispatch (it queues, and the
539
+ // normal promotion path runs it fresh). Resume only when a slot is
540
+ // immediately free.
541
+ return redispatchFromBrief(taskId);
542
+ }
543
+ emitter.emitFromRow(row, now());
544
+ const prepared = await prepareDriverHandle({ deps: deps.driver, row });
545
+ if (!prepared.ok) {
546
+ const finishedAt = now();
547
+ const synthesized = failLoudArtifact(row, prepared.detail ?? prepared.reason);
548
+ const terminal = markTerminal(deps.db, {
549
+ id: taskId,
550
+ state: "failed",
551
+ outcomeDetail: prepared.detail ?? prepared.reason,
552
+ finishedAt,
553
+ report: synthesized.report,
554
+ draft: synthesized.draft,
555
+ notify: true,
556
+ });
557
+ emitter.emitFromRow(terminal, finishedAt);
558
+ releaseAndPromote(taskId);
559
+ if (terminal)
560
+ await enqueueResultDelivery(terminal);
561
+ return { ok: false, reason: "failed", state: terminal?.state ?? "failed" };
562
+ }
563
+ const handle = prepared.handle;
564
+ liveHandles.set(taskId, handle);
565
+ // A cancel that arrived during boot recovery (before this handle
566
+ // existed) parked its reason in `pendingAborts` — forward it now so the
567
+ // resume unwinds instead of dropping the cancel.
568
+ const pendingAbort = pendingAborts.get(taskId);
569
+ if (pendingAbort !== undefined) {
570
+ pendingAborts.delete(taskId);
571
+ try {
572
+ handle.abortController.abort(new Error(pendingAbort));
573
+ }
574
+ catch (err) {
575
+ /* c8 ignore next 2 -- defensive */
576
+ logger.warn({ err, taskId, reason: pendingAbort }, "forwarding pending-cancel abort failed (boot resume)");
577
+ }
578
+ }
579
+ let result;
580
+ try {
581
+ result = await resumeFromBootDriver(deps.driver, row, handle);
582
+ }
583
+ catch (err) {
584
+ result = {
585
+ outcome: "resume_unavailable",
586
+ sdkSessionId: handle.sdkSessionId,
587
+ detail: err instanceof Error ? err.message : String(err),
588
+ costUsd: 0,
589
+ numTurns: 0,
590
+ durationMs: 0,
591
+ };
592
+ }
593
+ if (result.outcome === "resume_unavailable") {
594
+ logger.warn({ taskId, detail: result.detail }, "background-task resume-across-restart unavailable — re-dispatching from brief");
595
+ cleanupHandle(taskId);
596
+ await releaseDriverHandle(deps.driver, handle).catch(() => { });
597
+ releaseAndPromote(taskId);
598
+ return redispatchFromBrief(taskId);
599
+ }
600
+ return reconcileDriverOutcome({ taskId, handle, result });
601
+ }
602
+ /** Reset a single non-terminal row to pending (clearing its lost session)
603
+ * and re-run its brief through the normal pending→running→drive path. */
604
+ async function redispatchFromBrief(taskId) {
605
+ resetSingleForBootRedispatch(deps.db, taskId, now());
606
+ return runOnce(taskId);
607
+ }
608
+ async function expireForDeadline(taskId, kind, waitedMs) {
609
+ const row = getBackgroundTask(deps.db, taskId);
610
+ if (!row)
611
+ return { ok: false, reason: "task_missing", state: null };
612
+ if (row.state === "completed"
613
+ || row.state === "failed"
614
+ || row.state === "timeout"
615
+ || row.state === "cancelled") {
616
+ return { ok: false, reason: "already_terminal", state: row.state };
617
+ }
618
+ const outcomeDetail = kind === "clarification_deadline" ? "clarification_deadline" : "queue_timeout";
619
+ const parked = parkedHandles.get(taskId);
620
+ const live = liveHandles.get(taskId);
621
+ const handle = parked ?? live;
622
+ if (handle) {
623
+ try {
624
+ handle.abortController.abort(new Error(outcomeDetail));
625
+ }
626
+ catch (err) {
627
+ /* c8 ignore next 2 -- defensive */
628
+ logger.warn({ err, taskId, kind }, "expireForDeadline: abort failed");
629
+ }
630
+ }
631
+ if (parked) {
632
+ parkedHandles.delete(taskId);
633
+ if (deps.driver)
634
+ await releaseDriverHandle(deps.driver, parked).catch(() => { });
635
+ }
636
+ const finishedAt = now();
637
+ const synthesized = failLoudArtifact(row, outcomeDetail);
638
+ const terminal = markTerminal(deps.db, {
639
+ id: taskId,
640
+ state: "timeout",
641
+ outcomeDetail,
642
+ finishedAt,
643
+ report: synthesized.report,
644
+ draft: synthesized.draft,
645
+ notify: true,
646
+ significance: `task timed out (${outcomeDetail})`,
647
+ });
648
+ emitter.emitFromRow(terminal, finishedAt);
649
+ pendingAborts.delete(taskId);
650
+ // For a live-only handle (clarification deadline racing a still-live
651
+ // run), the abort unwinds the SDK into reconcileDriverOutcome which
652
+ // releases + promotes; releasing here too would double-promote.
653
+ if (parked || !live) {
654
+ releaseAndPromote(taskId);
655
+ }
656
+ if (terminal)
657
+ await enqueueResultDelivery(terminal);
658
+ logger.info({ taskId, kind, hadParked: !!parked, hadLive: !!live, waitedMs }, "background-task expired for deadline");
659
+ return { ok: false, reason: "timeout", state: terminal?.state ?? "timeout" };
660
+ }
661
+ function __peekParkedIds() {
662
+ return Array.from(parkedHandles.keys());
663
+ }
664
+ return {
665
+ runFromPost,
666
+ runFromScheduleRow,
667
+ cancel,
668
+ resumeAfterClarification,
669
+ resumeFromBoot,
670
+ expireForDeadline,
671
+ __peekParkedIds,
672
+ };
673
+ }