@jungjaehoon/mama-os 0.32.0 → 0.32.2

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 (327) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +15 -13
  3. package/dist/agent/agent-loop.d.ts +10 -1
  4. package/dist/agent/agent-loop.d.ts.map +1 -1
  5. package/dist/agent/agent-loop.js +221 -103
  6. package/dist/agent/agent-loop.js.map +1 -1
  7. package/dist/agent/backend-model-policy.d.ts +9 -0
  8. package/dist/agent/backend-model-policy.d.ts.map +1 -0
  9. package/dist/agent/backend-model-policy.js +21 -0
  10. package/dist/agent/backend-model-policy.js.map +1 -0
  11. package/dist/agent/backend-model-runner-factory.d.ts +27 -0
  12. package/dist/agent/backend-model-runner-factory.d.ts.map +1 -0
  13. package/dist/agent/backend-model-runner-factory.js +85 -0
  14. package/dist/agent/backend-model-runner-factory.js.map +1 -0
  15. package/dist/agent/cline-cli-adapter.d.ts +124 -0
  16. package/dist/agent/cline-cli-adapter.d.ts.map +1 -0
  17. package/dist/agent/cline-cli-adapter.js +1040 -0
  18. package/dist/agent/cline-cli-adapter.js.map +1 -0
  19. package/dist/agent/cline-native-tool-policy.d.ts +3 -0
  20. package/dist/agent/cline-native-tool-policy.d.ts.map +1 -0
  21. package/dist/agent/cline-native-tool-policy.js +50 -0
  22. package/dist/agent/cline-native-tool-policy.js.map +1 -0
  23. package/dist/agent/code-act/constants.d.ts +1 -1
  24. package/dist/agent/code-act/constants.d.ts.map +1 -1
  25. package/dist/agent/code-act/constants.js +13 -1
  26. package/dist/agent/code-act/constants.js.map +1 -1
  27. package/dist/agent/code-act/host-bridge.d.ts.map +1 -1
  28. package/dist/agent/code-act/host-bridge.js +35 -74
  29. package/dist/agent/code-act/host-bridge.js.map +1 -1
  30. package/dist/agent/codex-app-server-process.d.ts +5 -0
  31. package/dist/agent/codex-app-server-process.d.ts.map +1 -1
  32. package/dist/agent/codex-app-server-process.js +30 -0
  33. package/dist/agent/codex-app-server-process.js.map +1 -1
  34. package/dist/agent/codex-auxiliary-tools.d.ts +17 -0
  35. package/dist/agent/codex-auxiliary-tools.d.ts.map +1 -0
  36. package/dist/agent/codex-auxiliary-tools.js +267 -0
  37. package/dist/agent/codex-auxiliary-tools.js.map +1 -0
  38. package/dist/agent/codex-home.d.ts.map +1 -1
  39. package/dist/agent/codex-home.js +1 -0
  40. package/dist/agent/codex-home.js.map +1 -1
  41. package/dist/agent/gateway-tool-catalog.d.ts +14 -0
  42. package/dist/agent/gateway-tool-catalog.d.ts.map +1 -0
  43. package/dist/agent/gateway-tool-catalog.js +41 -0
  44. package/dist/agent/gateway-tool-catalog.js.map +1 -0
  45. package/dist/agent/gateway-tool-executor.d.ts +12 -1
  46. package/dist/agent/gateway-tool-executor.d.ts.map +1 -1
  47. package/dist/agent/gateway-tool-executor.js +149 -15
  48. package/dist/agent/gateway-tool-executor.js.map +1 -1
  49. package/dist/agent/gateway-tools.md +5 -7
  50. package/dist/agent/managed-agent-runtime-sync.d.ts.map +1 -1
  51. package/dist/agent/managed-agent-runtime-sync.js +17 -3
  52. package/dist/agent/managed-agent-runtime-sync.js.map +1 -1
  53. package/dist/agent/managed-agent-validation.js +1 -1
  54. package/dist/agent/managed-agent-validation.js.map +1 -1
  55. package/dist/agent/model-runner.d.ts +11 -1
  56. package/dist/agent/model-runner.d.ts.map +1 -1
  57. package/dist/agent/model-runner.js +12 -1
  58. package/dist/agent/model-runner.js.map +1 -1
  59. package/dist/agent/session-pool.d.ts +10 -9
  60. package/dist/agent/session-pool.d.ts.map +1 -1
  61. package/dist/agent/session-pool.js +12 -47
  62. package/dist/agent/session-pool.js.map +1 -1
  63. package/dist/agent/tool-registry.d.ts +6 -4
  64. package/dist/agent/tool-registry.d.ts.map +1 -1
  65. package/dist/agent/tool-registry.js +62 -30
  66. package/dist/agent/tool-registry.js.map +1 -1
  67. package/dist/agent/types.d.ts +36 -4
  68. package/dist/agent/types.d.ts.map +1 -1
  69. package/dist/agent/types.js.map +1 -1
  70. package/dist/api/graph-api.d.ts.map +1 -1
  71. package/dist/api/graph-api.js +15 -5
  72. package/dist/api/graph-api.js.map +1 -1
  73. package/dist/api/index.d.ts +10 -0
  74. package/dist/api/index.d.ts.map +1 -1
  75. package/dist/api/index.js +7 -16
  76. package/dist/api/index.js.map +1 -1
  77. package/dist/cli/commands/connector.d.ts +6 -1
  78. package/dist/cli/commands/connector.d.ts.map +1 -1
  79. package/dist/cli/commands/connector.js +61 -45
  80. package/dist/cli/commands/connector.js.map +1 -1
  81. package/dist/cli/commands/init.d.ts +1 -1
  82. package/dist/cli/commands/init.d.ts.map +1 -1
  83. package/dist/cli/commands/init.js +43 -10
  84. package/dist/cli/commands/init.js.map +1 -1
  85. package/dist/cli/commands/run.d.ts.map +1 -1
  86. package/dist/cli/commands/run.js +12 -0
  87. package/dist/cli/commands/run.js.map +1 -1
  88. package/dist/cli/commands/setup.d.ts +22 -0
  89. package/dist/cli/commands/setup.d.ts.map +1 -1
  90. package/dist/cli/commands/setup.js +79 -28
  91. package/dist/cli/commands/setup.js.map +1 -1
  92. package/dist/cli/commands/start.d.ts +13 -9
  93. package/dist/cli/commands/start.d.ts.map +1 -1
  94. package/dist/cli/commands/start.js +190 -97
  95. package/dist/cli/commands/start.js.map +1 -1
  96. package/dist/cli/commands/status.d.ts.map +1 -1
  97. package/dist/cli/commands/status.js +4 -0
  98. package/dist/cli/commands/status.js.map +1 -1
  99. package/dist/cli/config/config-manager.d.ts.map +1 -1
  100. package/dist/cli/config/config-manager.js +63 -14
  101. package/dist/cli/config/config-manager.js.map +1 -1
  102. package/dist/cli/config/types.d.ts +12 -5
  103. package/dist/cli/config/types.d.ts.map +1 -1
  104. package/dist/cli/config/types.js +3 -4
  105. package/dist/cli/config/types.js.map +1 -1
  106. package/dist/cli/index.js +6 -4
  107. package/dist/cli/index.js.map +1 -1
  108. package/dist/cli/runtime/agent-loop-init.d.ts +1 -1
  109. package/dist/cli/runtime/agent-loop-init.d.ts.map +1 -1
  110. package/dist/cli/runtime/agent-loop-init.js +8 -3
  111. package/dist/cli/runtime/agent-loop-init.js.map +1 -1
  112. package/dist/cli/runtime/api-routes-init.d.ts +38 -1
  113. package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
  114. package/dist/cli/runtime/api-routes-init.js +150 -30
  115. package/dist/cli/runtime/api-routes-init.js.map +1 -1
  116. package/dist/cli/runtime/api-server-init.d.ts +4 -0
  117. package/dist/cli/runtime/api-server-init.d.ts.map +1 -1
  118. package/dist/cli/runtime/api-server-init.js +3 -1
  119. package/dist/cli/runtime/api-server-init.js.map +1 -1
  120. package/dist/cli/runtime/connector-bootstrap.d.ts +8 -0
  121. package/dist/cli/runtime/connector-bootstrap.d.ts.map +1 -0
  122. package/dist/cli/runtime/connector-bootstrap.js +14 -0
  123. package/dist/cli/runtime/connector-bootstrap.js.map +1 -0
  124. package/dist/cli/runtime/connector-init.d.ts +4 -1
  125. package/dist/cli/runtime/connector-init.d.ts.map +1 -1
  126. package/dist/cli/runtime/connector-init.js +2 -8
  127. package/dist/cli/runtime/connector-init.js.map +1 -1
  128. package/dist/cli/runtime/envelope-bootstrap.d.ts +2 -1
  129. package/dist/cli/runtime/envelope-bootstrap.d.ts.map +1 -1
  130. package/dist/cli/runtime/envelope-bootstrap.js +7 -15
  131. package/dist/cli/runtime/envelope-bootstrap.js.map +1 -1
  132. package/dist/cli/runtime/gateway-init.d.ts +1 -1
  133. package/dist/cli/runtime/gateway-init.d.ts.map +1 -1
  134. package/dist/cli/runtime/gateway-init.js.map +1 -1
  135. package/dist/cli/runtime/mama-core-init.d.ts +10 -2
  136. package/dist/cli/runtime/mama-core-init.d.ts.map +1 -1
  137. package/dist/cli/runtime/mama-core-init.js +52 -68
  138. package/dist/cli/runtime/mama-core-init.js.map +1 -1
  139. package/dist/cli/runtime/memory-agent-init.d.ts.map +1 -1
  140. package/dist/cli/runtime/memory-agent-init.js +18 -5
  141. package/dist/cli/runtime/memory-agent-init.js.map +1 -1
  142. package/dist/cli/runtime/scheduler-init.d.ts +4 -0
  143. package/dist/cli/runtime/scheduler-init.d.ts.map +1 -1
  144. package/dist/cli/runtime/scheduler-init.js +24 -3
  145. package/dist/cli/runtime/scheduler-init.js.map +1 -1
  146. package/dist/cli/runtime/shutdown.d.ts +4 -0
  147. package/dist/cli/runtime/shutdown.d.ts.map +1 -1
  148. package/dist/cli/runtime/shutdown.js +30 -6
  149. package/dist/cli/runtime/shutdown.js.map +1 -1
  150. package/dist/cli/runtime/utilities.d.ts +2 -0
  151. package/dist/cli/runtime/utilities.d.ts.map +1 -1
  152. package/dist/cli/runtime/utilities.js +33 -0
  153. package/dist/cli/runtime/utilities.js.map +1 -1
  154. package/dist/connectors/index.d.ts +4 -1
  155. package/dist/connectors/index.d.ts.map +1 -1
  156. package/dist/connectors/index.js +3 -2
  157. package/dist/connectors/index.js.map +1 -1
  158. package/dist/connectors/private-connector-policy.d.ts +150 -0
  159. package/dist/connectors/private-connector-policy.d.ts.map +1 -0
  160. package/dist/connectors/private-connector-policy.js +289 -0
  161. package/dist/connectors/private-connector-policy.js.map +1 -0
  162. package/dist/connectors/private-prompt-overlay.d.ts +17 -0
  163. package/dist/connectors/private-prompt-overlay.d.ts.map +1 -0
  164. package/dist/connectors/private-prompt-overlay.js +156 -0
  165. package/dist/connectors/private-prompt-overlay.js.map +1 -0
  166. package/dist/db/migrations/operator-task-external-lifecycle.d.ts +8 -0
  167. package/dist/db/migrations/operator-task-external-lifecycle.d.ts.map +1 -0
  168. package/dist/db/migrations/operator-task-external-lifecycle.js +169 -0
  169. package/dist/db/migrations/operator-task-external-lifecycle.js.map +1 -0
  170. package/dist/envelope/enforcer.js +8 -7
  171. package/dist/envelope/enforcer.js.map +1 -1
  172. package/dist/envelope/reactive-config.d.ts.map +1 -1
  173. package/dist/envelope/reactive-config.js +2 -1
  174. package/dist/envelope/reactive-config.js.map +1 -1
  175. package/dist/envelope/tool-connector-scope.d.ts.map +1 -1
  176. package/dist/envelope/tool-connector-scope.js +4 -0
  177. package/dist/envelope/tool-connector-scope.js.map +1 -1
  178. package/dist/gateways/discord.d.ts.map +1 -1
  179. package/dist/gateways/discord.js +11 -6
  180. package/dist/gateways/discord.js.map +1 -1
  181. package/dist/gateways/image-analyzer.d.ts +5 -0
  182. package/dist/gateways/image-analyzer.d.ts.map +1 -1
  183. package/dist/gateways/image-analyzer.js +8 -0
  184. package/dist/gateways/image-analyzer.js.map +1 -1
  185. package/dist/gateways/message-router.d.ts +11 -1
  186. package/dist/gateways/message-router.d.ts.map +1 -1
  187. package/dist/gateways/message-router.js +146 -52
  188. package/dist/gateways/message-router.js.map +1 -1
  189. package/dist/gateways/slack.d.ts.map +1 -1
  190. package/dist/gateways/slack.js +6 -2
  191. package/dist/gateways/slack.js.map +1 -1
  192. package/dist/gateways/telegram-message-ledger.d.ts +8 -1
  193. package/dist/gateways/telegram-message-ledger.d.ts.map +1 -1
  194. package/dist/gateways/telegram-message-ledger.js +141 -24
  195. package/dist/gateways/telegram-message-ledger.js.map +1 -1
  196. package/dist/gateways/telegram.d.ts +2 -0
  197. package/dist/gateways/telegram.d.ts.map +1 -1
  198. package/dist/gateways/telegram.js +19 -12
  199. package/dist/gateways/telegram.js.map +1 -1
  200. package/dist/gateways/types.d.ts +1 -1
  201. package/dist/gateways/types.d.ts.map +1 -1
  202. package/dist/index.d.ts +1 -1
  203. package/dist/index.js +1 -1
  204. package/dist/multi-agent/agent-process-manager.d.ts +14 -4
  205. package/dist/multi-agent/agent-process-manager.d.ts.map +1 -1
  206. package/dist/multi-agent/agent-process-manager.js +144 -47
  207. package/dist/multi-agent/agent-process-manager.js.map +1 -1
  208. package/dist/multi-agent/dashboard-agent-persona.d.ts +3 -3
  209. package/dist/multi-agent/dashboard-agent-persona.d.ts.map +1 -1
  210. package/dist/multi-agent/dashboard-agent-persona.js +10 -13
  211. package/dist/multi-agent/dashboard-agent-persona.js.map +1 -1
  212. package/dist/multi-agent/memory-agent-persona.d.ts +1 -1
  213. package/dist/multi-agent/memory-agent-persona.d.ts.map +1 -1
  214. package/dist/multi-agent/memory-agent-persona.js +1 -1
  215. package/dist/multi-agent/runtime-process.d.ts +3 -0
  216. package/dist/multi-agent/runtime-process.d.ts.map +1 -1
  217. package/dist/multi-agent/runtime-process.js +10 -2
  218. package/dist/multi-agent/runtime-process.js.map +1 -1
  219. package/dist/multi-agent/types.d.ts +12 -2
  220. package/dist/multi-agent/types.d.ts.map +1 -1
  221. package/dist/multi-agent/types.js.map +1 -1
  222. package/dist/multi-agent/wiki-agent-persona.d.ts +2 -2
  223. package/dist/multi-agent/wiki-agent-persona.js +2 -2
  224. package/dist/multi-agent/workflow-types.d.ts +1 -1
  225. package/dist/multi-agent/workflow-types.d.ts.map +1 -1
  226. package/dist/onboarding/complete-autonomous-prompt.d.ts +1 -1
  227. package/dist/onboarding/complete-autonomous-prompt.d.ts.map +1 -1
  228. package/dist/onboarding/complete-autonomous-prompt.js +9 -9
  229. package/dist/operator/board-slot-instructions.d.ts +1 -1
  230. package/dist/operator/board-slot-instructions.js +2 -2
  231. package/dist/operator/board-slot-instructions.js.map +1 -1
  232. package/dist/operator/briefs.d.ts +3 -0
  233. package/dist/operator/briefs.d.ts.map +1 -1
  234. package/dist/operator/briefs.js +64 -3
  235. package/dist/operator/briefs.js.map +1 -1
  236. package/dist/operator/console-brief.d.ts +6 -3
  237. package/dist/operator/console-brief.d.ts.map +1 -1
  238. package/dist/operator/console-brief.js +39 -9
  239. package/dist/operator/console-brief.js.map +1 -1
  240. package/dist/operator/external-lifecycle-candidates.d.ts +56 -0
  241. package/dist/operator/external-lifecycle-candidates.d.ts.map +1 -0
  242. package/dist/operator/external-lifecycle-candidates.js +415 -0
  243. package/dist/operator/external-lifecycle-candidates.js.map +1 -0
  244. package/dist/operator/external-lifecycle.d.ts +82 -0
  245. package/dist/operator/external-lifecycle.d.ts.map +1 -0
  246. package/dist/operator/external-lifecycle.js +27 -0
  247. package/dist/operator/external-lifecycle.js.map +1 -0
  248. package/dist/operator/file-coordination.d.ts +7 -0
  249. package/dist/operator/file-coordination.d.ts.map +1 -0
  250. package/dist/operator/file-coordination.js +84 -0
  251. package/dist/operator/file-coordination.js.map +1 -0
  252. package/dist/operator/operator-trigger-loop.d.ts +19 -4
  253. package/dist/operator/operator-trigger-loop.d.ts.map +1 -1
  254. package/dist/operator/operator-trigger-loop.js +151 -23
  255. package/dist/operator/operator-trigger-loop.js.map +1 -1
  256. package/dist/operator/pending-report-store.d.ts +62 -3
  257. package/dist/operator/pending-report-store.d.ts.map +1 -1
  258. package/dist/operator/pending-report-store.js +297 -40
  259. package/dist/operator/pending-report-store.js.map +1 -1
  260. package/dist/operator/report-carry-delivery.d.ts +35 -0
  261. package/dist/operator/report-carry-delivery.d.ts.map +1 -0
  262. package/dist/operator/report-carry-delivery.js +55 -0
  263. package/dist/operator/report-carry-delivery.js.map +1 -0
  264. package/dist/operator/report-carry.d.ts +50 -30
  265. package/dist/operator/report-carry.d.ts.map +1 -1
  266. package/dist/operator/report-carry.js +323 -71
  267. package/dist/operator/report-carry.js.map +1 -1
  268. package/dist/operator/situation-report.d.ts +30 -11
  269. package/dist/operator/situation-report.d.ts.map +1 -1
  270. package/dist/operator/situation-report.js +79 -35
  271. package/dist/operator/situation-report.js.map +1 -1
  272. package/dist/operator/task-ledger.d.ts +101 -0
  273. package/dist/operator/task-ledger.d.ts.map +1 -1
  274. package/dist/operator/task-ledger.js +599 -124
  275. package/dist/operator/task-ledger.js.map +1 -1
  276. package/dist/operator/temporal-runtime.d.ts.map +1 -1
  277. package/dist/operator/temporal-runtime.js +3 -1
  278. package/dist/operator/temporal-runtime.js.map +1 -1
  279. package/dist/operator/temporal-worker.js +1 -1
  280. package/dist/operator/trigger-author.d.ts +13 -1
  281. package/dist/operator/trigger-author.d.ts.map +1 -1
  282. package/dist/operator/trigger-author.js +35 -0
  283. package/dist/operator/trigger-author.js.map +1 -1
  284. package/dist/operator/worker-run.d.ts +1 -1
  285. package/dist/operator/worker-run.d.ts.map +1 -1
  286. package/dist/operator/worker-run.js +27 -9
  287. package/dist/operator/worker-run.js.map +1 -1
  288. package/dist/operator/workorder-consumer.d.ts +12 -1
  289. package/dist/operator/workorder-consumer.d.ts.map +1 -1
  290. package/dist/operator/workorder-consumer.js +125 -6
  291. package/dist/operator/workorder-consumer.js.map +1 -1
  292. package/dist/operator/workorder-hooks.d.ts +7 -2
  293. package/dist/operator/workorder-hooks.d.ts.map +1 -1
  294. package/dist/operator/workorder-hooks.js +22 -0
  295. package/dist/operator/workorder-hooks.js.map +1 -1
  296. package/dist/operator/workorder-publishers.d.ts +9 -2
  297. package/dist/operator/workorder-publishers.d.ts.map +1 -1
  298. package/dist/operator/workorder-publishers.js +167 -5
  299. package/dist/operator/workorder-publishers.js.map +1 -1
  300. package/dist/scheduler/cron-worker.d.ts +8 -4
  301. package/dist/scheduler/cron-worker.d.ts.map +1 -1
  302. package/dist/scheduler/cron-worker.js +56 -39
  303. package/dist/scheduler/cron-worker.js.map +1 -1
  304. package/dist/setup/setup-actions.d.ts +26 -0
  305. package/dist/setup/setup-actions.d.ts.map +1 -0
  306. package/dist/setup/setup-actions.js +371 -0
  307. package/dist/setup/setup-actions.js.map +1 -0
  308. package/dist/setup/setup-prompt.d.ts +1 -1
  309. package/dist/setup/setup-prompt.d.ts.map +1 -1
  310. package/dist/setup/setup-prompt.js +28 -33
  311. package/dist/setup/setup-prompt.js.map +1 -1
  312. package/dist/setup/setup-server.d.ts +6 -1
  313. package/dist/setup/setup-server.d.ts.map +1 -1
  314. package/dist/setup/setup-server.js +55 -9
  315. package/dist/setup/setup-server.js.map +1 -1
  316. package/dist/setup/setup-websocket.d.ts +14 -2
  317. package/dist/setup/setup-websocket.d.ts.map +1 -1
  318. package/dist/setup/setup-websocket.js +121 -37
  319. package/dist/setup/setup-websocket.js.map +1 -1
  320. package/package.json +2 -2
  321. package/public/setup.html +9 -6
  322. package/public/viewer/js/modules/agents.js +5 -4
  323. package/public/viewer/js/modules/settings.js +34 -10
  324. package/public/viewer/src/modules/agents.ts +5 -4
  325. package/public/viewer/src/modules/settings.ts +49 -11
  326. package/public/viewer/src/utils/api.ts +3 -2
  327. package/scripts/generate-gateway-tools.ts +13 -3
