@vercel/factory 0.0.16 → 0.0.18

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 (186) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/api-contracts.d.mts +665 -715
  3. package/dist/api-contracts.mjs +91 -4
  4. package/dist/api-contracts.mjs.map +1 -1
  5. package/dist/api-task-graph.mjs +14 -5
  6. package/dist/api-task-graph.mjs.map +1 -1
  7. package/dist/api.d.mts +58 -11
  8. package/dist/api.mjs +715 -399
  9. package/dist/api.mjs.map +1 -1
  10. package/dist/approval-access.d.mts +1 -1
  11. package/dist/approval-contracts.d.mts +3 -4
  12. package/dist/approval-contracts.mjs +1 -2
  13. package/dist/approval-contracts.mjs.map +1 -1
  14. package/dist/blob/index.d.mts +1 -1
  15. package/dist/budget.mjs +20 -3
  16. package/dist/budget.mjs.map +1 -1
  17. package/dist/change-verification/brief.d.mts +1 -1
  18. package/dist/change-verification/dispatch.mjs +8 -1
  19. package/dist/change-verification/dispatch.mjs.map +1 -1
  20. package/dist/changes/eve-record-change.mjs +4 -1
  21. package/dist/changes/eve-record-change.mjs.map +1 -1
  22. package/dist/client-stream.d.mts +19 -2
  23. package/dist/client-stream.mjs +115 -53
  24. package/dist/client-stream.mjs.map +1 -1
  25. package/dist/client-transcript.d.mts +25 -2
  26. package/dist/client-transcript.mjs +95 -13
  27. package/dist/client-transcript.mjs.map +1 -1
  28. package/dist/client.d.mts +145 -19
  29. package/dist/client.mjs +89 -16
  30. package/dist/client.mjs.map +1 -1
  31. package/dist/code-review/contracts.d.mts +1 -0
  32. package/dist/code-review/eve-post-review.d.mts +4 -2
  33. package/dist/code-review/eve-post-review.mjs +8 -0
  34. package/dist/code-review/eve-post-review.mjs.map +1 -1
  35. package/dist/code-review/github-reporter.d.mts +6 -2
  36. package/dist/code-review/github-reporter.mjs +86 -35
  37. package/dist/code-review/github-reporter.mjs.map +1 -1
  38. package/dist/code-review/preparation.d.mts +2 -1
  39. package/dist/code-review/preparation.mjs +2 -1
  40. package/dist/code-review/preparation.mjs.map +1 -1
  41. package/dist/code-review/stall.mjs +11 -2
  42. package/dist/code-review/stall.mjs.map +1 -1
  43. package/dist/connectors/github.d.mts +1 -1
  44. package/dist/connectors/index.d.mts +1 -1
  45. package/dist/connectors/slack.d.mts +1 -1
  46. package/dist/delivery-metrics/metrics.d.mts +1 -1
  47. package/dist/dispatch.mjs +3 -3
  48. package/dist/dispatch.mjs.map +1 -1
  49. package/dist/eve/index.d.mts +1 -1
  50. package/dist/eve/index.mjs +8 -1
  51. package/dist/eve/index.mjs.map +1 -1
  52. package/dist/eve/task-execution.d.mts +4 -4
  53. package/dist/eve/transcript.d.mts +3 -1
  54. package/dist/eve/transcript.mjs.map +1 -1
  55. package/dist/finding-remediation/admission.d.mts +1 -1
  56. package/dist/finding-remediation/brief.d.mts +1 -1
  57. package/dist/github-app.d.mts +1 -1
  58. package/dist/github-publication.d.mts +1 -1
  59. package/dist/github.d.mts +9 -1
  60. package/dist/github.mjs +29 -11
  61. package/dist/github.mjs.map +1 -1
  62. package/dist/index.d.mts +1 -1
  63. package/dist/intake-contracts.d.mts +1 -0
  64. package/dist/intake-contracts.mjs +2 -1
  65. package/dist/intake-contracts.mjs.map +1 -1
  66. package/dist/integrations/github-issues.d.mts +53 -0
  67. package/dist/integrations/github-issues.mjs +170 -0
  68. package/dist/integrations/github-issues.mjs.map +1 -0
  69. package/dist/integrations/github.d.mts +2 -1
  70. package/dist/integrations/github.mjs +2 -1
  71. package/dist/multi-repository-changes/coordination.mjs +1 -1
  72. package/dist/planning/reconcile.mjs +15 -2
  73. package/dist/planning/reconcile.mjs.map +1 -1
  74. package/dist/postgres/index.d.mts +2 -1
  75. package/dist/postgres/index.mjs +11 -5
  76. package/dist/postgres/index.mjs.map +1 -1
  77. package/dist/postgres/indexed-codec.mjs +137 -0
  78. package/dist/postgres/indexed-codec.mjs.map +1 -0
  79. package/dist/postgres/indexed-schema.mjs +99 -0
  80. package/dist/postgres/indexed-schema.mjs.map +1 -0
  81. package/dist/postgres/indexed.d.mts +28 -0
  82. package/dist/postgres/indexed.mjs +277 -0
  83. package/dist/postgres/indexed.mjs.map +1 -0
  84. package/dist/postgres/record-predicates.mjs +23 -0
  85. package/dist/postgres/record-predicates.mjs.map +1 -0
  86. package/dist/presets/software-development/deepsec-brief.d.mts +1 -1
  87. package/dist/presets/software-development/dispatch.mjs +1 -1
  88. package/dist/presets/software-development/dispatch.mjs.map +1 -1
  89. package/dist/presets/software-development/recovery.mjs +9 -2
  90. package/dist/presets/software-development/recovery.mjs.map +1 -1
  91. package/dist/pull-requests/eve-fetch.d.mts +4 -1
  92. package/dist/pull-requests/eve-fetch.mjs +9 -0
  93. package/dist/pull-requests/eve-fetch.mjs.map +1 -1
  94. package/dist/pull-requests/github-publisher.d.mts +1 -1
  95. package/dist/pull-requests/github-publisher.mjs +19 -7
  96. package/dist/pull-requests/github-publisher.mjs.map +1 -1
  97. package/dist/repositories.mjs +20 -13
  98. package/dist/repositories.mjs.map +1 -1
  99. package/dist/sandbox/execution-run-ledger.d.mts +2 -2
  100. package/dist/sandbox/execution-run-ledger.mjs.map +1 -1
  101. package/dist/sandbox/execution.d.mts +2 -2
  102. package/dist/sandbox/execution.mjs.map +1 -1
  103. package/dist/schema/catalog.mjs +1 -1
  104. package/dist/schema/change.d.mts +1 -1
  105. package/dist/schema/communication.d.mts +2 -2
  106. package/dist/schema/factory-config.d.mts +1 -1
  107. package/dist/schema/outbox.d.mts +6 -6
  108. package/dist/schema/session-summary.d.mts +218 -0
  109. package/dist/schema/session-summary.mjs +68 -0
  110. package/dist/schema/session-summary.mjs.map +1 -0
  111. package/dist/schema/session.d.mts +39 -59
  112. package/dist/schema/session.mjs +6 -6
  113. package/dist/schema/session.mjs.map +1 -1
  114. package/dist/schema/task-graph.d.mts +24 -24
  115. package/dist/schema/transcript.d.mts +141 -3
  116. package/dist/schema/transcript.mjs +19 -3
  117. package/dist/schema/transcript.mjs.map +1 -1
  118. package/dist/schema/transitions.d.mts +1 -1
  119. package/dist/schema/transitions.mjs +1 -1
  120. package/dist/schema/transitions.mjs.map +1 -1
  121. package/dist/signals.d.mts +1 -1
  122. package/dist/stall.mjs +10 -2
  123. package/dist/stall.mjs.map +1 -1
  124. package/dist/storage.d.mts +10 -1
  125. package/dist/storage.mjs +7 -1
  126. package/dist/store/approvals.d.mts +14 -0
  127. package/dist/store/approvals.mjs +78 -0
  128. package/dist/store/approvals.mjs.map +1 -0
  129. package/dist/store/conversations.d.mts +59 -0
  130. package/dist/store/conversations.mjs +549 -0
  131. package/dist/store/conversations.mjs.map +1 -0
  132. package/dist/store/driver.d.mts +1 -1
  133. package/dist/store/driver.mjs.map +1 -1
  134. package/dist/store/engine.d.mts +38 -16
  135. package/dist/store/engine.mjs +229 -106
  136. package/dist/store/engine.mjs.map +1 -1
  137. package/dist/store/indexed-engine.d.mts +57 -0
  138. package/dist/store/indexed-engine.mjs +155 -0
  139. package/dist/store/indexed-engine.mjs.map +1 -0
  140. package/dist/store/indexed-memory.d.mts +7 -0
  141. package/dist/store/indexed-memory.mjs +200 -0
  142. package/dist/store/indexed-memory.mjs.map +1 -0
  143. package/dist/store/indexed-projections.d.mts +280 -0
  144. package/dist/store/indexed-projections.mjs +481 -0
  145. package/dist/store/indexed-projections.mjs.map +1 -0
  146. package/dist/store/indexed.d.mts +101 -0
  147. package/dist/store/indexed.mjs +19 -0
  148. package/dist/store/indexed.mjs.map +1 -0
  149. package/dist/store/iterate.d.mts +9 -0
  150. package/dist/store/iterate.mjs +23 -0
  151. package/dist/store/iterate.mjs.map +1 -0
  152. package/dist/store/query-scope.mjs +62 -0
  153. package/dist/store/query-scope.mjs.map +1 -0
  154. package/dist/store/query.d.mts +32 -0
  155. package/dist/store/query.mjs +103 -0
  156. package/dist/store/query.mjs.map +1 -0
  157. package/dist/store/session-history.mjs +24 -0
  158. package/dist/store/session-history.mjs.map +1 -0
  159. package/dist/store/slack-pr-notifications.d.mts +2 -2
  160. package/dist/store/slack-pr-notifications.mjs.map +1 -1
  161. package/dist/store/state.d.mts +26 -0
  162. package/dist/store/state.mjs +95 -0
  163. package/dist/store/state.mjs.map +1 -0
  164. package/dist/store/task-effects.d.mts +1 -1
  165. package/dist/store/task-graphs.d.mts +1 -1
  166. package/dist/store/task-messages.d.mts +1 -1
  167. package/dist/store/task-work.d.mts +1 -1
  168. package/dist/store/task-work.mjs +1 -1
  169. package/dist/store/transcripts.mjs +308 -0
  170. package/dist/store/transcripts.mjs.map +1 -0
  171. package/dist/sweep.d.mts +1 -1
  172. package/dist/sweep.mjs +12 -7
  173. package/dist/sweep.mjs.map +1 -1
  174. package/dist/task-communication.mjs +15 -1
  175. package/dist/task-communication.mjs.map +1 -1
  176. package/dist/task-protocols.d.mts +3 -3
  177. package/dist/tasks.d.mts +1 -1
  178. package/dist/vercel.d.mts +1 -1
  179. package/dist/work-triage/find-existing-work.d.mts +1 -1
  180. package/dist/work-triage/find-existing-work.mjs +12 -2
  181. package/dist/work-triage/find-existing-work.mjs.map +1 -1
  182. package/docs/index.md +4 -0
  183. package/docs/recipes/persistence-recovery.md +15 -9
  184. package/docs/recipes/postgres-indexed-engine.md +108 -0
  185. package/docs/recipes/postgres-indexed-storage.md +145 -0
  186. package/package.json +6 -1
