@vercel/factory 0.0.17 → 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 +46 -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 +3 -1
@@ -1 +1 @@
1
- {"version":3,"file":"find-existing-work.mjs","names":[],"sources":["../../src/work-triage/find-existing-work.ts"],"sourcesContent":["import type { Change } from \"../schema/change\";\nimport type { Task } from \"../schema/task\";\nimport type { FactoryStores } from \"../store/engine\";\nimport type { OpenPullRequest } from \"../pull-requests/contracts\";\nimport type { RepositoryId } from \"../schema/id\";\nimport type { RepositorySlug } from \"../schema/repository\";\n\n/** Provider-neutral readers used to discover work already in progress. */\nexport interface ExistingWorkLookupOptions {\n listOpenPullRequests: (repository: RepositorySlug) => Promise<readonly OpenPullRequest[]>;\n}\n\n/** Stores, repository identity, and provider lookup used for one existing-work query. */\nexport interface FindExistingWorkInput extends ExistingWorkLookupOptions {\n stores: Pick<FactoryStores, \"changes\" | \"repositories\" | \"tasks\">;\n repositoryId: RepositoryId;\n}\n\n/** The refusal or existing pull requests, Changes, and active Tasks found during triage. */\nexport type ExistingWorkLookupResult =\n | { found: false; reasonCode: \"repository_not_found\"; reason: string }\n | {\n found: true;\n openPullRequests: readonly OpenPullRequest[];\n openChanges: readonly Change[];\n activeTasks: readonly Task[];\n };\n\nconst openChangeStates: ReadonlySet<Change[\"state\"]> = new Set([\"verifying\", \"ready\"]);\nconst activeTaskStates: ReadonlySet<Task[\"state\"]> = new Set([\"queued\", \"running\", \"needs_human\"]);\n\n/** Find open pull requests, Changes, and active Tasks for one repository. */\nexport async function findExistingWork(\n input: FindExistingWorkInput,\n): Promise<ExistingWorkLookupResult> {\n const repository = await input.stores.repositories.get(input.repositoryId);\n if (repository === null) {\n return {\n found: false,\n reasonCode: \"repository_not_found\",\n reason: `repository ${input.repositoryId} does not exist`,\n };\n }\n const [openPullRequests, changes, tasks] = await Promise.all([\n input.listOpenPullRequests(repository.slug),\n input.stores.changes.list(),\n input.stores.tasks.list(),\n ]);\n return {\n found: true,\n openPullRequests,\n openChanges: changes.filter(\n (change) => change.repositoryId === input.repositoryId && openChangeStates.has(change.state),\n ),\n activeTasks: tasks.filter(\n (task) => task.repositoryIds.includes(input.repositoryId) && activeTaskStates.has(task.state),\n ),\n };\n}\n"],"mappings":";AA4BA,MAAM,mCAAiD,IAAI,IAAI,CAAC,aAAa,OAAO,CAAC;AACrF,MAAM,mCAA+C,IAAI,IAAI;CAAC;CAAU;CAAW;AAAa,CAAC;;AAGjG,eAAsB,iBACpB,OACmC;CACnC,MAAM,aAAa,MAAM,MAAM,OAAO,aAAa,IAAI,MAAM,YAAY;CACzE,IAAI,eAAe,MACjB,OAAO;EACL,OAAO;EACP,YAAY;EACZ,QAAQ,cAAc,MAAM,aAAa;CAC3C;CAEF,MAAM,CAAC,kBAAkB,SAAS,SAAS,MAAM,QAAQ,IAAI;EAC3D,MAAM,qBAAqB,WAAW,IAAI;EAC1C,MAAM,OAAO,QAAQ,KAAK;EAC1B,MAAM,OAAO,MAAM,KAAK;CAC1B,CAAC;CACD,OAAO;EACL,OAAO;EACP;EACA,aAAa,QAAQ,QAClB,WAAW,OAAO,iBAAiB,MAAM,gBAAgB,iBAAiB,IAAI,OAAO,KAAK,CAC7F;EACA,aAAa,MAAM,QAChB,SAAS,KAAK,cAAc,SAAS,MAAM,YAAY,KAAK,iBAAiB,IAAI,KAAK,KAAK,CAC9F;CACF;AACF"}
1
+ {"version":3,"file":"find-existing-work.mjs","names":[],"sources":["../../src/work-triage/find-existing-work.ts"],"sourcesContent":["import { collectFactoryRecords } from \"../store/iterate\";\nimport type { Change } from \"../schema/change\";\nimport type { Task } from \"../schema/task\";\nimport type { FactoryStores } from \"../store/engine\";\nimport type { OpenPullRequest } from \"../pull-requests/contracts\";\nimport type { RepositoryId } from \"../schema/id\";\nimport type { RepositorySlug } from \"../schema/repository\";\n\n/** Provider-neutral readers used to discover work already in progress. */\nexport interface ExistingWorkLookupOptions {\n listOpenPullRequests: (repository: RepositorySlug) => Promise<readonly OpenPullRequest[]>;\n}\n\n/** Stores, repository identity, and provider lookup used for one existing-work query. */\nexport interface FindExistingWorkInput extends ExistingWorkLookupOptions {\n stores: Pick<FactoryStores, \"changes\" | \"repositories\" | \"tasks\">;\n repositoryId: RepositoryId;\n}\n\n/** The refusal or existing pull requests, Changes, and active Tasks found during triage. */\nexport type ExistingWorkLookupResult =\n | { found: false; reasonCode: \"repository_not_found\"; reason: string }\n | {\n found: true;\n openPullRequests: readonly OpenPullRequest[];\n openChanges: readonly Change[];\n activeTasks: readonly Task[];\n };\n\nconst openChangeStates: ReadonlySet<Change[\"state\"]> = new Set([\"verifying\", \"ready\"]);\nconst activeTaskStates: ReadonlySet<Task[\"state\"]> = new Set([\"queued\", \"running\", \"needs_human\"]);\n\n/** Find open pull requests, Changes, and active Tasks for one repository. */\nexport async function findExistingWork(\n input: FindExistingWorkInput,\n): Promise<ExistingWorkLookupResult> {\n const repository = await input.stores.repositories.get(input.repositoryId);\n if (repository === null) {\n return {\n found: false,\n reasonCode: \"repository_not_found\",\n reason: `repository ${input.repositoryId} does not exist`,\n };\n }\n const [openPullRequests, changes, tasks] = await Promise.all([\n input.listOpenPullRequests(repository.slug),\n collectFactoryRecords(input.stores.changes.list, {\n limit: 100,\n match: [...openChangeStates].map((state) => ({ repositoryId: input.repositoryId, state })),\n }),\n collectFactoryRecords(input.stores.tasks.list, {\n limit: 100,\n match: { repositoryIds: [input.repositoryId] },\n }),\n ]);\n return {\n found: true,\n openPullRequests,\n openChanges: changes.filter(\n (change) => change.repositoryId === input.repositoryId && openChangeStates.has(change.state),\n ),\n activeTasks: tasks.filter(\n (task) => task.repositoryIds.includes(input.repositoryId) && activeTaskStates.has(task.state),\n ),\n };\n}\n"],"mappings":";;AA6BA,MAAM,mCAAiD,IAAI,IAAI,CAAC,aAAa,OAAO,CAAC;AACrF,MAAM,mCAA+C,IAAI,IAAI;CAAC;CAAU;CAAW;AAAa,CAAC;;AAGjG,eAAsB,iBACpB,OACmC;CACnC,MAAM,aAAa,MAAM,MAAM,OAAO,aAAa,IAAI,MAAM,YAAY;CACzE,IAAI,eAAe,MACjB,OAAO;EACL,OAAO;EACP,YAAY;EACZ,QAAQ,cAAc,MAAM,aAAa;CAC3C;CAEF,MAAM,CAAC,kBAAkB,SAAS,SAAS,MAAM,QAAQ,IAAI;EAC3D,MAAM,qBAAqB,WAAW,IAAI;EAC1C,sBAAsB,MAAM,OAAO,QAAQ,MAAM;GAC/C,OAAO;GACP,OAAO,CAAC,GAAG,gBAAgB,CAAC,CAAC,KAAK,WAAW;IAAE,cAAc,MAAM;IAAc;GAAM,EAAE;EAC3F,CAAC;EACD,sBAAsB,MAAM,OAAO,MAAM,MAAM;GAC7C,OAAO;GACP,OAAO,EAAE,eAAe,CAAC,MAAM,YAAY,EAAE;EAC/C,CAAC;CACH,CAAC;CACD,OAAO;EACL,OAAO;EACP;EACA,aAAa,QAAQ,QAClB,WAAW,OAAO,iBAAiB,MAAM,gBAAgB,iBAAiB,IAAI,OAAO,KAAK,CAC7F;EACA,aAAa,MAAM,QAChB,SAAS,KAAK,cAAc,SAAS,MAAM,YAAY,KAAK,iBAAiB,IAAI,KAAK,KAAK,CAC9F;CACF;AACF"}
package/docs/index.md CHANGED
@@ -32,6 +32,10 @@ lifecycle recovery, and inter-Task communication.
32
32
 