@@ -23,9 +23,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
23
23
  exports.TaskLedger = exports.WORKORDER_CHANNEL_PREFIX = exports.TEMPORAL_WORKORDER_MAX_ATTEMPTS = exports.WORKORDER_KINDS = exports.TASK_KINDS = exports.TASK_PRIORITIES = exports.TASK_STATUSES = void 0;
24
24
  const node_crypto_1 = require("node:crypto");
25
25
  const operator_task_temporal_js_1 = require("../db/migrations/operator-task-temporal.js");
26
+ const operator_task_external_lifecycle_js_1 = require("../db/migrations/operator-task-external-lifecycle.js");
26
27
  const effects_js_1 = require("../evidence/effects.js");
27
28
  const temporal_effect_js_1 = require("./temporal-effect.js");
28
29
  const task_temporal_js_1 = require("./task-temporal.js");
30
+ const external_lifecycle_candidates_js_1 = require("./external-lifecycle-candidates.js");
31
+ const workorder_publishers_js_1 = require("./workorder-publishers.js");
29
32
  exports.TASK_STATUSES = [
30
33
  'pending',
31
34
  'in_progress',
@@ -258,6 +261,7 @@ class TaskLedger {
258
261
  }
259
262
  }
260
263
  (0, operator_task_temporal_js_1.applyOperatorTaskTemporalMigration)(this.db);