@@ -0,0 +1,308 @@
1
+ import { executionReferenceSchema } from "../schema/task.mjs";
2
+ import { RecordExistsError, VersionConflictError } from "./driver.mjs";
3
+ import { transcriptBlockSchema, transcriptSnapshotSchema } from "../schema/transcript.mjs";
4
+ import { projectIndexedRecord } from "./indexed-projections.mjs";
5
+ import { createHash, randomUUID } from "node:crypto";
6
+ import { z } from "zod";
7
+ //#region src/store/transcripts.ts
8
+ const partBytes = 49152;
9
+ const previewBytes = 4096;
10
+ const headSchema = z.strictObject({
11
+ schemaVersion: z.literal(1),
12
+ execution: executionReferenceSchema,
13
+ snapshot: transcriptSnapshotSchema,
14
+ blockCount: z.number().int().nonnegative(),
15
+ updatedAt: z.iso.datetime()
16
+ });
17
+ const blockSchema = z.strictObject({
18
+ executionId: z.string(),
19
+ blockId: z.string(),
20
+ digest: z.string().regex(/^[a-f0-9]{64}$/u),
21
+ ordinal: z.number().int().nonnegative(),
22
+ parts: z.number().int().positive(),
23
+ bytes: z.number().int().positive(),
24
+ preview: transcriptBlockSchema
25
+ });
26
+ const chunkSchema = z.strictObject({ data: z.string() });
27
+ function jsonValue(value) {
28
+ return JSON.parse(JSON.stringify(value));
29
+ }
30
+ const transcriptPageQuerySchema = z.strictObject({
31
+ limit: z.number().int().min(1).max(100),
32
+ cursor: z.string().max(8192).optional()
33
+ });
34
+ function digest(value) {
35
+ return createHash("sha256").update(value).digest("hex");
36
+ }
37
+ function executionId(execution) {
38
+ executionReferenceSchema.parse(execution);
39
+ return `transcript_${digest(JSON.stringify([execution.provider, execution.sessionId])).slice(0, 32)}`;
40
+ }
41
+ function blockKey(id, blockId) {
42
+ return `block_${digest(JSON.stringify([id, blockId]))}`;
43
+ }
44
+ function chunkKey(id, hash, part) {
45
+ return `chunk_${digest(JSON.stringify([
46
+ id,
47
+ hash,
48
+ part
49
+ ]))}`;
50
+ }
51
+ function blockIndex(id) {
52
+ return `transcript-blocks:${id}`;
53
+ }
54
+ function transcriptBlockPreview(block) {
55
+ const bytes = Buffer.byteLength(JSON.stringify(block));
56
+ if (bytes <= previewBytes) return block;
57
+ const clipped = (text, length) => Array.from(text.replace(/[\uD800-\uDFFF]/gu, "�")).slice(0, length).join("");
58
+ const base = {
59
+ id: block.id,
60
+ title: clipped(block.title, 120),
61
+ text: clipped(block.text, 240),
62
+ createdAt: block.createdAt,
63
+ contentRef: {
64
+ blockId: block.id,
65
+ bytes
66
+ }
67
+ };
68
+ return transcriptBlockSchema.parse(block.kind === "tool" ? {
69
+ ...base,
70
+ kind: "tool",
71
+ tool: {
72
+ callId: block.tool.callId,
73
+ name: block.tool.name,
74
+ inputText: clipped(block.tool.inputText, 240),
75
+ state: block.tool.state
76
+ }
77
+ } : {
78
+ ...base,
79
+ kind: block.kind,
80
+ ...block.kind === "user" ? { attachments: block.attachments } : {}
81
+ });
82
+ }
83
+ function pageTranscriptSnapshot(execution, snapshot, input) {
84
+ const query = transcriptPageQuerySchema.parse(input);
85
+ const id = executionId(execution);
86
+ const before = query.cursor === void 0 ? snapshot.blocks.length : z.strictObject({
87
+ id: z.literal(id),
88
+ before: z.number().int().nonnegative()
89
+ }).parse(JSON.parse(Buffer.from(query.cursor, "base64url").toString("utf8"))).before;
90
+ const start = Math.max(0, before - query.limit);
91
+ return {
92
+ snapshot: {
93
+ ...snapshot,
94
+ blocks: snapshot.blocks.slice(start, before).map(transcriptBlockPreview)
95
+ },
96
+ nextCursor: start > 0 ? Buffer.from(JSON.stringify({
97
+ id,
98
+ before: start
99
+ })).toString("base64url") : null,
100
+ totalBlocks: snapshot.blocks.length
101
+ };
102
+ }
103
+ function createIndexedTranscriptStore(store, now) {
104
+ async function head(transaction, execution) {
105
+ const record = await transaction.get("transcripts", executionId(execution));
106
+ if (record === null) return null;
107
+ const value = headSchema.parse(record.value);
108
+ if (value.execution.provider !== execution.provider || value.execution.sessionId !== execution.sessionId) throw new Error("Transcript execution key does not match its stored value");
109
+ return {
110
+ value,
111
+ version: record.version
112
+ };
113
+ }
114
+ async function readBlock(transaction, id, block) {
115
+ if (block.executionId !== id) throw new Error("Transcript block execution mismatch");
116
+ const chunks = [];
117
+ for (let part = 0; part < block.parts; part++) {
118
+ const record = await transaction.get("transcriptChunks", chunkKey(id, block.digest, part));
119
+ if (record === null) throw new Error("Committed transcript content is missing");
120
+ chunks.push(Buffer.from(chunkSchema.parse(record.value).data, "base64"));
121
+ }
122
+ const encoded = Buffer.concat(chunks);
123
+ if (encoded.byteLength !== block.bytes || digest(encoded) !== block.digest) throw new Error("Transcript content checksum mismatch");
124
+ const value = transcriptBlockSchema.parse(JSON.parse(encoded.toString("utf8")));
125
+ if (value.id !== block.blockId || value.contentRef !== void 0) throw new Error("Transcript block identity mismatch");
126
+ return value;
127
+ }
128
+ async function readSnapshot(transaction, execution) {
129
+ const record = await head(transaction, execution);
130
+ if (record === null) return null;
131
+ const id = executionId(execution);
132
+ const blocks = [];
133
+ let cursor;
134
+ do {
135
+ const page = await transaction.query({
136
+ index: blockIndex(id),
137
+ limit: 100,
138
+ order: "asc",
139
+ cursor
140
+ });
141
+ for (const entry of page.entries) blocks.push(await readBlock(transaction, id, blockSchema.parse(entry.summary)));
142
+ cursor = page.nextCursor ?? void 0;
143
+ } while (cursor !== void 0);
144
+ if (blocks.length !== record.value.blockCount) throw new Error("Transcript block count mismatch");
145
+ return {
146
+ ...record.value.snapshot,
147
+ blocks
148
+ };
149
+ }
150
+ return {
151
+ get(execution) {
152
+ return store.transaction((transaction) => readSnapshot(transaction, execution));
153
+ },
154
+ async head(execution) {
155
+ const record = await head(store, execution);
156
+ return record === null ? null : {
157
+ ...record.value.snapshot,
158
+ blockCount: record.value.blockCount
159
+ };
160
+ },
161
+ async page(execution, input) {
162
+ const query = transcriptPageQuerySchema.parse(input);
163
+ return store.transaction(async (transaction) => {
164
+ const record = await head(transaction, execution);
165
+ if (record === null) return null;
166
+ const id = executionId(execution);
167
+ const page = await transaction.query({
168
+ index: blockIndex(id),
169
+ ...query,
170
+ order: "desc"
171
+ });
172
+ const blocks = page.entries.map((entry) => {
173
+ const block = blockSchema.parse(entry.summary);
174
+ if (block.executionId !== id) throw new Error("Transcript page execution mismatch");
175
+ return block.preview;
176
+ }).reverse();
177
+ return {
178
+ snapshot: {
179
+ ...record.value.snapshot,
180
+ blocks
181
+ },
182
+ nextCursor: page.nextCursor,
183
+ totalBlocks: record.value.blockCount
184
+ };
185
+ });
186
+ },
187
+ block(execution, blockId) {
188
+ return store.transaction(async (transaction) => {
189
+ const id = executionId(execution);
190
+ if (await head(transaction, execution) === null) return null;
191
+ const record = await transaction.get("transcriptBlocks", blockKey(id, blockId));
192
+ if (record === null) return null;
193
+ const block = blockSchema.parse(record.value);
194
+ if (block.blockId !== blockId) throw new Error("Transcript block key mismatch");
195
+ return readBlock(transaction, id, block);
196
+ });
197
+ },
198
+ async put(execution, input) {
199
+ const snapshot = transcriptSnapshotSchema.parse(input);
200
+ const id = executionId(execution);
201
+ const seen = /* @__PURE__ */ new Set();
202
+ const encoded = snapshot.blocks.map((block, ordinal) => {
203
+ if (block.contentRef !== void 0) throw new Error("Cannot persist a transcript preview as full content");
204
+ if (seen.has(block.id)) throw new Error("Duplicate transcript block identity");
205
+ seen.add(block.id);
206
+ const bytes = Buffer.from(JSON.stringify(block));
207
+ return {
208
+ key: blockKey(id, block.id),
209
+ bytes,
210
+ header: blockSchema.parse(jsonValue({
211
+ executionId: id,
212
+ blockId: block.id,
213
+ digest: digest(bytes),
214
+ ordinal,
215
+ parts: Math.ceil(bytes.length / partBytes),
216
+ bytes: bytes.length,
217
+ preview: transcriptBlockPreview(block)
218
+ }))
219
+ };
220
+ });
221
+ for (let attempt = 0;; attempt++) try {
222
+ return await store.transaction(async (transaction) => {
223
+ const previous = await head(transaction, execution);
224
+ if (previous !== null && previous.value.snapshot.cursor >= snapshot.cursor) return await readSnapshot(transaction, execution);
225
+ const old = /* @__PURE__ */ new Map();
226
+ let cursor;
227
+ do {
228
+ const page = await transaction.query({
229
+ index: blockIndex(id),
230
+ limit: 100,
231
+ cursor
232
+ });
233
+ for (const entry of page.entries) old.set(entry.id, {
234
+ value: blockSchema.parse(entry.summary),
235
+ version: entry.version
236
+ });
237
+ cursor = page.nextCursor ?? void 0;
238
+ } while (cursor !== void 0);
239
+ for (const block of encoded) {
240
+ const current = old.get(block.key);
241
+ old.delete(block.key);
242
+ if (current?.value.digest === block.header.digest && current.value.ordinal === block.header.ordinal) continue;
243
+ if (current?.value.digest !== block.header.digest) for (let part = 0; part < block.header.parts; part++) {
244
+ const key = chunkKey(id, block.header.digest, part);
245
+ if (await transaction.get("transcriptChunks", key) === null) await transaction.write({
246
+ collection: "transcriptChunks",
247
+ id: key,
248
+ expectedVersion: 0,
249
+ operationId: randomUUID(),
250
+ value: { data: block.bytes.subarray(part * partBytes, (part + 1) * partBytes).toString("base64") },
251
+ indexes: []
252
+ });
253
+ }
254
+ const inactive = current === void 0 ? await transaction.get("transcriptBlocks", block.key) : null;
255
+ await transaction.write({
256
+ collection: "transcriptBlocks",
257
+ id: block.key,
258
+ expectedVersion: current?.version ?? inactive?.version ?? 0,
259
+ operationId: randomUUID(),
260
+ value: block.header,
261
+ indexes: [{
262
+ index: blockIndex(id),
263
+ sortKey: String(block.header.ordinal).padStart(16, "0"),
264
+ summary: block.header
265
+ }]
266
+ });
267
+ }
268
+ for (const [key, previous] of old) await transaction.write({
269
+ collection: "transcriptBlocks",
270
+ id: key,
271
+ expectedVersion: previous.version,
272
+ operationId: randomUUID(),
273
+ value: previous.value,
274
+ indexes: []
275
+ });
276
+ const value = headSchema.parse(jsonValue({
277
+ schemaVersion: 1,
278
+ execution,
279
+ snapshot: {
280
+ ...snapshot,
281
+ blocks: []
282
+ },
283
+ blockCount: snapshot.blocks.length,
284
+ updatedAt: now()
285
+ }));
286
+ await transaction.write({
287
+ collection: "transcripts",
288
+ id,
289
+ expectedVersion: previous?.version ?? 0,
290
+ operationId: randomUUID(),
291
+ value,
292
+ indexes: projectIndexedRecord("transcripts", id, value).map((entry) => ({
293
+ ...entry,
294
+ summary: jsonValue(entry.summary)
295
+ }))
296
+ });
297
+ return snapshot;
298
+ });
299
+ } catch (error) {
300
+ if (attempt >= 4 || !(error instanceof VersionConflictError || error instanceof RecordExistsError)) throw error;
301
+ }
302
+ }
303
+ };
304
+ }
305
+ //#endregion
306
+ export { createIndexedTranscriptStore, pageTranscriptSnapshot };
307
+
308
+ //# sourceMappingURL=transcripts.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transcripts.mjs","names":[],"sources":["../../src/store/transcripts.ts"],"sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport { z } from \"zod\";\nimport {\n transcriptBlockSchema,\n transcriptSnapshotSchema,\n type TranscriptBlock,\n type TranscriptPageQuery,\n type TranscriptSnapshot,\n} from \"../schema/transcript\";\nimport { executionReferenceSchema, type ExecutionReference } from \"../schema/task\";\nimport { RecordExistsError, VersionConflictError } from \"./driver\";\nimport type { FactoryStores } from \"./engine\";\nimport type { IndexedStore } from \"./indexed\";\nimport { projectIndexedRecord } from \"./indexed-projections\";\n\nconst partBytes = 48 * 1024;\nconst previewBytes = 4 * 1024;\nconst headSchema = z.strictObject({\n schemaVersion: z.literal(1),\n execution: executionReferenceSchema,\n snapshot: transcriptSnapshotSchema,\n blockCount: z.number().int().nonnegative(),\n updatedAt: z.iso.datetime(),\n});\nconst blockSchema = z.strictObject({\n executionId: z.string(),\n blockId: z.string(),\n digest: z.string().regex(/^[a-f0-9]{64}$/u),\n ordinal: z.number().int().nonnegative(),\n parts: z.number().int().positive(),\n bytes: z.number().int().positive(),\n preview: transcriptBlockSchema,\n});\nconst chunkSchema = z.strictObject({ data: z.string() });\nfunction jsonValue(value: unknown): unknown {\n return JSON.parse(JSON.stringify(value)) as unknown;\n}\nconst transcriptPageQuerySchema = z.strictObject({\n limit: z.number().int().min(1).max(100),\n cursor: z.string().max(8192).optional(),\n});\n\nfunction digest(value: string | Buffer): string {\n return createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\nfunction executionId(execution: ExecutionReference): string {\n executionReferenceSchema.parse(execution);\n return `transcript_${digest(JSON.stringify([execution.provider, execution.sessionId])).slice(0, 32)}`;\n}\n\nfunction blockKey(id: string, blockId: string): string {\n return `block_${digest(JSON.stringify([id, blockId]))}`;\n}\n\nfunction chunkKey(id: string, hash: string, part: number): string {\n return `chunk_${digest(JSON.stringify([id, hash, part]))}`;\n}\n\nfunction blockIndex(id: string): string {\n return `transcript-blocks:${id}`;\n}\n\nfunction transcriptBlockPreview(block: TranscriptBlock): TranscriptBlock {\n const bytes = Buffer.byteLength(JSON.stringify(block));\n if (bytes <= previewBytes) return block;\n const clipped = (text: string, length: number) =>\n Array.from(text.replace(/[\\uD800-\\uDFFF]/gu, \"�\"))\n .slice(0, length)\n .join(\"\");\n const base = {\n id: block.id,\n title: clipped(block.title, 120),\n text: clipped(block.text, 240),\n createdAt: block.createdAt,\n contentRef: { blockId: block.id, bytes },\n };\n return transcriptBlockSchema.parse(\n block.kind === \"tool\"\n ? {\n ...base,\n kind: \"tool\",\n tool: {\n callId: block.tool.callId,\n name: block.tool.name,\n inputText: clipped(block.tool.inputText, 240),\n state: block.tool.state,\n },\n }\n : {\n ...base,\n kind: block.kind,\n ...(block.kind === \"user\" ? { attachments: block.attachments } : {}),\n },\n );\n}\n\nexport function pageTranscriptSnapshot(\n execution: ExecutionReference,\n snapshot: TranscriptSnapshot,\n input: TranscriptPageQuery,\n) {\n const query = transcriptPageQuerySchema.parse(input);\n const id = executionId(execution);\n const before =\n query.cursor === undefined\n ? snapshot.blocks.length\n : z\n .strictObject({\n id: z.literal(id),\n before: z.number().int().nonnegative(),\n })\n .parse(JSON.parse(Buffer.from(query.cursor, \"base64url\").toString(\"utf8\"))).before;\n const start = Math.max(0, before - query.limit);\n return {\n snapshot: {\n ...snapshot,\n blocks: snapshot.blocks.slice(start, before).map(transcriptBlockPreview),\n },\n nextCursor:\n start > 0 ? Buffer.from(JSON.stringify({ id, before: start })).toString(\"base64url\") : null,\n totalBlocks: snapshot.blocks.length,\n };\n}\n\nexport function createIndexedTranscriptStore(\n store: IndexedStore,\n now: () => string,\n): FactoryStores[\"transcripts\"] {\n async function head(transaction: IndexedStore, execution: ExecutionReference) {\n const record = await transaction.get(\"transcripts\", executionId(execution));\n if (record === null) return null;\n const value = headSchema.parse(record.value);\n if (\n value.execution.provider !== execution.provider ||\n value.execution.sessionId !== execution.sessionId\n )\n throw new Error(\"Transcript execution key does not match its stored value\");\n return { value, version: record.version };\n }\n\n async function readBlock(\n transaction: IndexedStore,\n id: string,\n block: z.infer<typeof blockSchema>,\n ) {\n if (block.executionId !== id) throw new Error(\"Transcript block execution mismatch\");\n const chunks: Buffer[] = [];\n for (let part = 0; part < block.parts; part++) {\n const record = await transaction.get(\"transcriptChunks\", chunkKey(id, block.digest, part));\n if (record === null) throw new Error(\"Committed transcript content is missing\");\n chunks.push(Buffer.from(chunkSchema.parse(record.value).data, \"base64\"));\n }\n const encoded = Buffer.concat(chunks);\n if (encoded.byteLength !== block.bytes || digest(encoded) !== block.digest)\n throw new Error(\"Transcript content checksum mismatch\");\n const value = transcriptBlockSchema.parse(JSON.parse(encoded.toString(\"utf8\")));\n if (value.id !== block.blockId || value.contentRef !== undefined)\n throw new Error(\"Transcript block identity mismatch\");\n return value;\n }\n\n async function readSnapshot(\n transaction: IndexedStore,\n execution: ExecutionReference,\n ): Promise<TranscriptSnapshot | null> {\n const record = await head(transaction, execution);\n if (record === null) return null;\n const id = executionId(execution);\n const blocks: TranscriptBlock[] = [];\n let cursor: string | undefined;\n do {\n const page = await transaction.query({\n index: blockIndex(id),\n limit: 100,\n order: \"asc\",\n cursor,\n });\n for (const entry of page.entries)\n blocks.push(await readBlock(transaction, id, blockSchema.parse(entry.summary)));\n cursor = page.nextCursor ?? undefined;\n } while (cursor !== undefined);\n if (blocks.length !== record.value.blockCount)\n throw new Error(\"Transcript block count mismatch\");\n return { ...record.value.snapshot, blocks };\n }\n\n return {\n get(execution) {\n return store.transaction((transaction) => readSnapshot(transaction, execution));\n },\n async head(execution) {\n const record = await head(store, execution);\n return record === null\n ? null\n : { ...record.value.snapshot, blockCount: record.value.blockCount };\n },\n async page(execution, input) {\n const query = transcriptPageQuerySchema.parse(input);\n return store.transaction(async (transaction) => {\n const record = await head(transaction, execution);\n if (record === null) return null;\n const id = executionId(execution);\n const page = await transaction.query({ index: blockIndex(id), ...query, order: \"desc\" });\n const blocks = page.entries\n .map((entry) => {\n const block = blockSchema.parse(entry.summary);\n if (block.executionId !== id) throw new Error(\"Transcript page execution mismatch\");\n return block.preview;\n })\n .reverse();\n return {\n snapshot: { ...record.value.snapshot, blocks },\n nextCursor: page.nextCursor,\n totalBlocks: record.value.blockCount,\n };\n });\n },\n block(execution, blockId) {\n return store.transaction(async (transaction) => {\n const id = executionId(execution);\n if ((await head(transaction, execution)) === null) return null;\n const record = await transaction.get(\"transcriptBlocks\", blockKey(id, blockId));\n if (record === null) return null;\n const block = blockSchema.parse(record.value);\n if (block.blockId !== blockId) throw new Error(\"Transcript block key mismatch\");\n return readBlock(transaction, id, block);\n });\n },\n async put(execution, input) {\n const snapshot = transcriptSnapshotSchema.parse(input);\n const id = executionId(execution);\n const seen = new Set<string>();\n const encoded = snapshot.blocks.map((block, ordinal) => {\n if (block.contentRef !== undefined)\n throw new Error(\"Cannot persist a transcript preview as full content\");\n if (seen.has(block.id)) throw new Error(\"Duplicate transcript block identity\");\n seen.add(block.id);\n const bytes = Buffer.from(JSON.stringify(block));\n return {\n key: blockKey(id, block.id),\n bytes,\n header: blockSchema.parse(\n jsonValue({\n executionId: id,\n blockId: block.id,\n digest: digest(bytes),\n ordinal,\n parts: Math.ceil(bytes.length / partBytes),\n bytes: bytes.length,\n preview: transcriptBlockPreview(block),\n }),\n ),\n };\n });\n for (let attempt = 0; ; attempt++) {\n try {\n return await store.transaction(async (transaction) => {\n const previous = await head(transaction, execution);\n if (previous !== null && previous.value.snapshot.cursor >= snapshot.cursor)\n return (await readSnapshot(transaction, execution))!;\n const old = new Map<string, { value: z.infer<typeof blockSchema>; version: number }>();\n let cursor: string | undefined;\n do {\n const page = await transaction.query({ index: blockIndex(id), limit: 100, cursor });\n for (const entry of page.entries)\n old.set(entry.id, {\n value: blockSchema.parse(entry.summary),\n version: entry.version,\n });\n cursor = page.nextCursor ?? undefined;\n } while (cursor !== undefined);\n for (const block of encoded) {\n const current = old.get(block.key);\n old.delete(block.key);\n if (\n current?.value.digest === block.header.digest &&\n current.value.ordinal === block.header.ordinal\n )\n continue;\n if (current?.value.digest !== block.header.digest) {\n for (let part = 0; part < block.header.parts; part++) {\n const key = chunkKey(id, block.header.digest, part);\n if ((await transaction.get(\"transcriptChunks\", key)) === null)\n await transaction.write({\n collection: \"transcriptChunks\",\n id: key,\n expectedVersion: 0,\n operationId: randomUUID(),\n value: {\n data: block.bytes\n .subarray(part * partBytes, (part + 1) * partBytes)\n .toString(\"base64\"),\n },\n indexes: [],\n });\n }\n }\n const inactive =\n current === undefined ? await transaction.get(\"transcriptBlocks\", block.key) : null;\n await transaction.write({\n collection: \"transcriptBlocks\",\n id: block.key,\n expectedVersion: current?.version ?? inactive?.version ?? 0,\n operationId: randomUUID(),\n value: block.header,\n indexes: [\n {\n index: blockIndex(id),\n sortKey: String(block.header.ordinal).padStart(16, \"0\"),\n summary: block.header,\n },\n ],\n });\n }\n for (const [key, previous] of old)\n await transaction.write({\n collection: \"transcriptBlocks\",\n id: key,\n expectedVersion: previous.version,\n operationId: randomUUID(),\n value: previous.value,\n indexes: [],\n });\n const value = headSchema.parse(\n jsonValue({\n schemaVersion: 1,\n execution,\n snapshot: { ...snapshot, blocks: [] },\n blockCount: snapshot.blocks.length,\n updatedAt: now(),\n }),\n );\n await transaction.write({\n collection: \"transcripts\",\n id,\n expectedVersion: previous?.version ?? 0,\n operationId: randomUUID(),\n value,\n indexes: projectIndexedRecord(\"transcripts\", id, value).map((entry) => ({\n ...entry,\n summary: jsonValue(entry.summary),\n })),\n });\n return snapshot;\n });\n } catch (error) {\n if (\n attempt >= 4 ||\n !(error instanceof VersionConflictError || error instanceof RecordExistsError)\n )\n throw error;\n }\n }\n },\n };\n}\n"],"mappings":"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"}
package/dist/sweep.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { TaskId } from "./schema/id.mjs";
2
- import { Repository } from "./schema/repository.mjs";
3
2
  import { ExecutionReference, Task } from "./schema/task.mjs";
