@vercel/factory 0.0.15 → 0.0.17

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 (214) hide show
  1. package/CHANGELOG.md +362 -0
  2. package/README.md +49 -261
  3. package/dist/agent-routes.d.mts +47 -3
  4. package/dist/agent-routes.mjs +28 -1
  5. package/dist/agent-routes.mjs.map +1 -1
  6. package/dist/api-contracts.d.mts +20 -1
  7. package/dist/api-contracts.mjs +2 -1
  8. package/dist/api-contracts.mjs.map +1 -1
  9. package/dist/api.d.mts +45 -2
  10. package/dist/api.mjs +199 -10
  11. package/dist/api.mjs.map +1 -1
  12. package/dist/approval-contracts.d.mts +6 -0
  13. package/dist/blob/index.d.mts +51 -14
  14. package/dist/blob/index.mjs +26 -10
  15. package/dist/blob/index.mjs.map +1 -1
  16. package/dist/budget.d.mts +7 -0
  17. package/dist/budget.mjs +6 -0
  18. package/dist/budget.mjs.map +1 -1
  19. package/dist/build-factory.d.mts +1 -0
  20. package/dist/change-verification/dispatch.d.mts +16 -3
  21. package/dist/change-verification/dispatch.mjs +45 -7
  22. package/dist/change-verification/dispatch.mjs.map +1 -1
  23. package/dist/change-verification/eve-tool.d.mts +2 -1
  24. package/dist/change-verification/eve-tool.mjs +35 -47
  25. package/dist/change-verification/eve-tool.mjs.map +1 -1
  26. package/dist/change-verification/result.mjs +130 -0
  27. package/dist/change-verification/result.mjs.map +1 -0
  28. package/dist/changes/eve-record-change.d.mts +2 -2
  29. package/dist/changes/eve-record-change.mjs +43 -9
  30. package/dist/changes/eve-record-change.mjs.map +1 -1
  31. package/dist/changes.d.mts +4 -3
  32. package/dist/changes.mjs +2 -2
  33. package/dist/changes.mjs.map +1 -1
  34. package/dist/client-events.d.mts +10 -3
  35. package/dist/client-events.mjs +6 -2
  36. package/dist/client-events.mjs.map +1 -1
  37. package/dist/client-stream.mjs +8 -2
  38. package/dist/client-stream.mjs.map +1 -1
  39. package/dist/client-transcript.mjs +5 -1
  40. package/dist/client-transcript.mjs.map +1 -1
  41. package/dist/client.d.mts +121 -12
  42. package/dist/client.mjs +117 -9
  43. package/dist/client.mjs.map +1 -1
  44. package/dist/code-review/contracts.d.mts +1 -0
  45. package/dist/code-review/eve-post-review.d.mts +4 -4
  46. package/dist/code-review/eve-post-review.mjs +29 -17
  47. package/dist/code-review/eve-post-review.mjs.map +1 -1
  48. package/dist/code-review/eve-review-comments.d.mts +13 -2
  49. package/dist/code-review/eve-review-comments.mjs +45 -11
  50. package/dist/code-review/eve-review-comments.mjs.map +1 -1
  51. package/dist/code-review/github-reporter.d.mts +2 -0
  52. package/dist/code-review/github-reporter.mjs +7 -5
  53. package/dist/code-review/github-reporter.mjs.map +1 -1
  54. package/dist/code-review.d.mts +3 -2
  55. package/dist/deepsec/eve-tool.mjs +3 -1
  56. package/dist/deepsec/eve-tool.mjs.map +1 -1
  57. package/dist/dispatch.d.mts +79 -8
  58. package/dist/dispatch.mjs +68 -9
  59. package/dist/dispatch.mjs.map +1 -1
  60. package/dist/eve/index.d.mts +70 -10
  61. package/dist/eve/index.mjs +93 -22
  62. package/dist/eve/index.mjs.map +1 -1
  63. package/dist/eve/invoke.mjs +24 -11
  64. package/dist/eve/invoke.mjs.map +1 -1
  65. package/dist/eve/session-client.d.mts +122 -4
  66. package/dist/eve/session-client.mjs +127 -13
  67. package/dist/eve/session-client.mjs.map +1 -1
  68. package/dist/eve/task-execution.d.mts +380 -0
  69. package/dist/eve/task-execution.mjs +57 -2
  70. package/dist/eve/task-execution.mjs.map +1 -1
  71. package/dist/eve/task-session.d.mts +44 -2
  72. package/dist/eve/task-session.mjs +44 -2
  73. package/dist/eve/task-session.mjs.map +1 -1
  74. package/dist/eve/transcript.mjs +5 -1
  75. package/dist/eve/transcript.mjs.map +1 -1
  76. package/dist/execution.d.mts +117 -9
  77. package/dist/execution.mjs +76 -6
  78. package/dist/execution.mjs.map +1 -1
  79. package/dist/finding-remediation/admission.d.mts +2 -0
  80. package/dist/finding-remediation/admission.mjs +4 -1
  81. package/dist/finding-remediation/admission.mjs.map +1 -1
  82. package/dist/findings.d.mts +1 -0
  83. package/dist/github-publication.d.mts +1 -0
  84. package/dist/github-publication.mjs +97 -84
  85. package/dist/github-publication.mjs.map +1 -1
  86. package/dist/github-transfer.d.mts +15 -6
  87. package/dist/github-transfer.mjs +214 -66
  88. package/dist/github-transfer.mjs.map +1 -1
  89. package/dist/github.d.mts +51 -11
  90. package/dist/github.mjs +126 -24
  91. package/dist/github.mjs.map +1 -1
  92. package/dist/inbox-activity.d.mts +53 -0
  93. package/dist/inbox-activity.mjs +41 -0
  94. package/dist/inbox-activity.mjs.map +1 -0
  95. package/dist/index.d.mts +3 -1
  96. package/dist/index.mjs +3 -2
  97. package/dist/intake-contracts.d.mts +0 -1
  98. package/dist/integrations/deepsec.d.mts +1 -0
  99. package/dist/integrations/github.d.mts +2 -2
  100. package/dist/integrations/github.mjs +2 -2
  101. package/dist/integrations/slack.d.mts +3 -1
  102. package/dist/integrations/slack.mjs +3 -1
  103. package/dist/integrations/vercel.d.mts +4 -2
  104. package/dist/integrations/vercel.mjs +3 -2
  105. package/dist/merge-resolution/eve-tools.d.mts +1 -0
  106. package/dist/merge-resolution/eve-tools.mjs +7 -2
  107. package/dist/merge-resolution/eve-tools.mjs.map +1 -1
  108. package/dist/model-settings.d.mts +41 -0
  109. package/dist/model-settings.mjs +35 -0
  110. package/dist/model-settings.mjs.map +1 -0
  111. package/dist/planning/reconcile.mjs +6 -0
  112. package/dist/planning/reconcile.mjs.map +1 -1
  113. package/dist/postgres/index.d.mts +43 -2
  114. package/dist/postgres/index.mjs +40 -2
  115. package/dist/postgres/index.mjs.map +1 -1
  116. package/dist/presets/software-development/dispatch.d.mts +4 -1
  117. package/dist/presets/software-development/dispatch.mjs +2 -1
  118. package/dist/presets/software-development/dispatch.mjs.map +1 -1
  119. package/dist/presets/software-development/task-communication.d.mts +1 -0
  120. package/dist/presets/software-development/task-communication.mjs +48 -11
  121. package/dist/presets/software-development/task-communication.mjs.map +1 -1
  122. package/dist/presets/software-development.d.mts +1 -0
  123. package/dist/pull-requests/github-publisher.d.mts +15 -1
  124. package/dist/pull-requests/github-publisher.mjs +61 -1
  125. package/dist/pull-requests/github-publisher.mjs.map +1 -1
  126. package/dist/pull-requests.d.mts +1 -0
  127. package/dist/sandbox/index.d.mts +1 -0
  128. package/dist/schema/agent-route.d.mts +19 -1
  129. package/dist/schema/agent-route.mjs +19 -1
  130. package/dist/schema/agent-route.mjs.map +1 -1
  131. package/dist/schema/factory-config.d.mts +27 -0
  132. package/dist/schema/factory-config.mjs +33 -3
  133. package/dist/schema/factory-config.mjs.map +1 -1
  134. package/dist/schema/repository.d.mts +4 -0
  135. package/dist/schema/repository.mjs +5 -1
  136. package/dist/schema/repository.mjs.map +1 -1
  137. package/dist/schema/session.d.mts +1 -0
  138. package/dist/schema/session.mjs +1 -0
  139. package/dist/schema/session.mjs.map +1 -1
  140. package/dist/schema/slack-pr-notifications.d.mts +12 -0
  141. package/dist/schema/slack-pr-notifications.mjs +11 -0
  142. package/dist/schema/slack-pr-notifications.mjs.map +1 -0
  143. package/dist/schema/task-graph.d.mts +39 -0
  144. package/dist/schema/task.d.mts +1 -0
  145. package/dist/schema/task.mjs +2 -1
  146. package/dist/schema/task.mjs.map +1 -1
  147. package/dist/schema/transcript.d.mts +6 -0
  148. package/dist/schema/transcript.mjs +2 -1
  149. package/dist/schema/transcript.mjs.map +1 -1
  150. package/dist/schema/work.d.mts +52 -3
  151. package/dist/schema/work.mjs.map +1 -1
  152. package/dist/session-previews.d.mts +76 -0
  153. package/dist/session-previews.mjs +55 -0
  154. package/dist/session-previews.mjs.map +1 -0
  155. package/dist/session-review.d.mts +120 -0
  156. package/dist/session-review.mjs +79 -0
  157. package/dist/session-review.mjs.map +1 -0
  158. package/dist/signal-triage.mjs +1 -1
  159. package/dist/signals.d.mts +1 -0
  160. package/dist/stall.d.mts +4 -1
  161. package/dist/stall.mjs +6 -2
  162. package/dist/stall.mjs.map +1 -1
  163. package/dist/store/driver.d.mts +1 -1
  164. package/dist/store/driver.mjs.map +1 -1
  165. package/dist/store/engine.d.mts +206 -8
  166. package/dist/store/engine.mjs +147 -13
  167. package/dist/store/engine.mjs.map +1 -1
  168. package/dist/store/memory.d.mts +18 -1
  169. package/dist/store/memory.mjs +18 -1
  170. package/dist/store/memory.mjs.map +1 -1
  171. package/dist/store/slack-pr-notifications.d.mts +44 -0
  172. package/dist/store/slack-pr-notifications.mjs +121 -0
  173. package/dist/store/slack-pr-notifications.mjs.map +1 -0
  174. package/dist/store/task-work.d.mts +121 -6
  175. package/dist/store/task-work.mjs +7 -4
  176. package/dist/store/task-work.mjs.map +1 -1
  177. package/dist/sweep.d.mts +28 -6
  178. package/dist/sweep.mjs +34 -6
  179. package/dist/sweep.mjs.map +1 -1
  180. package/dist/task-graph-view.d.mts +3 -0
  181. package/dist/tasks.d.mts +3 -3
  182. package/dist/tasks.mjs +3 -3
  183. package/dist/vercel-git.d.mts +35 -3
  184. package/dist/vercel-git.mjs +265 -33
  185. package/dist/vercel-git.mjs.map +1 -1
  186. package/dist/vercel-github-api.d.mts +103 -0
  187. package/dist/vercel-github-api.mjs +363 -0
  188. package/dist/vercel-github-api.mjs.map +1 -0
  189. package/dist/vercel.d.mts +3 -2
  190. package/dist/vercel.mjs +3 -2
  191. package/dist/vercel.mjs.map +1 -1
  192. package/dist/work-triage.d.mts +1 -0
  193. package/dist/workflows.d.mts +102 -4
  194. package/dist/workflows.mjs +55 -2
  195. package/dist/workflows.mjs.map +1 -1
  196. package/dist/workspace-files-git.d.mts +15 -0
  197. package/dist/workspace-files-git.mjs +61 -0
  198. package/dist/workspace-files-git.mjs.map +1 -0
  199. package/dist/workspace-files.d.mts +107 -0
  200. package/dist/workspace-files.mjs +74 -0
  201. package/dist/workspace-files.mjs.map +1 -0
  202. package/docs/getting-started.md +104 -0
  203. package/docs/index.md +100 -0
  204. package/docs/recipes/cancellation.md +215 -0
  205. package/docs/recipes/custom-workflow.md +153 -0
  206. package/docs/recipes/dependent-tasks.md +207 -0
  207. package/docs/recipes/eve-agent.md +277 -0
  208. package/docs/recipes/human-input.md +204 -0
  209. package/docs/recipes/persistence-recovery.md +268 -0
  210. package/docs/recipes/retry-recovery.md +241 -0
  211. package/docs/recipes/task-messaging.md +215 -0
  212. package/docs/recipes/typed-eve-result.md +161 -0
  213. package/docs/runtime-integration.md +137 -0
  214. package/package.json +20 -6
