@vercel/factory 0.0.17 → 0.0.19
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.
- package/CHANGELOG.md +52 -0
- package/dist/api-contracts.d.mts +665 -715
- package/dist/api-contracts.mjs +91 -4
- package/dist/api-contracts.mjs.map +1 -1
- package/dist/api-task-graph.mjs +14 -5
- package/dist/api-task-graph.mjs.map +1 -1
- package/dist/api.d.mts +58 -11
- package/dist/api.mjs +715 -399
- package/dist/api.mjs.map +1 -1
- package/dist/approval-access.d.mts +1 -1
- package/dist/approval-contracts.d.mts +3 -4
- package/dist/approval-contracts.mjs +1 -2
- package/dist/approval-contracts.mjs.map +1 -1
- package/dist/blob/index.d.mts +1 -1
- package/dist/budget.mjs +20 -3
- package/dist/budget.mjs.map +1 -1
- package/dist/change-verification/brief.d.mts +1 -1
- package/dist/change-verification/dispatch.mjs +8 -1
- package/dist/change-verification/dispatch.mjs.map +1 -1
- package/dist/changes/eve-record-change.mjs +4 -1
- package/dist/changes/eve-record-change.mjs.map +1 -1
- package/dist/client-stream.d.mts +19 -2
- package/dist/client-stream.mjs +115 -53
- package/dist/client-stream.mjs.map +1 -1
- package/dist/client-transcript.d.mts +25 -2
- package/dist/client-transcript.mjs +95 -13
- package/dist/client-transcript.mjs.map +1 -1
- package/dist/client.d.mts +145 -19
- package/dist/client.mjs +89 -16
- package/dist/client.mjs.map +1 -1
- package/dist/code-review/contracts.d.mts +1 -0
- package/dist/code-review/eve-post-review.d.mts +4 -2
- package/dist/code-review/eve-post-review.mjs +8 -0
- package/dist/code-review/eve-post-review.mjs.map +1 -1
- package/dist/code-review/github-reporter.d.mts +6 -2
- package/dist/code-review/github-reporter.mjs +86 -35
- package/dist/code-review/github-reporter.mjs.map +1 -1
- package/dist/code-review/preparation.d.mts +2 -1
- package/dist/code-review/preparation.mjs +2 -1
- package/dist/code-review/preparation.mjs.map +1 -1
- package/dist/code-review/stall.mjs +11 -2
- package/dist/code-review/stall.mjs.map +1 -1
- package/dist/connectors/github.d.mts +1 -1
- package/dist/connectors/index.d.mts +1 -1
- package/dist/connectors/slack.d.mts +1 -1
- package/dist/delivery-metrics/metrics.d.mts +1 -1
- package/dist/dispatch.mjs +3 -3
- package/dist/dispatch.mjs.map +1 -1
- package/dist/eve/index.d.mts +1 -1
- package/dist/eve/index.mjs +8 -1
- package/dist/eve/index.mjs.map +1 -1
- package/dist/eve/task-execution.d.mts +4 -4
- package/dist/eve/transcript.d.mts +3 -1
- package/dist/eve/transcript.mjs.map +1 -1
- package/dist/finding-remediation/admission.d.mts +1 -1
- package/dist/finding-remediation/brief.d.mts +1 -1
- package/dist/github-app.d.mts +1 -1
- package/dist/github-publication.d.mts +1 -1
- package/dist/github.d.mts +9 -1
- package/dist/github.mjs +29 -11
- package/dist/github.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/intake-contracts.d.mts +1 -0
- package/dist/intake-contracts.mjs +2 -1
- package/dist/intake-contracts.mjs.map +1 -1
- package/dist/integrations/github-issues.d.mts +64 -0
- package/dist/integrations/github-issues.mjs +175 -0
- package/dist/integrations/github-issues.mjs.map +1 -0
- package/dist/integrations/github.d.mts +2 -1
- package/dist/integrations/github.mjs +2 -1
- package/dist/multi-repository-changes/coordination.mjs +1 -1
- package/dist/planning/reconcile.mjs +15 -2
- package/dist/planning/reconcile.mjs.map +1 -1
- package/dist/postgres/index.d.mts +2 -1
- package/dist/postgres/index.mjs +11 -5
- package/dist/postgres/index.mjs.map +1 -1
- package/dist/postgres/indexed-codec.mjs +137 -0
- package/dist/postgres/indexed-codec.mjs.map +1 -0
- package/dist/postgres/indexed-schema.mjs +99 -0
- package/dist/postgres/indexed-schema.mjs.map +1 -0
- package/dist/postgres/indexed.d.mts +28 -0
- package/dist/postgres/indexed.mjs +277 -0
- package/dist/postgres/indexed.mjs.map +1 -0
- package/dist/postgres/record-predicates.mjs +23 -0
- package/dist/postgres/record-predicates.mjs.map +1 -0
- package/dist/presets/software-development/deepsec-brief.d.mts +1 -1
- package/dist/presets/software-development/dispatch.mjs +1 -1
- package/dist/presets/software-development/dispatch.mjs.map +1 -1
- package/dist/presets/software-development/recovery.mjs +9 -2
- package/dist/presets/software-development/recovery.mjs.map +1 -1
- package/dist/pull-requests/eve-fetch.d.mts +4 -1
- package/dist/pull-requests/eve-fetch.mjs +9 -0
- package/dist/pull-requests/eve-fetch.mjs.map +1 -1
- package/dist/pull-requests/github-publisher.d.mts +1 -1
- package/dist/pull-requests/github-publisher.mjs +19 -7
- package/dist/pull-requests/github-publisher.mjs.map +1 -1
- package/dist/repositories.mjs +20 -13
- package/dist/repositories.mjs.map +1 -1
- package/dist/sandbox/execution-run-ledger.d.mts +2 -2
- package/dist/sandbox/execution-run-ledger.mjs.map +1 -1
- package/dist/sandbox/execution.d.mts +2 -2
- package/dist/sandbox/execution.mjs.map +1 -1
- package/dist/schema/catalog.mjs +1 -1
- package/dist/schema/change.d.mts +1 -1
- package/dist/schema/communication.d.mts +2 -2
- package/dist/schema/factory-config.d.mts +1 -1
- package/dist/schema/outbox.d.mts +6 -6
- package/dist/schema/session-summary.d.mts +218 -0
- package/dist/schema/session-summary.mjs +68 -0
- package/dist/schema/session-summary.mjs.map +1 -0
- package/dist/schema/session.d.mts +39 -59
- package/dist/schema/session.mjs +6 -6
- package/dist/schema/session.mjs.map +1 -1
- package/dist/schema/task-graph.d.mts +24 -24
- package/dist/schema/transcript.d.mts +141 -3
- package/dist/schema/transcript.mjs +19 -3
- package/dist/schema/transcript.mjs.map +1 -1
- package/dist/schema/transitions.d.mts +1 -1
- package/dist/schema/transitions.mjs +1 -1
- package/dist/schema/transitions.mjs.map +1 -1
- package/dist/signals.d.mts +1 -1
- package/dist/stall.mjs +10 -2
- package/dist/stall.mjs.map +1 -1
- package/dist/storage.d.mts +10 -1
- package/dist/storage.mjs +7 -1
- package/dist/store/approvals.d.mts +14 -0
- package/dist/store/approvals.mjs +78 -0
- package/dist/store/approvals.mjs.map +1 -0
- package/dist/store/conversations.d.mts +59 -0
- package/dist/store/conversations.mjs +549 -0
- package/dist/store/conversations.mjs.map +1 -0
- package/dist/store/driver.d.mts +1 -1
- package/dist/store/driver.mjs.map +1 -1
- package/dist/store/engine.d.mts +38 -16
- package/dist/store/engine.mjs +229 -106
- package/dist/store/engine.mjs.map +1 -1
- package/dist/store/indexed-engine.d.mts +57 -0
- package/dist/store/indexed-engine.mjs +155 -0
- package/dist/store/indexed-engine.mjs.map +1 -0
- package/dist/store/indexed-memory.d.mts +7 -0
- package/dist/store/indexed-memory.mjs +200 -0
- package/dist/store/indexed-memory.mjs.map +1 -0
- package/dist/store/indexed-projections.d.mts +280 -0
- package/dist/store/indexed-projections.mjs +481 -0
- package/dist/store/indexed-projections.mjs.map +1 -0
- package/dist/store/indexed.d.mts +101 -0
- package/dist/store/indexed.mjs +19 -0
- package/dist/store/indexed.mjs.map +1 -0
- package/dist/store/iterate.d.mts +9 -0
- package/dist/store/iterate.mjs +23 -0
- package/dist/store/iterate.mjs.map +1 -0
- package/dist/store/query-scope.mjs +62 -0
- package/dist/store/query-scope.mjs.map +1 -0
- package/dist/store/query.d.mts +32 -0
- package/dist/store/query.mjs +103 -0
- package/dist/store/query.mjs.map +1 -0
- package/dist/store/session-history.mjs +24 -0
- package/dist/store/session-history.mjs.map +1 -0
- package/dist/store/slack-pr-notifications.d.mts +2 -2
- package/dist/store/slack-pr-notifications.mjs.map +1 -1
- package/dist/store/state.d.mts +26 -0
- package/dist/store/state.mjs +95 -0
- package/dist/store/state.mjs.map +1 -0
- package/dist/store/task-effects.d.mts +1 -1
- package/dist/store/task-graphs.d.mts +1 -1
- package/dist/store/task-messages.d.mts +1 -1
- package/dist/store/task-work.d.mts +1 -1
- package/dist/store/task-work.mjs +1 -1
- package/dist/store/transcripts.mjs +308 -0
- package/dist/store/transcripts.mjs.map +1 -0
- package/dist/sweep.d.mts +1 -1
- package/dist/sweep.mjs +12 -7
- package/dist/sweep.mjs.map +1 -1
- package/dist/task-communication.mjs +15 -1
- package/dist/task-communication.mjs.map +1 -1
- package/dist/task-protocols.d.mts +3 -3
- package/dist/tasks.d.mts +1 -1
- package/dist/vercel.d.mts +1 -1
- package/dist/work-triage/find-existing-work.d.mts +1 -1
- package/dist/work-triage/find-existing-work.mjs +12 -2
- package/dist/work-triage/find-existing-work.mjs.map +1 -1
- package/docs/index.md +4 -0
- package/docs/recipes/persistence-recovery.md +15 -9
- package/docs/recipes/postgres-indexed-engine.md +108 -0
- package/docs/recipes/postgres-indexed-storage.md +145 -0
- package/package.json +3 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { repositorySlugSchema } from "../schema/repository.mjs";
|
|
2
|
+
import { ensureGitHubLabel } from "../github.mjs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/integrations/github-issues.ts
|
|
6
|
+
const issueSchema = z.object({
|
|
7
|
+
number: z.number().int().positive(),
|
|
8
|
+
html_url: z.url()
|
|
9
|
+
});
|
|
10
|
+
const labelSchema = z.object({
|
|
11
|
+
name: z.string().min(1).max(50),
|
|
12
|
+
color: z.string().regex(/^[\da-f]{6}$/iu),
|
|
13
|
+
description: z.string().max(100)
|
|
14
|
+
});
|
|
15
|
+
const publicationSchema = z.strictObject({
|
|
16
|
+
repository: repositorySlugSchema,
|
|
17
|
+
key: z.string(),
|
|
18
|
+
createdAt: z.iso.datetime(),
|
|
19
|
+
labels: z.array(labelSchema),
|
|
20
|
+
issue: issueSchema.optional()
|
|
21
|
+
});
|
|
22
|
+
const pathnameFor = (digest) => `github-issue-publications/${digest}.json`;
|
|
23
|
+
/** Creates an authorized publisher and a recovery operation that never creates issues or reservations. */
|
|
24
|
+
function createGitHubIssuePublisher(options) {
|
|
25
|
+
repositorySlugSchema.parse(options.repository);
|
|
26
|
+
const labels = z.array(labelSchema).max(10).parse(options.labels);
|
|
27
|
+
async function resolve(key, input) {
|
|
28
|
+
z.string().min(1).max(500).parse(key);
|
|
29
|
+
await options.authorize();
|
|
30
|
+
const digest = createHash("sha256").update(JSON.stringify([options.repository, key])).digest("hex");
|
|
31
|
+
const pathname = pathnameFor(digest);
|
|
32
|
+
const marker = `<!-- factory-issue:${digest} -->`;
|
|
33
|
+
const api = `https://api.github.com/repos/${options.repository}`;
|
|
34
|
+
const request = async (url, init) => {
|
|
35
|
+
await options.authorize();
|
|
36
|
+
return (options.fetch ?? globalThis.fetch)(url, {
|
|
37
|
+
...init,
|
|
38
|
+
redirect: "error",
|
|
39
|
+
signal: AbortSignal.timeout(3e4),
|
|
40
|
+
headers: {
|
|
41
|
+
Authorization: `Bearer ${await options.token()}`,
|
|
42
|
+
Accept: "application/vnd.github+json",
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const read = () => options.state.read({ pathname });
|
|
49
|
+
let stored = await read();
|
|
50
|
+
let claimed = false;
|
|
51
|
+
if (!stored) {
|
|
52
|
+
if (!input) return null;
|
|
53
|
+
for (const label of labels) await ensureGitHubLabel({
|
|
54
|
+
repository: options.repository,
|
|
55
|
+
token: "",
|
|
56
|
+
fetch: request,
|
|
57
|
+
...label
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
await options.state.write({
|
|
61
|
+
pathname,
|
|
62
|
+
overwrite: false,
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
repository: options.repository,
|
|
65
|
+
key,
|
|
66
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
67
|
+
labels
|
|
68
|
+
})
|
|
69
|
+
});
|
|
70
|
+
claimed = true;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (!await read()) throw error;
|
|
73
|
+
}
|
|
74
|
+
stored = await read();
|
|
75
|
+
}
|
|
76
|
+
if (!stored) throw new Error("Issue publication reservation is missing");
|
|
77
|
+
const publication = publicationSchema.parse(JSON.parse(stored.body));
|
|
78
|
+
if (publication.repository !== options.repository || publication.key !== key) throw new Error("Issue publication identity mismatch");
|
|
79
|
+
const result = (issue) => ({
|
|
80
|
+
status: "published",
|
|
81
|
+
number: issue.number,
|
|
82
|
+
url: issue.html_url
|
|
83
|
+
});
|
|
84
|
+
if (publication.issue) return result(publication.issue);
|
|
85
|
+
const save = async (issue) => {
|
|
86
|
+
try {
|
|
87
|
+
await options.state.write({
|
|
88
|
+
pathname,
|
|
89
|
+
overwrite: true,
|
|
90
|
+
ifMatch: stored.etag,
|
|
91
|
+
body: JSON.stringify({
|
|
92
|
+
...publication,
|
|
93
|
+
issue
|
|
94
|
+
})
|
|
95
|
+
});
|
|
96
|
+
} catch (error) {
|
|
97
|
+
const winner = await read();
|
|
98
|
+
if (!winner || publicationSchema.parse(JSON.parse(winner.body)).issue?.number !== issue.number) throw error;
|
|
99
|
+
}
|
|
100
|
+
return result(issue);
|
|
101
|
+
};
|
|
102
|
+
if (claimed && input) {
|
|
103
|
+
const response = await request(`${api}/issues`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
body: JSON.stringify({
|
|
106
|
+
title: input.title,
|
|
107
|
+
body: `${input.body}\n\n${marker}`,
|
|
108
|
+
labels: publication.labels.map(({ name }) => name)
|
|
109
|
+
})
|
|
110
|
+
});
|
|
111
|
+
if (!response.ok) throw new Error(`GitHub issue creation returned ${response.status}; retry this source key to reconcile`);
|
|
112
|
+
return save(issueSchema.parse(await response.json()));
|
|
113
|
+
}
|
|
114
|
+
const since = (/* @__PURE__ */ new Date(Date.parse(publication.createdAt) - 6e4)).toISOString();
|
|
115
|
+
for (let page = 1; page <= 10; page++) {
|
|
116
|
+
const response = await request(`${api}/issues?${new URLSearchParams({
|
|
117
|
+
state: "all",
|
|
118
|
+
since,
|
|
119
|
+
sort: "created",
|
|
120
|
+
direction: "asc",
|
|
121
|
+
per_page: "100",
|
|
122
|
+
page: String(page)
|
|
123
|
+
})}`);
|
|
124
|
+
if (!response.ok) throw new Error(`GitHub issue reconciliation returned ${response.status}`);
|
|
125
|
+
const issues = z.array(issueSchema.extend({
|
|
126
|
+
body: z.string().nullable(),
|
|
127
|
+
pull_request: z.unknown().optional()
|
|
128
|
+
})).parse(await response.json());
|
|
129
|
+
const matches = issues.filter((issue) => !issue.pull_request && issue.body?.includes(marker));
|
|
130
|
+
if (matches.length > 1) throw new Error("Multiple GitHub issues have the publication marker; inspect manually");
|
|
131
|
+
if (matches[0]) return save(matches[0]);
|
|
132
|
+
if (issues.length < 100) break;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
status: "pending",
|
|
136
|
+
reason: "An earlier publication may still be in flight. No second issue was created. Inspect GitHub and the publication record before resetting this source key."
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
/** Publishes once per source key, reconciling any existing reservation without another POST. */
|
|
141
|
+
async publish(input) {
|
|
142
|
+
z.object({
|
|
143
|
+
key: z.string().min(1).max(500),
|
|
144
|
+
title: z.string().min(1).max(256),
|
|
145
|
+
body: z.string().min(1).max(5e4)
|
|
146
|
+
}).parse(input);
|
|
147
|
+
if (input.body.includes("<!-- factory-issue:")) throw new Error("Issue body contains a reserved publication marker");
|
|
148
|
+
const result = await resolve(input.key, input);
|
|
149
|
+
if (!result) throw new Error("Issue publication reservation is missing");
|
|
150
|
+
return result;
|
|
151
|
+
},
|
|
152
|
+
/** Recovers an existing publication by source key, or returns null without reserving or posting. */
|
|
153
|
+
reconcile(key) {
|
|
154
|
+
return resolve(key);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** Reads a confirmed Factory publication matching the repository, source marker, and issue. */
|
|
159
|
+
async function readGitHubIssuePublication(options) {
|
|
160
|
+
const digest = /<!-- factory-issue:([a-f0-9]{64}) -->/u.exec(options.body)?.[1];
|
|
161
|
+
if (!digest) return null;
|
|
162
|
+
const stored = await options.state.read({ pathname: pathnameFor(digest) });
|
|
163
|
+
if (!stored) return null;
|
|
164
|
+
const publication = publicationSchema.parse(JSON.parse(stored.body));
|
|
165
|
+
const { issue } = publication;
|
|
166
|
+
const expectedDigest = createHash("sha256").update(JSON.stringify([publication.repository, publication.key])).digest("hex");
|
|
167
|
+
return publication.repository === options.repository && issue?.number === options.number && digest === expectedDigest ? {
|
|
168
|
+
...publication,
|
|
169
|
+
issue
|
|
170
|
+
} : null;
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
export { createGitHubIssuePublisher, readGitHubIssuePublication };
|
|
174
|
+
|
|
175
|
+
//# sourceMappingURL=github-issues.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-issues.mjs","names":[],"sources":["../../src/integrations/github-issues.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { z } from \"zod\";\nimport { repositorySlugSchema, type RepositorySlug } from \"../schema/repository\";\nimport type { ConditionalStateStore } from \"../store/state\";\nimport { ensureGitHubLabel } from \"../github\";\n\nconst issueSchema = z.object({ number: z.number().int().positive(), html_url: z.url() });\nconst labelSchema = z.object({\n name: z.string().min(1).max(50),\n color: z.string().regex(/^[\\da-f]{6}$/iu),\n description: z.string().max(100),\n});\nconst publicationSchema = z.strictObject({\n repository: repositorySlugSchema,\n key: z.string(),\n createdAt: z.iso.datetime(),\n labels: z.array(labelSchema),\n issue: issueSchema.optional(),\n});\ntype State = Pick<ConditionalStateStore, \"read\" | \"write\">;\nconst pathnameFor = (digest: string) => `github-issue-publications/${digest}.json`;\n\n/** Host-owned credentials, repository scope, labels, and durable state for issue publication. */\nexport interface GitHubIssuePublisherOptions {\n repository: RepositorySlug;\n state: State;\n labels: readonly { name: string; color: string; description: string }[];\n authorize(): Promise<void>;\n token(): Promise<string>;\n fetch?: typeof globalThis.fetch;\n}\n\n/** A stable source identity and the sanitized issue the caller wants to publish. */\nexport interface PublishGitHubIssueOptions {\n key: string;\n title: string;\n body: string;\n}\n\n/** A confirmed GitHub issue, or an ambiguous write that requires reconciliation. */\nexport type PublishGitHubIssueResult =\n | { status: \"published\"; number: number; url: string }\n | { status: \"pending\"; reason: string };\n\n/** Creates an authorized publisher and a recovery operation that never creates issues or reservations. */\nexport function createGitHubIssuePublisher(options: GitHubIssuePublisherOptions) {\n repositorySlugSchema.parse(options.repository);\n const labels = z.array(labelSchema).max(10).parse(options.labels);\n async function resolve(\n key: string,\n input?: PublishGitHubIssueOptions,\n ): Promise<PublishGitHubIssueResult | null> {\n z.string().min(1).max(500).parse(key);\n await options.authorize();\n const digest = createHash(\"sha256\")\n .update(JSON.stringify([options.repository, key]))\n .digest(\"hex\");\n const pathname = pathnameFor(digest);\n const marker = `<!-- factory-issue:${digest} -->`;\n const api = `https://api.github.com/repos/${options.repository}`;\n const request: typeof globalThis.fetch = async (url, init) => {\n await options.authorize();\n return (options.fetch ?? globalThis.fetch)(url, {\n ...init,\n redirect: \"error\",\n signal: AbortSignal.timeout(30000),\n headers: {\n Authorization: `Bearer ${await options.token()}`,\n Accept: \"application/vnd.github+json\",\n \"Content-Type\": \"application/json\",\n \"X-GitHub-Api-Version\": \"2022-11-28\",\n },\n });\n };\n const read = () => options.state.read({ pathname });\n let stored = await read();\n let claimed = false;\n if (!stored) {\n if (!input) return null;\n // Label setup has no non-idempotent issue side effect and can be retried before claiming.\n for (const label of labels)\n await ensureGitHubLabel({\n repository: options.repository,\n token: \"\",\n fetch: request,\n ...label,\n });\n try {\n await options.state.write({\n pathname,\n overwrite: false,\n body: JSON.stringify({\n repository: options.repository,\n key,\n createdAt: new Date().toISOString(),\n labels,\n }),\n });\n claimed = true;\n } catch (error) {\n if (!(await read())) throw error;\n }\n stored = await read();\n }\n if (!stored) throw new Error(\"Issue publication reservation is missing\");\n const publication = publicationSchema.parse(JSON.parse(stored.body));\n if (publication.repository !== options.repository || publication.key !== key)\n throw new Error(\"Issue publication identity mismatch\");\n const result = (issue: z.infer<typeof issueSchema>): PublishGitHubIssueResult => ({\n status: \"published\",\n number: issue.number,\n url: issue.html_url,\n });\n if (publication.issue) return result(publication.issue);\n const save = async (issue: z.infer<typeof issueSchema>) => {\n try {\n await options.state.write({\n pathname,\n overwrite: true,\n ifMatch: stored.etag,\n body: JSON.stringify({ ...publication, issue }),\n });\n } catch (error) {\n const winner = await read();\n if (\n !winner ||\n publicationSchema.parse(JSON.parse(winner.body)).issue?.number !== issue.number\n )\n throw error;\n }\n return result(issue);\n };\n if (claimed && input) {\n // GitHub has no issue-creation idempotency key. Any failure after this claim is ambiguous.\n const response = await request(`${api}/issues`, {\n method: \"POST\",\n body: JSON.stringify({\n title: input.title,\n body: `${input.body}\\n\\n${marker}`,\n labels: publication.labels.map(({ name }) => name),\n }),\n });\n if (!response.ok)\n throw new Error(\n `GitHub issue creation returned ${response.status}; retry this source key to reconcile`,\n );\n return save(issueSchema.parse(await response.json()));\n }\n const since = new Date(Date.parse(publication.createdAt) - 60000).toISOString();\n for (let page = 1; page <= 10; page++) {\n const query = new URLSearchParams({\n state: \"all\",\n since,\n sort: \"created\",\n direction: \"asc\",\n per_page: \"100\",\n page: String(page),\n });\n const response = await request(`${api}/issues?${query}`);\n if (!response.ok) throw new Error(`GitHub issue reconciliation returned ${response.status}`);\n const issues = z\n .array(\n issueSchema.extend({ body: z.string().nullable(), pull_request: z.unknown().optional() }),\n )\n .parse(await response.json());\n const matches = issues.filter((issue) => !issue.pull_request && issue.body?.includes(marker));\n if (matches.length > 1)\n throw new Error(\"Multiple GitHub issues have the publication marker; inspect manually\");\n if (matches[0]) return save(matches[0]);\n if (issues.length < 100) break;\n }\n return {\n status: \"pending\",\n reason:\n \"An earlier publication may still be in flight. No second issue was created. Inspect GitHub and the publication record before resetting this source key.\",\n };\n }\n return {\n /** Publishes once per source key, reconciling any existing reservation without another POST. */\n async publish(input: PublishGitHubIssueOptions): Promise<PublishGitHubIssueResult> {\n z.object({\n key: z.string().min(1).max(500),\n title: z.string().min(1).max(256),\n body: z.string().min(1).max(50000),\n }).parse(input);\n if (input.body.includes(\"<!-- factory-issue:\"))\n throw new Error(\"Issue body contains a reserved publication marker\");\n const result = await resolve(input.key, input);\n if (!result) throw new Error(\"Issue publication reservation is missing\");\n return result;\n },\n /** Recovers an existing publication by source key, or returns null without reserving or posting. */\n reconcile(key: string): Promise<PublishGitHubIssueResult | null> {\n return resolve(key);\n },\n };\n}\n\n/** Confirmed source-key publication and host-owned metadata for a GitHub issue. */\nexport interface GitHubIssuePublication {\n repository: RepositorySlug;\n key: string;\n createdAt: string;\n labels: readonly { name: string; color: string; description: string }[];\n issue: { number: number; html_url: string };\n}\n\n/** Reads a confirmed Factory publication matching the repository, source marker, and issue. */\nexport async function readGitHubIssuePublication(options: {\n state: Pick<ConditionalStateStore, \"read\">;\n repository: RepositorySlug;\n number: number;\n body: string;\n}): Promise<GitHubIssuePublication | null> {\n const digest = /<!-- factory-issue:([a-f0-9]{64}) -->/u.exec(options.body)?.[1];\n if (!digest) return null;\n const stored = await options.state.read({ pathname: pathnameFor(digest) });\n if (!stored) return null;\n const publication = publicationSchema.parse(JSON.parse(stored.body));\n const { issue } = publication;\n const expectedDigest = createHash(\"sha256\")\n .update(JSON.stringify([publication.repository, publication.key]))\n .digest(\"hex\");\n return publication.repository === options.repository &&\n issue?.number === options.number &&\n digest === expectedDigest\n ? { ...publication, issue }\n : null;\n}\n"],"mappings":"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"}
|
|
@@ -8,4 +8,5 @@ import { GitHubReviewReporter, GitHubReviewReporterOptions, ReconcileReviewCheck
|
|
|
8
8
|
import { GitHubPullRequestPublicationGate, GitHubPullRequestPublicationGateOptions, GitHubPullRequestPublisher, GitHubPullRequestPublisherOptions, PublishGitHubPullRequestInput, PushGitHubCheckpointInput, RequireTaskPullRequestInput, createGitHubPullRequestPublicationGate, createGitHubPullRequestPublisher } from "../pull-requests/github-publisher.mjs";
|
|
9
9
|
import { GitHubPullRequestLabelManager, GitHubPullRequestLabelManagerOptions, ManageGitHubPullRequestLabelInput, createGitHubPullRequestLabelManager } from "../pull-requests/github-label-manager.mjs";
|
|
10
10
|
import { GitHubOpenPullRequestLister, GitHubOpenPullRequestListerOptions, createGitHubOpenPullRequestLister } from "../work-triage/github.mjs";
|
|
11
|
-
|
|
11
|
+
import { GitHubIssuePublication, GitHubIssuePublisherOptions, PublishGitHubIssueOptions, PublishGitHubIssueResult, createGitHubIssuePublisher, readGitHubIssuePublication } from "./github-issues.mjs";
|
|
12
|
+
export { type AnswerReviewCommentInput, type CheckRunAnnotation, type CheckRunAnnotationLevel, type CheckRunConclusion, type CheckRunFields, type CheckRunOutput, type CheckRunProgressStatus, type CheckoutGitHubRepositoryInput, type CommittedGitChange, type CompletedCheckRunFields, type ConfiguredEventRepositoryInput, type ConflictedGitHubPullRequest, type CreateCheckRunInput, type CreateGitHubBranchInput, type CreateGitHubCommitInput, type CreateGitHubPublicationPullRequestInput, type CreateGitHubTreeInput, type CreatePullRequestCommentInput, type CreatePullRequestInput, type CreatedGitHubPublicationPullRequest, type CreatedGitHubPullRequest, type EnsureGitHubLabelInput, type FactoryMentionOptions, type FactoryQuestionReply, type FactoryReviewComment, type FetchGitHubBundleInput, type FetchGitHubRefsInput, type GitHubAppFactoryInput, type GitHubAppInstallationToken, type GitHubAppLike, type GitHubAppPermissions, type GitHubCheckRun, GitHubCheckoutConflictError, type GitHubCiFailureSignal, type GitHubCommit, type GitHubCommunicationAdapterOptions, type GitHubDefaultBranchPush, type GitHubGitTransport, type GitHubIssueComment, type GitHubIssueCommentRecord, type GitHubIssuePublication, type GitHubIssuePublisherOptions, type GitHubIssueSignal, type GitHubOpenPullRequestLister, type GitHubOpenPullRequestListerOptions, GitHubPublicationApiError, type GitHubPublicationClient, type GitHubPublicationClientOptions, type GitHubPublicationLimits, type GitHubPublicationPullRequest, type GitHubPullRequest, type GitHubPullRequestClosed, type GitHubPullRequestDiscussionComment, type GitHubPullRequestDiscussionPage, type GitHubPullRequestDiscussionReview, type GitHubPullRequestHead, type GitHubPullRequestLabelManager, type GitHubPullRequestLabelManagerOptions, type GitHubPullRequestPublicationGate, type GitHubPullRequestPublicationGateOptions, type GitHubPullRequestPublisher, type GitHubPullRequestPublisherOptions, type GitHubPullRequestReference, type GitHubPullRequestSignal, type GitHubPullRequestTarget, type GitHubRefMapping, type GitHubReplyTarget, type GitHubRepositoryAuthorRequest, type GitHubRepositoryBundle, type GitHubRepositoryRequest, type GitHubReviewComment, GitHubReviewCommentAuthorError, type GitHubReviewReporter, type GitHubReviewReporterOptions, type GitHubTreeDeletion, type GitHubTreeEntry, type GitHubTreeUpsert, type GitTransferSandbox, type IncompleteCheckRunFields, type ListCheckRunsForRefInput, type ListConflictedPullRequestsInput, type ListGitHubPublicationPullRequestsInput, type ListOpenPullRequestsInput, type ListPullRequestCommentsInput, type ListPullRequestDiscussionInput, type ManageGitHubPullRequestLabelInput, type MarkGitHubPullRequestReadyInput, type MintGitHubAppInstallationTokenInput, type NormalizeFactoryCommentInput, type NormalizeFactoryReviewCommentInput, type ObservedCheckRunStatus, type PublishGitHubIssueOptions, type PublishGitHubIssueResult, type PublishGitHubPullRequestInput, type PublishedGitHubReview, type PullRequestLabelInput, type PullRequestReviewComment, type PullRequestReviewEvent, type PushGitHubBundleInput, type PushGitHubCheckpointInput, type PushGitHubHeadInput, type ReadCommittedGitChangeInput, type ReadGitHubBranchHeadInput, type ReadGitHubCommitInput, type ReadGitHubPullRequestInput, type ReadGitTransferFileInput, type ReadReviewCommentInput, type ReconcileReviewChecksInput, type ReconcileReviewChecksOptions, type ReconcileReviewChecksResult, type RequireTaskPullRequestInput, type ReviewCommentAuthorPolicy, type RunSandboxGitInput, type SandboxGit, type SandboxGitCommandInput, type SandboxGitCommandResult, StalePullRequestHeadError, type SubmitPullRequestReviewInput, type SubmittedPullRequestReview, type UpdateCheckRunInput, type UpdateGitHubBranchInput, type UpdatePullRequestCommentInput, type VerifyGitHubSignatureInput, type WithSandboxGitBundleInput, type WriteGitTransferFileInput, addLabel, answerReviewComment, checkoutGitHubRepository, closePullRequest, configuredEventRepositorySlug, createCheckRun, createGitHubCommunicationAdapter, createGitHubIssuePublisher, createGitHubOpenPullRequestLister, createGitHubPublicationClient, createGitHubPullRequestLabelManager, createGitHubPullRequestPublicationGate, createGitHubPullRequestPublisher, createGitHubReviewReporter, createPullRequest, createPullRequestComment, ensureGitHubLabel, factorySessionBranch, factoryTaskBranch, hasGitHubRepositoryWriteAccess, isSafeGitRef, listCheckRunsForRef, listConflictedPullRequests, listOpenPullRequests, listPullRequestComments, listPullRequestDiscussion, mintGitHubAppInstallationToken, normalizeFactoryQuestionReply, normalizeFactoryReviewComment, normalizeGitHubDefaultBranchPush, normalizeGitHubIssueOpened, normalizeGitHubPullRequest, normalizeGitHubPullRequestClosed, normalizeGitHubWorkflowRunFailure, parseGitHubReplyAddress, readCommittedGitChange, readGitHubIssuePublication, readIssueComment, readPullRequest, readPullRequestHead, readReviewComment, removeLabel, renderGitHubCommunicationEvent, runSandboxGit, stripFactoryQuestionMention, submitPullRequestReview, updateCheckRun, updatePullRequestComment, verifyGitHubSignature };
|
|
@@ -6,4 +6,5 @@ import { createGitHubReviewReporter } from "../code-review/github-reporter.mjs";
|
|
|
6
6
|
import { createGitHubPullRequestPublicationGate, createGitHubPullRequestPublisher } from "../pull-requests/github-publisher.mjs";
|
|
7
7
|
import { createGitHubPullRequestLabelManager } from "../pull-requests/github-label-manager.mjs";
|
|
8
8
|
import { createGitHubOpenPullRequestLister } from "../work-triage/github.mjs";
|
|
9
|
-
|
|
9
|
+
import { createGitHubIssuePublisher, readGitHubIssuePublication } from "./github-issues.mjs";
|
|
10
|
+
export { GitHubCheckoutConflictError, GitHubPublicationApiError, GitHubReviewCommentAuthorError, StalePullRequestHeadError, addLabel, answerReviewComment, checkoutGitHubRepository, closePullRequest, configuredEventRepositorySlug, createCheckRun, createGitHubCommunicationAdapter, createGitHubIssuePublisher, createGitHubOpenPullRequestLister, createGitHubPublicationClient, createGitHubPullRequestLabelManager, createGitHubPullRequestPublicationGate, createGitHubPullRequestPublisher, createGitHubReviewReporter, createPullRequest, createPullRequestComment, ensureGitHubLabel, factorySessionBranch, factoryTaskBranch, hasGitHubRepositoryWriteAccess, isSafeGitRef, listCheckRunsForRef, listConflictedPullRequests, listOpenPullRequests, listPullRequestComments, listPullRequestDiscussion, mintGitHubAppInstallationToken, normalizeFactoryQuestionReply, normalizeFactoryReviewComment, normalizeGitHubDefaultBranchPush, normalizeGitHubIssueOpened, normalizeGitHubPullRequest, normalizeGitHubPullRequestClosed, normalizeGitHubWorkflowRunFailure, parseGitHubReplyAddress, readCommittedGitChange, readGitHubIssuePublication, readIssueComment, readPullRequest, readPullRequestHead, readReviewComment, removeLabel, renderGitHubCommunicationEvent, runSandboxGit, stripFactoryQuestionMention, submitPullRequestReview, updateCheckRun, updatePullRequestComment, verifyGitHubSignature };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { repositoryScope } from "../schema/id.mjs";
|
|
2
2
|
import { parseAgentRouteBinding } from "../schema/agent-route.mjs";
|
|
3
3
|
import { taskWork } from "../schema/work.mjs";
|
|
4
|
-
import { routeTaskWork } from "../agent-routes.mjs";
|
|
5
4
|
import { RecordNotFoundError } from "../store/driver.mjs";
|
|
5
|
+
import { routeTaskWork } from "../agent-routes.mjs";
|
|
6
6
|
//#region src/multi-repository-changes/coordination.ts
|
|
7
7
|
/** Supplied workflow: one waiting parent and one routed child per distinct repository. */
|
|
8
8
|
async function createMultiRepositoryChangeTasks(stores, input) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFactoryRecords } from "../store/iterate.mjs";
|
|
1
2
|
import { routeKey } from "../agent-routes.mjs";
|
|
2
3
|
import { BudgetExhaustedError, DispatchRefusedError, dispatchTask } from "../dispatch.mjs";
|
|
3
4
|
import { taskMessageIdSchema } from "../schema/task-message.mjs";
|
|
@@ -24,7 +25,13 @@ function plannerReconciler(options, config, state, turns) {
|
|
|
24
25
|
failed: [],
|
|
25
26
|
errors: []
|
|
26
27
|
};
|
|
27
|
-
const rows = new Map((snapshot ?? await stores.tasks.list
|
|
28
|
+
const rows = new Map((snapshot ?? await collectFactoryRecords(stores.tasks.list, {
|
|
29
|
+
limit: 100,
|
|
30
|
+
fields: [{
|
|
31
|
+
path: ["work", "route"],
|
|
32
|
+
test: "exists"
|
|
33
|
+
}]
|
|
34
|
+
})).map((task) => [task.id, task]));
|
|
28
35
|
const errors = (taskId, error) => result.errors.push({
|
|
29
36
|
taskId,
|
|
30
37
|
message: (error instanceof Error ? error.message : String(error)).slice(0, 1e3)
|
|
@@ -229,7 +236,13 @@ function plannerReconciler(options, config, state, turns) {
|
|
|
229
236
|
})) continue;
|
|
230
237
|
launchAttempted = true;
|
|
231
238
|
const previousDecision = activation?.finished && activation.attempt === current.attempt - 1 ? await turns.recorded(current, activation.triggerId) : null;
|
|
232
|
-
const continuation = (previousDecision?.action === "delegate" || previousDecision?.action === "wait") && !(await stores.receipts.list
|
|
239
|
+
const continuation = (previousDecision?.action === "delegate" || previousDecision?.action === "wait") && !(await collectFactoryRecords(stores.receipts.list, {
|
|
240
|
+
limit: 100,
|
|
241
|
+
match: {
|
|
242
|
+
taskId: current.id,
|
|
243
|
+
attempt: current.attempt - 1
|
|
244
|
+
}
|
|
245
|
+
})).some((receipt) => receipt.taskId === current.id && receipt.attempt === current.attempt - 1 && (receipt.state === "failed" || receipt.state === "stalled")) && (await stores.messages.pending({
|
|
233
246
|
principal: runtimePrincipal,
|
|
234
247
|
toTaskId: current.id
|
|
235
248
|
})).some((message) => message.type.id === "handoff" && message.type.version === 1);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile.mjs","names":[],"sources":["../../src/planning/reconcile.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { routeKey } from \"../agent-routes\";\nimport { BudgetExhaustedError, dispatchTask, DispatchRefusedError } from \"../dispatch\";\nimport type { TaskId } from \"../schema/id\";\nimport type { Task } from \"../schema/task\";\nimport { taskMessageIdSchema } from \"../schema/task-message\";\nimport { TaskMessageSourceFenceError } from \"../store/task-messages\";\nimport { handoffPayloadSchema, handoffTaskMessageProtocol } from \"../task-protocols\";\nimport type { PlannerConfiguration } from \"./configuration\";\nimport type { PlannerReconcileResult, PlannerRuntimeOptions } from \"./contracts\";\nimport { cancelDescendants } from \"./effects\";\nimport { plannerCancellationBatch } from \"./cancellation\";\nimport { cancelBlockedPlannerChildren } from \"./dependencies\";\nimport { runtimePrincipal, terminal, type PlannerState } from \"./state\";\nimport type { PlannerTurns } from \"./turns\";\n\nconst MAX_MESSAGES = 32;\nconst TURN_TIMEOUT_MS = 15 * 60_000;\nexport function plannerReconciler(\n options: PlannerRuntimeOptions,\n config: PlannerConfiguration,\n state: PlannerState,\n turns: PlannerTurns,\n) {\n const { stores } = options;\n return async (snapshot?: readonly Task[]): Promise<PlannerReconcileResult> => {\n const result: {\n queued: TaskId[];\n dispatched: TaskId[];\n delivered: number;\n recovered: number;\n discarded: number;\n cancelled: TaskId[];\n cancellationPending?: TaskId[];\n failed: TaskId[];\n errors: { taskId: TaskId; message: string }[];\n } = {\n queued: [],\n dispatched: [],\n delivered: 0,\n recovered: 0,\n discarded: 0,\n cancelled: [],\n failed: [],\n errors: [],\n };\n const rows = new Map((snapshot ?? (await stores.tasks.list())).map((task) => [task.id, task]));\n const errors = (taskId: TaskId, error: unknown) =>\n result.errors.push({\n taskId,\n message: (error instanceof Error ? error.message : String(error)).slice(0, 1000),\n });\n const roots = new Set(\n [...rows.values()].filter(config.isPlanner).map((task) => task.rootTaskId),\n );\n for (const root of roots) {\n try {\n for (const task of await stores.work.recover(root)) rows.set(task.id, task);\n } catch (error) {\n errors(root, error);\n }\n }\n const cancellations = plannerCancellationBatch(options);\n for (const root of roots) {\n try {\n for (const task of await cancelBlockedPlannerChildren(options, config, root)) {\n rows.set(task.id, task);\n result.cancelled.push(task.id);\n }\n } catch (error) {\n errors(root, error);\n }\n }\n const planners = [...rows.values()].filter(config.isPlanner);\n for (const planner of planners) {\n try {\n const current = await state.task(planner.id);\n if (current.state !== \"cancelled\" && current.state !== \"failed\") continue;\n result.cancelled.push(...(await cancelDescendants(options, current, cancellations)));\n } catch (error) {\n errors(planner.id, error);\n }\n }\n for (const candidate of rows.values()) {\n const parent = candidate.parentTaskId ? rows.get(candidate.parentTaskId) : undefined;\n if (!parent || !config.isPlanner(parent)) continue;\n try {\n const current = await stores.tasks.get(candidate.id);\n if (current) await cancellations.stop(current);\n } catch (error) {\n errors(candidate.id, error);\n }\n }\n let deliveryAttempts = 0;\n for (const candidate of rows.values()) {\n if (deliveryAttempts >= MAX_MESSAGES) break;\n if (\n !candidate.parentTaskId ||\n !candidate.work.route ||\n !config.allowed.has(routeKey(candidate.work.route))\n )\n continue;\n try {\n const child = await stores.tasks.get(candidate.id);\n if (!child || !terminal(child) || child.question) continue;\n const parent = await stores.tasks.get(candidate.parentTaskId);\n if (\n !parent ||\n !config.isPlanner(parent) ||\n ![\"queued\", \"running\", \"needs_human\"].includes(parent.state)\n )\n continue;\n await state.authority(parent);\n const operationId = `result:${child.id}:${child.attempt}:${child.state}`;\n const id = taskMessageIdSchema.parse(\n `msg_${createHash(\"sha256\")\n .update(JSON.stringify([child.id, operationId]))\n .digest(\"hex\")}`,\n );\n if (\n await stores.messages.get({\n principal: runtimePrincipal,\n toTaskId: parent.id,\n messageId: id,\n })\n )\n continue;\n const output = child.workResult?.output;\n // The envelope shares the 64 KiB JSON bound with its output; large results stay on the canonical Task.\n const includeOutput =\n output !== undefined && Buffer.byteLength(JSON.stringify(output), \"utf8\") <= 48_000;\n const payload = handoffPayloadSchema.parse({\n outcome: child.state === \"succeeded\" ? \"completed\" : \"failed\",\n summary: `${child.work.title}: ${child.state}`.slice(0, 1000),\n ...(includeOutput ? { output } : {}),\n artifacts: [],\n concerns:\n includeOutput || output === undefined\n ? []\n : [\"Output omitted from handoff; read canonical Task workResult.\"],\n deviations: [],\n });\n deliveryAttempts++;\n await stores.messages.sendResult({\n principal: runtimePrincipal,\n message: {\n operationId,\n fromTaskId: child.id,\n toTaskId: parent.id,\n expectAttempt: child.attempt,\n type: handoffTaskMessageProtocol.binding,\n payload,\n },\n });\n result.delivered++;\n } catch (error) {\n errors(candidate.id, error);\n }\n }\n let processed = 0;\n let launchAttempted = false;\n for (const planner of planners) {\n try {\n let current = await state.task(planner.id);\n let recoveryLeased = false;\n while (processed < MAX_MESSAGES) {\n const head = (\n await stores.messages.pending({\n principal: runtimePrincipal,\n toTaskId: current.id,\n })\n )[0];\n if (!head) break;\n if (head.type.id !== \"planner-start\") {\n const source = await stores.tasks.get(head.fromTaskId);\n if (\n !source ||\n source.attempt !== head.expectAttempt ||\n (source.state === \"cancelled\" && head.delivery !== \"result\")\n ) {\n await stores.messages.discard({\n principal: runtimePrincipal,\n toTaskId: current.id,\n messageId: head.id,\n reason: \"Source attempt changed or was cancelled; await canonical result delivery\",\n });\n result.discarded++;\n processed++;\n continue;\n }\n }\n const decision =\n current.state === \"cancelled\" ? null : await turns.recorded(current, head.id);\n if (terminal(current) && (!decision || current.state === \"cancelled\")) {\n await stores.messages.discard({\n principal: runtimePrincipal,\n toTaskId: current.id,\n messageId: head.id,\n reason: `Planner ${current.state}; no replayable committed decision`,\n });\n result.discarded++;\n processed++;\n continue;\n }\n if (!decision || ![\"running\", \"succeeded\", \"failed\"].includes(current.state)) break;\n try {\n if (current.state === \"running\")\n await turns.prepare({ taskId: current.id, attempt: current.attempt });\n const recovered = await turns.process(current, head.id, undefined, cancellations);\n if (recovered.status === \"idle\") {\n recoveryLeased = true;\n break;\n }\n result.recovered++;\n processed++;\n current = await state.task(current.id);\n } catch (error) {\n if (!(error instanceof TaskMessageSourceFenceError)) throw error;\n await stores.messages.discard({\n principal: runtimePrincipal,\n toTaskId: current.id,\n messageId: head.id,\n reason: \"Source attempt changed; discard stale result and await canonical delivery\",\n });\n result.discarded++;\n processed++;\n }\n }\n current = await state.task(current.id);\n if (terminal(current) || current.state === \"needs_human\" || recoveryLeased) continue;\n if (\n current.attempt > config.limits.maxTurns &&\n [\"queued\", \"running\", \"stalled\"].includes(current.state)\n ) {\n await stores.tasks.transition(current.id, \"failed\", {\n expectFrom: current.state,\n expectAttempt: current.attempt,\n reason: \"Planner maxTurns exhausted\",\n });\n result.failed.push(current.id);\n continue;\n }\n await state.authority(current);\n const activation = (await state.activation(current.id))?.value;\n if (current.state === \"running\") {\n const active = activation?.attempt === current.attempt ? activation : undefined;\n const since = Math.max(\n Date.parse(active?.startedAt ?? current.updatedAt),\n Date.parse(current.lastProgressAt ?? current.updatedAt),\n );\n const expired = Date.parse(state.now()) - since >= TURN_TIMEOUT_MS;\n const pending = await stores.messages.pending({\n principal: runtimePrincipal,\n toTaskId: current.id,\n });\n const handoff = pending.some(\n (message) => message.type.id === \"handoff\" && message.type.version === 1,\n );\n const unsubmitted = active && !(await turns.recorded(current, active.triggerId));\n if ((active?.finished && (handoff || unsubmitted)) || (!active?.finished && expired)) {\n if (!active?.finished && !current.waitingForMessages) {\n current = await stores.tasks.transition(current.id, \"stalled\", {\n expectFrom: \"running\",\n expectAttempt: current.attempt,\n reason: \"Planner turn or finished hook timed out\",\n phase: \"workflow\",\n });\n } else {\n current = await replace(current);\n }\n }\n }\n if (current.state === \"stalled\") current = await replace(current);\n if (current.state !== \"queued\") continue;\n if (current.attempt > config.limits.maxTurns) {\n await stores.tasks.transition(current.id, \"failed\", {\n expectFrom: \"queued\",\n expectAttempt: current.attempt,\n reason: \"Planner maxTurns exhausted\",\n });\n result.failed.push(current.id);\n continue;\n }\n if (\n launchAttempted ||\n current.approval === \"required\" ||\n current.approval === \"denied\" ||\n !(await stores.graphs.dependenciesSatisfied({\n rootTaskId: current.rootTaskId,\n taskId: current.id,\n }))\n )\n continue;\n launchAttempted = true;\n const previousDecision =\n activation?.finished && activation.attempt === current.attempt - 1\n ? await turns.recorded(current, activation.triggerId)\n : null;\n const continuation =\n (previousDecision?.action === \"delegate\" || previousDecision?.action === \"wait\") &&\n !(await stores.receipts.list()).some(\n (receipt) =>\n receipt.taskId === current.id &&\n receipt.attempt === current.attempt - 1 &&\n (receipt.state === \"failed\" || receipt.state === \"stalled\"),\n ) &&\n (\n await stores.messages.pending({\n principal: runtimePrincipal,\n toTaskId: current.id,\n })\n ).some((message) => message.type.id === \"handoff\" && message.type.version === 1);\n const launched = await dispatchTask(stores, {\n taskId: current.id,\n route: config.plannerRoute,\n ...(continuation ? { continuationAttempt: current.attempt } : {}),\n maxLaunchAttempts: config.limits.maxTurns,\n ...(options.budget ? { budget: options.budget } : {}),\n async launch(running) {\n await state.authority(running);\n const prepared = await turns.prepare({ taskId: running.id, attempt: running.attempt });\n await state.authority(running);\n return options.launchPlanner(running, prepared.prompt);\n },\n });\n rows.set(launched.id, launched);\n result.dispatched.push(launched.id);\n } catch (error) {\n if (error instanceof BudgetExhaustedError) result.failed.push(planner.id);\n if (!(error instanceof DispatchRefusedError)) errors(planner.id, error);\n }\n }\n result.errors.push(...cancellations.errors);\n result.cancellationPending = [...cancellations.pending];\n return result;\n\n async function replace(task: Task): Promise<Task> {\n const exhausted = task.attempt >= config.limits.maxTurns;\n const next = await stores.tasks.transition(task.id, exhausted ? \"failed\" : \"queued\", {\n expectFrom: task.state,\n expectAttempt: task.attempt,\n phase: \"workflow\",\n reason: exhausted\n ? \"Planner maxTurns exhausted\"\n : \"Fresh planner activation after finished turn or timeout\",\n });\n if (exhausted) result.failed.push(task.id);\n else result.queued.push(task.id);\n rows.set(next.id, next);\n return next;\n }\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"reconcile.mjs","names":[],"sources":["../../src/planning/reconcile.ts"],"sourcesContent":["import { collectFactoryRecords } from \"../store/iterate\";\nimport { createHash } from \"node:crypto\";\nimport { routeKey } from \"../agent-routes\";\nimport { BudgetExhaustedError, dispatchTask, DispatchRefusedError } from \"../dispatch\";\nimport type { TaskId } from \"../schema/id\";\nimport type { Task } from \"../schema/task\";\nimport { taskMessageIdSchema } from \"../schema/task-message\";\nimport { TaskMessageSourceFenceError } from \"../store/task-messages\";\nimport { handoffPayloadSchema, handoffTaskMessageProtocol } from \"../task-protocols\";\nimport type { PlannerConfiguration } from \"./configuration\";\nimport type { PlannerReconcileResult, PlannerRuntimeOptions } from \"./contracts\";\nimport { cancelDescendants } from \"./effects\";\nimport { plannerCancellationBatch } from \"./cancellation\";\nimport { cancelBlockedPlannerChildren } from \"./dependencies\";\nimport { runtimePrincipal, terminal, type PlannerState } from \"./state\";\nimport type { PlannerTurns } from \"./turns\";\n\nconst MAX_MESSAGES = 32;\nconst TURN_TIMEOUT_MS = 15 * 60_000;\nexport function plannerReconciler(\n options: PlannerRuntimeOptions,\n config: PlannerConfiguration,\n state: PlannerState,\n turns: PlannerTurns,\n) {\n const { stores } = options;\n return async (snapshot?: readonly Task[]): Promise<PlannerReconcileResult> => {\n const result: {\n queued: TaskId[];\n dispatched: TaskId[];\n delivered: number;\n recovered: number;\n discarded: number;\n cancelled: TaskId[];\n cancellationPending?: TaskId[];\n failed: TaskId[];\n errors: { taskId: TaskId; message: string }[];\n } = {\n queued: [],\n dispatched: [],\n delivered: 0,\n recovered: 0,\n discarded: 0,\n cancelled: [],\n failed: [],\n errors: [],\n };\n const rows = new Map(\n (\n snapshot ??\n (await collectFactoryRecords(stores.tasks.list, {\n limit: 100,\n fields: [{ path: [\"work\", \"route\"], test: \"exists\" }],\n }))\n ).map((task) => [task.id, task]),\n );\n const errors = (taskId: TaskId, error: unknown) =>\n result.errors.push({\n taskId,\n message: (error instanceof Error ? error.message : String(error)).slice(0, 1000),\n });\n const roots = new Set(\n [...rows.values()].filter(config.isPlanner).map((task) => task.rootTaskId),\n );\n for (const root of roots) {\n try {\n for (const task of await stores.work.recover(root)) rows.set(task.id, task);\n } catch (error) {\n errors(root, error);\n }\n }\n const cancellations = plannerCancellationBatch(options);\n for (const root of roots) {\n try {\n for (const task of await cancelBlockedPlannerChildren(options, config, root)) {\n rows.set(task.id, task);\n result.cancelled.push(task.id);\n }\n } catch (error) {\n errors(root, error);\n }\n }\n const planners = [...rows.values()].filter(config.isPlanner);\n for (const planner of planners) {\n try {\n const current = await state.task(planner.id);\n if (current.state !== \"cancelled\" && current.state !== \"failed\") continue;\n result.cancelled.push(...(await cancelDescendants(options, current, cancellations)));\n } catch (error) {\n errors(planner.id, error);\n }\n }\n for (const candidate of rows.values()) {\n const parent = candidate.parentTaskId ? rows.get(candidate.parentTaskId) : undefined;\n if (!parent || !config.isPlanner(parent)) continue;\n try {\n const current = await stores.tasks.get(candidate.id);\n if (current) await cancellations.stop(current);\n } catch (error) {\n errors(candidate.id, error);\n }\n }\n let deliveryAttempts = 0;\n for (const candidate of rows.values()) {\n if (deliveryAttempts >= MAX_MESSAGES) break;\n if (\n !candidate.parentTaskId ||\n !candidate.work.route ||\n !config.allowed.has(routeKey(candidate.work.route))\n )\n continue;\n try {\n const child = await stores.tasks.get(candidate.id);\n if (!child || !terminal(child) || child.question) continue;\n const parent = await stores.tasks.get(candidate.parentTaskId);\n if (\n !parent ||\n !config.isPlanner(parent) ||\n ![\"queued\", \"running\", \"needs_human\"].includes(parent.state)\n )\n continue;\n await state.authority(parent);\n const operationId = `result:${child.id}:${child.attempt}:${child.state}`;\n const id = taskMessageIdSchema.parse(\n `msg_${createHash(\"sha256\")\n .update(JSON.stringify([child.id, operationId]))\n .digest(\"hex\")}`,\n );\n if (\n await stores.messages.get({\n principal: runtimePrincipal,\n toTaskId: parent.id,\n messageId: id,\n })\n )\n continue;\n const output = child.workResult?.output;\n // The envelope shares the 64 KiB JSON bound with its output; large results stay on the canonical Task.\n const includeOutput =\n output !== undefined && Buffer.byteLength(JSON.stringify(output), \"utf8\") <= 48_000;\n const payload = handoffPayloadSchema.parse({\n outcome: child.state === \"succeeded\" ? \"completed\" : \"failed\",\n summary: `${child.work.title}: ${child.state}`.slice(0, 1000),\n ...(includeOutput ? { output } : {}),\n artifacts: [],\n concerns:\n includeOutput || output === undefined\n ? []\n : [\"Output omitted from handoff; read canonical Task workResult.\"],\n deviations: [],\n });\n deliveryAttempts++;\n await stores.messages.sendResult({\n principal: runtimePrincipal,\n message: {\n operationId,\n fromTaskId: child.id,\n toTaskId: parent.id,\n expectAttempt: child.attempt,\n type: handoffTaskMessageProtocol.binding,\n payload,\n },\n });\n result.delivered++;\n } catch (error) {\n errors(candidate.id, error);\n }\n }\n let processed = 0;\n let launchAttempted = false;\n for (const planner of planners) {\n try {\n let current = await state.task(planner.id);\n let recoveryLeased = false;\n while (processed < MAX_MESSAGES) {\n const head = (\n await stores.messages.pending({\n principal: runtimePrincipal,\n toTaskId: current.id,\n })\n )[0];\n if (!head) break;\n if (head.type.id !== \"planner-start\") {\n const source = await stores.tasks.get(head.fromTaskId);\n if (\n !source ||\n source.attempt !== head.expectAttempt ||\n (source.state === \"cancelled\" && head.delivery !== \"result\")\n ) {\n await stores.messages.discard({\n principal: runtimePrincipal,\n toTaskId: current.id,\n messageId: head.id,\n reason: \"Source attempt changed or was cancelled; await canonical result delivery\",\n });\n result.discarded++;\n processed++;\n continue;\n }\n }\n const decision =\n current.state === \"cancelled\" ? null : await turns.recorded(current, head.id);\n if (terminal(current) && (!decision || current.state === \"cancelled\")) {\n await stores.messages.discard({\n principal: runtimePrincipal,\n toTaskId: current.id,\n messageId: head.id,\n reason: `Planner ${current.state}; no replayable committed decision`,\n });\n result.discarded++;\n processed++;\n continue;\n }\n if (!decision || ![\"running\", \"succeeded\", \"failed\"].includes(current.state)) break;\n try {\n if (current.state === \"running\")\n await turns.prepare({ taskId: current.id, attempt: current.attempt });\n const recovered = await turns.process(current, head.id, undefined, cancellations);\n if (recovered.status === \"idle\") {\n recoveryLeased = true;\n break;\n }\n result.recovered++;\n processed++;\n current = await state.task(current.id);\n } catch (error) {\n if (!(error instanceof TaskMessageSourceFenceError)) throw error;\n await stores.messages.discard({\n principal: runtimePrincipal,\n toTaskId: current.id,\n messageId: head.id,\n reason: \"Source attempt changed; discard stale result and await canonical delivery\",\n });\n result.discarded++;\n processed++;\n }\n }\n current = await state.task(current.id);\n if (terminal(current) || current.state === \"needs_human\" || recoveryLeased) continue;\n if (\n current.attempt > config.limits.maxTurns &&\n [\"queued\", \"running\", \"stalled\"].includes(current.state)\n ) {\n await stores.tasks.transition(current.id, \"failed\", {\n expectFrom: current.state,\n expectAttempt: current.attempt,\n reason: \"Planner maxTurns exhausted\",\n });\n result.failed.push(current.id);\n continue;\n }\n await state.authority(current);\n const activation = (await state.activation(current.id))?.value;\n if (current.state === \"running\") {\n const active = activation?.attempt === current.attempt ? activation : undefined;\n const since = Math.max(\n Date.parse(active?.startedAt ?? current.updatedAt),\n Date.parse(current.lastProgressAt ?? current.updatedAt),\n );\n const expired = Date.parse(state.now()) - since >= TURN_TIMEOUT_MS;\n const pending = await stores.messages.pending({\n principal: runtimePrincipal,\n toTaskId: current.id,\n });\n const handoff = pending.some(\n (message) => message.type.id === \"handoff\" && message.type.version === 1,\n );\n const unsubmitted = active && !(await turns.recorded(current, active.triggerId));\n if ((active?.finished && (handoff || unsubmitted)) || (!active?.finished && expired)) {\n if (!active?.finished && !current.waitingForMessages) {\n current = await stores.tasks.transition(current.id, \"stalled\", {\n expectFrom: \"running\",\n expectAttempt: current.attempt,\n reason: \"Planner turn or finished hook timed out\",\n phase: \"workflow\",\n });\n } else {\n current = await replace(current);\n }\n }\n }\n if (current.state === \"stalled\") current = await replace(current);\n if (current.state !== \"queued\") continue;\n if (current.attempt > config.limits.maxTurns) {\n await stores.tasks.transition(current.id, \"failed\", {\n expectFrom: \"queued\",\n expectAttempt: current.attempt,\n reason: \"Planner maxTurns exhausted\",\n });\n result.failed.push(current.id);\n continue;\n }\n if (\n launchAttempted ||\n current.approval === \"required\" ||\n current.approval === \"denied\" ||\n !(await stores.graphs.dependenciesSatisfied({\n rootTaskId: current.rootTaskId,\n taskId: current.id,\n }))\n )\n continue;\n launchAttempted = true;\n const previousDecision =\n activation?.finished && activation.attempt === current.attempt - 1\n ? await turns.recorded(current, activation.triggerId)\n : null;\n const continuation =\n (previousDecision?.action === \"delegate\" || previousDecision?.action === \"wait\") &&\n !(\n await collectFactoryRecords(stores.receipts.list, {\n limit: 100,\n match: { taskId: current.id, attempt: current.attempt - 1 },\n })\n ).some(\n (receipt) =>\n receipt.taskId === current.id &&\n receipt.attempt === current.attempt - 1 &&\n (receipt.state === \"failed\" || receipt.state === \"stalled\"),\n ) &&\n (\n await stores.messages.pending({\n principal: runtimePrincipal,\n toTaskId: current.id,\n })\n ).some((message) => message.type.id === \"handoff\" && message.type.version === 1);\n const launched = await dispatchTask(stores, {\n taskId: current.id,\n route: config.plannerRoute,\n ...(continuation ? { continuationAttempt: current.attempt } : {}),\n maxLaunchAttempts: config.limits.maxTurns,\n ...(options.budget ? { budget: options.budget } : {}),\n async launch(running) {\n await state.authority(running);\n const prepared = await turns.prepare({ taskId: running.id, attempt: running.attempt });\n await state.authority(running);\n return options.launchPlanner(running, prepared.prompt);\n },\n });\n rows.set(launched.id, launched);\n result.dispatched.push(launched.id);\n } catch (error) {\n if (error instanceof BudgetExhaustedError) result.failed.push(planner.id);\n if (!(error instanceof DispatchRefusedError)) errors(planner.id, error);\n }\n }\n result.errors.push(...cancellations.errors);\n result.cancellationPending = [...cancellations.pending];\n return result;\n\n async function replace(task: Task): Promise<Task> {\n const exhausted = task.attempt >= config.limits.maxTurns;\n const next = await stores.tasks.transition(task.id, exhausted ? \"failed\" : \"queued\", {\n expectFrom: task.state,\n expectAttempt: task.attempt,\n phase: \"workflow\",\n reason: exhausted\n ? \"Planner maxTurns exhausted\"\n : \"Fresh planner activation after finished turn or timeout\",\n });\n if (exhausted) result.failed.push(task.id);\n else result.queued.push(task.id);\n rows.set(next.id, next);\n return next;\n }\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { StoreDriver } from "../store/driver.mjs";
|
|
3
|
+
import { PostgresIndexedStoreOptions, createPostgresIndexedStore } from "./indexed.mjs";
|
|
3
4
|
import { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";
|
|
4
5
|
//#region src/postgres/index.d.ts
|
|
5
6
|
/** Drizzle PostgreSQL database shape required by the Factory driver and migrations. */
|
|
@@ -180,5 +181,5 @@ declare function applyFactoryMigrations(db: FactoryDatabase): Promise<readonly s
|
|
|
180
181
|
*/
|
|
181
182
|
declare function createPostgresDriver(db: FactoryDatabase): StoreDriver;
|
|
182
183
|
//#endregion
|
|
183
|
-
export { FactoryDatabase, FactoryMigration, applyFactoryMigrations, createPostgresDriver, factoryMigrations, factoryMigrationsLedger, factoryRecords };
|
|
184
|
+
export { FactoryDatabase, FactoryMigration, type PostgresIndexedStoreOptions, applyFactoryMigrations, createPostgresDriver, createPostgresIndexedStore, factoryMigrations, factoryMigrationsLedger, factoryRecords };
|
|
184
185
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/postgres/index.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { RecordExistsError, RecordNotFoundError, VersionConflictError } from "../store/driver.mjs";
|
|
2
|
+
import { indexedMigration, indexedReadsMigration } from "./indexed-schema.mjs";
|
|
3
|
+
import { createPostgresIndexedStore } from "./indexed.mjs";
|
|
2
4
|
import { and, eq, sql } from "drizzle-orm";
|
|
3
5
|
import { integer, jsonb, pgTable, primaryKey, text, timestamp } from "drizzle-orm/pg-core";
|
|
4
6
|
//#region src/postgres/index.ts
|
|
@@ -19,16 +21,20 @@ const factoryMigrationsLedger = pgTable("factory_migrations", {
|
|
|
19
21
|
* they run before the ledger records them, so an interrupted run re-executes
|
|
20
22
|
* them on retry. Never edit or reorder an entry that has shipped.
|
|
21
23
|
*/
|
|
22
|
-
const factoryMigrations = [
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
const factoryMigrations = [
|
|
25
|
+
{
|
|
26
|
+
tag: "0000_factory_records",
|
|
27
|
+
statements: [`CREATE TABLE IF NOT EXISTS "factory_records" (
|
|
25
28
|
"collection" text NOT NULL,
|
|
26
29
|
"id" text NOT NULL,
|
|
27
30
|
"value" jsonb NOT NULL,
|
|
28
31
|
"version" integer NOT NULL,
|
|
29
32
|
CONSTRAINT "factory_records_pk" PRIMARY KEY ("collection", "id")
|
|
30
33
|
)`]
|
|
31
|
-
}
|
|
34
|
+
},
|
|
35
|
+
indexedMigration,
|
|
36
|
+
indexedReadsMigration
|
|
37
|
+
];
|
|
32
38
|
const migrationsLedgerDdl = `CREATE TABLE IF NOT EXISTS "factory_migrations" (
|
|
33
39
|
"tag" text PRIMARY KEY,
|
|
34
40
|
"applied_at" timestamptz NOT NULL DEFAULT now()
|
|
@@ -133,6 +139,6 @@ function createPostgresDriver(db) {
|
|
|
133
139
|
};
|
|
134
140
|
}
|
|
135
141
|
//#endregion
|
|
136
|
-
export { applyFactoryMigrations, createPostgresDriver, factoryMigrations, factoryMigrationsLedger, factoryRecords };
|
|
142
|
+
export { applyFactoryMigrations, createPostgresDriver, createPostgresIndexedStore, factoryMigrations, factoryMigrationsLedger, factoryRecords };
|
|
137
143
|
|
|
138
144
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/postgres/index.ts"],"sourcesContent":["import { and, eq, sql } from \"drizzle-orm\";\nimport {\n integer,\n jsonb,\n pgTable,\n primaryKey,\n text,\n timestamp,\n type PgDatabase,\n type PgQueryResultHKT,\n} from \"drizzle-orm/pg-core\";\nimport {\n RecordExistsError,\n RecordNotFoundError,\n VersionConflictError,\n type CollectionName,\n type StoreDriver,\n} from \"../store/driver\";\n\n/** Drizzle PostgreSQL database shape required by the Factory driver and migrations. */\nexport type FactoryDatabase = PgDatabase<PgQueryResultHKT>;\n\n/** Shared versioned-record table used for every Factory store collection. */\nexport const factoryRecords = pgTable(\n \"factory_records\",\n {\n collection: text(\"collection\").notNull(),\n id: text(\"id\").notNull(),\n value: jsonb(\"value\").$type<unknown>().notNull(),\n version: integer(\"version\").notNull(),\n },\n (table) => [primaryKey({ columns: [table.collection, table.id] })],\n);\n\n/** Append-only ledger of checked-in Factory migrations already applied to a database. */\nexport const factoryMigrationsLedger = pgTable(\"factory_migrations\", {\n tag: text(\"tag\").primaryKey(),\n appliedAt: timestamp(\"applied_at\", { withTimezone: true }).notNull().defaultNow(),\n});\n\n/** One immutable, idempotent SQL migration identified by its stable tag. */\nexport interface FactoryMigration {\n /** Stable append-only ledger key; shipped tags must never be renamed or reordered. */\n readonly tag: string;\n /** Idempotent SQL statements executed in order before the tag is recorded. */\n readonly statements: readonly string[];\n}\n\n/**\n * Checked-in, append-only migration list. Statements must be idempotent:\n * they run before the ledger records them, so an interrupted run re-executes\n * them on retry. Never edit or reorder an entry that has shipped.\n */\nexport const factoryMigrations: readonly FactoryMigration[] = [\n {\n tag: \"0000_factory_records\",\n statements: [\n `CREATE TABLE IF NOT EXISTS \"factory_records\" (\n \"collection\" text NOT NULL,\n \"id\" text NOT NULL,\n \"value\" jsonb NOT NULL,\n \"version\" integer NOT NULL,\n CONSTRAINT \"factory_records_pk\" PRIMARY KEY (\"collection\", \"id\")\n )`,\n ],\n },\n];\n\nconst migrationsLedgerDdl = `CREATE TABLE IF NOT EXISTS \"factory_migrations\" (\n \"tag\" text PRIMARY KEY,\n \"applied_at\" timestamptz NOT NULL DEFAULT now()\n)`;\n\n/**\n * Applies every unapplied checked-in Factory schema migration.\n *\n * @remarks\n * Call this explicitly during deployment or process startup before serving Factory operations.\n * Statements run in listed order and the migration tag is recorded afterward. Because those\n * writes are not one interactive transaction, every statement is idempotent: if the process stops\n * between a statement and its ledger insert, rerunning this function safely attempts that migration\n * again. The deployment or startup owner must serialize migration calls for a database; HTTP\n * database drivers may not support the connection-scoped locking needed to do that here.\n *\n * These migrations create and evolve Factory SQL tables only. They do not reinterpret or migrate\n * older JSON entity shapes stored in `factory_records`; releases that change persisted entity\n * schemas still require an explicit application data migration or reset decision.\n *\n * The caller owns the database connection and should retry the whole call after resolving a SQL\n * failure. A rejected promise means startup must not assume all tables are ready.\n *\n * @param db - Drizzle PostgreSQL database with DDL access for Factory migrations.\n * @returns Tags this invocation found unapplied and ran, in checked-in order.\n * @throws When ledger creation, a migration statement, or ledger persistence fails.\n */\nexport async function applyFactoryMigrations(db: FactoryDatabase): Promise<readonly string[]> {\n await db.execute(sql.raw(migrationsLedgerDdl));\n const applied: string[] = [];\n for (const migration of factoryMigrations) {\n const existing = await db\n .select({ tag: factoryMigrationsLedger.tag })\n .from(factoryMigrationsLedger)\n .where(eq(factoryMigrationsLedger.tag, migration.tag))\n .limit(1);\n if (existing.length > 0) {\n continue;\n }\n for (const statement of migration.statements) {\n await db.execute(sql.raw(statement));\n }\n await db.insert(factoryMigrationsLedger).values({ tag: migration.tag }).onConflictDoNothing();\n applied.push(migration.tag);\n }\n return applied;\n}\n\n/**\n * Creates an atomic compare-and-swap driver over the Factory PostgreSQL tables.\n *\n * @remarks\n * The caller must apply {@link applyFactoryMigrations} before serving Factory operations. Inserts\n * use the table's primary key for create-if-absent behavior; updates require the expected version\n * and throw `VersionConflictError` when another writer wins. The driver never creates schema at\n * runtime and does not own the database connection lifecycle.\n * Reads return the latest committed row visible to the supplied connection. The engine validates\n * stored values and retries only the conflicts its operation explicitly supports; callers should\n * surface other database failures and reconcile from canonical state before repeating effects.\n *\n * @param db - Drizzle PostgreSQL database connected with DML access to the migrated tables.\n * @returns A `postgres` driver suitable for `createStores`.\n * @throws Store operations throw `RecordExistsError`, `RecordNotFoundError`, or\n * `VersionConflictError` for their corresponding persistence races, plus underlying database\n * errors for unavailable or rejected queries.\n */\nexport function createPostgresDriver(db: FactoryDatabase): StoreDriver {\n async function readRecord(\n collection: CollectionName,\n id: string,\n ): Promise<{ value: unknown; version: number } | null> {\n const rows = await db\n .select({ value: factoryRecords.value, version: factoryRecords.version })\n .from(factoryRecords)\n .where(and(eq(factoryRecords.collection, collection), eq(factoryRecords.id, id)))\n .limit(1);\n const row = rows[0];\n return row === undefined ? null : { value: row.value, version: row.version };\n }\n\n return {\n name: \"postgres\",\n\n get(collection, id) {\n return readRecord(collection, id);\n },\n\n async insert({ collection, id, value }) {\n const inserted = await db\n .insert(factoryRecords)\n .values({ collection, id, value, version: 1 })\n .onConflictDoNothing()\n .returning({ id: factoryRecords.id });\n if (inserted.length === 0) {\n throw new RecordExistsError(collection, id);\n }\n },\n\n async update({ collection, id, value, expectedVersion }) {\n const updated = await db\n .update(factoryRecords)\n .set({ value, version: expectedVersion + 1 })\n .where(\n and(\n eq(factoryRecords.collection, collection),\n eq(factoryRecords.id, id),\n eq(factoryRecords.version, expectedVersion),\n ),\n )\n .returning({ id: factoryRecords.id });\n if (updated.length === 0) {\n const existing = await readRecord(collection, id);\n if (existing === null) {\n throw new RecordNotFoundError(collection, id);\n }\n throw new VersionConflictError(collection, id, expectedVersion);\n }\n },\n\n async list(collection) {\n const rows = await db\n .select({\n id: factoryRecords.id,\n value: factoryRecords.value,\n version: factoryRecords.version,\n })\n .from(factoryRecords)\n .where(eq(factoryRecords.collection, collection));\n return rows.map((row) => ({\n id: row.id,\n record: { value: row.value, version: row.version },\n }));\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/postgres/index.ts"],"sourcesContent":["import { and, eq, sql } from \"drizzle-orm\";\nimport { indexedMigration, indexedReadsMigration } from \"./indexed-schema\";\nexport { createPostgresIndexedStore, type PostgresIndexedStoreOptions } from \"./indexed\";\nimport {\n integer,\n jsonb,\n pgTable,\n primaryKey,\n text,\n timestamp,\n type PgDatabase,\n type PgQueryResultHKT,\n} from \"drizzle-orm/pg-core\";\nimport {\n RecordExistsError,\n RecordNotFoundError,\n VersionConflictError,\n type CollectionName,\n type StoreDriver,\n} from \"../store/driver\";\n\n/** Drizzle PostgreSQL database shape required by the Factory driver and migrations. */\nexport type FactoryDatabase = PgDatabase<PgQueryResultHKT>;\n\n/** Shared versioned-record table used for every Factory store collection. */\nexport const factoryRecords = pgTable(\n \"factory_records\",\n {\n collection: text(\"collection\").notNull(),\n id: text(\"id\").notNull(),\n value: jsonb(\"value\").$type<unknown>().notNull(),\n version: integer(\"version\").notNull(),\n },\n (table) => [primaryKey({ columns: [table.collection, table.id] })],\n);\n\n/** Append-only ledger of checked-in Factory migrations already applied to a database. */\nexport const factoryMigrationsLedger = pgTable(\"factory_migrations\", {\n tag: text(\"tag\").primaryKey(),\n appliedAt: timestamp(\"applied_at\", { withTimezone: true }).notNull().defaultNow(),\n});\n\n/** One immutable, idempotent SQL migration identified by its stable tag. */\nexport interface FactoryMigration {\n /** Stable append-only ledger key; shipped tags must never be renamed or reordered. */\n readonly tag: string;\n /** Idempotent SQL statements executed in order before the tag is recorded. */\n readonly statements: readonly string[];\n}\n\n/**\n * Checked-in, append-only migration list. Statements must be idempotent:\n * they run before the ledger records them, so an interrupted run re-executes\n * them on retry. Never edit or reorder an entry that has shipped.\n */\nexport const factoryMigrations: readonly FactoryMigration[] = [\n {\n tag: \"0000_factory_records\",\n statements: [\n `CREATE TABLE IF NOT EXISTS \"factory_records\" (\n \"collection\" text NOT NULL,\n \"id\" text NOT NULL,\n \"value\" jsonb NOT NULL,\n \"version\" integer NOT NULL,\n CONSTRAINT \"factory_records_pk\" PRIMARY KEY (\"collection\", \"id\")\n )`,\n ],\n },\n indexedMigration,\n indexedReadsMigration,\n];\n\nconst migrationsLedgerDdl = `CREATE TABLE IF NOT EXISTS \"factory_migrations\" (\n \"tag\" text PRIMARY KEY,\n \"applied_at\" timestamptz NOT NULL DEFAULT now()\n)`;\n\n/**\n * Applies every unapplied checked-in Factory schema migration.\n *\n * @remarks\n * Call this explicitly during deployment or process startup before serving Factory operations.\n * Statements run in listed order and the migration tag is recorded afterward. Because those\n * writes are not one interactive transaction, every statement is idempotent: if the process stops\n * between a statement and its ledger insert, rerunning this function safely attempts that migration\n * again. The deployment or startup owner must serialize migration calls for a database; HTTP\n * database drivers may not support the connection-scoped locking needed to do that here.\n *\n * These migrations create and evolve Factory SQL tables only. They do not reinterpret or migrate\n * older JSON entity shapes stored in `factory_records`; releases that change persisted entity\n * schemas still require an explicit application data migration or reset decision.\n *\n * The caller owns the database connection and should retry the whole call after resolving a SQL\n * failure. A rejected promise means startup must not assume all tables are ready.\n *\n * @param db - Drizzle PostgreSQL database with DDL access for Factory migrations.\n * @returns Tags this invocation found unapplied and ran, in checked-in order.\n * @throws When ledger creation, a migration statement, or ledger persistence fails.\n */\nexport async function applyFactoryMigrations(db: FactoryDatabase): Promise<readonly string[]> {\n await db.execute(sql.raw(migrationsLedgerDdl));\n const applied: string[] = [];\n for (const migration of factoryMigrations) {\n const existing = await db\n .select({ tag: factoryMigrationsLedger.tag })\n .from(factoryMigrationsLedger)\n .where(eq(factoryMigrationsLedger.tag, migration.tag))\n .limit(1);\n if (existing.length > 0) {\n continue;\n }\n for (const statement of migration.statements) {\n await db.execute(sql.raw(statement));\n }\n await db.insert(factoryMigrationsLedger).values({ tag: migration.tag }).onConflictDoNothing();\n applied.push(migration.tag);\n }\n return applied;\n}\n\n/**\n * Creates an atomic compare-and-swap driver over the Factory PostgreSQL tables.\n *\n * @remarks\n * The caller must apply {@link applyFactoryMigrations} before serving Factory operations. Inserts\n * use the table's primary key for create-if-absent behavior; updates require the expected version\n * and throw `VersionConflictError` when another writer wins. The driver never creates schema at\n * runtime and does not own the database connection lifecycle.\n * Reads return the latest committed row visible to the supplied connection. The engine validates\n * stored values and retries only the conflicts its operation explicitly supports; callers should\n * surface other database failures and reconcile from canonical state before repeating effects.\n *\n * @param db - Drizzle PostgreSQL database connected with DML access to the migrated tables.\n * @returns A `postgres` driver suitable for `createStores`.\n * @throws Store operations throw `RecordExistsError`, `RecordNotFoundError`, or\n * `VersionConflictError` for their corresponding persistence races, plus underlying database\n * errors for unavailable or rejected queries.\n */\nexport function createPostgresDriver(db: FactoryDatabase): StoreDriver {\n async function readRecord(\n collection: CollectionName,\n id: string,\n ): Promise<{ value: unknown; version: number } | null> {\n const rows = await db\n .select({ value: factoryRecords.value, version: factoryRecords.version })\n .from(factoryRecords)\n .where(and(eq(factoryRecords.collection, collection), eq(factoryRecords.id, id)))\n .limit(1);\n const row = rows[0];\n return row === undefined ? null : { value: row.value, version: row.version };\n }\n\n return {\n name: \"postgres\",\n\n get(collection, id) {\n return readRecord(collection, id);\n },\n\n async insert({ collection, id, value }) {\n const inserted = await db\n .insert(factoryRecords)\n .values({ collection, id, value, version: 1 })\n .onConflictDoNothing()\n .returning({ id: factoryRecords.id });\n if (inserted.length === 0) {\n throw new RecordExistsError(collection, id);\n }\n },\n\n async update({ collection, id, value, expectedVersion }) {\n const updated = await db\n .update(factoryRecords)\n .set({ value, version: expectedVersion + 1 })\n .where(\n and(\n eq(factoryRecords.collection, collection),\n eq(factoryRecords.id, id),\n eq(factoryRecords.version, expectedVersion),\n ),\n )\n .returning({ id: factoryRecords.id });\n if (updated.length === 0) {\n const existing = await readRecord(collection, id);\n if (existing === null) {\n throw new RecordNotFoundError(collection, id);\n }\n throw new VersionConflictError(collection, id, expectedVersion);\n }\n },\n\n async list(collection) {\n const rows = await db\n .select({\n id: factoryRecords.id,\n value: factoryRecords.value,\n version: factoryRecords.version,\n })\n .from(factoryRecords)\n .where(eq(factoryRecords.collection, collection));\n return rows.map((row) => ({\n id: row.id,\n record: { value: row.value, version: row.version },\n }));\n },\n };\n}\n"],"mappings":"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"}
|