4
3
  import { AgentRouteKey } from "./agent-routes.mjs";
4
+ import { Repository } from "./schema/repository.mjs";
5
5
  import { FactoryStores } from "./store/engine.mjs";
6
6
  import { DispatchBudget, DispatchRefusalCode } from "./dispatch.mjs";
7
7
  //#region src/sweep.d.ts
package/dist/sweep.mjs CHANGED
@@ -1,6 +1,8 @@
1
+ import { collectFactoryRecords } from "./store/iterate.mjs";
2
+ import { budgetTasks } from "./budget.mjs";
3
+ import { VersionConflictError } from "./store/driver.mjs";
1
4
  import { InvalidTransitionError } from "./schema/transitions.mjs";
2
5
  import { routeKey } from "./agent-routes.mjs";
3
- import { VersionConflictError } from "./store/driver.mjs";
4
6
  import { DispatchRefusedError, dispatchTask } from "./dispatch.mjs";
5
7
  import { TaskAttemptConflictError } from "./store/engine.mjs";
6
8
  //#region src/sweep.ts
@@ -61,9 +63,12 @@ async function sweepQueued(stores, options) {
61
63
  const maxDispatches = options.maxDispatches ?? Number.POSITIVE_INFINITY;
62
64
  if (!(Number.isInteger(maxDispatches) || maxDispatches === Number.POSITIVE_INFINITY) || maxDispatches < 1) throw new Error("maxDispatches must be a positive integer");
63
65
  let attempted = 0;
64
- const taskSnapshot = options.tasks ?? await stores.tasks.list();
65
- const budgetSnapshot = options.budget?.tasks ?? (options.budget && options.tasks ? await stores.tasks.list() : taskSnapshot);
66
- const budgetTasks = new Map(budgetSnapshot.map((task) => [task.id, task]));
66
+ const taskSnapshot = options.tasks ?? await collectFactoryRecords(stores.tasks.list, {
67
+ limit: 100,
68
+ match: { state: "queued" }
69
+ });
70
+ const budgetSnapshot = options.budget?.tasks ?? (options.budget && options.budget.enforce !== false ? await budgetTasks(stores) : []);
71
+ const budgetTasks$1 = new Map(budgetSnapshot.map((task) => [task.id, task]));
67
72
  const queued = taskSnapshot.filter((task) => task.state === "queued").sort((a, b) => b.attempt - a.attempt || a.createdAt.localeCompare(b.createdAt));
68
73
  for (const task of queued) {
69
74
  const route = task.work.route;
@@ -130,15 +135,15 @@ async function sweepQueued(stores, options) {
130
135
  },
131
136
  budget: options.budget === void 0 ? void 0 : {
132
137
  ...options.budget,
133
- tasks: [...budgetTasks.values()]
138
+ tasks: [...budgetTasks$1.values()]
134
139
  }
135
140
  });