@@ -1,6 +1,23 @@
1
1
  import { StoreDriver } from "./driver.mjs";
2
2
  //#region src/store/memory.d.ts
3
- /** Creates a process-local, non-durable StoreDriver for development and tests. */
3
+ /**
4
+ * Creates an isolated process-local StoreDriver for development, examples, and tests.
5
+ *
6
+ * @remarks
7
+ * Every call creates a new empty store. Records survive only for the lifetime of the returned
8
+ * object and are cloned at the driver boundary, so mutating caller-owned values cannot mutate
9
+ * stored state. Use a persistent adapter from `@vercel/factory/storage/blob` or
10
+ * `@vercel/factory/storage/postgres` in deployed runtimes.
11
+ *
12
+ * @returns A new empty driver whose `name` is `memory`.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { createInMemoryDriver, createStores } from "@vercel/factory/storage";
17
+ *
18
+ * const stores = createStores({ driver: createInMemoryDriver() });
19
+ * ```
20
+ */
4
21
  declare function createInMemoryDriver(): StoreDriver;
5
22
  //#endregion
6
23
  export { createInMemoryDriver };
@@ -1,6 +1,23 @@
1
1
  import { RecordExistsError, RecordNotFoundError, VersionConflictError } from "./driver.mjs";
2
2
  //#region src/store/memory.ts