33
33
  ## Behavioral contracts
34
34
 
35
+ For the fresh-data storage transition, see [indexed Postgres persistence](recipes/postgres-indexed-storage.md)
36
+ and [the indexed write engine](recipes/postgres-indexed-engine.md). The indexed engine exposes
37
+ bounded source queries and is not a drop-in replacement for the catalog-based reference runtime.
38
+
35
39
  The installed declarations are the source of truth for operation semantics. In particular:
36
40
 
37
41
  - `createStores` and `FactoryStores.tasks` document persistence, deduplication, transition fences,
@@ -172,17 +172,21 @@ async function verify() {
172
172
  throw new Error("Expected the completed Task to survive another restart");
173
173
  }
174
174
  const output = greetingWorkflow.output.parse(task.workResult?.output);
175
- const succeededReceiptsBefore = (await stores.receipts.list()).filter(
176
- (receipt) => receipt.taskId === task.id && receipt.state === "succeeded",
177
- ).length;
175
+ const succeededReceiptsBefore = (
176
+ await stores.receipts
177
+ .list({ limit: 100, match: { taskId: task.id } })
178
+ .then((page) => page.items)
179
+ ).filter((receipt) => receipt.taskId === task.id && receipt.state === "succeeded").length;
178
180
  const replayed = await stores.work.completeWorkflow({
179
181
  task: { taskId: task.id, attempt: task.attempt },
180
182
  workflow: greetingWorkflow.binding,
181
183
  output,
182
184
  });