136
- budgetTasks.set(dispatched.id, dispatched);
141
+ budgetTasks$1.set(dispatched.id, dispatched);
137
142
  result.dispatched.push(task.id);
138
143
  } catch (error) {
139
144
  if (options.budget !== void 0) {
140
145
  const current = await stores.tasks.get(task.id).catch(() => null);
141
- if (current !== null) budgetTasks.set(current.id, current);
146
+ if (current !== null) budgetTasks$1.set(current.id, current);
142
147
  }
143
148
  if (error instanceof DispatchRefusedError) {
144
149
  result.skipped.push({
@@ -1 +1 @@
1
- {"version":3,"file":"sweep.mjs","names":[],"sources":["../src/sweep.ts"],"sourcesContent":["import { routeKey, type AgentRouteKey } from \"./agent-routes\";\nimport { dispatchTask, DispatchRefusedError, type DispatchBudget } from \"./dispatch\";\nimport type { DispatchRefusalCode } from \"./dispatch\";\nimport type { TaskId } from \"./schema/id\";\nimport type { Repository } from \"./schema/repository\";\nimport type { ExecutionReference, Task } from \"./schema/task\";\nimport { InvalidTransitionError } from \"./schema/transitions\";\nimport { VersionConflictError } from \"./store/driver\";\nimport { TaskAttemptConflictError, type FactoryStores } from \"./store/engine\";\n\n/** Graph, receipt, repository, and Task capabilities required by queued-work dispatch. */\nexport type SweepQueuedStores = Pick<\n FactoryStores,\n \"graphs\" | \"receipts\" | \"repositories\" | \"tasks\"\n>;\n\n/**\n * Single-repository adapter for an exact agent route. Supplied by the app, which knows its\n * agents' addresses. Returns the provider's execution reference when it has one.\n *\n * @param task - Running Task already admitted by the dispatch gate.\n * @param repository - Exact enabled repository selected from the Task scope.\n * @returns Provider execution reference that dispatch can persist for recovery.\n */\nexport type TaskLauncher = (task: Task, repository: Repository) => Promise<ExecutionReference>;\n\n/** Exact-route launchers, budget gate, and pass bounds for queued Task dispatch. */\nexport interface SweepQueuedOptions {\n /** Single-repository agent adapters keyed by routeKey(binding), e.g. code-change@1. */\n launchers: Readonly<Partial<Record<AgentRouteKey, TaskLauncher>>>;\n /** Reserve-before-execution limits, passed through to every dispatch. */\n budget?: DispatchBudget;\n /** Maximum task launches attempted in one pass; omitted means no limit. */\n maxDispatches?: number;\n /** A dispatcher-wide snapshot avoids another complete task read. */\n tasks?: readonly Task[];\n}\n\n/** Tasks dispatched, deferred by a gate, or failed by one queued-work pass. */\nexport interface SweepQueuedResult {\n /** Task IDs whose provider execution reference was durably recorded. */\n readonly dispatched: readonly TaskId[];\n /** Tasks left in the queue because a declared gate may become eligible later. */\n readonly skipped: readonly {\n readonly taskId: TaskId;\n readonly reasonCode: SweepSkipReasonCode;\n readonly reason: string;\n }[];\n /** Tasks whose routing is invalid or whose launch threw. */\n readonly failed: readonly {\n readonly taskId: TaskId;\n readonly reasonCode: SweepFailureReasonCode;\n readonly reason: string;\n }[];\n}\n\n/** Stable reason a queued Task remained eligible for a later dispatch pass. */\nexport type SweepSkipReasonCode =\n | DispatchRefusalCode\n | \"workflow_owned\"\n | \"awaiting_approval\"\n | \"dispatch_limit\"\n | \"task_changed\";\n\n/** Stable reason a queued Task could not be routed or launched. */\nexport type SweepFailureReasonCode =\n | \"invalid_repository_scope\"\n | \"repository_not_found\"\n | \"launcher_not_registered\"\n | \"launch_failed\";\n\ntype MutableSweepQueuedResult = {\n dispatched: TaskId[];\n skipped: { taskId: TaskId; reasonCode: SweepSkipReasonCode; reason: string }[];\n failed: { taskId: TaskId; reasonCode: SweepFailureReasonCode; reason: string }[];\n};\n\nasync function failUnrouteable(\n stores: SweepQueuedStores,\n task: Task,\n reasonCode: SweepFailureReasonCode,\n reason: string,\n result: MutableSweepQueuedResult,\n): Promise<void> {\n try {\n await stores.tasks.transition(task.id, \"failed\", { reason, expectFrom: \"queued\" });\n result.failed.push({ taskId: task.id, reasonCode, reason });\n } catch (error) {\n if (error instanceof InvalidTransitionError || error instanceof VersionConflictError) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"task_changed\",\n reason: \"task changed during routing\",\n });\n return;\n }\n throw error;\n }\n}\n\n/**\n * Runs one bounded pass over queued Tasks and dispatches each eligible exact route.\n *\n * @remarks\n * Candidates are ordered by newest attempt first and then creation time. Route-less work remains\n * queued for its workflow owner; unmet dependencies, approvals, and pass limits appear in\n * `skipped`. Invalid repository scope, missing repositories or launchers, and launch failures\n * appear in `failed`, with unrouteable Tasks transitioned to `failed` when this sweep wins the\n * race. A result entry describes this pass rather than guaranteeing the Task's lifecycle state:\n * an ambiguously accepted launch is reported as `launch_failed` while its Task intentionally stays\n * running for recovery. Callers should surface every failure, reread those Tasks before deciding\n * remediation, and schedule another pass for skipped work.\n *\n * Overlapping passes are safe: {@link dispatchTask} atomically admits one winner before invoking a\n * launcher. A supplied `tasks` snapshot bounds reads and candidate selection for this pass; it can\n * become stale, and corresponding races are returned as `task_changed` rather than hidden.\n * `maxDispatches` counts provider launches started during the pass, including launches that later\n * fail, so `dispatched.length` may be smaller than the limit.\n *\n * @param stores - Graph, receipt, repository, and Task stores used during the pass.\n * @param options - Exact-route launchers plus optional budget, Task snapshot, and dispatch limit.\n * @returns IDs dispatched and structured skipped/failed outcomes for every considered candidate.\n * @throws When `maxDispatches` is not a positive integer or an unexpected persistence error occurs.\n * @see The shipped `docs/recipes/retry-recovery.md` recipe for scheduled recovery.\n */\nexport async function sweepQueued(\n stores: SweepQueuedStores,\n options: SweepQueuedOptions,\n): Promise<SweepQueuedResult> {\n const result: MutableSweepQueuedResult = { dispatched: [], skipped: [], failed: [] };\n const maxDispatches = options.maxDispatches ?? Number.POSITIVE_INFINITY;\n if (\n !(Number.isInteger(maxDispatches) || maxDispatches === Number.POSITIVE_INFINITY) ||\n maxDispatches < 1\n ) {\n throw new Error(\"maxDispatches must be a positive integer\");\n }\n let attempted = 0;\n const taskSnapshot = options.tasks ?? (await stores.tasks.list());\n const budgetSnapshot =\n options.budget?.tasks ??\n (options.budget && options.tasks ? await stores.tasks.list() : taskSnapshot);\n const budgetTasks = new Map(budgetSnapshot.map((task) => [task.id, task]));\n const queued = taskSnapshot\n .filter((task) => task.state === \"queued\")\n .sort((a, b) => b.attempt - a.attempt || a.createdAt.localeCompare(b.createdAt));\n\n for (const task of queued) {\n const route = task.work.route;\n if (route === undefined) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"workflow_owned\",\n reason: \"no agent route; workflow owns execution\",\n });\n continue;\n }\n const [repositoryId] = task.repositoryIds;\n if (task.repositoryIds.length !== 1 || repositoryId === undefined) {\n await failUnrouteable(\n stores,\n task,\n \"invalid_repository_scope\",\n \"sweep launcher adapter requires exactly one repository\",\n result,\n );\n continue;\n }\n if (task.approval === \"required\") {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"awaiting_approval\",\n reason: \"awaiting human approval\",\n });\n continue;\n }\n const key = routeKey(route);\n const launcher = Object.hasOwn(options.launchers, key) ? options.launchers[key] : undefined;\n if (launcher === undefined) {\n await failUnrouteable(\n stores,\n task,\n \"launcher_not_registered\",\n `no registered launcher handles agent route ${key}`,\n result,\n );\n continue;\n }\n if (\n !(await stores.graphs.dependenciesSatisfied({\n rootTaskId: task.rootTaskId,\n taskId: task.id,\n }))\n ) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"dependencies_unsatisfied\",\n reason: \"graph dependencies must all be succeeded before dispatch\",\n });\n continue;\n }\n if (attempted >= maxDispatches) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"dispatch_limit\",\n reason: \"per-sweep dispatch limit reached\",\n });\n continue;\n }\n const repository = await stores.repositories.get(repositoryId);\n if (repository === null) {\n await failUnrouteable(stores, task, \"repository_not_found\", \"task has no repository\", result);\n continue;\n }\n let launchStarted = false;\n try {\n const dispatched = await dispatchTask(stores, {\n taskId: task.id,\n route,\n launch: (dispatched) => {\n attempted += 1;\n launchStarted = true;\n return launcher(dispatched, repository);\n },\n budget:\n options.budget === undefined\n ? undefined\n : { ...options.budget, tasks: [...budgetTasks.values()] },\n });\n budgetTasks.set(dispatched.id, dispatched);\n result.dispatched.push(task.id);\n } catch (error) {\n if (options.budget !== undefined) {\n const current = await stores.tasks.get(task.id).catch(() => null);\n if (current !== null) budgetTasks.set(current.id, current);\n }\n if (error instanceof DispatchRefusedError) {\n // Dependencies not ready, or another sweep won the race. Leave it.\n result.skipped.push({ taskId: task.id, reasonCode: error.code, reason: error.reason });\n continue;\n }\n if (\n !launchStarted &&\n (error instanceof InvalidTransitionError ||\n error instanceof TaskAttemptConflictError ||\n error instanceof VersionConflictError)\n ) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"task_changed\",\n reason: \"task changed before dispatch\",\n });\n continue;\n }\n result.failed.push({\n taskId: task.id,\n reasonCode: \"launch_failed\",\n reason: error instanceof Error ? error.message : String(error),\n });\n }\n }\n return result;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sweep.mjs","names":["readBudgetTasks","budgetTasks"],"sources":["../src/sweep.ts"],"sourcesContent":["import { budgetTasks as readBudgetTasks } from \"./budget\";\nimport { collectFactoryRecords } from \"./store/iterate\";\nimport { routeKey, type AgentRouteKey } from \"./agent-routes\";\nimport { dispatchTask, DispatchRefusedError, type DispatchBudget } from \"./dispatch\";\nimport type { DispatchRefusalCode } from \"./dispatch\";\nimport type { TaskId } from \"./schema/id\";\nimport type { Repository } from \"./schema/repository\";\nimport type { ExecutionReference, Task } from \"./schema/task\";\nimport { InvalidTransitionError } from \"./schema/transitions\";\nimport { VersionConflictError } from \"./store/driver\";\nimport { TaskAttemptConflictError, type FactoryStores } from \"./store/engine\";\n\n/** Graph, receipt, repository, and Task capabilities required by queued-work dispatch. */\nexport type SweepQueuedStores = Pick<\n FactoryStores,\n \"graphs\" | \"receipts\" | \"repositories\" | \"tasks\"\n>;\n\n/**\n * Single-repository adapter for an exact agent route. Supplied by the app, which knows its\n * agents' addresses. Returns the provider's execution reference when it has one.\n *\n * @param task - Running Task already admitted by the dispatch gate.\n * @param repository - Exact enabled repository selected from the Task scope.\n * @returns Provider execution reference that dispatch can persist for recovery.\n */\nexport type TaskLauncher = (task: Task, repository: Repository) => Promise<ExecutionReference>;\n\n/** Exact-route launchers, budget gate, and pass bounds for queued Task dispatch. */\nexport interface SweepQueuedOptions {\n /** Single-repository agent adapters keyed by routeKey(binding), e.g. code-change@1. */\n launchers: Readonly<Partial<Record<AgentRouteKey, TaskLauncher>>>;\n /** Reserve-before-execution limits, passed through to every dispatch. */\n budget?: DispatchBudget;\n /** Maximum task launches attempted in one pass; omitted means no limit. */\n maxDispatches?: number;\n /** A dispatcher-wide snapshot avoids another complete task read. */\n tasks?: readonly Task[];\n}\n\n/** Tasks dispatched, deferred by a gate, or failed by one queued-work pass. */\nexport interface SweepQueuedResult {\n /** Task IDs whose provider execution reference was durably recorded. */\n readonly dispatched: readonly TaskId[];\n /** Tasks left in the queue because a declared gate may become eligible later. */\n readonly skipped: readonly {\n readonly taskId: TaskId;\n readonly reasonCode: SweepSkipReasonCode;\n readonly reason: string;\n }[];\n /** Tasks whose routing is invalid or whose launch threw. */\n readonly failed: readonly {\n readonly taskId: TaskId;\n readonly reasonCode: SweepFailureReasonCode;\n readonly reason: string;\n }[];\n}\n\n/** Stable reason a queued Task remained eligible for a later dispatch pass. */\nexport type SweepSkipReasonCode =\n | DispatchRefusalCode\n | \"workflow_owned\"\n | \"awaiting_approval\"\n | \"dispatch_limit\"\n | \"task_changed\";\n\n/** Stable reason a queued Task could not be routed or launched. */\nexport type SweepFailureReasonCode =\n | \"invalid_repository_scope\"\n | \"repository_not_found\"\n | \"launcher_not_registered\"\n | \"launch_failed\";\n\ntype MutableSweepQueuedResult = {\n dispatched: TaskId[];\n skipped: { taskId: TaskId; reasonCode: SweepSkipReasonCode; reason: string }[];\n failed: { taskId: TaskId; reasonCode: SweepFailureReasonCode; reason: string }[];\n};\n\nasync function failUnrouteable(\n stores: SweepQueuedStores,\n task: Task,\n reasonCode: SweepFailureReasonCode,\n reason: string,\n result: MutableSweepQueuedResult,\n): Promise<void> {\n try {\n await stores.tasks.transition(task.id, \"failed\", { reason, expectFrom: \"queued\" });\n result.failed.push({ taskId: task.id, reasonCode, reason });\n } catch (error) {\n if (error instanceof InvalidTransitionError || error instanceof VersionConflictError) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"task_changed\",\n reason: \"task changed during routing\",\n });\n return;\n }\n throw error;\n }\n}\n\n/**\n * Runs one bounded pass over queued Tasks and dispatches each eligible exact route.\n *\n * @remarks\n * Candidates are ordered by newest attempt first and then creation time. Route-less work remains\n * queued for its workflow owner; unmet dependencies, approvals, and pass limits appear in\n * `skipped`. Invalid repository scope, missing repositories or launchers, and launch failures\n * appear in `failed`, with unrouteable Tasks transitioned to `failed` when this sweep wins the\n * race. A result entry describes this pass rather than guaranteeing the Task's lifecycle state:\n * an ambiguously accepted launch is reported as `launch_failed` while its Task intentionally stays\n * running for recovery. Callers should surface every failure, reread those Tasks before deciding\n * remediation, and schedule another pass for skipped work.\n *\n * Overlapping passes are safe: {@link dispatchTask} atomically admits one winner before invoking a\n * launcher. A supplied `tasks` snapshot bounds reads and candidate selection for this pass; it can\n * become stale, and corresponding races are returned as `task_changed` rather than hidden.\n * `maxDispatches` counts provider launches started during the pass, including launches that later\n * fail, so `dispatched.length` may be smaller than the limit.\n *\n * @param stores - Graph, receipt, repository, and Task stores used during the pass.\n * @param options - Exact-route launchers plus optional budget, Task snapshot, and dispatch limit.\n * @returns IDs dispatched and structured skipped/failed outcomes for every considered candidate.\n * @throws When `maxDispatches` is not a positive integer or an unexpected persistence error occurs.\n * @see The shipped `docs/recipes/retry-recovery.md` recipe for scheduled recovery.\n */\nexport async function sweepQueued(\n stores: SweepQueuedStores,\n options: SweepQueuedOptions,\n): Promise<SweepQueuedResult> {\n const result: MutableSweepQueuedResult = { dispatched: [], skipped: [], failed: [] };\n const maxDispatches = options.maxDispatches ?? Number.POSITIVE_INFINITY;\n if (\n !(Number.isInteger(maxDispatches) || maxDispatches === Number.POSITIVE_INFINITY) ||\n maxDispatches < 1\n ) {\n throw new Error(\"maxDispatches must be a positive integer\");\n }\n let attempted = 0;\n const taskSnapshot =\n options.tasks ??\n (await collectFactoryRecords(stores.tasks.list, { limit: 100, match: { state: \"queued\" } }));\n const budgetSnapshot =\n options.budget?.tasks ??\n (options.budget && options.budget.enforce !== false ? await readBudgetTasks(stores) : []);\n const budgetTasks = new Map(budgetSnapshot.map((task) => [task.id, task]));\n const queued = taskSnapshot\n .filter((task) => task.state === \"queued\")\n .sort((a, b) => b.attempt - a.attempt || a.createdAt.localeCompare(b.createdAt));\n\n for (const task of queued) {\n const route = task.work.route;\n if (route === undefined) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"workflow_owned\",\n reason: \"no agent route; workflow owns execution\",\n });\n continue;\n }\n const [repositoryId] = task.repositoryIds;\n if (task.repositoryIds.length !== 1 || repositoryId === undefined) {\n await failUnrouteable(\n stores,\n task,\n \"invalid_repository_scope\",\n \"sweep launcher adapter requires exactly one repository\",\n result,\n );\n continue;\n }\n if (task.approval === \"required\") {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"awaiting_approval\",\n reason: \"awaiting human approval\",\n });\n continue;\n }\n const key = routeKey(route);\n const launcher = Object.hasOwn(options.launchers, key) ? options.launchers[key] : undefined;\n if (launcher === undefined) {\n await failUnrouteable(\n stores,\n task,\n \"launcher_not_registered\",\n `no registered launcher handles agent route ${key}`,\n result,\n );\n continue;\n }\n if (\n !(await stores.graphs.dependenciesSatisfied({\n rootTaskId: task.rootTaskId,\n taskId: task.id,\n }))\n ) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"dependencies_unsatisfied\",\n reason: \"graph dependencies must all be succeeded before dispatch\",\n });\n continue;\n }\n if (attempted >= maxDispatches) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"dispatch_limit\",\n reason: \"per-sweep dispatch limit reached\",\n });\n continue;\n }\n const repository = await stores.repositories.get(repositoryId);\n if (repository === null) {\n await failUnrouteable(stores, task, \"repository_not_found\", \"task has no repository\", result);\n continue;\n }\n let launchStarted = false;\n try {\n const dispatched = await dispatchTask(stores, {\n taskId: task.id,\n route,\n launch: (dispatched) => {\n attempted += 1;\n launchStarted = true;\n return launcher(dispatched, repository);\n },\n budget:\n options.budget === undefined\n ? undefined\n : { ...options.budget, tasks: [...budgetTasks.values()] },\n });\n budgetTasks.set(dispatched.id, dispatched);\n result.dispatched.push(task.id);\n } catch (error) {\n if (options.budget !== undefined) {\n const current = await stores.tasks.get(task.id).catch(() => null);\n if (current !== null) budgetTasks.set(current.id, current);\n }\n if (error instanceof DispatchRefusedError) {\n // Dependencies not ready, or another sweep won the race. Leave it.\n result.skipped.push({ taskId: task.id, reasonCode: error.code, reason: error.reason });\n continue;\n }\n if (\n !launchStarted &&\n (error instanceof InvalidTransitionError ||\n error instanceof TaskAttemptConflictError ||\n error instanceof VersionConflictError)\n ) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"task_changed\",\n reason: \"task changed before dispatch\",\n });\n continue;\n }\n result.failed.push({\n taskId: task.id,\n reasonCode: \"launch_failed\",\n reason: error instanceof Error ? error.message : String(error),\n });\n }\n }\n return result;\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { collectFactoryRecords } from "./store/iterate.mjs";
1
2
  //#region src/task-communication.ts
2
3
  /**
3
4
  * Build the generic durable receipt reconciler around an application's event
@@ -6,7 +7,20 @@
6
7
  */
7
8
  function createTaskCommunicationReconciler(projection) {
8
9
  return async function reconcileTaskCommunications(stores, snapshot) {
9
- const tasks = snapshot ?? await stores.tasks.list();
10
+ const tasks = snapshot ?? [...new Map((await Promise.all([collectFactoryRecords(stores.tasks.list, {
11
+ limit: 100,
12
+ match: { state: "needs_human" },
13
+ fields: [{
14
+ path: ["question"],
15
+ test: "exists"
16
+ }]
17
+ }), collectFactoryRecords(stores.tasks.list, {
18
+ limit: 100,
19
+ fields: [{
20
+ path: ["pendingCommunicationReceiptIds"],
21
+ test: "notEmpty"
22
+ }]
23
+ })])).flat().map((task) => [task.id, task])).values()];
10
24
  let enqueued = 0;
11
25
  for (const task of tasks) {
12
26
  const presentsQuestion = task.question !== void 0;
@@ -1 +1 @@
1
- {"version":3,"file":"task-communication.mjs","names":[],"sources":["../src/task-communication.ts"],"sourcesContent":["import type { CommunicationEvent } from \"./schema/communication\";\nimport type { MaybePromise } from \"./schema/common\";\nimport type { Receipt } from \"./schema/receipt\";\nimport type { Task } from \"./schema/task\";\nimport type { FactoryStores } from \"./store/engine\";\n\n/** Outbox, receipt, and Task capabilities required by communication reconciliation. */\nexport type TaskCommunicationStores = Pick<FactoryStores, \"outbox\" | \"receipts\" | \"tasks\">;\n\n/** Number of connector-neutral events newly enqueued during reconciliation. */\nexport interface TaskCommunicationResult {\n enqueued: number;\n}\n\n/** Repairs pending Task communication projections against the durable outbox. */\nexport type TaskCommunicationReconciler<\n Stores extends TaskCommunicationStores = TaskCommunicationStores,\n> = (stores: Stores, snapshot?: readonly Task[]) => Promise<TaskCommunicationResult>;\n\n/** Durable Task and receipt context supplied to a lifecycle event projection. */\nexport interface TaskLifecycleProjectionInput<\n Stores extends TaskCommunicationStores = TaskCommunicationStores,\n> {\n stores: Stores;\n tasks: readonly Task[];\n task: Task;\n receipt: Receipt;\n}\n\n/** Application policy for projecting durable task state into connector-neutral events. */\nexport interface TaskCommunicationProjection<\n Stores extends TaskCommunicationStores = TaskCommunicationStores,\n> {\n question(task: Task): MaybePromise<readonly CommunicationEvent[]>;\n lifecycle(\n input: TaskLifecycleProjectionInput<Stores>,\n ): MaybePromise<readonly CommunicationEvent[]>;\n}\n\n/**\n * Build the generic durable receipt reconciler around an application's event\n * projection. Events are acknowledged only after every projected event has\n * reached the outbox.\n */\nexport function createTaskCommunicationReconciler<Stores extends TaskCommunicationStores>(\n projection: TaskCommunicationProjection<Stores>,\n): TaskCommunicationReconciler<Stores> {\n return async function reconcileTaskCommunications(\n stores: Stores,\n snapshot?: readonly Task[],\n ): Promise<TaskCommunicationResult> {\n const tasks = snapshot ?? (await stores.tasks.list());\n let enqueued = 0;\n for (const task of tasks) {\n const presentsQuestion = task.question !== undefined;\n // A connector may confirm before the parent starts waiting, or before\n // its delivery stamp is persisted.\n if (\n presentsQuestion &&\n task.state === \"needs_human\" &&\n task.needsHumanNotifiedAt !== undefined\n ) {\n await stores.tasks.recordNeedsHumanDelivery(task.id, { attempt: task.attempt });\n }\n if (\n presentsQuestion &&\n task.state === \"needs_human\" &&\n task.needsHumanNotifiedAt === undefined &&\n !task.pendingCommunicationReceiptIds?.length\n ) {\n for (const event of await projection.question(task)) {\n await stores.outbox.enqueue(event);\n enqueued += 1;\n }\n }\n for (const receiptId of task.pendingCommunicationReceiptIds ?? []) {\n const receipt = await stores.receipts.get(receiptId);\n if (receipt === null || receipt.taskId !== task.id) {\n throw new Error(`Task ${task.id} references missing lifecycle receipt ${receiptId}.`);\n }\n if (receipt.state === \"needs_human\" && presentsQuestion) {\n if (task.state === \"needs_human\" && task.attempt === receipt.attempt) {\n for (const event of await projection.question(task)) {\n await stores.outbox.enqueue(event);\n enqueued += 1;\n }\n }\n await stores.tasks.acknowledgeCommunication(task.id, receiptId);\n continue;\n }\n for (const event of await projection.lifecycle({ stores, tasks, task, receipt })) {\n await stores.outbox.enqueue(event);\n enqueued += 1;\n }\n await stores.tasks.acknowledgeCommunication(task.id, receiptId);\n }\n }\n return { enqueued };\n };\n}\n"],"mappings":";;;;;;AA4CA,SAAgB,kCACd,YACqC;CACrC,OAAO,eAAe,4BACpB,QACA,UACkC;EAClC,MAAM,QAAQ,YAAa,MAAM,OAAO,MAAM,KAAK;EACnD,IAAI,WAAW;EACf,KAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,mBAAmB,KAAK,aAAa,KAAA;GAG3C,IACE,oBACA,KAAK,UAAU,iBACf,KAAK,yBAAyB,KAAA,GAE9B,MAAM,OAAO,MAAM,yBAAyB,KAAK,IAAI,EAAE,SAAS,KAAK,QAAQ,CAAC;GAEhF,IACE,oBACA,KAAK,UAAU,iBACf,KAAK,yBAAyB,KAAA,KAC9B,CAAC,KAAK,gCAAgC,QAEtC,KAAK,MAAM,SAAS,MAAM,WAAW,SAAS,IAAI,GAAG;IACnD,MAAM,OAAO,OAAO,QAAQ,KAAK;IACjC,YAAY;GACd;GAEF,KAAK,MAAM,aAAa,KAAK,kCAAkC,CAAC,GAAG;IACjE,MAAM,UAAU,MAAM,OAAO,SAAS,IAAI,SAAS;IACnD,IAAI,YAAY,QAAQ,QAAQ,WAAW,KAAK,IAC9C,MAAM,IAAI,MAAM,QAAQ,KAAK,GAAG,wCAAwC,UAAU,EAAE;IAEtF,IAAI,QAAQ,UAAU,iBAAiB,kBAAkB;KACvD,IAAI,KAAK,UAAU,iBAAiB,KAAK,YAAY,QAAQ,SAC3D,KAAK,MAAM,SAAS,MAAM,WAAW,SAAS,IAAI,GAAG;MACnD,MAAM,OAAO,OAAO,QAAQ,KAAK;MACjC,YAAY;KACd;KAEF,MAAM,OAAO,MAAM,yBAAyB,KAAK,IAAI,SAAS;KAC9D;IACF;IACA,KAAK,MAAM,SAAS,MAAM,WAAW,UAAU;KAAE;KAAQ;KAAO;KAAM;IAAQ,CAAC,GAAG;KAChF,MAAM,OAAO,OAAO,QAAQ,KAAK;KACjC,YAAY;IACd;IACA,MAAM,OAAO,MAAM,yBAAyB,KAAK,IAAI,SAAS;GAChE;EACF;EACA,OAAO,EAAE,SAAS;CACpB;AACF"}
1
+ {"version":3,"file":"task-communication.mjs","names":[],"sources":["../src/task-communication.ts"],"sourcesContent":["import { collectFactoryRecords } from \"./store/iterate\";\nimport type { CommunicationEvent } from \"./schema/communication\";\nimport type { MaybePromise } from \"./schema/common\";\nimport type { Receipt } from \"./schema/receipt\";\nimport type { Task } from \"./schema/task\";\nimport type { FactoryStores } from \"./store/engine\";\n\n/** Outbox, receipt, and Task capabilities required by communication reconciliation. */\nexport type TaskCommunicationStores = Pick<FactoryStores, \"outbox\" | \"receipts\" | \"tasks\">;\n\n/** Number of connector-neutral events newly enqueued during reconciliation. */\nexport interface TaskCommunicationResult {\n enqueued: number;\n}\n\n/** Repairs pending Task communication projections against the durable outbox. */\nexport type TaskCommunicationReconciler<\n Stores extends TaskCommunicationStores = TaskCommunicationStores,\n> = (stores: Stores, snapshot?: readonly Task[]) => Promise<TaskCommunicationResult>;\n\n/** Durable Task and receipt context supplied to a lifecycle event projection. */\nexport interface TaskLifecycleProjectionInput<\n Stores extends TaskCommunicationStores = TaskCommunicationStores,\n> {\n stores: Stores;\n tasks: readonly Task[];\n task: Task;\n receipt: Receipt;\n}\n\n/** Application policy for projecting durable task state into connector-neutral events. */\nexport interface TaskCommunicationProjection<\n Stores extends TaskCommunicationStores = TaskCommunicationStores,\n> {\n question(task: Task): MaybePromise<readonly CommunicationEvent[]>;\n lifecycle(\n input: TaskLifecycleProjectionInput<Stores>,\n ): MaybePromise<readonly CommunicationEvent[]>;\n}\n\n/**\n * Build the generic durable receipt reconciler around an application's event\n * projection. Events are acknowledged only after every projected event has\n * reached the outbox.\n */\nexport function createTaskCommunicationReconciler<Stores extends TaskCommunicationStores>(\n projection: TaskCommunicationProjection<Stores>,\n): TaskCommunicationReconciler<Stores> {\n return async function reconcileTaskCommunications(\n stores: Stores,\n snapshot?: readonly Task[],\n ): Promise<TaskCommunicationResult> {\n const tasks = snapshot ?? [\n ...new Map(\n (\n await Promise.all([\n collectFactoryRecords(stores.tasks.list, {\n limit: 100,\n match: { state: \"needs_human\" },\n fields: [{ path: [\"question\"], test: \"exists\" }],\n }),\n collectFactoryRecords(stores.tasks.list, {\n limit: 100,\n fields: [{ path: [\"pendingCommunicationReceiptIds\"], test: \"notEmpty\" }],\n }),\n ])\n )\n .flat()\n .map((task) => [task.id, task]),\n ).values(),\n ];\n let enqueued = 0;\n for (const task of tasks) {\n const presentsQuestion = task.question !== undefined;\n // A connector may confirm before the parent starts waiting, or before\n // its delivery stamp is persisted.\n if (\n presentsQuestion &&\n task.state === \"needs_human\" &&\n task.needsHumanNotifiedAt !== undefined\n ) {\n await stores.tasks.recordNeedsHumanDelivery(task.id, { attempt: task.attempt });\n }\n if (\n presentsQuestion &&\n task.state === \"needs_human\" &&\n task.needsHumanNotifiedAt === undefined &&\n !task.pendingCommunicationReceiptIds?.length\n ) {\n for (const event of await projection.question(task)) {\n await stores.outbox.enqueue(event);\n enqueued += 1;\n }\n }\n for (const receiptId of task.pendingCommunicationReceiptIds ?? []) {\n const receipt = await stores.receipts.get(receiptId);\n if (receipt === null || receipt.taskId !== task.id) {\n throw new Error(`Task ${task.id} references missing lifecycle receipt ${receiptId}.`);\n }\n if (receipt.state === \"needs_human\" && presentsQuestion) {\n if (task.state === \"needs_human\" && task.attempt === receipt.attempt) {\n for (const event of await projection.question(task)) {\n await stores.outbox.enqueue(event);\n enqueued += 1;\n }\n }\n await stores.tasks.acknowledgeCommunication(task.id, receiptId);\n continue;\n }\n for (const event of await projection.lifecycle({ stores, tasks, task, receipt })) {\n await stores.outbox.enqueue(event);\n enqueued += 1;\n }\n await stores.tasks.acknowledgeCommunication(task.id, receiptId);\n }\n }\n return { enqueued };\n };\n}\n"],"mappings":";;;;;;;AA6CA,SAAgB,kCACd,YACqC;CACrC,OAAO,eAAe,4BACpB,QACA,UACkC;EAClC,MAAM,QAAQ,YAAY,CACxB,GAAG,IAAI,KAEH,MAAM,QAAQ,IAAI,CAChB,sBAAsB,OAAO,MAAM,MAAM;GACvC,OAAO;GACP,OAAO,EAAE,OAAO,cAAc;GAC9B,QAAQ,CAAC;IAAE,MAAM,CAAC,UAAU;IAAG,MAAM;GAAS,CAAC;EACjD,CAAC,GACD,sBAAsB,OAAO,MAAM,MAAM;GACvC,OAAO;GACP,QAAQ,CAAC;IAAE,MAAM,CAAC,gCAAgC;IAAG,MAAM;GAAW,CAAC;EACzE,CAAC,CACH,CAAC,EAAA,CAEA,KAAK,CAAC,CACN,KAAK,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAClC,CAAC,CAAC,OAAO,CACX;EACA,IAAI,WAAW;EACf,KAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,mBAAmB,KAAK,aAAa,KAAA;GAG3C,IACE,oBACA,KAAK,UAAU,iBACf,KAAK,yBAAyB,KAAA,GAE9B,MAAM,OAAO,MAAM,yBAAyB,KAAK,IAAI,EAAE,SAAS,KAAK,QAAQ,CAAC;GAEhF,IACE,oBACA,KAAK,UAAU,iBACf,KAAK,yBAAyB,KAAA,KAC9B,CAAC,KAAK,gCAAgC,QAEtC,KAAK,MAAM,SAAS,MAAM,WAAW,SAAS,IAAI,GAAG;IACnD,MAAM,OAAO,OAAO,QAAQ,KAAK;IACjC,YAAY;GACd;GAEF,KAAK,MAAM,aAAa,KAAK,kCAAkC,CAAC,GAAG;IACjE,MAAM,UAAU,MAAM,OAAO,SAAS,IAAI,SAAS;IACnD,IAAI,YAAY,QAAQ,QAAQ,WAAW,KAAK,IAC9C,MAAM,IAAI,MAAM,QAAQ,KAAK,GAAG,wCAAwC,UAAU,EAAE;IAEtF,IAAI,QAAQ,UAAU,iBAAiB,kBAAkB;KACvD,IAAI,KAAK,UAAU,iBAAiB,KAAK,YAAY,QAAQ,SAC3D,KAAK,MAAM,SAAS,MAAM,WAAW,SAAS,IAAI,GAAG;MACnD,MAAM,OAAO,OAAO,QAAQ,KAAK;MACjC,YAAY;KACd;KAEF,MAAM,OAAO,MAAM,yBAAyB,KAAK,IAAI,SAAS;KAC9D;IACF;IACA,KAAK,MAAM,SAAS,MAAM,WAAW,UAAU;KAAE;KAAQ;KAAO;KAAM;IAAQ,CAAC,GAAG;KAChF,MAAM,OAAO,OAAO,QAAQ,KAAK;KACjC,YAAY;IACd;IACA,MAAM,OAAO,MAAM,yBAAyB,KAAK,IAAI,SAAS;GAChE;EACF;EACA,OAAO,EAAE,SAAS;CACpB;AACF"}
@@ -13,8 +13,8 @@ declare const assignmentPayloadSchema: z.ZodObject<{
13
13
  declare const handoffPayloadSchema: z.ZodObject<{
14
14
  outcome: z.ZodEnum<{
15
15
  failed: "failed";
16
- completed: "completed";
17
16
  needs_input: "needs_input";
17
+ completed: "completed";
18
18
  }>;
19
19
  summary: z.ZodString;
20
20
  output: z.ZodOptional<z.ZodCustom<JsonValue, JsonValue>>;
@@ -38,8 +38,8 @@ declare const assignmentTaskMessageProtocol: Readonly<TaskMessageProtocol<z.ZodO
38
38
  declare const handoffTaskMessageProtocol: Readonly<TaskMessageProtocol<z.ZodObject<{
39
39
  outcome: z.ZodEnum<{
40
40
  failed: "failed";
41
- completed: "completed";
42
41
  needs_input: "needs_input";
42
+ completed: "completed";
43
43
  }>;
44
44
  summary: z.ZodString;
45
45
  output: z.ZodOptional<z.ZodCustom<JsonValue, JsonValue>>;
@@ -57,8 +57,8 @@ declare const standardTaskMessageProtocols: readonly (Readonly<TaskMessageProtoc
57
57
  }, z.core.$strict>, "assignment", 1>> | Readonly<TaskMessageProtocol<z.ZodObject<{
58
58
  outcome: z.ZodEnum<{
59
59
  failed: "failed";
60
- completed: "completed";
61
60
  needs_input: "needs_input";
61
+ completed: "completed";
62
62
  }>;
63
63
  summary: z.ZodString;
64
64
  output: z.ZodOptional<z.ZodCustom<JsonValue, JsonValue>>;
package/dist/tasks.d.mts CHANGED
@@ -1,4 +1,3 @@
1
- import { ApprovalPolicy, ApprovalPriority, ApprovalRule, ApprovalSubject, allowByDefaultApprovalPolicy, approvalPolicySchema, approvalPrioritySchema, approvalRuleSchema, requiresApproval } from "./approval.mjs";
2
1
  import { AnswerId, TaskId, answerIdSchema, taskIdSchema } from "./schema/id.mjs";
3
2
  import { ExecutionReference, MessageWaitingTask, NeedsHumanTask, SucceededTask, Task, TaskApproval, TaskKind, TaskOrigin, TaskQuestion, TaskState, TaskUsage, executionReferenceSchema, harnessSchema, isMessageWaitingTask, isNeedsHumanTask, isSucceededTask, standardTaskKinds, taskApprovalSchema, taskExecutionRepositoryIdsSchema, taskKindSchema, taskOriginSchema, taskQuestionSchema, taskSchema, taskStateSchema, taskUsageSchema } from "./schema/task.mjs";
4
3
  import { MAX_TASK_INBOX_PENDING, SendTaskMessageInput, TaskInbox, TaskMessage, TaskMessageClaim, TaskMessageDiscard, TaskMessageForProtocol, TaskMessageId, TaskMessageOperationId, TaskMessagePrincipal, TaskMessageProcessing, TaskMessageProtocol, TaskMessageProtocolBinding, TaskMessageProtocolBindingInput, TaskMessageProtocolInput, TaskMessageProtocolInputPayload, TaskMessageProtocolStoredPayload, TaskMessageProtocols, defineTaskMessageProtocol, defineTaskMessageProtocols, parseTaskMessageProtocolBinding, sendTaskMessageInputSchema, taskInboxSchema, taskMessageClaimSchema, taskMessageDiscardSchema, taskMessageIdSchema, taskMessageInput, taskMessageOperationIdSchema, taskMessagePrincipalSchema, taskMessageProcessingSchema, taskMessageProtocolBindingSchema, taskMessageProtocolSchema, taskMessageReservationSchema, taskMessageSchema } from "./schema/task-message.mjs";
@@ -10,6 +9,7 @@ import { CreateTaskEffectStoreOptions, RecordTaskEffectInput, ResolveTaskEffectI
10
9
  import { CompleteTaskWorkInput, CompleteWorkflowTaskWorkInput, CreateTaskWorkStoreOptions, ResumeTaskWorkInput, TaskWorkCompletionFence, TaskWorkStore, TaskWorkStores, TaskWorkUsage, WaitForTaskMessagesInput, createTaskWorkStore } from "./store/task-work.mjs";
11
10
  import { AnswerEvent, answerEventSchema } from "./schema/answer.mjs";
12
11
  import { AgentSessionBinding, AnswerEventConflictError, AnswerQuestionInput, AnswerQuestionResult, ApprovalDecision, ApprovalError, CreateTaskInput, RejectionDecision, RootTaskOrigin, TaskAttemptConflictError, TaskPatch, TaskTransitionArguments, TaskTransitionOptions } from "./store/engine.mjs";
12
+ import { ApprovalPolicy, ApprovalPriority, ApprovalRule, ApprovalSubject, allowByDefaultApprovalPolicy, approvalPolicySchema, approvalPrioritySchema, approvalRuleSchema, requiresApproval } from "./approval.mjs";
13
13
  import { ApprovalAccess, ApprovalAccessDecisionInput, ApprovalAccessError, ApprovalAccessOptions, ApprovalAccessStores, createApprovalAccess } from "./approval-access.mjs";
14
14
  import { taskInitialState, taskTransitions } from "./schema/transitions.mjs";
15
15
  import { ProcessTaskMessageOptions, ProcessTaskMessageResult, TaskMessageProcessingStores, processTaskMessage, readTaskMessageDecision } from "./task-message-processing.mjs";
package/dist/vercel.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { RepositorySlug } from "./schema/repository.mjs";
2
1
  import { WorkNotification, WorkNotifications } from "./store/work-notifications.mjs";
2
+ import { RepositorySlug } from "./schema/repository.mjs";
3
3
  import { VercelManagedGitHubApi, VercelManagedGitHubApiAction, VercelManagedGitHubApiError, VercelManagedGitHubApiOptions, VercelManagedGitHubApiSandboxCreateInput, VercelManagedGitHubIssue, VercelManagedGitHubIssueInput, VercelManagedGitHubPublication, VercelManagedGitHubPublicationInput, VercelManagedGitHubPullRequest, VercelManagedGitHubPullRequestInput, createVercelManagedGitHubApi } from "./vercel-github-api.mjs";
4
4
  import { VercelGitHubGitTransportOptions, VercelGitHubTokenInput, VercelManagedGitHubGitTransportOptions, VercelManagedGitHubSandboxCreateInput, createVercelGitHubGitTransport, createVercelManagedGitHubGitTransport } from "./vercel-git.mjs";
5
5
  //#region src/vercel.d.ts
@@ -1,7 +1,7 @@
1
1
  import { RepositoryId } from "../schema/id.mjs";
2
- import { RepositorySlug } from "../schema/repository.mjs";
3
2
  import { Task } from "../schema/task.mjs";
4
3
  import { Change } from "../schema/change.mjs";
4
+ import { RepositorySlug } from "../schema/repository.mjs";
5
5
  import { FactoryStores } from "../store/engine.mjs";
6
6
  import { OpenPullRequest } from "../pull-requests/contracts.mjs";
7
7
  //#region src/work-triage/find-existing-work.d.ts
@@ -1,3 +1,4 @@
1
+ import { collectFactoryRecords } from "../store/iterate.mjs";
1
2
  //#region src/work-triage/find-existing-work.ts
2
3
  const openChangeStates = /* @__PURE__ */ new Set(["verifying", "ready"]);
3
4
  const activeTaskStates = /* @__PURE__ */ new Set([
@@ -15,8 +16,17 @@ async function findExistingWork(input) {
15
16
  };
16
17
  const [openPullRequests, changes, tasks] = await Promise.all([
17
18
  input.listOpenPullRequests(repository.slug),
18
- input.stores.changes.list(),
19
- input.stores.tasks.list()
19
+ collectFactoryRecords(input.stores.changes.list, {
20
+ limit: 100,
21
+ match: [...openChangeStates].map((state) => ({
22
+ repositoryId: input.repositoryId,
23
+ state
24
+ }))
25
+ }),
26
+ collectFactoryRecords(input.stores.tasks.list, {
27
+ limit: 100,
28
+ match: { repositoryIds: [input.repositoryId] }
29
+ })
20
30
  ]);
21
31
  return {
22
32
  found: true,