3
- /** Creates a process-local, non-durable StoreDriver for development and tests. */
3
+ /**
4
+ * Creates an isolated process-local StoreDriver for development, examples, and tests.
5
+ *
6
+ * @remarks
7
+ * Every call creates a new empty store. Records survive only for the lifetime of the returned
8
+ * object and are cloned at the driver boundary, so mutating caller-owned values cannot mutate
9
+ * stored state. Use a persistent adapter from `@vercel/factory/storage/blob` or
10
+ * `@vercel/factory/storage/postgres` in deployed runtimes.
11
+ *
12
+ * @returns A new empty driver whose `name` is `memory`.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { createInMemoryDriver, createStores } from "@vercel/factory/storage";
17
+ *
18
+ * const stores = createStores({ driver: createInMemoryDriver() });
19
+ * ```
20
+ */
4
21
  function createInMemoryDriver() {
5
22
  const collections = /* @__PURE__ */ new Map();
6
23
  function collection(name) {
@@ -1 +1 @@
1
- {"version":3,"file":"memory.mjs","names":[],"sources":["../../src/store/memory.ts"],"sourcesContent":["import {\n RecordExistsError,\n RecordNotFoundError,\n VersionConflictError,\n type CollectionName,\n type StoredEntry,\n type StoredRecord,\n type StoreDriver,\n} from \"./driver\";\n\n/** Creates a process-local, non-durable StoreDriver for development and tests. */\nexport function createInMemoryDriver(): StoreDriver {\n const collections = new Map<CollectionName, Map<string, StoredRecord>>();\n\n function collection(name: CollectionName): Map<string, StoredRecord> {\n let existing = collections.get(name);\n if (existing === undefined) {\n existing = new Map();\n collections.set(name, existing);\n }\n return existing;\n }\n\n return {\n name: \"memory\",\n\n get(name, id) {\n const record = collection(name).get(id);\n return Promise.resolve(record === undefined ? null : structuredClone(record));\n },\n\n insert({ collection: name, id, value }) {\n const records = collection(name);\n if (records.has(id)) {\n return Promise.reject(new RecordExistsError(name, id));\n }\n records.set(id, { value: structuredClone(value), version: 1 });\n return Promise.resolve();\n },\n\n update({ collection: name, id, value, expectedVersion }) {\n const records = collection(name);\n const existing = records.get(id);\n if (existing === undefined) {\n return Promise.reject(new RecordNotFoundError(name, id));\n }\n if (existing.version !== expectedVersion) {\n return Promise.reject(new VersionConflictError(name, id, expectedVersion));\n }\n records.set(id, { value: structuredClone(value), version: existing.version + 1 });\n return Promise.resolve();\n },\n\n list(name) {\n const entries: StoredEntry[] = [...collection(name)].map(([id, record]) => ({\n id,\n record: structuredClone(record),\n }));\n return Promise.resolve(entries);\n },\n };\n}\n"],"mappings":";;;AAWA,SAAgB,uBAAoC;CAClD,MAAM,8BAAc,IAAI,IAA+C;CAEvE,SAAS,WAAW,MAAiD;EACnE,IAAI,WAAW,YAAY,IAAI,IAAI;EACnC,IAAI,aAAa,KAAA,GAAW;GAC1B,2BAAW,IAAI,IAAI;GACnB,YAAY,IAAI,MAAM,QAAQ;EAChC;EACA,OAAO;CACT;CAEA,OAAO;EACL,MAAM;EAEN,IAAI,MAAM,IAAI;GACZ,MAAM,SAAS,WAAW,IAAI,CAAC,CAAC,IAAI,EAAE;GACtC,OAAO,QAAQ,QAAQ,WAAW,KAAA,IAAY,OAAO,gBAAgB,MAAM,CAAC;EAC9E;EAEA,OAAO,EAAE,YAAY,MAAM,IAAI,SAAS;GACtC,MAAM,UAAU,WAAW,IAAI;GAC/B,IAAI,QAAQ,IAAI,EAAE,GAChB,OAAO,QAAQ,OAAO,IAAI,kBAAkB,MAAM,EAAE,CAAC;GAEvD,QAAQ,IAAI,IAAI;IAAE,OAAO,gBAAgB,KAAK;IAAG,SAAS;GAAE,CAAC;GAC7D,OAAO,QAAQ,QAAQ;EACzB;EAEA,OAAO,EAAE,YAAY,MAAM,IAAI,OAAO,mBAAmB;GACvD,MAAM,UAAU,WAAW,IAAI;GAC/B,MAAM,WAAW,QAAQ,IAAI,EAAE;GAC/B,IAAI,aAAa,KAAA,GACf,OAAO,QAAQ,OAAO,IAAI,oBAAoB,MAAM,EAAE,CAAC;GAEzD,IAAI,SAAS,YAAY,iBACvB,OAAO,QAAQ,OAAO,IAAI,qBAAqB,MAAM,IAAI,eAAe,CAAC;GAE3E,QAAQ,IAAI,IAAI;IAAE,OAAO,gBAAgB,KAAK;IAAG,SAAS,SAAS,UAAU;GAAE,CAAC;GAChF,OAAO,QAAQ,QAAQ;EACzB;EAEA,KAAK,MAAM;GACT,MAAM,UAAyB,CAAC,GAAG,WAAW,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,aAAa;IAC1E;IACA,QAAQ,gBAAgB,MAAM;GAChC,EAAE;GACF,OAAO,QAAQ,QAAQ,OAAO;EAChC;CACF;AACF"}
1
+ {"version":3,"file":"memory.mjs","names":[],"sources":["../../src/store/memory.ts"],"sourcesContent":["import {\n RecordExistsError,\n RecordNotFoundError,\n VersionConflictError,\n type CollectionName,\n type StoredEntry,\n type StoredRecord,\n type StoreDriver,\n} from \"./driver\";\n\n/**\n * Creates an isolated process-local StoreDriver for development, examples, and tests.\n *\n * @remarks\n * Every call creates a new empty store. Records survive only for the lifetime of the returned\n * object and are cloned at the driver boundary, so mutating caller-owned values cannot mutate\n * stored state. Use a persistent adapter from `@vercel/factory/storage/blob` or\n * `@vercel/factory/storage/postgres` in deployed runtimes.\n *\n * @returns A new empty driver whose `name` is `memory`.\n *\n * @example\n * ```ts\n * import { createInMemoryDriver, createStores } from \"@vercel/factory/storage\";\n *\n * const stores = createStores({ driver: createInMemoryDriver() });\n * ```\n */\nexport function createInMemoryDriver(): StoreDriver {\n const collections = new Map<CollectionName, Map<string, StoredRecord>>();\n\n function collection(name: CollectionName): Map<string, StoredRecord> {\n let existing = collections.get(name);\n if (existing === undefined) {\n existing = new Map();\n collections.set(name, existing);\n }\n return existing;\n }\n\n return {\n name: \"memory\",\n\n get(name, id) {\n const record = collection(name).get(id);\n return Promise.resolve(record === undefined ? null : structuredClone(record));\n },\n\n insert({ collection: name, id, value }) {\n const records = collection(name);\n if (records.has(id)) {\n return Promise.reject(new RecordExistsError(name, id));\n }\n records.set(id, { value: structuredClone(value), version: 1 });\n return Promise.resolve();\n },\n\n update({ collection: name, id, value, expectedVersion }) {\n const records = collection(name);\n const existing = records.get(id);\n if (existing === undefined) {\n return Promise.reject(new RecordNotFoundError(name, id));\n }\n if (existing.version !== expectedVersion) {\n return Promise.reject(new VersionConflictError(name, id, expectedVersion));\n }\n records.set(id, { value: structuredClone(value), version: existing.version + 1 });\n return Promise.resolve();\n },\n\n list(name) {\n const entries: StoredEntry[] = [...collection(name)].map(([id, record]) => ({\n id,\n record: structuredClone(record),\n }));\n return Promise.resolve(entries);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,uBAAoC;CAClD,MAAM,8BAAc,IAAI,IAA+C;CAEvE,SAAS,WAAW,MAAiD;EACnE,IAAI,WAAW,YAAY,IAAI,IAAI;EACnC,IAAI,aAAa,KAAA,GAAW;GAC1B,2BAAW,IAAI,IAAI;GACnB,YAAY,IAAI,MAAM,QAAQ;EAChC;EACA,OAAO;CACT;CAEA,OAAO;EACL,MAAM;EAEN,IAAI,MAAM,IAAI;GACZ,MAAM,SAAS,WAAW,IAAI,CAAC,CAAC,IAAI,EAAE;GACtC,OAAO,QAAQ,QAAQ,WAAW,KAAA,IAAY,OAAO,gBAAgB,MAAM,CAAC;EAC9E;EAEA,OAAO,EAAE,YAAY,MAAM,IAAI,SAAS;GACtC,MAAM,UAAU,WAAW,IAAI;GAC/B,IAAI,QAAQ,IAAI,EAAE,GAChB,OAAO,QAAQ,OAAO,IAAI,kBAAkB,MAAM,EAAE,CAAC;GAEvD,QAAQ,IAAI,IAAI;IAAE,OAAO,gBAAgB,KAAK;IAAG,SAAS;GAAE,CAAC;GAC7D,OAAO,QAAQ,QAAQ;EACzB;EAEA,OAAO,EAAE,YAAY,MAAM,IAAI,OAAO,mBAAmB;GACvD,MAAM,UAAU,WAAW,IAAI;GAC/B,MAAM,WAAW,QAAQ,IAAI,EAAE;GAC/B,IAAI,aAAa,KAAA,GACf,OAAO,QAAQ,OAAO,IAAI,oBAAoB,MAAM,EAAE,CAAC;GAEzD,IAAI,SAAS,YAAY,iBACvB,OAAO,QAAQ,OAAO,IAAI,qBAAqB,MAAM,IAAI,eAAe,CAAC;GAE3E,QAAQ,IAAI,IAAI;IAAE,OAAO,gBAAgB,KAAK;IAAG,SAAS,SAAS,UAAU;GAAE,CAAC;GAChF,OAAO,QAAQ,QAAQ;EACzB;EAEA,KAAK,MAAM;GACT,MAAM,UAAyB,CAAC,GAAG,WAAW,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,aAAa;IAC1E;IACA,QAAQ,gBAAgB,MAAM;GAChC,EAAE;GACF,OAAO,QAAQ,QAAQ,OAAO;EAChC;CACF;AACF"}
@@ -0,0 +1,44 @@
1
+ import { RepositorySlug } from "../schema/repository.mjs";
2
+ import { StoreDriver } from "./driver.mjs";
3
+ import { SlackPrNotifications } from "../schema/slack-pr-notifications.mjs";
4
+ import { z } from "zod";
5
+ //#region src/store/slack-pr-notifications.d.ts
6
+ declare const commandSchema: z.ZodObject<{
7
+ operationId: z.ZodString;
8
+ actor: z.ZodString;
9
+ preference: z.ZodUnion<readonly [z.ZodReadonly<z.ZodObject<{
10
+ channelId: z.ZodString;
11
+ mentionUserIds: z.ZodReadonly<z.ZodArray<z.ZodString>>;
12
+ }, z.core.$strict>>, z.ZodNull, z.ZodLiteral<"config">]>;
13
+ }, z.core.$strict>;
14
+ /** Effective PR notification settings and the source of the current choice. */
15
+ interface SlackPrNotificationSnapshot {
16
+ readonly revision: number;
17
+ readonly source: "config" | "slack";
18
+ readonly settings: SlackPrNotifications | null;
19
+ readonly updatedBy?: string;
20
+ readonly updatedAt?: string;
21
+ }
22
+ /** A trusted Slack command: an object enables notifications, null disables them, and config resets. */
23
+ type UpdateSlackPrNotificationsInput = z.infer<typeof commandSchema>;
24
+ /** Durable PR notification preferences scoped to a connector, workspace, and optional repository. */
25
+ interface SlackPrNotificationStore {
26
+ get(): Promise<SlackPrNotificationSnapshot>;
27
+ /** The caller authenticates the operator; operationId must identify the original Slack message. */
28
+ update(input: UpdateSlackPrNotificationsInput): Promise<SlackPrNotificationSnapshot>;
29
+ }
30
+ /** Persistence scope and Git-configured fallback for PR notification preferences. */
31
+ interface SlackPrNotificationStoreOptions {
32
+ readonly driver: StoreDriver;
33
+ readonly connector: string;
34
+ readonly workspaceId: string;
35
+ /** Omit for the factory-wide default; supply a slug to isolate one repository's preferences. */
36
+ readonly repository?: RepositorySlug;
37
+ readonly defaults?: SlackPrNotifications | null;
38
+ readonly now?: () => string;
39
+ }
40
+ /** Creates directly addressed settings with immutable command intents and compare-and-swap updates. */
41
+ declare function createSlackPrNotificationStore({ driver, connector, workspaceId, repository, defaults, now }: SlackPrNotificationStoreOptions): SlackPrNotificationStore;
42
+ //#endregion
43
+ export { SlackPrNotificationSnapshot, SlackPrNotificationStore, SlackPrNotificationStoreOptions, UpdateSlackPrNotificationsInput, createSlackPrNotificationStore };
44
+ //# sourceMappingURL=slack-pr-notifications.d.mts.map
@@ -0,0 +1,121 @@
1
+ import { repositorySlugSchema } from "../schema/repository.mjs";
2
+ import { slackPrNotificationsSchema } from "../schema/slack-pr-notifications.mjs";
3
+ import { createHash } from "node:crypto";
4
+ import { z } from "zod";
5
+ //#region src/store/slack-pr-notifications.ts
6
+ const preferenceSchema = z.union([
7
+ slackPrNotificationsSchema,
8
+ z.null(),
9
+ z.literal("config")
10
+ ]);
11
+ const commandSchema = z.strictObject({
12
+ operationId: z.string().min(1).max(512).describe("Stable identity of the authenticated Slack message."),
13
+ actor: z.string().regex(/^slack:T[A-Z0-9]+:[UW][A-Z0-9]+$/u).describe("Authenticated Slack operator making this choice."),
14
+ preference: preferenceSchema.describe("Destination override, null to disable, or config to restore the file default.")
15
+ });
16
+ const recordSchema = commandSchema.extend({
17
+ schemaVersion: z.literal(1).describe("Persisted settings format version."),
18
+ expectedRevision: z.number().int().nonnegative().describe("Settings revision observed when this command was first recorded."),
19
+ updatedAt: z.iso.datetime().describe("Time the command was first recorded.")
20
+ });
21
+ function isConflict(error, ...names) {
22
+ return error instanceof Error && names.includes(error.name);
23
+ }
24
+ /** Creates directly addressed settings with immutable command intents and compare-and-swap updates. */
25
+ function createSlackPrNotificationStore({ driver, connector, workspaceId, repository, defaults, now = () => (/* @__PURE__ */ new Date()).toISOString() }) {
26
+ z.string().regex(/^slack\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u).parse(connector);
27
+ z.string().regex(/^T[A-Z0-9]+$/u).parse(workspaceId);
28
+ if (repository !== void 0) repositorySlugSchema.parse(repository);
29
+ const fallback = defaults == null ? null : slackPrNotificationsSchema.parse(defaults);
30
+ const hash = (value) => createHash("sha256").update(value).digest("hex");
31
+ const id = `slack-pr-${hash(JSON.stringify(repository === void 0 ? [connector, workspaceId] : [
32
+ connector,
33
+ workspaceId,
34
+ repository
35
+ ]))}`;
36
+ const collection = "communicationSettings";
37
+ async function read() {
38
+ const record = await driver.get(collection, id);
39
+ return record === null ? null : {
40
+ version: record.version,
41
+ value: recordSchema.parse(record.value)
42
+ };
43
+ }
44
+ function snapshot(record) {
45
+ const preference = record === null ? "config" : record.value.preference;
46
+ return {
47
+ revision: record?.version ?? 0,
48
+ source: preference === "config" ? "config" : "slack",
49
+ settings: preference === "config" ? fallback : preference,
50
+ ...record && {
51
+ updatedBy: record.value.actor,
52
+ updatedAt: record.value.updatedAt
53
+ }
54
+ };
55
+ }
56
+ async function update(value) {
57
+ const input = commandSchema.parse(value);
58
+ if (!input.actor.startsWith(`slack:${workspaceId}:`)) throw new Error("PR notification operator belongs to another Slack workspace.");
59
+ const commandId = `${id}-command-${hash(input.operationId)}`;
60
+ let command = await driver.get(collection, commandId);
61
+ if (command === null) {
62
+ const current = await read();
63
+ const intent = recordSchema.parse({
64
+ ...input,
65
+ schemaVersion: 1,
66
+ expectedRevision: current?.version ?? 0,
67
+ updatedAt: now()
68
+ });
69
+ try {
70
+ await driver.insert({
71
+ collection,
72
+ id: commandId,
73
+ value: intent
74
+ });
75
+ } catch (error) {
76
+ if (!isConflict(error, "RecordExistsError")) throw error;
77
+ }
78
+ command = await driver.get(collection, commandId);
79
+ }
80
+ if (command === null) throw new Error("PR notification command was not saved.");
81
+ const intent = recordSchema.parse(command.value);
82
+ if (JSON.stringify({
83
+ operationId: intent.operationId,
84
+ actor: intent.actor,
85
+ preference: intent.preference
86
+ }) !== JSON.stringify(input)) throw new Error("This Slack message already requested different PR notification settings.");
87
+ const current = await read();
88
+ if (current?.value.operationId === input.operationId) return snapshot(current);
89
+ if ((current?.version ?? 0) !== intent.expectedRevision) throw new Error("PR notification settings changed after this command. Send a new Slack message to change them again.");
90
+ try {
91
+ if (current === null) await driver.insert({
92
+ collection,
93
+ id,
94
+ value: intent
95
+ });
96
+ else await driver.update({
97
+ collection,
98
+ id,
99
+ value: intent,
100
+ expectedVersion: current.version
101
+ });
102
+ } catch (error) {
103
+ if (!isConflict(error, "RecordExistsError", "VersionConflictError")) throw error;
104
+ const winner = await read();
105
+ if (winner?.value.operationId === input.operationId) return snapshot(winner);
106
+ throw new Error("Another command changed PR notification settings. Send a new Slack message to change them again.", { cause: error });
107
+ }
108
+ return snapshot({
109
+ value: intent,
110
+ version: intent.expectedRevision + 1
111
+ });
112
+ }
113
+ return {
114
+ get: async () => snapshot(await read()),
115
+ update
116
+ };
117
+ }
118
+ //#endregion
119
+ export { createSlackPrNotificationStore };
120
+
121
+ //# sourceMappingURL=slack-pr-notifications.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-pr-notifications.mjs","names":[],"sources":["../../src/store/slack-pr-notifications.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { z } from \"zod\";\nimport {\n slackPrNotificationsSchema,\n type SlackPrNotifications,\n} from \"../schema/slack-pr-notifications\";\nimport type { StoreDriver } from \"./driver\";\nimport { repositorySlugSchema, type RepositorySlug } from \"../schema/repository\";\n\nconst preferenceSchema = z.union([slackPrNotificationsSchema, z.null(), z.literal(\"config\")]);\nconst commandSchema = z.strictObject({\n operationId: z\n .string()\n .min(1)\n .max(512)\n .describe(\"Stable identity of the authenticated Slack message.\"),\n actor: z\n .string()\n .regex(/^slack:T[A-Z0-9]+:[UW][A-Z0-9]+$/u)\n .describe(\"Authenticated Slack operator making this choice.\"),\n preference: preferenceSchema.describe(\n \"Destination override, null to disable, or config to restore the file default.\",\n ),\n});\nconst recordSchema = commandSchema.extend({\n schemaVersion: z.literal(1).describe(\"Persisted settings format version.\"),\n expectedRevision: z\n .number()\n .int()\n .nonnegative()\n .describe(\"Settings revision observed when this command was first recorded.\"),\n updatedAt: z.iso.datetime().describe(\"Time the command was first recorded.\"),\n});\n\n// Drivers loaded through separate package entry points have distinct error constructors.\nfunction isConflict(error: unknown, ...names: string[]): boolean {\n return error instanceof Error && names.includes(error.name);\n}\n\n/** Effective PR notification settings and the source of the current choice. */\nexport interface SlackPrNotificationSnapshot {\n readonly revision: number;\n readonly source: \"config\" | \"slack\";\n readonly settings: SlackPrNotifications | null;\n readonly updatedBy?: string;\n readonly updatedAt?: string;\n}\n\n/** A trusted Slack command: an object enables notifications, null disables them, and config resets. */\nexport type UpdateSlackPrNotificationsInput = z.infer<typeof commandSchema>;\n\n/** Durable PR notification preferences scoped to a connector, workspace, and optional repository. */\nexport interface SlackPrNotificationStore {\n get(): Promise<SlackPrNotificationSnapshot>;\n /** The caller authenticates the operator; operationId must identify the original Slack message. */\n update(input: UpdateSlackPrNotificationsInput): Promise<SlackPrNotificationSnapshot>;\n}\n\n/** Persistence scope and Git-configured fallback for PR notification preferences. */\nexport interface SlackPrNotificationStoreOptions {\n readonly driver: StoreDriver;\n readonly connector: string;\n readonly workspaceId: string;\n /** Omit for the factory-wide default; supply a slug to isolate one repository's preferences. */\n readonly repository?: RepositorySlug;\n readonly defaults?: SlackPrNotifications | null;\n readonly now?: () => string;\n}\n\n/** Creates directly addressed settings with immutable command intents and compare-and-swap updates. */\nexport function createSlackPrNotificationStore({\n driver,\n connector,\n workspaceId,\n repository,\n defaults,\n now = () => new Date().toISOString(),\n}: SlackPrNotificationStoreOptions): SlackPrNotificationStore {\n z.string()\n .regex(/^slack\\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u)\n .parse(connector);\n z.string()\n .regex(/^T[A-Z0-9]+$/u)\n .parse(workspaceId);\n if (repository !== undefined) repositorySlugSchema.parse(repository);\n const fallback = defaults == null ? null : slackPrNotificationsSchema.parse(defaults);\n const hash = (value: string) => createHash(\"sha256\").update(value).digest(\"hex\");\n const scope =\n repository === undefined ? [connector, workspaceId] : [connector, workspaceId, repository];\n const id = `slack-pr-${hash(JSON.stringify(scope))}`;\n const collection = \"communicationSettings\";\n\n async function read() {\n const record = await driver.get(collection, id);\n return record === null\n ? null\n : { version: record.version, value: recordSchema.parse(record.value) };\n }\n\n function snapshot(record: Awaited<ReturnType<typeof read>>): SlackPrNotificationSnapshot {\n const preference = record === null ? \"config\" : record.value.preference;\n return {\n revision: record?.version ?? 0,\n source: preference === \"config\" ? \"config\" : \"slack\",\n settings: preference === \"config\" ? fallback : preference,\n ...(record && { updatedBy: record.value.actor, updatedAt: record.value.updatedAt }),\n };\n }\n\n async function update(value: UpdateSlackPrNotificationsInput) {\n const input = commandSchema.parse(value);\n if (!input.actor.startsWith(`slack:${workspaceId}:`))\n throw new Error(\"PR notification operator belongs to another Slack workspace.\");\n const commandId = `${id}-command-${hash(input.operationId)}`;\n let command = await driver.get(collection, commandId);\n if (command === null) {\n const current = await read();\n const intent = recordSchema.parse({\n ...input,\n schemaVersion: 1,\n expectedRevision: current?.version ?? 0,\n updatedAt: now(),\n });\n try {\n await driver.insert({ collection, id: commandId, value: intent });\n } catch (error) {\n if (!isConflict(error, \"RecordExistsError\")) throw error;\n }\n command = await driver.get(collection, commandId);\n }\n if (command === null) throw new Error(\"PR notification command was not saved.\");\n const intent = recordSchema.parse(command.value);\n if (\n JSON.stringify({\n operationId: intent.operationId,\n actor: intent.actor,\n preference: intent.preference,\n }) !== JSON.stringify(input)\n )\n throw new Error(\"This Slack message already requested different PR notification settings.\");\n\n const current = await read();\n if (current?.value.operationId === input.operationId) return snapshot(current);\n if ((current?.version ?? 0) !== intent.expectedRevision)\n throw new Error(\n \"PR notification settings changed after this command. Send a new Slack message to change them again.\",\n );\n try {\n if (current === null) await driver.insert({ collection, id, value: intent });\n else await driver.update({ collection, id, value: intent, expectedVersion: current.version });\n } catch (error) {\n if (!isConflict(error, \"RecordExistsError\", \"VersionConflictError\")) throw error;\n const winner = await read();\n if (winner?.value.operationId === input.operationId) return snapshot(winner);\n throw new Error(\n \"Another command changed PR notification settings. Send a new Slack message to change them again.\",\n { cause: error },\n );\n }\n return snapshot({ value: intent, version: intent.expectedRevision + 1 });\n }\n\n return { get: async () => snapshot(await read()), update };\n}\n"],"mappings":";;;;;AASA,MAAM,mBAAmB,EAAE,MAAM;CAAC;CAA4B,EAAE,KAAK;CAAG,EAAE,QAAQ,QAAQ;AAAC,CAAC;AAC5F,MAAM,gBAAgB,EAAE,aAAa;CACnC,aAAa,EACV,OAAO,CAAC,CACR,IAAI,CAAC,CAAC,CACN,IAAI,GAAG,CAAC,CACR,SAAS,qDAAqD;CACjE,OAAO,EACJ,OAAO,CAAC,CACR,MAAM,mCAAmC,CAAC,CAC1C,SAAS,kDAAkD;CAC9D,YAAY,iBAAiB,SAC3B,+EACF;AACF,CAAC;AACD,MAAM,eAAe,cAAc,OAAO;CACxC,eAAe,EAAE,QAAQ,CAAC,CAAC,CAAC,SAAS,oCAAoC;CACzE,kBAAkB,EACf,OAAO,CAAC,CACR,IAAI,CAAC,CACL,YAAY,CAAC,CACb,SAAS,kEAAkE;CAC9E,WAAW,EAAE,IAAI,SAAS,CAAC,CAAC,SAAS,sCAAsC;AAC7E,CAAC;AAGD,SAAS,WAAW,OAAgB,GAAG,OAA0B;CAC/D,OAAO,iBAAiB,SAAS,MAAM,SAAS,MAAM,IAAI;AAC5D;;AAiCA,SAAgB,+BAA+B,EAC7C,QACA,WACA,aACA,YACA,UACA,6BAAY,IAAI,KAAK,EAAA,CAAE,YAAY,KACyB;CAC5D,EAAE,OAAO,CAAC,CACP,MAAM,2CAA2C,CAAC,CAClD,MAAM,SAAS;CAClB,EAAE,OAAO,CAAC,CACP,MAAM,eAAe,CAAC,CACtB,MAAM,WAAW;CACpB,IAAI,eAAe,KAAA,GAAW,qBAAqB,MAAM,UAAU;CACnE,MAAM,WAAW,YAAY,OAAO,OAAO,2BAA2B,MAAM,QAAQ;CACpF,MAAM,QAAQ,UAAkB,WAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,KAAK;CAG/E,MAAM,KAAK,YAAY,KAAK,KAAK,UAD/B,eAAe,KAAA,IAAY,CAAC,WAAW,WAAW,IAAI;EAAC;EAAW;EAAa;CAAU,CAC3C,CAAC;CACjD,MAAM,aAAa;CAEnB,eAAe,OAAO;EACpB,MAAM,SAAS,MAAM,OAAO,IAAI,YAAY,EAAE;EAC9C,OAAO,WAAW,OACd,OACA;GAAE,SAAS,OAAO;GAAS,OAAO,aAAa,MAAM,OAAO,KAAK;EAAE;CACzE;CAEA,SAAS,SAAS,QAAuE;EACvF,MAAM,aAAa,WAAW,OAAO,WAAW,OAAO,MAAM;EAC7D,OAAO;GACL,UAAU,QAAQ,WAAW;GAC7B,QAAQ,eAAe,WAAW,WAAW;GAC7C,UAAU,eAAe,WAAW,WAAW;GAC/C,GAAI,UAAU;IAAE,WAAW,OAAO,MAAM;IAAO,WAAW,OAAO,MAAM;GAAU;EACnF;CACF;CAEA,eAAe,OAAO,OAAwC;EAC5D,MAAM,QAAQ,cAAc,MAAM,KAAK;EACvC,IAAI,CAAC,MAAM,MAAM,WAAW,SAAS,YAAY,EAAE,GACjD,MAAM,IAAI,MAAM,8DAA8D;EAChF,MAAM,YAAY,GAAG,GAAG,WAAW,KAAK,MAAM,WAAW;EACzD,IAAI,UAAU,MAAM,OAAO,IAAI,YAAY,SAAS;EACpD,IAAI,YAAY,MAAM;GACpB,MAAM,UAAU,MAAM,KAAK;GAC3B,MAAM,SAAS,aAAa,MAAM;IAChC,GAAG;IACH,eAAe;IACf,kBAAkB,SAAS,WAAW;IACtC,WAAW,IAAI;GACjB,CAAC;GACD,IAAI;IACF,MAAM,OAAO,OAAO;KAAE;KAAY,IAAI;KAAW,OAAO;IAAO,CAAC;GAClE,SAAS,OAAO;IACd,IAAI,CAAC,WAAW,OAAO,mBAAmB,GAAG,MAAM;GACrD;GACA,UAAU,MAAM,OAAO,IAAI,YAAY,SAAS;EAClD;EACA,IAAI,YAAY,MAAM,MAAM,IAAI,MAAM,wCAAwC;EAC9E,MAAM,SAAS,aAAa,MAAM,QAAQ,KAAK;EAC/C,IACE,KAAK,UAAU;GACb,aAAa,OAAO;GACpB,OAAO,OAAO;GACd,YAAY,OAAO;EACrB,CAAC,MAAM,KAAK,UAAU,KAAK,GAE3B,MAAM,IAAI,MAAM,0EAA0E;EAE5F,MAAM,UAAU,MAAM,KAAK;EAC3B,IAAI,SAAS,MAAM,gBAAgB,MAAM,aAAa,OAAO,SAAS,OAAO;EAC7E,KAAK,SAAS,WAAW,OAAO,OAAO,kBACrC,MAAM,IAAI,MACR,qGACF;EACF,IAAI;GACF,IAAI,YAAY,MAAM,MAAM,OAAO,OAAO;IAAE;IAAY;IAAI,OAAO;GAAO,CAAC;QACtE,MAAM,OAAO,OAAO;IAAE;IAAY;IAAI,OAAO;IAAQ,iBAAiB,QAAQ;GAAQ,CAAC;EAC9F,SAAS,OAAO;GACd,IAAI,CAAC,WAAW,OAAO,qBAAqB,sBAAsB,GAAG,MAAM;GAC3E,MAAM,SAAS,MAAM,KAAK;GAC1B,IAAI,QAAQ,MAAM,gBAAgB,MAAM,aAAa,OAAO,SAAS,MAAM;GAC3E,MAAM,IAAI,MACR,oGACA,EAAE,OAAO,MAAM,CACjB;EACF;EACA,OAAO,SAAS;GAAE,OAAO;GAAQ,SAAS,OAAO,mBAAmB;EAAE,CAAC;CACzE;CAEA,OAAO;EAAE,KAAK,YAAY,SAAS,MAAM,KAAK,CAAC;EAAG;CAAO;AAC3D"}
@@ -1,6 +1,6 @@
1
1
  import { TaskId } from "../schema/id.mjs";
2
2
  import { JsonValue, WorkflowBinding } from "../schema/work.mjs";
3
- import { Task } from "../schema/task.mjs";
3
+ import { ExecutionReference, Task, TaskState } from "../schema/task.mjs";
4
4
  import { WorkflowRegistry } from "../workflows.mjs";
5
5
  import { IsoDateTimeClock } from "../schema/common.mjs";
6
6
  import { TaskAttemptRef } from "../schema/execution.mjs";
@@ -9,57 +9,172 @@ import { FactoryStores } from "./engine.mjs";
9
9
  //#region src/store/task-work.d.ts
10
10
  /** Workflow-facing Task operations for completion, message waiting, recovery, and usage aggregation. */
11
11
  interface TaskWorkStore {
12
- /** Complete through the dynamic registry boundary, which validates unknown output at runtime. */
12
+ /**
13
+ * Completes through the dynamic registry boundary, which validates unknown output at runtime.
14
+ *
15
+ * @remarks
16
+ * The exact Task attempt is mandatory. The engine validates output against the Task's stored
17
+ * workflow, persists it with the `succeeded` transition, and returns only after that write. A
18
+ * replay with the same validated output returns the existing succeeded Task; different output
19
+ * is refused. This method does not authenticate a provider callback—use a fence and establish
20
+ * caller authority before invoking it.
21
+ *
22
+ * @param input - Exact Task attempt, unknown candidate output, and optional execution fence.
23
+ * @returns The durably succeeded Task containing its validated `workResult`.
24
+ * @throws When the attempt, lifecycle state, execution fence, or runtime output validation fails.
25
+ */
13
26
  complete(input: CompleteTaskWorkInput): Promise<Task>;
14
- /** Complete work while statically checking output against its exact workflow binding. */
27
+ /**
28
+ * Completes work while statically checking output against its exact workflow binding.
29
+ *
30
+ * @remarks
31
+ * In addition to `complete` semantics, the supplied binding must equal the Task's persisted
32
+ * workflow ID and version. Retain historical workflow definitions in the registry while stored
33
+ * Tasks may still use them. For Eve tools, authenticate first with `requireTaskExecution` and
34
+ * pass its attempt and execution as the completion fence.
35
+ *
36
+ * @param input - Exact Task attempt, typed workflow binding and output, and optional fence.
37
+ * @returns The durably succeeded Task containing its validated `workResult`.
38
+ * @throws When the workflow, attempt, lifecycle state, execution fence, or output is invalid.
39
+ */
15
40
  completeWorkflow<Output extends JsonValue>(input: CompleteWorkflowTaskWorkInput<Output>): Promise<Task>;
41
+ /**
42
+ * Marks a running exact Task attempt as intentionally inactive while awaiting messages.
43
+ *
44
+ * @remarks
45
+ * This keeps the Task in `running`; it does not stall, cancel, or create a new attempt. The
46
+ * waiting marker and optional workflow phase are durable when the promise resolves. Repeating
47
+ * the same target state is a no-op. A stale attempt or non-running Task is refused, so callers
48
+ * should reread the Task before deciding whether a later attempt still needs the message.
49
+ *
50
+ * @param input - Exact running attempt and optional workflow phase.
51
+ * @returns The updated running Task with `waitingForMessages` set.
52
+ * @throws When the Task is absent, no longer running, or on a different attempt.
53
+ */
16
54
  waitForMessages(input: WaitForTaskMessagesInput): Promise<Task>;
55
+ /**
56
+ * Resumes an exact message-waiting Task attempt and records fresh progress.
57
+ *
58
+ * @remarks
59
+ * The Task remains `running`, its waiting marker is cleared, and `lastProgressAt` advances. The
60
+ * write is durable when the promise resolves and repeating it for an already-resumed attempt is
61
+ * a no-op. Resuming does not claim or acknowledge messages; the workflow owns that ordering.
62
+ *
63
+ * @param input - Exact running attempt to resume.
64
+ * @returns The updated running Task with message waiting cleared.
65
+ * @throws When the Task is absent, no longer running, or on a different attempt.
66
+ */
17
67
  resume(input: ResumeTaskWorkInput): Promise<Task>;
18
- /** Repair admitted creation snapshots by direct graph IDs, never by scanning the bucket. */
68
+ /**
69
+ * Repairs admitted creation snapshots by direct graph IDs, never by scanning the bucket.
70
+ *
71
+ * @remarks
72
+ * The canonical graph contains immutable admission snapshots. Recovery validates every
73
+ * admission against the exact registered workflow, restores only missing Task records, and
74
+ * returns existing records with their current mutable progress. It is safe to repeat after an
75
+ * interrupted Task creation.
76
+ *
77
+ * This operation repairs Factory persistence only. It does not start or reattach a provider
78
+ * execution, rerun workflow code, retry a Task, or migrate old Task JSON. After recovery, the
79
+ * caller must inspect each Task's lifecycle and recorded execution and apply workflow-owned
80
+ * reconciliation policy.
81
+ *
82
+ * @param rootTaskId - Root of the canonical Task graph to repair and read.
83
+ * @returns Every validated Task admitted to that graph.
84
+ * @throws When the graph is absent, a stored workflow version is not registered, persisted data
85
+ * is invalid, or a current Task no longer matches its immutable admission identity.
86
+ */
19
87
  recover(rootTaskId: TaskId): Promise<readonly Task[]>;
20
- /** Each Task contributes local measured usage once; no parent roll-up is charged again. */
88
+ /**
89
+ * Aggregates Task-local measured usage once without charging parent roll-ups again.
90
+ *
91
+ * @remarks
92
+ * This first performs the same persistence repair and validation as {@link recover}. For each
93
+ * Task, recorded step usage is authoritative; otherwise a settled amount is used, then zero.
94
+ * The result is a current snapshot, not a reservation or billing transaction, and calling it
95
+ * does not write settlement receipts.
96
+ *
97
+ * @param rootTaskId - Root of the canonical Task graph to aggregate.
98
+ * @returns Cost, token, and Task counts for the recovered graph.
99
+ * @throws The same persistence and validation errors as {@link recover}.
100
+ */
21
101
  usage(rootTaskId: TaskId): Promise<TaskWorkUsage>;
22
102
  }
23
103
  /** Exact Task attempt and candidate workflow output used to complete work. */
24
104
  interface CompleteTaskWorkInput {
105
+ /** Task ID and current positive attempt; stale attempts are refused. */
25
106
  task: TaskAttemptRef;
107
+ /** Candidate value validated against the Task's exact registered output schema. */
26
108
  output: unknown;
109
+ /**
110
+ * Optional source-state and execution fence for provider callbacks. Omit only when trusted
111
+ * workflow code already owns lifecycle authority.
112
+ */
113
+ fence?: TaskWorkCompletionFence;
27
114
  }
28
115
  /** Exact Task attempt, workflow contract, and statically checked output used to complete work. */
29
116
  interface CompleteWorkflowTaskWorkInput<Output extends JsonValue = JsonValue> {
117
+ /** Task ID and current positive attempt; stale attempts are refused. */
30
118
  task: TaskAttemptRef;
119
+ /** Exact binding required to match the Task's persisted workflow. */
31
120
  workflow: WorkflowBinding<JsonValue, Output>;
121
+ /** Candidate output statically carried by the binding and validated again at runtime. */
32
122
  output: NoInfer<Output>;
123
+ /**
124
+ * Optional source-state and execution fence for provider callbacks. Omit only when trusted
125
+ * workflow code already owns lifecycle authority.
126
+ */
127
+ fence?: TaskWorkCompletionFence;
128
+ }
129
+ /** Atomic source-state and provider-execution conditions for a workflow completion write. */
130
+ interface TaskWorkCompletionFence {
131
+ /** Lifecycle state required atomically with completion, normally `running`. */
132
+ readonly from: TaskState;
133
+ /** Provider execution reference required to remain current at the completion write. */
134
+ readonly execution: ExecutionReference;
33
135
  }
34
136
  /** Aggregated measured usage across one canonical Task graph. */
35
137
  interface TaskWorkUsage {
138
+ /** Sum of measured Task usage, falling back to settlement only when no usage is recorded. */
36
139
  costUsd: number;
140
+ /** Sum of recorded provider input tokens across Tasks. */
37
141
  inputTokens: number;
142
+ /** Sum of recorded provider output tokens across Tasks. */
38
143
  outputTokens: number;
144
+ /** Number of Tasks admitted to the recovered canonical graph. */
39
145
  taskCount: number;
40
146
  }
41
147
  /** Exact running Task attempt and optional workflow phase used to await messages. */
42
148
  interface WaitForTaskMessagesInput {
149
+ /** Task ID and current positive attempt; stale attempts are refused. */
43
150
  task: TaskAttemptRef;
151
+ /** Optional durable workflow-owned phase label retained across waiting and resumption. */
44
152
  phase?: string;
45
153
  }
46
154
  /** Exact running Task attempt to resume after message delivery. */
47
155
  interface ResumeTaskWorkInput {
156
+ /** Task ID and current positive attempt; stale attempts are refused. */
48
157
  task: TaskAttemptRef;
49
158
  }
50
159
  /** Canonical Task graph, receipt, and Task capabilities used by workflow-facing operations. */
51
160
  type TaskWorkStores = Pick<FactoryStores, "graphs" | "receipts" | "tasks">;
52
161
  /** Engine-owned dependencies used to construct workflow-facing Task operations. */
53
162
  interface CreateTaskWorkStoreOptions {
163
+ /** Policy-free persistence driver used for direct canonical Task reads. */
54
164
  driver: StoreDriver;
165
+ /** ISO timestamp source shared with the surrounding store engine. */
55
166
  now: IsoDateTimeClock;
167
+ /** Lazily resolves graph, receipt, and Task operations after engine construction. */
56
168
  stores: () => TaskWorkStores;
169
+ /** Exact workflow versions used to validate stored input and completion output. */
57
170
  workflows: WorkflowRegistry;
171
+ /** Persists a validated Task with compare-and-swap semantics. */
58
172
  update(task: Task, version: number): Promise<void>;
173
+ /** Recreates a missing canonical Task snapshot from its immutable graph admission. */
59
174
  restore(task: Task): Promise<Task>;
60
175
  }
61
176
  /** Creates workflow-facing Task operations backed by canonical graph and Task stores. */
62
177
  declare function createTaskWorkStore(options: CreateTaskWorkStoreOptions): TaskWorkStore;
63
178
  //#endregion
64
- export { CompleteTaskWorkInput, CompleteWorkflowTaskWorkInput, CreateTaskWorkStoreOptions, ResumeTaskWorkInput, TaskWorkStore, TaskWorkStores, TaskWorkUsage, WaitForTaskMessagesInput, createTaskWorkStore };
179
+ export { CompleteTaskWorkInput, CompleteWorkflowTaskWorkInput, CreateTaskWorkStoreOptions, ResumeTaskWorkInput, TaskWorkCompletionFence, TaskWorkStore, TaskWorkStores, TaskWorkUsage, WaitForTaskMessagesInput, createTaskWorkStore };
65
180
  //# sourceMappingURL=task-work.d.mts.map
@@ -76,10 +76,11 @@ function createTaskWorkStore(options) {
76
76
  }
77
77
  }
78
78
  async function complete(input, expectedWorkflow) {
79
- const { task: { taskId: id, attempt: expectAttempt }, output: value } = input;
79
+ const { task: { taskId: id, attempt: expectAttempt }, output: value, fence } = input;
80
80
  for (;;) {
81
81
  const { task, work } = await read(id);
82
82
  assertAttempt(task, expectAttempt);
83
+ if (fence !== void 0 && !isDeepStrictEqual(task.execution, fence.execution)) throw new Error(`Task ${id} execution has been replaced`);
83
84
  if (expectedWorkflow !== void 0 && !isDeepStrictEqual(work.workflow, expectedWorkflow)) throw new Error(`Task ${id} uses workflow ${work.workflow.id}@${work.workflow.version}, not ${expectedWorkflow.id}@${expectedWorkflow.version}`);
84
85
  if (task.state === "succeeded") {
85
86
  const output = workflows.validateOutput(work.workflow, value);
@@ -90,7 +91,8 @@ function createTaskWorkStore(options) {
90
91
  return await stores().tasks.transition(id, "succeeded", {
91
92
  output: value,
92
93
  expectAttempt,
93
- expectFrom: task.state,
94
+ expectFrom: fence?.from ?? task.state,
95
+ ...fence === void 0 ? {} : { expectExecution: fence.execution },
94
96
  phase: "workflow"
95
97
  });
96
98
  } catch (error) {
@@ -102,9 +104,10 @@ function createTaskWorkStore(options) {
102
104
  }
103
105
  return {
104
106
  complete,
105
- completeWorkflow: ({ task, workflow, output }) => complete({
107
+ completeWorkflow: ({ task, workflow, output, fence }) => complete({
106
108
  task,
107
- output
109
+ output,
110
+ fence
108
111
  }, workflow),
109
112
  waitForMessages: ({ task: { taskId, attempt }, phase }) => setWaiting(taskId, attempt, true, phase),
110
113
  resume: ({ task: { taskId, attempt } }) => setWaiting(taskId, attempt, false),
@@ -1 +1 @@
1
- {"version":3,"file":"task-work.mjs","names":[],"sources":["../../src/store/task-work.ts"],"sourcesContent":["import { isDeepStrictEqual } from \"node:util\";\nimport { z } from \"zod\";\nimport type { IsoDateTimeClock } from \"../schema/common\";\nimport type { TaskAttemptRef } from \"../schema/execution\";\nimport type { TaskId } from \"../schema/id\";\nimport { taskSchema, type Task } from \"../schema/task\";\nimport { InvalidTransitionError } from \"../schema/transitions\";\nimport type { JsonValue, WorkflowBinding } from \"../schema/work\";\nimport type { WorkflowRegistry } from \"../workflows\";\nimport { RecordNotFoundError, VersionConflictError, type StoreDriver } from \"./driver\";\nimport type { FactoryStores } from \"./engine\";\nimport { assertTaskAdmission } from \"./task-graphs\";\n\n/** Workflow-facing Task operations for completion, message waiting, recovery, and usage aggregation. */\nexport interface TaskWorkStore {\n /** Complete through the dynamic registry boundary, which validates unknown output at runtime. */\n complete(input: CompleteTaskWorkInput): Promise<Task>;\n /** Complete work while statically checking output against its exact workflow binding. */\n completeWorkflow<Output extends JsonValue>(\n input: CompleteWorkflowTaskWorkInput<Output>,\n ): Promise<Task>;\n waitForMessages(input: WaitForTaskMessagesInput): Promise<Task>;\n resume(input: ResumeTaskWorkInput): Promise<Task>;\n /** Repair admitted creation snapshots by direct graph IDs, never by scanning the bucket. */\n recover(rootTaskId: TaskId): Promise<readonly Task[]>;\n /** Each Task contributes local measured usage once; no parent roll-up is charged again. */\n usage(rootTaskId: TaskId): Promise<TaskWorkUsage>;\n}\n\n/** Exact Task attempt and candidate workflow output used to complete work. */\nexport interface CompleteTaskWorkInput {\n task: TaskAttemptRef;\n output: unknown;\n}\n\n/** Exact Task attempt, workflow contract, and statically checked output used to complete work. */\nexport interface CompleteWorkflowTaskWorkInput<Output extends JsonValue = JsonValue> {\n task: TaskAttemptRef;\n workflow: WorkflowBinding<JsonValue, Output>;\n output: NoInfer<Output>;\n}\n\n/** Aggregated measured usage across one canonical Task graph. */\nexport interface TaskWorkUsage {\n costUsd: number;\n inputTokens: number;\n outputTokens: number;\n taskCount: number;\n}\n\n/** Exact running Task attempt and optional workflow phase used to await messages. */\nexport interface WaitForTaskMessagesInput {\n task: TaskAttemptRef;\n phase?: string;\n}\n\n/** Exact running Task attempt to resume after message delivery. */\nexport interface ResumeTaskWorkInput {\n task: TaskAttemptRef;\n}\n\n/** Canonical Task graph, receipt, and Task capabilities used by workflow-facing operations. */\nexport type TaskWorkStores = Pick<FactoryStores, \"graphs\" | \"receipts\" | \"tasks\">;\n\n/** Engine-owned dependencies used to construct workflow-facing Task operations. */\nexport interface CreateTaskWorkStoreOptions {\n driver: StoreDriver;\n now: IsoDateTimeClock;\n stores: () => TaskWorkStores;\n workflows: WorkflowRegistry;\n update(task: Task, version: number): Promise<void>;\n restore(task: Task): Promise<Task>;\n}\n\n/** Creates workflow-facing Task operations backed by canonical graph and Task stores. */\nexport function createTaskWorkStore(options: CreateTaskWorkStoreOptions): TaskWorkStore {\n const { driver, now, stores, workflows, update, restore } = options;\n function parseTask(value: unknown): Task {\n const task = taskSchema.parse(value);\n workflows.validateStoredInput(task.work.workflow, task.work.input);\n if (task.workResult) workflows.validateStoredOutput(task.work.workflow, task.workResult.output);\n return task;\n }\n async function read(id: TaskId) {\n const record = await driver.get(\"tasks\", id);\n if (!record) throw new RecordNotFoundError(\"tasks\", id);\n const task = parseTask(record.value);\n if (task.id !== id) throw new Error(`Task ${id} does not match its storage key`);\n return { task, version: record.version, work: task.work };\n }\n async function recover(rootTaskId: TaskId): Promise<readonly Task[]> {\n const graph = await stores().graphs.get(rootTaskId);\n if (!graph) throw new RecordNotFoundError(\"taskGraphs\", rootTaskId);\n const tasks: Task[] = [];\n for (const node of graph.nodes) {\n const admission = parseTask(node.admission);\n const existing = await stores().tasks.get(node.taskId);\n const task = parseTask(existing ?? (await restore(admission)));\n assertTaskAdmission({ task, admission });\n tasks.push(task);\n }\n return tasks;\n }\n function assertAttempt(task: Task, expectAttempt: number) {\n z.number().int().positive().parse(expectAttempt);\n if (task.attempt !== expectAttempt)\n throw new Error(`Task ${task.id} is on attempt ${task.attempt}, not ${expectAttempt}`);\n }\n async function setWaiting(\n id: TaskId,\n expectAttempt: number,\n waiting: boolean,\n phase?: string,\n ): Promise<Task> {\n for (;;) {\n const { task, version } = await read(id);\n assertAttempt(task, expectAttempt);\n if (task.state !== \"running\")\n throw new Error(`Task ${id} must be running to ${waiting ? \"wait\" : \"resume\"}`);\n const next = taskSchema.parse({\n ...task,\n waitingForMessages: waiting,\n workflowPhase: phase ?? task.workflowPhase,\n lastProgressAt: waiting ? task.lastProgressAt : now(),\n updatedAt: now(),\n });\n if (task.waitingForMessages === waiting && next.workflowPhase === task.workflowPhase)\n return task;\n await stores().receipts.append({\n taskId: id,\n repositoryIds: task.repositoryIds,\n kind: task.kind,\n state: waiting ? \"waiting\" : \"resumed\",\n phase: \"workflow\",\n attempt: task.attempt,\n reason: waiting\n ? \"Workflow is waiting for Task messages\"\n : \"Workflow resumed message processing\",\n dedupeKey: `work:${id}:${version}:${waiting ? \"wait\" : \"resume\"}`,\n });\n try {\n await update(next, version);\n return next;\n } catch (error) {\n if (!(error instanceof VersionConflictError)) throw error;\n }\n }\n }\n async function complete(\n input: CompleteTaskWorkInput,\n expectedWorkflow?: WorkflowBinding,\n ): Promise<Task> {\n const {\n task: { taskId: id, attempt: expectAttempt },\n output: value,\n } = input;\n for (;;) {\n const { task, work } = await read(id);\n assertAttempt(task, expectAttempt);\n if (expectedWorkflow !== undefined && !isDeepStrictEqual(work.workflow, expectedWorkflow)) {\n throw new Error(\n `Task ${id} uses workflow ${work.workflow.id}@${work.workflow.version}, not ${expectedWorkflow.id}@${expectedWorkflow.version}`,\n );\n }\n if (task.state === \"succeeded\") {\n const output = workflows.validateOutput(work.workflow, value);\n if (!isDeepStrictEqual(task.workResult?.output, output)) {\n throw new Error(`Task ${id} already recorded a different workflow output`);\n }\n return task;\n }\n try {\n return await stores().tasks.transition(id, \"succeeded\", {\n output: value,\n expectAttempt,\n expectFrom: task.state,\n phase: \"workflow\",\n });\n } catch (error) {\n if (error instanceof VersionConflictError) continue;\n if (error instanceof InvalidTransitionError && (await read(id)).task.state === \"succeeded\")\n continue;\n throw error;\n }\n }\n }\n return {\n complete,\n completeWorkflow: ({ task, workflow, output }) => complete({ task, output }, workflow),\n waitForMessages: ({ task: { taskId, attempt }, phase }) =>\n setWaiting(taskId, attempt, true, phase),\n resume: ({ task: { taskId, attempt } }) => setWaiting(taskId, attempt, false),\n recover,\n async usage(rootTaskId) {\n const tasks = await recover(rootTaskId);\n let micros = 0;\n let inputTokens = 0;\n let outputTokens = 0;\n for (const task of tasks) {\n micros += Math.round((task.usage?.costUsd ?? task.settledUsd ?? 0) * 1_000_000);\n inputTokens += task.usage?.inputTokens ?? 0;\n outputTokens += task.usage?.outputTokens ?? 0;\n }\n return { costUsd: micros / 1_000_000, inputTokens, outputTokens, taskCount: tasks.length };\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"task-work.mjs","names":[],"sources":["../../src/store/task-work.ts"],"sourcesContent":["import { isDeepStrictEqual } from \"node:util\";\nimport { z } from \"zod\";\nimport type { IsoDateTimeClock } from \"../schema/common\";\nimport type { TaskAttemptRef } from \"../schema/execution\";\nimport type { TaskId } from \"../schema/id\";\nimport { taskSchema, type ExecutionReference, type Task, type TaskState } from \"../schema/task\";\nimport { InvalidTransitionError } from \"../schema/transitions\";\nimport type { JsonValue, WorkflowBinding } from \"../schema/work\";\nimport type { WorkflowRegistry } from \"../workflows\";\nimport { RecordNotFoundError, VersionConflictError, type StoreDriver } from \"./driver\";\nimport type { FactoryStores } from \"./engine\";\nimport { assertTaskAdmission } from \"./task-graphs\";\n\n/** Workflow-facing Task operations for completion, message waiting, recovery, and usage aggregation. */\nexport interface TaskWorkStore {\n /**\n * Completes through the dynamic registry boundary, which validates unknown output at runtime.\n *\n * @remarks\n * The exact Task attempt is mandatory. The engine validates output against the Task's stored\n * workflow, persists it with the `succeeded` transition, and returns only after that write. A\n * replay with the same validated output returns the existing succeeded Task; different output\n * is refused. This method does not authenticate a provider callback—use a fence and establish\n * caller authority before invoking it.\n *\n * @param input - Exact Task attempt, unknown candidate output, and optional execution fence.\n * @returns The durably succeeded Task containing its validated `workResult`.\n * @throws When the attempt, lifecycle state, execution fence, or runtime output validation fails.\n */\n complete(input: CompleteTaskWorkInput): Promise<Task>;\n /**\n * Completes work while statically checking output against its exact workflow binding.\n *\n * @remarks\n * In addition to `complete` semantics, the supplied binding must equal the Task's persisted\n * workflow ID and version. Retain historical workflow definitions in the registry while stored\n * Tasks may still use them. For Eve tools, authenticate first with `requireTaskExecution` and\n * pass its attempt and execution as the completion fence.\n *\n * @param input - Exact Task attempt, typed workflow binding and output, and optional fence.\n * @returns The durably succeeded Task containing its validated `workResult`.\n * @throws When the workflow, attempt, lifecycle state, execution fence, or output is invalid.\n */\n completeWorkflow<Output extends JsonValue>(\n input: CompleteWorkflowTaskWorkInput<Output>,\n ): Promise<Task>;\n /**\n * Marks a running exact Task attempt as intentionally inactive while awaiting messages.\n *\n * @remarks\n * This keeps the Task in `running`; it does not stall, cancel, or create a new attempt. The\n * waiting marker and optional workflow phase are durable when the promise resolves. Repeating\n * the same target state is a no-op. A stale attempt or non-running Task is refused, so callers\n * should reread the Task before deciding whether a later attempt still needs the message.\n *\n * @param input - Exact running attempt and optional workflow phase.\n * @returns The updated running Task with `waitingForMessages` set.\n * @throws When the Task is absent, no longer running, or on a different attempt.\n */\n waitForMessages(input: WaitForTaskMessagesInput): Promise<Task>;\n /**\n * Resumes an exact message-waiting Task attempt and records fresh progress.\n *\n * @remarks\n * The Task remains `running`, its waiting marker is cleared, and `lastProgressAt` advances. The\n * write is durable when the promise resolves and repeating it for an already-resumed attempt is\n * a no-op. Resuming does not claim or acknowledge messages; the workflow owns that ordering.\n *\n * @param input - Exact running attempt to resume.\n * @returns The updated running Task with message waiting cleared.\n * @throws When the Task is absent, no longer running, or on a different attempt.\n */\n resume(input: ResumeTaskWorkInput): Promise<Task>;\n /**\n * Repairs admitted creation snapshots by direct graph IDs, never by scanning the bucket.\n *\n * @remarks\n * The canonical graph contains immutable admission snapshots. Recovery validates every\n * admission against the exact registered workflow, restores only missing Task records, and\n * returns existing records with their current mutable progress. It is safe to repeat after an\n * interrupted Task creation.\n *\n * This operation repairs Factory persistence only. It does not start or reattach a provider\n * execution, rerun workflow code, retry a Task, or migrate old Task JSON. After recovery, the\n * caller must inspect each Task's lifecycle and recorded execution and apply workflow-owned\n * reconciliation policy.\n *\n * @param rootTaskId - Root of the canonical Task graph to repair and read.\n * @returns Every validated Task admitted to that graph.\n * @throws When the graph is absent, a stored workflow version is not registered, persisted data\n * is invalid, or a current Task no longer matches its immutable admission identity.\n */\n recover(rootTaskId: TaskId): Promise<readonly Task[]>;\n /**\n * Aggregates Task-local measured usage once without charging parent roll-ups again.\n *\n * @remarks\n * This first performs the same persistence repair and validation as {@link recover}. For each\n * Task, recorded step usage is authoritative; otherwise a settled amount is used, then zero.\n * The result is a current snapshot, not a reservation or billing transaction, and calling it\n * does not write settlement receipts.\n *\n * @param rootTaskId - Root of the canonical Task graph to aggregate.\n * @returns Cost, token, and Task counts for the recovered graph.\n * @throws The same persistence and validation errors as {@link recover}.\n */\n usage(rootTaskId: TaskId): Promise<TaskWorkUsage>;\n}\n\n/** Exact Task attempt and candidate workflow output used to complete work. */\nexport interface CompleteTaskWorkInput {\n /** Task ID and current positive attempt; stale attempts are refused. */\n task: TaskAttemptRef;\n /** Candidate value validated against the Task's exact registered output schema. */\n output: unknown;\n /**\n * Optional source-state and execution fence for provider callbacks. Omit only when trusted\n * workflow code already owns lifecycle authority.\n */\n fence?: TaskWorkCompletionFence;\n}\n\n/** Exact Task attempt, workflow contract, and statically checked output used to complete work. */\nexport interface CompleteWorkflowTaskWorkInput<Output extends JsonValue = JsonValue> {\n /** Task ID and current positive attempt; stale attempts are refused. */\n task: TaskAttemptRef;\n /** Exact binding required to match the Task's persisted workflow. */\n workflow: WorkflowBinding<JsonValue, Output>;\n /** Candidate output statically carried by the binding and validated again at runtime. */\n output: NoInfer<Output>;\n /**\n * Optional source-state and execution fence for provider callbacks. Omit only when trusted\n * workflow code already owns lifecycle authority.\n */\n fence?: TaskWorkCompletionFence;\n}\n\n/** Atomic source-state and provider-execution conditions for a workflow completion write. */\nexport interface TaskWorkCompletionFence {\n /** Lifecycle state required atomically with completion, normally `running`. */\n readonly from: TaskState;\n /** Provider execution reference required to remain current at the completion write. */\n readonly execution: ExecutionReference;\n}\n\n/** Aggregated measured usage across one canonical Task graph. */\nexport interface TaskWorkUsage {\n /** Sum of measured Task usage, falling back to settlement only when no usage is recorded. */\n costUsd: number;\n /** Sum of recorded provider input tokens across Tasks. */\n inputTokens: number;\n /** Sum of recorded provider output tokens across Tasks. */\n outputTokens: number;\n /** Number of Tasks admitted to the recovered canonical graph. */\n taskCount: number;\n}\n\n/** Exact running Task attempt and optional workflow phase used to await messages. */\nexport interface WaitForTaskMessagesInput {\n /** Task ID and current positive attempt; stale attempts are refused. */\n task: TaskAttemptRef;\n /** Optional durable workflow-owned phase label retained across waiting and resumption. */\n phase?: string;\n}\n\n/** Exact running Task attempt to resume after message delivery. */\nexport interface ResumeTaskWorkInput {\n /** Task ID and current positive attempt; stale attempts are refused. */\n task: TaskAttemptRef;\n}\n\n/** Canonical Task graph, receipt, and Task capabilities used by workflow-facing operations. */\nexport type TaskWorkStores = Pick<FactoryStores, \"graphs\" | \"receipts\" | \"tasks\">;\n\n/** Engine-owned dependencies used to construct workflow-facing Task operations. */\nexport interface CreateTaskWorkStoreOptions {\n /** Policy-free persistence driver used for direct canonical Task reads. */\n driver: StoreDriver;\n /** ISO timestamp source shared with the surrounding store engine. */\n now: IsoDateTimeClock;\n /** Lazily resolves graph, receipt, and Task operations after engine construction. */\n stores: () => TaskWorkStores;\n /** Exact workflow versions used to validate stored input and completion output. */\n workflows: WorkflowRegistry;\n /** Persists a validated Task with compare-and-swap semantics. */\n update(task: Task, version: number): Promise<void>;\n /** Recreates a missing canonical Task snapshot from its immutable graph admission. */\n restore(task: Task): Promise<Task>;\n}\n\n/** Creates workflow-facing Task operations backed by canonical graph and Task stores. */\nexport function createTaskWorkStore(options: CreateTaskWorkStoreOptions): TaskWorkStore {\n const { driver, now, stores, workflows, update, restore } = options;\n function parseTask(value: unknown): Task {\n const task = taskSchema.parse(value);\n workflows.validateStoredInput(task.work.workflow, task.work.input);\n if (task.workResult) workflows.validateStoredOutput(task.work.workflow, task.workResult.output);\n return task;\n }\n async function read(id: TaskId) {\n const record = await driver.get(\"tasks\", id);\n if (!record) throw new RecordNotFoundError(\"tasks\", id);\n const task = parseTask(record.value);\n if (task.id !== id) throw new Error(`Task ${id} does not match its storage key`);\n return { task, version: record.version, work: task.work };\n }\n async function recover(rootTaskId: TaskId): Promise<readonly Task[]> {\n const graph = await stores().graphs.get(rootTaskId);\n if (!graph) throw new RecordNotFoundError(\"taskGraphs\", rootTaskId);\n const tasks: Task[] = [];\n for (const node of graph.nodes) {\n const admission = parseTask(node.admission);\n const existing = await stores().tasks.get(node.taskId);\n const task = parseTask(existing ?? (await restore(admission)));\n assertTaskAdmission({ task, admission });\n tasks.push(task);\n }\n return tasks;\n }\n function assertAttempt(task: Task, expectAttempt: number) {\n z.number().int().positive().parse(expectAttempt);\n if (task.attempt !== expectAttempt)\n throw new Error(`Task ${task.id} is on attempt ${task.attempt}, not ${expectAttempt}`);\n }\n async function setWaiting(\n id: TaskId,\n expectAttempt: number,\n waiting: boolean,\n phase?: string,\n ): Promise<Task> {\n for (;;) {\n const { task, version } = await read(id);\n assertAttempt(task, expectAttempt);\n if (task.state !== \"running\")\n throw new Error(`Task ${id} must be running to ${waiting ? \"wait\" : \"resume\"}`);\n const next = taskSchema.parse({\n ...task,\n waitingForMessages: waiting,\n workflowPhase: phase ?? task.workflowPhase,\n lastProgressAt: waiting ? task.lastProgressAt : now(),\n updatedAt: now(),\n });\n if (task.waitingForMessages === waiting && next.workflowPhase === task.workflowPhase)\n return task;\n await stores().receipts.append({\n taskId: id,\n repositoryIds: task.repositoryIds,\n kind: task.kind,\n state: waiting ? \"waiting\" : \"resumed\",\n phase: \"workflow\",\n attempt: task.attempt,\n reason: waiting\n ? \"Workflow is waiting for Task messages\"\n : \"Workflow resumed message processing\",\n dedupeKey: `work:${id}:${version}:${waiting ? \"wait\" : \"resume\"}`,\n });\n try {\n await update(next, version);\n return next;\n } catch (error) {\n if (!(error instanceof VersionConflictError)) throw error;\n }\n }\n }\n async function complete(\n input: CompleteTaskWorkInput,\n expectedWorkflow?: WorkflowBinding,\n ): Promise<Task> {\n const {\n task: { taskId: id, attempt: expectAttempt },\n output: value,\n fence,\n } = input;\n for (;;) {\n const { task, work } = await read(id);\n assertAttempt(task, expectAttempt);\n if (fence !== undefined && !isDeepStrictEqual(task.execution, fence.execution)) {\n throw new Error(`Task ${id} execution has been replaced`);\n }\n if (expectedWorkflow !== undefined && !isDeepStrictEqual(work.workflow, expectedWorkflow)) {\n throw new Error(\n `Task ${id} uses workflow ${work.workflow.id}@${work.workflow.version}, not ${expectedWorkflow.id}@${expectedWorkflow.version}`,\n );\n }\n if (task.state === \"succeeded\") {\n const output = workflows.validateOutput(work.workflow, value);\n if (!isDeepStrictEqual(task.workResult?.output, output)) {\n throw new Error(`Task ${id} already recorded a different workflow output`);\n }\n return task;\n }\n try {\n return await stores().tasks.transition(id, \"succeeded\", {\n output: value,\n expectAttempt,\n expectFrom: fence?.from ?? task.state,\n ...(fence === undefined ? {} : { expectExecution: fence.execution }),\n phase: \"workflow\",\n });\n } catch (error) {\n if (error instanceof VersionConflictError) continue;\n if (error instanceof InvalidTransitionError && (await read(id)).task.state === \"succeeded\")\n continue;\n throw error;\n }\n }\n }\n return {\n complete,\n completeWorkflow: ({ task, workflow, output, fence }) =>\n complete({ task, output, fence }, workflow),\n waitForMessages: ({ task: { taskId, attempt }, phase }) =>\n setWaiting(taskId, attempt, true, phase),\n resume: ({ task: { taskId, attempt } }) => setWaiting(taskId, attempt, false),\n recover,\n async usage(rootTaskId) {\n const tasks = await recover(rootTaskId);\n let micros = 0;\n let inputTokens = 0;\n let outputTokens = 0;\n for (const task of tasks) {\n micros += Math.round((task.usage?.costUsd ?? task.settledUsd ?? 0) * 1_000_000);\n inputTokens += task.usage?.inputTokens ?? 0;\n outputTokens += task.usage?.outputTokens ?? 0;\n }\n return { costUsd: micros / 1_000_000, inputTokens, outputTokens, taskCount: tasks.length };\n },\n };\n}\n"],"mappings":"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"}
package/dist/sweep.d.mts CHANGED
@@ -10,6 +10,10 @@ type SweepQueuedStores = Pick<FactoryStores, "graphs" | "receipts" | "repositori
10
10
  /**
11
11
  * Single-repository adapter for an exact agent route. Supplied by the app, which knows its
12
12
  * agents' addresses. Returns the provider's execution reference when it has one.
13
+ *
14
+ * @param task - Running Task already admitted by the dispatch gate.
15
+ * @param repository - Exact enabled repository selected from the Task scope.
16
+ * @returns Provider execution reference that dispatch can persist for recovery.
13
17
  */
14
18
  type TaskLauncher = (task: Task, repository: Repository) => Promise<ExecutionReference>;
15
19
  /** Exact-route launchers, budget gate, and pass bounds for queued Task dispatch. */
@@ -25,6 +29,7 @@ interface SweepQueuedOptions {
25
29
  }
26
30
  /** Tasks dispatched, deferred by a gate, or failed by one queued-work pass. */
27
31
  interface SweepQueuedResult {
32
+ /** Task IDs whose provider execution reference was durably recorded. */
28
33
  readonly dispatched: readonly TaskId[];
29
34
  /** Tasks left in the queue because a declared gate may become eligible later. */
30
35
  readonly skipped: readonly {
@@ -44,12 +49,29 @@ type SweepSkipReasonCode = DispatchRefusalCode | "workflow_owned" | "awaiting_ap
44
49
  /** Stable reason a queued Task could not be routed or launched. */
45
50
  type SweepFailureReasonCode = "invalid_repository_scope" | "repository_not_found" | "launcher_not_registered" | "launch_failed";
46
51
  /**
47
- * One pass of the factory's dispatcher, meant to run on a schedule inside the
48
- * deployment: every queued task that is allowed to run is started, in
49
- * creation order. Nothing outside the factory ever starts an agent; the CLI
50
- * only writes tasks and approvals, and this picks them up.
51
- * Idempotent by construction: dispatchTask's single-winner transition means
52
- * two overlapping sweeps cannot start the same task twice.
52
+ * Runs one bounded pass over queued Tasks and dispatches each eligible exact route.
53
+ *
54
+ * @remarks
55
+ * Candidates are ordered by newest attempt first and then creation time. Route-less work remains
56
+ * queued for its workflow owner; unmet dependencies, approvals, and pass limits appear in
57
+ * `skipped`. Invalid repository scope, missing repositories or launchers, and launch failures
58
+ * appear in `failed`, with unrouteable Tasks transitioned to `failed` when this sweep wins the
59
+ * race. A result entry describes this pass rather than guaranteeing the Task's lifecycle state:
60
+ * an ambiguously accepted launch is reported as `launch_failed` while its Task intentionally stays
61
+ * running for recovery. Callers should surface every failure, reread those Tasks before deciding
62
+ * remediation, and schedule another pass for skipped work.
63
+ *
64
+ * Overlapping passes are safe: {@link dispatchTask} atomically admits one winner before invoking a
65
+ * launcher. A supplied `tasks` snapshot bounds reads and candidate selection for this pass; it can
66
+ * become stale, and corresponding races are returned as `task_changed` rather than hidden.
67
+ * `maxDispatches` counts provider launches started during the pass, including launches that later
68
+ * fail, so `dispatched.length` may be smaller than the limit.
69
+ *
70
+ * @param stores - Graph, receipt, repository, and Task stores used during the pass.
71
+ * @param options - Exact-route launchers plus optional budget, Task snapshot, and dispatch limit.
72
+ * @returns IDs dispatched and structured skipped/failed outcomes for every considered candidate.
73
+ * @throws When `maxDispatches` is not a positive integer or an unexpected persistence error occurs.
74
+ * @see The shipped `docs/recipes/retry-recovery.md` recipe for scheduled recovery.
53
75
  */
54
76
  declare function sweepQueued(stores: SweepQueuedStores, options: SweepQueuedOptions): Promise<SweepQueuedResult>;
55
77
  //#endregion