183
- const succeededReceiptsAfterReplay = (await stores.receipts.list()).filter(
184
- (receipt) => receipt.taskId === task.id && receipt.state === "succeeded",
185
- ).length;
185
+ const succeededReceiptsAfterReplay = (
186
+ await stores.receipts
187
+ .list({ limit: 100, match: { taskId: task.id } })
188
+ .then((page) => page.items)
189
+ ).filter((receipt) => receipt.taskId === task.id && receipt.state === "succeeded").length;
186
190
 
187
191
  let conflictingOutputRejected = false;
188
192
  try {
@@ -194,9 +198,11 @@ async function verify() {
194
198
  } catch {
195
199
  conflictingOutputRejected = true;
196
200
  }
197
- const succeededReceiptCount = (await stores.receipts.list()).filter(
198
- (receipt) => receipt.taskId === task.id && receipt.state === "succeeded",
199
- ).length;
201
+ const succeededReceiptCount = (
202
+ await stores.receipts
203
+ .list({ limit: 100, match: { taskId: task.id } })
204
+ .then((page) => page.items)
205
+ ).filter((receipt) => receipt.taskId === task.id && receipt.state === "succeeded").length;
200
206
  if (
201
207
  replayed.state !== "succeeded" ||
202
208
  replayed.workResult?.completedAt !== task.workResult?.completedAt ||
@@ -0,0 +1,108 @@
1
+ # Indexed Factory writes
2
+
3
+ `createIndexedStores` connects Factory's validating engine to `IndexedStore`. Task admission,
4
+ transitions, receipts, sessions, Changes, transcripts, graph recovery, messages, outbox, and
5
+ workflow records use the same structured backend. Each canonical write commits its compact
6
+ source summary and membership counts atomically. The engine stops writing catalog shards and
7
+ Task-route snapshots. Image bytes use a separately supplied object store.
8
+
9
+ The reference application, scaffold, HTTP API, dispatch, and recovery readers use this engine.
10
+ Canonical `.list(query)` methods require a page size and return `{ items, nextCursor }`.
11
+ The optional `conversations` configuration supplies application routes, continuation agent,
12
+ and the workspace owner for automatic Task attachment.
13
+ There is no backfill, dual write, or Blob fallback in the indexed engine.
14
+
15
+ ## Queries and counts
16
+
17
+ `indexes.query` accepts one typed scope and a required page size of 1–100. `indexes.count` reads
18
+ the maintained count for that same scope. Pages contain validated source summaries and versions;
19
+ they do not hydrate canonical records. Use the existing entity `get(id)` methods for point reads.
20
+
21
+ | Source | Additional scopes beyond all records and repository membership |
22
+ | ---------------- | ------------------------------------------------------------------------------------------------------------ |
23
+ | Task | State (optionally repository), root graph, exact reply destination, source attention (optionally repository) |
24
+ | Session | Owner (optionally repository), Task binding, exact execution |
25
+ | External session | Owner (optionally repository), exact execution |
26
+ | Change | State (optionally repository), creating Task, source attention (optionally repository) |
27
+ | Receipt | Task |
28
+ | Transcript | Source attention; exact execution remains a point read |
29
+
30
+ Authentication and authorization belong to the application. These are trusted APIs, not raw HTTP
31
+ parameters. An owner-scoped session query uses the authenticated owner. Repository membership is
32
+ discovery, not an access grant: a caller must be allowed to read **every** repository on a record.
33
+ An `all` scope requires authority across that collection in the namespace. Cursors carry position,
34
+ not authorization. Unsupported filter combinations and search require indexed read models; do not
35
+ exhaust pages and filter the whole result in memory.
36
+
37
+ Attention here describes **source entities**: Tasks requiring approval/input or failed/stalled;
38
+ ready Changes; transcripts waiting for input or failed. A Task and its Change can both need action.
39
+ These separate counts must not be summed into the UI's conversation count. The integrated `stores.conversations` projection deduplicates conversations, preserves
40
+ workflow-question precedence, and supports bounded policy-filtered pages and maintained facet totals. A transcript's state alone is not an internal session's attention decision.
41
+
42
+ All entities retain the existing validating engine's receipt-first, graph-admission, retry, and
43
+ version-fencing rules. Each canonical write and its derived projections share one storage transaction; external provider
44
+ effects and separate engine operations are not one transaction. Each driver call has
45
+ a fresh operation identity so repeated inserts and stale CAS writes still raise conflicts; the
46
+ Postgres adapter retains that identity while reconciling an ambiguous commit response.
47
+
48
+ The foundation limits still apply: 256 KiB per canonical record, 8 KiB per summary, and 32
49
+ memberships. A Task consumes up to `5 + 3 × repositoryCount` memberships when it needs attention.
50
+ Session notifications, continuations, execution histories, and transcript blocks/chunks use
51
+ separate paged records. Oversized writes fail without committing that record or its memberships.
52
+ Only compact summaries truncate display titles; canonical messages remain intact. Migrations,
53
+ connection management, grants, and environment isolation follow the
54
+ [Postgres foundation guide](postgres-indexed-storage.md).
55
+
56
+ ## Runnable local example
57
+
58
+ This example uses PGlite and local image storage, without provider credentials. Use a pooled
59
+ interactive Postgres connection and object storage for images in a deployed application.
60
+
61
+ <!-- runnable-example:start -->
62
+
63
+ ```ts
64
+ import assert from "node:assert/strict";
65
+ import { PGlite } from "@electric-sql/pglite";
66
+ import { drizzle } from "drizzle-orm/pglite";
67
+ import { createIndexedStores, createInMemoryDriver } from "@vercel/factory/storage";
68
+ import {
69
+ applyFactoryMigrations,
70
+ createPostgresIndexedStore,
71
+ } from "@vercel/factory/storage/postgres";
72
+ import { taskWork } from "@vercel/factory/workflows";
73
+
74
+ const client = new PGlite();
75
+ try {
76
+ const db = drizzle(client);
77
+ await applyFactoryMigrations(db);
78
+ const store = createPostgresIndexedStore({ db, namespace: "indexed-engine-example-v1" });
79
+ const stores = createIndexedStores({ store, images: createInMemoryDriver() });
80
+ const input = {
81
+ repositoryIds: ["repo_example"] as const,
82
+ kind: "investigation",
83
+ origin: { operator: "alice" },
84
+ replyTo: { channel: "api", address: "ses_example" },
85
+ work: taskWork({ title: "Investigate board latency", input: { privateContext: "full input" } }),
86
+ dedupeKey: "example-admission",
87
+ };
88
+ const task = await stores.tasks.create(input);
89
+ assert.equal((await stores.tasks.create(input)).id, task.id);
90
+ await stores.tasks.transition(task.id, "running");
91
+ assert.equal((await stores.tasks.create(input)).state, "running");
92
+ const scope = { collection: "tasks", by: "state", state: "running" } as const;
93
+ const page = await stores.indexes.query({ scope, limit: 30 });
94
+ assert.equal(page.entries.length, 1);
95
+ assert.equal(page.entries[0]?.summary.id, task.id);
96
+ assert.equal(JSON.stringify(page).includes("privateContext"), false);
97
+ assert.equal(await stores.indexes.count(scope), 1);
98
+ assert.equal(
99
+ (await stores.tasks.list({ limit: 1, match: { state: "running" } })).items[0]?.id,
100
+ task.id,
101
+ );
102
+ assert.equal((await stores.tasks.get(task.id))?.work.title, "Investigate board latency");
103
+ } finally {
104
+ await client.close();
105
+ }
106
+ ```
107
+
108
+ <!-- runnable-example:end -->
@@ -0,0 +1,145 @@
1
+ # Indexed Postgres storage
2
+
3
+ Use `createPostgresIndexedStore` for new consumers that need compact ordered pages and maintained
4
+ counts. It implements `IndexedStore`, the bounded contract first developed for the DynamoDB
5
+ foundation. This implementation uses PostgreSQL B-trees, row-version conditions, and transactions.
6
+ The reference runtime uses it through `createIndexedStores`; it never reads a legacy Blob catalog.
7
+
8
+ ## Tables and queries
9
+
10
+ `applyFactoryMigrations` adds migration `0001_indexed_storage` with four tables:
11
+
12
+ | Table | Contents |
13
+ | ---------------------------- | ----------------------------------------------------------------- |
14
+ | `factory_indexed_records` | Full canonical JSON, version, namespace/collection/id primary key |
15
+ | `factory_indexed_entries` | Compact summaries, source versions, and ordered index memberships |
16
+ | `factory_indexed_counts` | Transactionally maintained total for each namespace/index |
17
+ | `factory_indexed_operations` | Durable operation fingerprints and committed versions for retries |
18
+
19
+ Entries have a B-tree primary key on `(namespace, index_key, sort_key, collection, id)`. A second
20
+ unique index on `(namespace, collection, id, index_key)` supports replacing one record's memberships.
21
+ Query keys use `C` collation. Queries use tuple cursor comparisons and a limit of 1–100, fetching
22
+ one additional row to determine whether another page exists. `query` returns summaries only. `queryRecords` applies bounded, parameterized JSON predicates
23
+ and ordered point reads; conversation cards are compact records rather than full Tasks.
24
+ Counts read a single counter row; they do not count or download the matching entries.
25
+
26
+ The application supplies each complete membership set, including authorized workspace/owner scope.
27
+ For example, a repository board, its state-filtered board, and its attention list are distinct query
28
+ scopes. The validating engine defines those memberships and the operator conversation schema. Map supported
29
+ filters to exact scopes or add indexed SQL projections during integration; never consume every
30
+ page and filter in application memory. Arbitrary filter combinations and search need their own
31
+ query/index design. Cursors are positions bound to namespace, index, and direction, not access
32
+ tokens. Pages and counts from separate calls do not share a snapshot; mutable entries can move.
33
+
34
+ Migration `0002_indexed_reads` adds count-group metadata and JSON/full-text indexes. Maintained
35
+ facet counts provide totals without hydrating Tasks. Sparse filters can still visit many ordered
36
+ memberships; exact search counts scale with matching compact records. The local benchmark in the
37
+ cutover guide records actual handler timings and SQL plans at 1,000 and 10,000 conversations.
38
+
39
+ ## Connection and migration ownership
40
+
41
+ Use Drizzle with `pg.Pool` or Neon’s WebSocket `Pool`, connected to the primary. Interactive
42
+ transactions are required; `drizzle-orm/neon-http` is not supported. The caller owns bounded pool
43
+ size, connection/statement timeouts, TLS, and cleanup. Initialize connections lazily when a build
44
+ does not have database credentials. A pooled URL does not replace the need for interactive
45
+ transaction support. Place the database near the application functions and measure cold starts.
46
+
47
+ Run `applyFactoryMigrations(db)` explicitly with a migration role and serialize migration runners.
48
+ Runtime requests never perform DDL. The runtime role needs schema USAGE and only these grants:
49
+
50
+ ```sql
51
+ GRANT SELECT, INSERT, UPDATE ON factory_indexed_records, factory_indexed_counts TO factory_runtime;
52
+ GRANT SELECT, INSERT, DELETE ON factory_indexed_entries TO factory_runtime;
53
+ GRANT SELECT, INSERT ON factory_indexed_operations TO factory_runtime;
54
+ ```
55
+
56
+ Namespaces separate environments and fresh generations logically; they are not database access
57
+ controls. Use separate databases/credentials for Production and Preview. Start the fresh-data
58
+ cutover with a new namespace, retire old pinned work, and avoid backfill, dual writes, or Blob
59
+ fallback. Large files, images, diffs, and report artifacts remain in object storage. Growing transcripts/histories use paged records in the reference runtime. The
60
+ [cutover guide](https://github.com/vercel-labs/agent-factory/blob/code/postgres-migration/docs/postgres-cutover.md) describes preview setup and the promotion boundary.
61
+
62
+ ## Write behavior
63
+
64
+ Interactive transactions use serializable isolation with bounded retries for serialization failures
65
+ and deadlocks. One transaction claims the operation ID, applies a version-fenced record write, replaces its
66
+ memberships, adjusts counts, and commits the operation receipt. An insert expects version zero;
67
+ an update expects the current positive version. Concurrent writers cannot both win the same
68
+ version. Identical retries return the original version even after later updates. A different
69
+ intent with the same operation ID raises `StoreOperationConflictError`. After an ambiguous commit
70
+ response, the adapter checks the receipt; if that check is unavailable, retry the identical intent.
71
+ Operation receipts are retained indefinitely. There is no generic delete or automatic cleanup.
72
+
73
+ The engine owns entity schemas, lifecycle rules, immutable records, summary contents, and access
74
+ policy. The adapter validates storage keys and finite JSON, but never infers Task attention.
75
+ Bounds are 256 KiB per record, 8 KiB per summary, 32 memberships, and JSON depth 32. Shared counter
76
+ rows serialize membership changes within a scope; benchmark write contention before activation.
77
+ These limits require splitting growing histories instead of storing an unlimited JSON document.
78
+
79
+ ## Runnable local example
80
+
81
+ This example uses PGlite to exercise the published interface without credentials. Install
82
+ `@vercel/factory`, `@electric-sql/pglite`, `drizzle-orm`, `eve`, and `zod`, then run it with Node 24.
83
+
84
+ <!-- runnable-example:start -->
85
+
86
+ ```ts
87
+ import assert from "node:assert/strict";
88
+ import { PGlite } from "@electric-sql/pglite";
89
+ import { drizzle } from "drizzle-orm/pglite";
90
+ import type { WriteIndexedRecordInput } from "@vercel/factory/storage";
91
+ import {
92
+ applyFactoryMigrations,
93
+ createPostgresIndexedStore,
94
+ } from "@vercel/factory/storage/postgres";
95
+
96
+ const client = new PGlite();
97
+ try {
98
+ const db = drizzle(client);
99
+ await applyFactoryMigrations(db);
100
+ const store = createPostgresIndexedStore({ db, namespace: "local-example-v1" });
101
+ // Values are opaque here; an application validates its full entity before calling storage.
102
+ const input: WriteIndexedRecordInput = {
103
+ collection: "sessions",
104
+ id: "ses_example",
105
+ operationId: "create-example",
106
+ expectedVersion: 0,
107
+ value: { title: "Example conversation", context: "Full data is read by ID" },
108
+ indexes: [
109
+ {
110
+ index: "workspace:example:conversations",
111
+ sortKey: "2026-09-22T12:00:00.000Z",
112
+ summary: { title: "Example conversation" },
113
+ },
114
+ ],
115
+ };
116
+ assert.deepEqual(await store.write(input), { version: 1 });
117
+ assert.deepEqual(await store.write(input), { version: 1 });
118
+ assert.equal(await store.count(input.indexes[0]!.index), 1);
119
+ const page = await store.query({ index: input.indexes[0]!.index, limit: 30 });
120
+ assert.equal(page.entries.length, 1);
121
+ assert.equal(page.nextCursor, null);
122
+ assert.deepEqual(page.entries[0]!.summary, { title: "Example conversation" });
123
+ assert.deepEqual((await store.get("sessions", input.id))?.value, input.value);
124
+ } finally {
125
+ await client.close();
126
+ }
127
+ ```
128
+
129
+ <!-- runnable-example:end -->
130
+
131
+ ## Verification
132
+
133
+ `pnpm check` runs the PGlite storage suite. The example is available for manual local use;
134
+ it is not an additional packed-scaffold or consumer-docs smoke test. CI also uses a pinned
135
+ PostgreSQL 17 service for concurrent transactions, retry/rollback behavior, least-privilege grants,
136
+ and a 10,000-entry `EXPLAIN (ANALYZE, BUFFERS)` index-seek check. To run that suite locally, use an
137
+ isolated PostgreSQL database named `factory_test` on loopback:
138
+
139
+ ```sh
140
+ FACTORY_POSTGRES_TEST_URL=postgresql://factory:factory-test@127.0.0.1:55432/factory_test \
141
+ pnpm --filter @vercel/factory exec vitest run src/postgres/indexed.test.ts
142
+ ```
143
+
144
+ These checks establish storage correctness and query shape. Deployed board, attention, and chat
145
+ latency at realistic payload sizes and increasing record counts must be measured after integration.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/factory",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "description": "Schema, state machines, and kernel primitives for Agent Factory.",
5
5
  "homepage": "https://github.com/vercel-labs/agent-factory/tree/main/packages/factory",
6
6
  "bugs": "https://github.com/vercel-labs/agent-factory/issues",
@@ -145,10 +145,12 @@
145
145
  "@ai-sdk/harness-claude-code": "1.0.112",
146
146
  "@ai-sdk/sandbox-vercel": "1.0.108",
147
147
  "@electric-sql/pglite": "^0.5.7",
148
+ "@types/pg": "^8.23.1",
148
149
  "@vercel/blob": "^2.8.0",
149
150
  "@vercel/sandbox": "2.10.0-beta.0",
150
151
  "drizzle-orm": "^0.45.2",
151
152
  "eve": "^0.57.0",
153
+ "pg": "^8.23.0",
152
154
  "tsdown": "^0.22.14",
153
155
  "typescript": "5.9.3",
154
156
  "typescript-current": "npm:typescript@7.0.2",