264
+ (0, operator_task_external_lifecycle_js_1.applyOperatorTaskExternalLifecycleMigration)(this.db);
261
265
  // Old-predicate unique index (no terminal exclusion) -> swap in place.
262
266
  const idxRow = this.db
263
267
  .prepare(`SELECT sql FROM sqlite_master WHERE type='index' AND name='idx_operator_tasks_source'`)
@@ -419,6 +423,432 @@ class TaskLedger {
419
423
  const row = this.db.prepare(`SELECT * FROM operator_tasks WHERE id = ?`).get(id);
420
424
  return row ? this.toRecord(row) : null;
421
425
  }
426
+ /** Returns one active, explicitly receipted external identity for an owner task. */
427
+ getExternalBinding(taskId) {
428
+ const row = this.db
429
+ .prepare(`SELECT id, revision, task_id, connector, source_type, external_source_id,
430
+ last_observation_seq, created_by_attempt_id
431
+ FROM operator_external_task_bindings
432
+ WHERE task_id = ? AND active = 1`)
433
+ .get(taskId);
434
+ return row ? this.externalBindingFromRow(row) : null;
435
+ }
436
+ /**
437
+ * Host-only lookup material for immutable external-lifecycle candidates.
438
+ * The caller supplies exact event/source ids obtained from the connector index;
439
+ * no connector prose is read through this ledger surface.
440
+ */
441
+ getExternalLifecycleCandidateSupport(eventIds, externalSourceIds) {
442
+ const uniqueEventIds = [...new Set(eventIds)];
443
+ const uniqueSourceIds = [...new Set(externalSourceIds)];
444
+ const directTaskIdsByEventId = new Map();
445
+ const effectTaskIdsByEventId = new Map();
446
+ const bindings = [];
447
+ if (uniqueEventIds.length > 0) {
448
+ const placeholders = uniqueEventIds.map(() => '?').join(',');
449
+ const directRows = this.db
450
+ .prepare(`SELECT source_event_id, id FROM operator_tasks
451
+ WHERE kind = 'owner' AND source_event_id IN (${placeholders})`)
452
+ .all(...uniqueEventIds);
453
+ for (const row of directRows) {
454
+ const ids = directTaskIdsByEventId.get(row.source_event_id) ?? [];
455
+ directTaskIdsByEventId.set(row.source_event_id, [...ids, row.id]);
456
+ }
457
+ const effectRows = this.db
458
+ .prepare(`SELECT DISTINCT json_each.value AS event_id, evidence_effects.target_id AS task_id
459
+ FROM evidence_effects, json_each(evidence_effects.source_event_ids_json)
460
+ WHERE evidence_effects.target_type = 'task'
461
+ AND json_each.value IN (${placeholders})`)
462
+ .all(...uniqueEventIds);
463
+ for (const row of effectRows) {
464
+ const taskId = Number(row.task_id);
465
+ if (!Number.isSafeInteger(taskId) || taskId < 1)
466
+ continue;
467
+ const ids = effectTaskIdsByEventId.get(row.event_id) ?? [];
468
+ effectTaskIdsByEventId.set(row.event_id, [...ids, taskId]);
469
+ }
470
+ }
471
+ if (uniqueSourceIds.length > 0) {
472
+ const placeholders = uniqueSourceIds.map(() => '?').join(',');
473
+ const rows = this.db
474
+ .prepare(`SELECT id, revision, task_id, connector, source_type, external_source_id,
475
+ last_observation_seq
476
+ FROM operator_external_task_bindings
477
+ WHERE active = 1 AND connector = 'kagemusha' AND source_type = 'kanban_card'
478
+ AND external_source_id IN (${placeholders})`)
479
+ .all(...uniqueSourceIds);
480
+ for (const row of rows) {
481
+ bindings.push({
482
+ bindingId: row.id,
483
+ bindingRevision: row.revision,
484
+ taskId: row.task_id,
485
+ connector: row.connector,
486
+ sourceType: row.source_type,
487
+ externalSourceId: row.external_source_id,
488
+ lastObservationSeq: row.last_observation_seq,
489
+ });
490
+ }
491
+ }
492
+ const taskIds = new Set();
493
+ for (const ids of directTaskIdsByEventId.values())
494
+ ids.forEach((id) => taskIds.add(id));
495
+ for (const ids of effectTaskIdsByEventId.values())
496
+ ids.forEach((id) => taskIds.add(id));
497
+ for (const binding of bindings)
498
+ taskIds.add(binding.taskId);
499
+ const tasksById = new Map();
500
+ if (taskIds.size > 0) {
501
+ const ids = [...taskIds];
502
+ const placeholders = ids.map(() => '?').join(',');
503
+ const rows = this.db
504
+ .prepare(`SELECT id, revision FROM operator_tasks WHERE kind = 'owner' AND id IN (${placeholders})`)
505
+ .all(...ids);
506
+ for (const row of rows)
507
+ tasksById.set(row.id, { taskId: row.id, revision: row.revision });
508
+ }
509
+ return { taskHints: { directTaskIdsByEventId, effectTaskIdsByEventId }, tasksById, bindings };
510
+ }
511
+ getReceiptedExternalCandidateIds(candidateIds) {
512
+ const ids = [...new Set(candidateIds)];
513
+ if (ids.length === 0)
514
+ return new Set();
515
+ const placeholders = ids.map(() => '?').join(',');
516
+ const rows = this.db
517
+ .prepare(`SELECT candidate_id FROM operator_external_binding_receipts WHERE candidate_id IN (${placeholders})
518
+ UNION
519
+ SELECT candidate_id FROM operator_external_lifecycle_receipts WHERE candidate_id IN (${placeholders})`)
520
+ .all(...ids, ...ids);
521
+ return new Set(rows.map((row) => row.candidate_id));
522
+ }
523
+ /**
524
+ * Recover a candidate only from the payload of the exact currently claimed
525
+ * board attempt. Revalidate the serialized payload: database bytes are not
526
+ * a capability merely because they originated in a previous host process.
527
+ */
528
+ loadBoardCandidate(attemptId, candidateId, kind) {
529
+ const attempt = this.getWorkOrderById(attemptId);
530
+ if (!attempt || attempt.workKind !== 'board' || attempt.status !== 'in_progress') {
531
+ throw new Error(`external lifecycle candidate requires claimed board attempt ${attemptId}`);
532
+ }
533
+ const { attempts: _attempts, ...payload } = attempt.payload;
534
+ (0, workorder_publishers_js_1.validateWorkOrderPayload)('board', payload);
535
+ if (payload.mode !== 'reconcile' || !payload.candidates) {
536
+ throw new Error(`external lifecycle candidate ${candidateId} is absent from attempt ${attemptId}`);
537
+ }
538
+ const candidatePayload = payload.candidates;
539
+ const candidates = kind === 'binding'
540
+ ? candidatePayload.bindingCandidates
541
+ : candidatePayload.lifecycleCandidates;
542
+ const candidate = candidates.find((value) => value.candidateId === candidateId);
543
+ if (!candidate) {
544
+ throw new Error(`external lifecycle ${kind} candidate ${candidateId} is absent from attempt ${attemptId}`);
545
+ }
546
+ return candidate;
547
+ }
548
+ /**
549
+ * Read the complete, immutable candidate set for a board attempt against
550
+ * its durable decision receipts. This deliberately works for stale claims
551
+ * as well as live in-progress attempts: recovery must never infer effects
552
+ * from a runner result when the database can answer exactly.
553
+ */
554
+ inspectBoardCandidateAttempt(attemptId) {
555
+ const attempt = this.getWorkOrderById(attemptId);
556
+ if (!attempt || attempt.workKind !== 'board') {
557
+ throw new Error(`board candidate inspection requires board attempt ${attemptId}`);
558
+ }
559
+ const { attempts: _attempts, ...payload } = attempt.payload;
560
+ (0, workorder_publishers_js_1.validateWorkOrderPayload)('board', payload);
561
+ if (payload.mode !== 'reconcile' || !payload.candidates) {
562
+ return { disposition: 'none' };
563
+ }
564
+ const candidates = payload.candidates;
565
+ const all = [...candidates.bindingCandidates, ...candidates.lifecycleCandidates];
566
+ if (all.length === 0) {
567
+ return { disposition: 'none' };
568
+ }
569
+ const outcomes = [];
570
+ const missingCandidateIds = [];
571
+ for (const candidate of all) {
572
+ const receipt = this.getExternalCandidateReceipt(candidate.candidateId);
573
+ if (!receipt) {
574
+ missingCandidateIds.push(candidate.candidateId);
575
+ continue;
576
+ }
577
+ if (receipt.kind !== candidate.kind) {
578
+ throw new Error(`external lifecycle candidate ${candidate.candidateId} has a receipt of the wrong kind`);
579
+ }
580
+ outcomes.push(receipt.outcome);
581
+ }
582
+ if (missingCandidateIds.length === 0) {
583
+ return { disposition: 'complete', outcomes };
584
+ }
585
+ if (missingCandidateIds.length === all.length) {
586
+ return { disposition: 'zero' };
587
+ }
588
+ return { disposition: 'partial', missingCandidateIds };
589
+ }
590
+ applyExternalBindingDecision(attemptId, input, origin) {
591
+ (0, external_lifecycle_candidates_js_1.validateExternalLifecycleDecision)('binding', input);
592
+ if (origin.workOrderAttemptId !== attemptId) {
593
+ throw new Error(`external binding decision requires trusted attempt origin ${attemptId}`);
594
+ }
595
+ let receipt = null;
596
+ this.db.exec('BEGIN IMMEDIATE');
597
+ try {
598
+ const candidate = this.loadBoardCandidate(attemptId, input.candidate_id, 'binding');
599
+ if (candidate.kind !== 'binding') {
600
+ throw new Error(`external lifecycle candidate ${input.candidate_id} has the wrong kind`);
601
+ }
602
+ if (input.expected_revision !== candidate.taskRevision) {
603
+ throw new Error(`external binding candidate ${candidate.candidateId} revision does not match its host snapshot`);
604
+ }
605
+ if (origin.causeEventIds?.length !== 1 || origin.causeEventIds[0] !== candidate.eventId) {
606
+ throw new Error(`external binding candidate ${candidate.candidateId} requires its exact host origin event`);
607
+ }
608
+ const existing = this.getExternalCandidateReceipt(candidate.candidateId);
609
+ if (existing) {
610
+ if (existing.kind !== 'binding') {
611
+ throw new Error(`external lifecycle candidate ${candidate.candidateId} already has another receipt kind`);
612
+ }
613
+ this.assertBindingReplayMatches(existing, candidate, input);
614
+ receipt = existing;
615
+ }
616
+ else {
617
+ const task = this.getRowById(candidate.taskId);
618
+ if (!task || task.kind !== 'owner') {
619
+ throw new Error(`external binding candidate ${candidate.candidateId} references no owner task`);
620
+ }
621
+ const outcome = task.revision === candidate.taskRevision
622
+ ? input.decision === 'bind'
623
+ ? 'bound'
624
+ : 'declined'
625
+ : 'superseded';
626
+ let bindingId;
627
+ if (outcome === 'bound') {
628
+ const collision = this.db
629
+ .prepare(`SELECT id, task_id, external_source_id FROM operator_external_task_bindings
630
+ WHERE active = 1 AND (task_id = ? OR (connector = ? AND source_type = ? AND external_source_id = ?))`)
631
+ .get(candidate.taskId, candidate.connector, candidate.sourceType, candidate.externalSourceId);
632
+ if (collision) {
633
+ throw new Error(`external binding conflict for task ${candidate.taskId} or ${candidate.externalSourceId}`);
634
+ }
635
+ const now = this.now();
636
+ const inserted = this.db
637
+ .prepare(`INSERT INTO operator_external_task_bindings
638
+ (task_id, connector, source_type, external_source_id, last_observation_seq,
639
+ created_by_attempt_id, active, created_at, updated_at)
640
+ VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`)
641
+ .run(candidate.taskId, candidate.connector, candidate.sourceType, candidate.externalSourceId, candidate.operatorObservationSeq, attemptId, now, now);
642
+ bindingId = Number(inserted.lastInsertRowid);
643
+ }
644
+ this.insertBindingReceipt(candidate, attemptId, input, outcome, bindingId, origin);
645
+ receipt = {
646
+ kind: 'binding',
647
+ candidateId: candidate.candidateId,
648
+ workOrderAttemptId: attemptId,
649
+ taskId: candidate.taskId,
650
+ outcome,
651
+ reason: input.reason,
652
+ ...(bindingId === undefined ? {} : { bindingId }),
653
+ };
654
+ }
655
+ this.db.exec('COMMIT');
656
+ }
657
+ catch (error) {
658
+ this.db.exec('ROLLBACK');
659
+ throw error;
660
+ }
661
+ return receipt;
662
+ }
663
+ applyExternalLifecycleDecision(attemptId, input, origin) {
664
+ (0, external_lifecycle_candidates_js_1.validateExternalLifecycleDecision)('lifecycle', input);
665
+ if (origin.workOrderAttemptId !== attemptId) {
666
+ throw new Error(`external lifecycle decision requires trusted attempt origin ${attemptId}`);
667
+ }
668
+ let receipt = null;
669
+ this.db.exec('BEGIN IMMEDIATE');
670
+ try {
671
+ const candidate = this.loadBoardCandidate(attemptId, input.candidate_id, 'lifecycle');
672
+ if (candidate.kind !== 'lifecycle') {
673
+ throw new Error(`external lifecycle candidate ${input.candidate_id} has the wrong kind`);
674
+ }
675
+ if (input.expected_revision !== candidate.taskRevision) {
676
+ throw new Error(`external lifecycle candidate ${candidate.candidateId} revision does not match its host snapshot`);
677
+ }
678
+ if (origin.causeEventIds?.length !== 1 || origin.causeEventIds[0] !== candidate.eventId) {
679
+ throw new Error(`external lifecycle candidate ${candidate.candidateId} requires its exact host origin event`);
680
+ }
681
+ const existing = this.getExternalCandidateReceipt(candidate.candidateId);
682
+ if (existing) {
683
+ if (existing.kind !== 'lifecycle') {
684
+ throw new Error(`external lifecycle candidate ${candidate.candidateId} already has another receipt kind`);
685
+ }
686
+ this.assertLifecycleReplayMatches(existing, candidate, input);
687
+ receipt = existing;
688
+ }
689
+ else {
690
+ const task = this.getRowById(candidate.taskId);
691
+ const binding = this.getExternalBinding(candidate.taskId);
692
+ if (!task || task.kind !== 'owner' || !binding) {
693
+ throw new Error(`external lifecycle candidate ${candidate.candidateId} has no active binding`);
694
+ }
695
+ const superseded = binding.id !== candidate.bindingId ||
696
+ binding.revision !== candidate.bindingRevision ||
697
+ binding.connector !== candidate.connector ||
698
+ binding.sourceType !== candidate.sourceType ||
699
+ binding.externalSourceId !== candidate.externalSourceId ||
700
+ binding.createdByAttemptId >= attemptId ||
701
+ task.revision !== candidate.taskRevision ||
702
+ candidate.operatorObservationSeq <= binding.lastObservationSeq;
703
+ const taskRevisionBefore = task.revision;
704
+ let taskRevisionAfter = task.revision;
705
+ let outcome;
706
+ if (superseded) {
707
+ outcome = 'superseded';
708
+ }
709
+ else if (input.decision === 'apply') {
710
+ const transitioned = this.transitionTaskInTransaction(candidate.taskId, { status: candidate.proposedStatus, latest_event: candidate.evidenceSummary }, origin, true);
711
+ taskRevisionAfter = transitioned.revision;
712
+ outcome = 'applied';
713
+ }
714
+ else {
715
+ outcome = 'retained';
716
+ }
717
+ if (outcome !== 'superseded') {
718
+ const updated = this.db
719
+ .prepare(`UPDATE operator_external_task_bindings
720
+ SET last_observation_seq = ?, revision = revision + 1, updated_at = ?
721
+ WHERE id = ? AND revision = ? AND last_observation_seq < ?`)
722
+ .run(candidate.operatorObservationSeq, this.now(), candidate.bindingId, candidate.bindingRevision, candidate.operatorObservationSeq);
723
+ if (updated.changes !== 1) {
724
+ throw new Error(`external lifecycle candidate ${candidate.candidateId} binding watermark changed`);
725
+ }
726
+ }
727
+ this.insertLifecycleReceipt(candidate, attemptId, input, outcome, taskRevisionBefore, taskRevisionAfter, origin);
728
+ receipt = {
729
+ kind: 'lifecycle',
730
+ candidateId: candidate.candidateId,
731
+ workOrderAttemptId: attemptId,
732
+ taskId: candidate.taskId,
733
+ outcome,
734
+ reason: input.reason,
735
+ taskRevisionBefore,
736
+ taskRevisionAfter,
737
+ };
738
+ }
739
+ this.db.exec('COMMIT');
740
+ }
741
+ catch (error) {
742
+ this.db.exec('ROLLBACK');
743
+ throw error;
744
+ }
745
+ return receipt;
746
+ }
747
+ getExternalCandidateReceipt(candidateId) {
748
+ const binding = this.db
749
+ .prepare(`SELECT candidate_id, workorder_attempt_id, task_id, outcome, reason, binding_id
750
+ FROM operator_external_binding_receipts WHERE candidate_id = ?`)
751
+ .get(candidateId);
752
+ const lifecycle = this.db
753
+ .prepare(`SELECT candidate_id, workorder_attempt_id, task_id, outcome, reason,
754
+ task_revision_before, task_revision_after
755
+ FROM operator_external_lifecycle_receipts WHERE candidate_id = ?`)
756
+ .get(candidateId);
757
+ if (binding && lifecycle) {
758
+ throw new Error(`external lifecycle candidate ${candidateId} has duplicate global receipts`);
759
+ }
760
+ const receiptKind = binding ? 'binding' : lifecycle ? 'lifecycle' : null;
761
+ const identity = this.db
762
+ .prepare(`SELECT receipt_kind FROM operator_external_receipt_identities WHERE candidate_id = ?`)
763
+ .get(candidateId);
764
+ if (receiptKind === null) {
765
+ if (identity) {
766
+ throw new Error(`external lifecycle candidate ${candidateId} has an orphan receipt identity`);
767
+ }
768
+ return null;
769
+ }
770
+ if (!identity || identity.receipt_kind !== receiptKind) {
771
+ throw new Error(`external lifecycle candidate ${candidateId} has inconsistent global receipt identity`);
772
+ }
773
+ if (binding) {
774
+ return {
775
+ kind: 'binding',
776
+ candidateId: binding.candidate_id,
777
+ workOrderAttemptId: binding.workorder_attempt_id,
778
+ taskId: binding.task_id,
779
+ outcome: binding.outcome,
780
+ reason: binding.reason,
781
+ ...(binding.binding_id === null ? {} : { bindingId: binding.binding_id }),
782
+ };
783
+ }
784
+ return lifecycle
785
+ ? {
786
+ kind: 'lifecycle',
787
+ candidateId: lifecycle.candidate_id,
788
+ workOrderAttemptId: lifecycle.workorder_attempt_id,
789
+ taskId: lifecycle.task_id,
790
+ outcome: lifecycle.outcome,
791
+ reason: lifecycle.reason,
792
+ taskRevisionBefore: lifecycle.task_revision_before,
793
+ taskRevisionAfter: lifecycle.task_revision_after,
794
+ }
795
+ : null;
796
+ }
797
+ externalBindingFromRow(row) {
798
+ return {
799
+ id: row.id,
800
+ revision: row.revision,
801
+ taskId: row.task_id,
802
+ connector: row.connector,
803
+ sourceType: row.source_type,
804
+ externalSourceId: row.external_source_id,
805
+ lastObservationSeq: row.last_observation_seq,
806
+ createdByAttemptId: row.created_by_attempt_id,
807
+ };
808
+ }
809
+ assertBindingReplayMatches(receipt, candidate, input) {
810
+ const stored = this.db
811
+ .prepare(`SELECT decision FROM operator_external_binding_receipts WHERE candidate_id = ?`)
812
+ .get(candidate.candidateId);
813
+ if (!stored ||
814
+ stored.decision !== input.decision ||
815
+ receipt.taskId !== candidate.taskId ||
816
+ receipt.reason !== input.reason) {
817
+ throw new Error(`external binding receipt ${candidate.candidateId} conflicts with replay decision`);
818
+ }
819
+ }
820
+ insertBindingReceipt(candidate, attemptId, input, outcome, bindingId, origin) {
821
+ this.db
822
+ .prepare(`INSERT INTO operator_external_binding_receipts
823
+ (candidate_id, decision, workorder_attempt_id, task_id, event_id, connector, source_type,
824
+ external_source_id, channel_partition, content_sha256, source_timestamp_ms,
825
+ operator_ingest_seq, operator_observation_seq, task_revision, outcome, reason, binding_id,
826
+ origin_run_id, origin_cause_event_ids, created_at)
827
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
828
+ .run(candidate.candidateId, input.decision, attemptId, candidate.taskId, candidate.eventId, candidate.connector, candidate.sourceType, candidate.externalSourceId, candidate.channelPartition, candidate.contentSha256, candidate.sourceTimestampMs, candidate.operatorIngestSeq, candidate.operatorObservationSeq, candidate.taskRevision, outcome, input.reason, bindingId ?? null, origin.runId ?? null, JSON.stringify(origin.causeEventIds ?? []), this.now());
829
+ }
830
+ assertLifecycleReplayMatches(receipt, candidate, input) {
831
+ const stored = this.db
832
+ .prepare(`SELECT decision FROM operator_external_lifecycle_receipts WHERE candidate_id = ?`)
833
+ .get(candidate.candidateId);
834
+ if (!stored ||
835
+ stored.decision !== input.decision ||
836
+ receipt.taskId !== candidate.taskId ||
837
+ receipt.reason !== input.reason) {
838
+ throw new Error(`external lifecycle receipt ${candidate.candidateId} conflicts with replay decision`);
839
+ }
840
+ }
841
+ insertLifecycleReceipt(candidate, attemptId, input, outcome, taskRevisionBefore, taskRevisionAfter, origin) {
842
+ this.db
843
+ .prepare(`INSERT INTO operator_external_lifecycle_receipts
844
+ (candidate_id, decision, workorder_attempt_id, task_id, event_id, connector, source_type,
845
+ external_source_id, channel_partition, content_sha256, source_timestamp_ms,
846
+ operator_ingest_seq, operator_observation_seq, binding_id, binding_revision,
847
+ task_revision_before, task_revision_after, outcome, reason, origin_run_id,
848
+ origin_cause_event_ids, created_at)
849
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
850
+ .run(candidate.candidateId, input.decision, attemptId, candidate.taskId, candidate.eventId, candidate.connector, candidate.sourceType, candidate.externalSourceId, candidate.channelPartition, candidate.contentSha256, candidate.sourceTimestampMs, candidate.operatorIngestSeq, candidate.operatorObservationSeq, candidate.bindingId, candidate.bindingRevision, taskRevisionBefore, taskRevisionAfter, outcome, input.reason, origin.runId ?? null, JSON.stringify(origin.causeEventIds ?? []), this.now());
851
+ }
422
852
  /**
423
853
  * Create a task. Idempotent under at-least-once delivery: a duplicate
424
854
  * (source_channel, source_event_id) UPSERTS - the existing row gets the new
@@ -585,6 +1015,37 @@ class TaskLedger {
585
1015
  (0, effects_js_1.recordUnattributedChange)(this.db, common, input.origin.causeKind ?? 'owner_message');
586
1016
  }
587
1017
  update(id, patch, origin = {}) {
1018
+ this.validateTaskUpdatePatch(patch);
1019
+ this.db.exec('BEGIN IMMEDIATE');
1020
+ try {
1021
+ const task = this.transitionTaskInTransaction(id, patch, origin);
1022
+ this.db.exec('COMMIT');
1023
+ return task;
1024
+ }
1025
+ catch (error) {
1026
+ this.db.exec('ROLLBACK');
1027
+ throw error;
1028
+ }
1029
+ }
1030
+ /**
1031
+ * The one mutation primitive for owner-task state. Callers own the outer
1032
+ * transaction so lifecycle receipts, effects, generation ownership, and the
1033
+ * row revision can commit or roll back as one unit.
1034
+ */
1035
+ transitionTaskInTransaction(id, patch, origin, skipCandidateGuard = false) {
1036
+ this.validateTaskUpdatePatch(patch);
1037
+ if (!skipCandidateGuard) {
1038
+ this.assertCandidateTaskMutationAllowed(id, patch, origin);
1039
+ }
1040
+ const existing = this.db.prepare('SELECT * FROM operator_tasks WHERE id = ?').get(id);
1041
+ if (!existing)
1042
+ throw new Error(`task_update: no task with id ${id}`);
1043
+ if (existing.kind === 'system') {
1044
+ throw new Error(`task_update: task ${id} is a system workorder row (host-managed)`);
1045
+ }
1046
+ return this.transitionOwnerTaskRowInTransaction(id, existing, patch, origin);
1047
+ }
1048
+ validateTaskUpdatePatch(patch) {
588
1049
  if (patch.status === 'failed') {
589
1050
  throw new Error(`task_update: 'failed' is a system-only status`);
590
1051
  }
@@ -598,138 +1059,152 @@ class TaskLedger {
598
1059
  if (patch.title !== undefined && patch.title.trim() === '') {
599
1060
  throw new Error('task title must be a non-empty string');
600
1061
  }
601
- this.db.exec('BEGIN IMMEDIATE');
602
- try {
603
- const existing = this.db.prepare('SELECT * FROM operator_tasks WHERE id = ?').get(id);
604
- if (!existing)
605
- throw new Error(`task_update: no task with id ${id}`);
606
- if (existing.kind === 'system') {
607
- throw new Error(`task_update: task ${id} is a system workorder row (host-managed)`);
1062
+ }
1063
+ transitionOwnerTaskRowInTransaction(id, existing, patch, origin) {
1064
+ const next = {
1065
+ title: existing.title,
1066
+ status: existing.status,
1067
+ priority: existing.priority,
1068
+ assignee: existing.assignee,
1069
+ deadline: existing.deadline,
1070
+ due_at: existing.due_at,
1071
+ deadline_offset_minutes: existing.deadline_offset_minutes,
1072
+ latest_event: existing.latest_event,
1073
+ confirmed: existing.confirmed,
1074
+ temporal_epoch: existing.temporal_epoch,
1075
+ temporal_reconciled_occurrence_key: existing.temporal_reconciled_occurrence_key,
1076
+ last_temporal_checked_at: existing.last_temporal_checked_at,
1077
+ next_temporal_check_at: existing.next_temporal_check_at,
1078
+ last_temporal_attempt_id: existing.last_temporal_attempt_id,
1079
+ };
1080
+ if (patch.title !== undefined)
1081
+ next.title = patch.title.trim();
1082
+ if (patch.status !== undefined)
1083
+ next.status = patch.status;
1084
+ if (patch.priority !== undefined)
1085
+ next.priority = patch.priority;
1086
+ if (patch.assignee !== undefined)
1087
+ next.assignee = patch.assignee;
1088
+ if (patch.latest_event !== undefined)
1089
+ next.latest_event = patch.latest_event;
1090
+ if (patch.confirmed !== undefined)
1091
+ next.confirmed = patch.confirmed ? 1 : 0;
1092
+ const hasDueAt = Object.prototype.hasOwnProperty.call(patch, 'due_at');
1093
+ const hasDeadline = Object.prototype.hasOwnProperty.call(patch, 'deadline');
1094
+ if (hasDueAt && patch.due_at !== null && patch.due_at !== undefined) {
1095
+ const exactDue = (0, task_temporal_js_1.parseExactDueAt)(patch.due_at);
1096
+ if (hasDeadline && patch.deadline !== exactDue.deadline) {
1097
+ throw new Error('task_update: due_at and deadline conflict');
608
1098
  }
609
- const next = {
610
- title: existing.title,
611
- status: existing.status,
612
- priority: existing.priority,
613
- assignee: existing.assignee,
614
- deadline: existing.deadline,
615
- due_at: existing.due_at,
616
- deadline_offset_minutes: existing.deadline_offset_minutes,
617
- latest_event: existing.latest_event,
618
- confirmed: existing.confirmed,
619
- temporal_epoch: existing.temporal_epoch,
620
- temporal_reconciled_occurrence_key: existing.temporal_reconciled_occurrence_key,
621
- last_temporal_checked_at: existing.last_temporal_checked_at,
622
- next_temporal_check_at: existing.next_temporal_check_at,
623
- last_temporal_attempt_id: existing.last_temporal_attempt_id,
624
- };
625
- if (patch.title !== undefined)
626
- next.title = patch.title.trim();
627
- if (patch.status !== undefined)
628
- next.status = patch.status;
629
- if (patch.priority !== undefined)
630
- next.priority = patch.priority;
631
- if (patch.assignee !== undefined)
632
- next.assignee = patch.assignee;
633
- if (patch.latest_event !== undefined)
634
- next.latest_event = patch.latest_event;
635
- if (patch.confirmed !== undefined)
636
- next.confirmed = patch.confirmed ? 1 : 0;
637
- const hasDueAt = Object.prototype.hasOwnProperty.call(patch, 'due_at');
638
- const hasDeadline = Object.prototype.hasOwnProperty.call(patch, 'deadline');
639
- if (hasDueAt && patch.due_at !== null && patch.due_at !== undefined) {
640
- const exactDue = (0, task_temporal_js_1.parseExactDueAt)(patch.due_at);
641
- if (hasDeadline && patch.deadline !== exactDue.deadline) {
642
- throw new Error('task_update: due_at and deadline conflict');
643
- }
644
- next.due_at = exactDue.dueAt;
645
- next.deadline = exactDue.deadline;
646
- next.deadline_offset_minutes = exactDue.offsetMinutes;
647
- }
648
- else if (hasDeadline) {
649
- next.deadline = patch.deadline ?? null;
650
- next.due_at = null;
651
- if (patch.deadline === null) {
652
- next.deadline_offset_minutes = null;
653
- }
1099
+ next.due_at = exactDue.dueAt;
1100
+ next.deadline = exactDue.deadline;
1101
+ next.deadline_offset_minutes = exactDue.offsetMinutes;
1102
+ }
1103
+ else if (hasDeadline) {
1104
+ next.deadline = patch.deadline ?? null;
1105
+ next.due_at = null;
1106
+ if (patch.deadline === null) {
1107
+ next.deadline_offset_minutes = null;
654
1108
  }
655
- else if (hasDueAt) {
656
- next.due_at = null;
1109
+ }
1110
+ else if (hasDueAt) {
1111
+ next.due_at = null;
1112
+ }
1113
+ const temporalChanged = next.due_at !== existing.due_at || next.deadline !== existing.deadline;
1114
+ const terminalToOpen = (existing.status === 'done' || existing.status === 'cancelled') &&
1115
+ next.status !== 'done' &&
1116
+ next.status !== 'cancelled';
1117
+ const openToTerminal = existing.status !== 'done' &&
1118
+ existing.status !== 'cancelled' &&
1119
+ (next.status === 'done' || next.status === 'cancelled');
1120
+ if (temporalChanged || terminalToOpen) {
1121
+ next.temporal_epoch = existing.temporal_epoch + 1;
1122
+ next.temporal_reconciled_occurrence_key = null;
1123
+ next.last_temporal_checked_at = null;
1124
+ next.next_temporal_check_at = null;
1125
+ next.last_temporal_attempt_id = null;
1126
+ }
1127
+ const persistedColumns = [
1128
+ 'title',
1129
+ 'status',
1130
+ 'priority',
1131
+ 'assignee',
1132
+ 'deadline',
1133
+ 'due_at',
1134
+ 'deadline_offset_minutes',
1135
+ 'latest_event',
1136
+ 'confirmed',
1137
+ 'temporal_epoch',
1138
+ 'temporal_reconciled_occurrence_key',
1139
+ 'last_temporal_checked_at',
1140
+ 'next_temporal_check_at',
1141
+ 'last_temporal_attempt_id',
1142
+ ];
1143
+ const changedColumns = persistedColumns.filter((column) => next[column] !== existing[column]);
1144
+ if (changedColumns.length === 0) {
1145
+ return this.toRecord(existing);
1146
+ }
1147
+ else {
1148
+ const nextRevision = existing.revision + 1;
1149
+ const sets = changedColumns.map((column) => `${column} = ?`);
1150
+ const values = changedColumns.map((column) => next[column]);
1151
+ const updatedAt = this.now();
1152
+ sets.push('revision = ?', 'updated_at = ?');
1153
+ values.push(nextRevision, updatedAt);
1154
+ const updated = this.db
1155
+ .prepare(`UPDATE operator_tasks SET ${sets.join(', ')} WHERE id = ? AND revision = ?`)
1156
+ .run(...values, id, existing.revision);
1157
+ if (updated.changes !== 1) {
1158
+ throw new Error(`task_update: task ${id} revision changed during transition`);
657
1159
  }
658
- const temporalChanged = next.due_at !== existing.due_at || next.deadline !== existing.deadline;
659
- const terminalToOpen = (existing.status === 'done' || existing.status === 'cancelled') &&
660
- next.status !== 'done' &&
661
- next.status !== 'cancelled';
662
- const openToTerminal = existing.status !== 'done' &&
663
- existing.status !== 'cancelled' &&
664
- (next.status === 'done' || next.status === 'cancelled');
1160
+ this.recordTaskChange({
1161
+ kind: 'task_update',
1162
+ taskId: id,
1163
+ origin,
1164
+ channel: existing.source_channel,
1165
+ payload: {
1166
+ revision: nextRevision,
1167
+ changed: Object.fromEntries(changedColumns.map((column) => [column, next[column]])),
1168
+ },
1169
+ atMs: updatedAt,
1170
+ });
665
1171
  if (temporalChanged || terminalToOpen) {
666
- next.temporal_epoch = existing.temporal_epoch + 1;
667
- next.temporal_reconciled_occurrence_key = null;
668
- next.last_temporal_checked_at = null;
669
- next.next_temporal_check_at = null;
670
- next.last_temporal_attempt_id = null;
1172
+ this.supersedeTemporalGenerationsInTransaction(id, Number(next.temporal_epoch));
671
1173
  }
672
- const persistedColumns = [
673
- 'title',
674
- 'status',
675
- 'priority',
676
- 'assignee',
677
- 'deadline',
678
- 'due_at',
679
- 'deadline_offset_minutes',
680
- 'latest_event',
681
- 'confirmed',
682
- 'temporal_epoch',
683
- 'temporal_reconciled_occurrence_key',
684
- 'last_temporal_checked_at',
685
- 'next_temporal_check_at',
686
- 'last_temporal_attempt_id',
687
- ];
688
- const changedColumns = persistedColumns.filter((column) => next[column] !== existing[column]);
689
- if (changedColumns.length === 0) {
690
- // Nothing moved, so nothing is recorded. This is the whole point of the ledger:
691
- // a call that ran is not a change, and the two must not be countable as one.
692
- //
693
- // The read-back happens after the catch, not here: a throw between COMMIT and
694
- // return would send this into the catch and issue ROLLBACK with no open
695
- // transaction, replacing the real error with a confusing one.
696
- this.db.exec('COMMIT');
697
- }
698
- else {
699
- const nextRevision = existing.revision + 1;
700
- const sets = changedColumns.map((column) => `${column} = ?`);
701
- const values = changedColumns.map((column) => next[column]);
702
- const updatedAt = this.now();
703
- sets.push('revision = ?', 'updated_at = ?');
704
- values.push(nextRevision, updatedAt);
705
- this.db
706
- .prepare(`UPDATE operator_tasks SET ${sets.join(', ')} WHERE id = ?`)
707
- .run(...values, id);
708
- this.recordTaskChange({
709
- kind: 'task_update',
710
- taskId: id,
711
- origin,
712
- channel: existing.source_channel,
713
- payload: {
714
- revision: nextRevision,
715
- changed: Object.fromEntries(changedColumns.map((column) => [column, next[column]])),
716
- },
717
- atMs: updatedAt,
718
- });
719
- if (temporalChanged || terminalToOpen) {
720
- this.supersedeTemporalGenerationsInTransaction(id, Number(next.temporal_epoch));
721
- }
722
- else if (openToTerminal) {
723
- this.supersedeAllActiveTemporalGenerationsInTransaction(id);
724
- }
725
- this.db.exec('COMMIT');
1174
+ else if (openToTerminal) {
1175
+ this.supersedeAllActiveTemporalGenerationsInTransaction(id);
726
1176
  }
1177
+ const transitioned = this.getRowById(id);
1178
+ if (!transitioned)
1179
+ throw new Error(`task_update: task ${id} disappeared during transition`);
1180
+ return transitioned;
727
1181
  }
728
- catch (error) {
729
- this.db.exec('ROLLBACK');
730
- throw error;
1182
+ }
1183
+ assertCandidateTaskMutationAllowed(taskId, patch, origin) {
1184
+ if (origin.workOrderAttemptId === undefined ||
1185
+ (!Object.prototype.hasOwnProperty.call(patch, 'status') &&
1186
+ !Object.prototype.hasOwnProperty.call(patch, 'latest_event'))) {
1187
+ return;
1188
+ }
1189
+ const attempt = this.getWorkOrderById(origin.workOrderAttemptId);
1190
+ // Unlike a lifecycle decision, this is a standing mutation guard. A late
1191
+ // direct or nested tool call can retain the host-issued attempt context
1192
+ // after its board workorder has reached a terminal status, and its durable
1193
+ // candidate payload must remain authoritative in that case. Decision
1194
+ // application still calls loadBoardCandidate(), which requires a claimed
1195
+ // in-progress attempt.
1196
+ if (!attempt || attempt.workKind !== 'board') {
1197
+ return;
1198
+ }
1199
+ const { attempts: _attempts, ...payload } = attempt.payload;
1200
+ (0, workorder_publishers_js_1.validateWorkOrderPayload)('board', payload);
1201
+ if (payload.mode !== 'reconcile' || !payload.candidates) {
1202
+ return;
1203
+ }
1204
+ const candidates = payload.candidates;
1205
+ if ([...candidates.bindingCandidates, ...candidates.lifecycleCandidates].some((candidate) => candidate.taskId === taskId)) {
1206
+ throw new Error(`task_update: candidate-bound lifecycle task ${taskId} may change status/latest_event only through its receipted decision`);
731
1207
  }
732
- return this.getById(id);
733
1208
  }
734
1209
  supersedeTemporalGenerationsInTransaction(taskId, currentEpoch, excludeGenerationKey, updatedAt = this.now()) {
735
1210
  const reason = 'task temporal occurrence superseded';