@mastra/factory 0.2.2-alpha.0 → 0.2.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/auth.d.ts +2 -2
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +401 -314
  5. package/dist/auth.js.map +1 -1
  6. package/dist/capabilities/connection.js +1 -1
  7. package/dist/capabilities/intake.js +1 -1
  8. package/dist/capabilities/version-control.js +1 -1
  9. package/dist/factory.d.ts.map +1 -1
  10. package/dist/factory.js +441 -39455
  11. package/dist/factory.js.map +1 -1
  12. package/dist/index.js +2 -39466
  13. package/dist/integrations/base.js +1 -1
  14. package/dist/integrations/github/config.js +36 -22
  15. package/dist/integrations/github/config.js.map +1 -1
  16. package/dist/integrations/github/factory-session.js +33 -37
  17. package/dist/integrations/github/factory-session.js.map +1 -1
  18. package/dist/integrations/github/integration.js +1037 -3624
  19. package/dist/integrations/github/integration.js.map +1 -1
  20. package/dist/integrations/github/issue-triage.js +71 -71
  21. package/dist/integrations/github/issue-triage.js.map +1 -1
  22. package/dist/integrations/github/pat.js +47 -36
  23. package/dist/integrations/github/pat.js.map +1 -1
  24. package/dist/integrations/github/provenance.js +72 -101
  25. package/dist/integrations/github/provenance.js.map +1 -1
  26. package/dist/integrations/github/routes.d.ts.map +1 -1
  27. package/dist/integrations/github/routes.js +1272 -1833
  28. package/dist/integrations/github/routes.js.map +1 -1
  29. package/dist/integrations/github/rules.js +203 -471
  30. package/dist/integrations/github/rules.js.map +1 -1
  31. package/dist/integrations/github/sandbox.d.ts +2 -0
  32. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  33. package/dist/integrations/github/sandbox.js +401 -326
  34. package/dist/integrations/github/sandbox.js.map +1 -1
  35. package/dist/integrations/github/session-subscriptions.js +165 -223
  36. package/dist/integrations/github/session-subscriptions.js.map +1 -1
  37. package/dist/integrations/github/subscriptions.js +43 -51
  38. package/dist/integrations/github/subscriptions.js.map +1 -1
  39. package/dist/integrations/github/token-refresh.js +15 -18
  40. package/dist/integrations/github/token-refresh.js.map +1 -1
  41. package/dist/integrations/github/webhook.js +359 -343
  42. package/dist/integrations/github/webhook.js.map +1 -1
  43. package/dist/integrations/linear/agent-tools.js +77 -100
  44. package/dist/integrations/linear/agent-tools.js.map +1 -1
  45. package/dist/integrations/linear/integration.js +611 -1309
  46. package/dist/integrations/linear/integration.js.map +1 -1
  47. package/dist/integrations/linear/routes.js +227 -230
  48. package/dist/integrations/linear/routes.js.map +1 -1
  49. package/dist/integrations/linear/rules.js +126 -346
  50. package/dist/integrations/linear/rules.js.map +1 -1
  51. package/dist/integrations/linear/storage.js +1 -1
  52. package/dist/integrations/platform/api-client.js +131 -135
  53. package/dist/integrations/platform/api-client.js.map +1 -1
  54. package/dist/integrations/platform/github/event-worker.js +275 -573
  55. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  56. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  57. package/dist/integrations/platform/github/integration.js +845 -3942
  58. package/dist/integrations/platform/github/integration.js.map +1 -1
  59. package/dist/integrations/platform/linear/integration.js +457 -1309
  60. package/dist/integrations/platform/linear/integration.js.map +1 -1
  61. package/dist/integrations/workos/integration.js +80 -85
  62. package/dist/integrations/workos/integration.js.map +1 -1
  63. package/dist/routes/config.d.ts.map +1 -1
  64. package/dist/routes/config.js +933 -915
  65. package/dist/routes/config.js.map +1 -1
  66. package/dist/routes/custom-provider-source.js +110 -93
  67. package/dist/routes/custom-provider-source.js.map +1 -1
  68. package/dist/routes/fs.js +401 -349
  69. package/dist/routes/fs.js.map +1 -1
  70. package/dist/routes/intake.js +165 -165
  71. package/dist/routes/intake.js.map +1 -1
  72. package/dist/routes/oauth.js +447 -25166
  73. package/dist/routes/oauth.js.map +1 -1
  74. package/dist/routes/projects.js +437 -376
  75. package/dist/routes/projects.js.map +1 -1
  76. package/dist/routes/provider-credentials.js +76 -58
  77. package/dist/routes/provider-credentials.js.map +1 -1
  78. package/dist/routes/route.js +12 -8
  79. package/dist/routes/route.js.map +1 -1
  80. package/dist/routes/skills.js +160 -173
  81. package/dist/routes/skills.js.map +1 -1
  82. package/dist/routes/surface.js +250 -28618
  83. package/dist/routes/surface.js.map +1 -1
  84. package/dist/routes/tenant-credentials.js +141 -166
  85. package/dist/routes/tenant-credentials.js.map +1 -1
  86. package/dist/routes/work-items.js +654 -862
  87. package/dist/routes/work-items.js.map +1 -1
  88. package/dist/rules/binding-context.js +23 -35
  89. package/dist/rules/binding-context.js.map +1 -1
  90. package/dist/rules/defaults.js +200 -309
  91. package/dist/rules/defaults.js.map +1 -1
  92. package/dist/rules/dispatcher.d.ts.map +1 -1
  93. package/dist/rules/dispatcher.js +438 -677
  94. package/dist/rules/dispatcher.js.map +1 -1
  95. package/dist/rules/index.js +5 -599
  96. package/dist/rules/processor.js +344 -596
  97. package/dist/rules/processor.js.map +1 -1
  98. package/dist/rules/resolve.js +21 -18
  99. package/dist/rules/resolve.js.map +1 -1
  100. package/dist/rules/start-coordinator.js +168 -157
  101. package/dist/rules/start-coordinator.js.map +1 -1
  102. package/dist/rules/tools.js +50 -85
  103. package/dist/rules/tools.js.map +1 -1
  104. package/dist/rules/transition-service.js +183 -454
  105. package/dist/rules/transition-service.js.map +1 -1
  106. package/dist/rules/types.js +33 -29
  107. package/dist/rules/types.js.map +1 -1
  108. package/dist/rules/validation.js +266 -281
  109. package/dist/rules/validation.js.map +1 -1
  110. package/dist/sandbox/fleet.js +272 -250
  111. package/dist/sandbox/fleet.js.map +1 -1
  112. package/dist/sandbox/reattach.js +13 -6
  113. package/dist/sandbox/reattach.js.map +1 -1
  114. package/dist/server-error.js +12 -16
  115. package/dist/server-error.js.map +1 -1
  116. package/dist/skills/service.js +30 -35
  117. package/dist/skills/service.js.map +1 -1
  118. package/dist/spa-static.d.ts.map +1 -1
  119. package/dist/spa-static.js +91 -64
  120. package/dist/spa-static.js.map +1 -1
  121. package/dist/state-signing.js +73 -44
  122. package/dist/state-signing.js.map +1 -1
  123. package/dist/storage/domains/audit/agent-audit.js +60 -59
  124. package/dist/storage/domains/audit/agent-audit.js.map +1 -1
  125. package/dist/storage/domains/audit/base.js +169 -112
  126. package/dist/storage/domains/audit/base.js.map +1 -1
  127. package/dist/storage/domains/audit/domain.js +155 -153
  128. package/dist/storage/domains/audit/domain.js.map +1 -1
  129. package/dist/storage/domains/credentials/base.js +259 -243
  130. package/dist/storage/domains/credentials/base.js.map +1 -1
  131. package/dist/storage/domains/custom-providers/base.js +138 -143
  132. package/dist/storage/domains/custom-providers/base.js.map +1 -1
  133. package/dist/storage/domains/intake/base.js +75 -57
  134. package/dist/storage/domains/intake/base.js.map +1 -1
  135. package/dist/storage/domains/integrations/base.js +310 -245
  136. package/dist/storage/domains/integrations/base.js.map +1 -1
  137. package/dist/storage/domains/memory-settings/base.js +117 -102
  138. package/dist/storage/domains/memory-settings/base.js.map +1 -1
  139. package/dist/storage/domains/model-packs/base.js +89 -88
  140. package/dist/storage/domains/model-packs/base.js.map +1 -1
  141. package/dist/storage/domains/projects/base.js +97 -92
  142. package/dist/storage/domains/projects/base.js.map +1 -1
  143. package/dist/storage/domains/queue-health/base.js +111 -76
  144. package/dist/storage/domains/queue-health/base.js.map +1 -1
  145. package/dist/storage/domains/source-control/base.d.ts +9 -0
  146. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  147. package/dist/storage/domains/source-control/base.js +777 -702
  148. package/dist/storage/domains/source-control/base.js.map +1 -1
  149. package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
  150. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  151. package/dist/storage/domains/source-control/inmemory.js +322 -344
  152. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  153. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  154. package/dist/storage/domains/work-items/base.js +1266 -1132
  155. package/dist/storage/domains/work-items/base.js.map +1 -1
  156. package/dist/storage/domains/work-items/metrics.js +202 -184
  157. package/dist/storage/domains/work-items/metrics.js.map +1 -1
  158. package/dist/timing.d.ts +15 -0
  159. package/dist/timing.d.ts.map +1 -0
  160. package/dist/timing.js +34 -0
  161. package/dist/timing.js.map +1 -0
  162. package/dist/workspace.d.ts +2 -3
  163. package/dist/workspace.d.ts.map +1 -1
  164. package/dist/workspace.js +236 -438
  165. package/dist/workspace.js.map +1 -1
  166. package/factory-skills/factory-review/SKILL.md +9 -2
  167. package/package.json +9 -9
  168. package/dist/capabilities/connection.js.map +0 -1
  169. package/dist/capabilities/intake.js.map +0 -1
  170. package/dist/capabilities/version-control.js.map +0 -1
  171. package/dist/index.js.map +0 -1
  172. package/dist/integrations/base.js.map +0 -1
  173. package/dist/integrations/linear/storage.js.map +0 -1
  174. package/dist/rules/index.js.map +0 -1
@@ -1,1173 +1,1307 @@
1
- // src/storage/domains/work-items/base.ts
2
1
  import { createHash } from "crypto";
3
2
  import { FactoryStorageDomain, UniqueViolationError } from "@mastra/core/storage";
3
+ //#region src/storage/domains/work-items/base.ts
4
+ /**
5
+ * Factory work-items storage domain.
6
+ *
7
+ * Work items belong to a first-class Factory project. External intake items use
8
+ * a provider-neutral source reference; manual work items have no source.
9
+ * Stage history is server-owned, while session and metadata patches merge
10
+ * atomically so concurrent actors do not overwrite each other. The authoritative
11
+ * Factory transition path keeps one exclusive current stage per item.
12
+ */
4
13
  function stableJson(value) {
5
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
6
- if (value && typeof value === "object") {
7
- return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`).join(",")}}`;
8
- }
9
- return JSON.stringify(value) ?? "null";
14
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
15
+ if (value && typeof value === "object") return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`).join(",")}}`;
16
+ return JSON.stringify(value) ?? "null";
10
17
  }
11
18
  function factoryDecisionHash(decision) {
12
- return createHash("sha256").update(stableJson(decision)).digest("hex");
19
+ return createHash("sha256").update(stableJson(decision)).digest("hex");
13
20
  }
14
- var AUTOMATION_ACTORS = /* @__PURE__ */ new Set([
15
- "factory",
16
- "system",
17
- "automation",
18
- "factory-rule-dispatcher",
19
- "factory-tool-result-rule"
21
+ /**
22
+ * Sentinel actor ids that mark a stage transition as automation-driven (vs a
23
+ * human's WorkOS user id): generic sentinels plus the system ids the Factory
24
+ * rules engine stamps (see `actorId` in factory/rules/transition-service.ts).
25
+ * Metrics treat any other actor — including a missing `exitedBy` on
26
+ * pre-existing entries — as human.
27
+ */
28
+ const AUTOMATION_ACTORS = /* @__PURE__ */ new Set([
29
+ "factory",
30
+ "system",
31
+ "automation",
32
+ "factory-rule-dispatcher",
33
+ "factory-tool-result-rule"
20
34
  ]);
35
+ /**
36
+ * Whether an actor id marks a transition no human performed on the Factory
37
+ * board: a sentinel automation id, an agent binding (`agent:*`), or an
38
+ * external-webhook actor (`github:*` — a human may have acted on GitHub, but
39
+ * the board move itself was automated).
40
+ */
21
41
  function isAutomationActor(by) {
22
- if (by === void 0) return false;
23
- return AUTOMATION_ACTORS.has(by) || by.startsWith("agent:") || by.startsWith("github:");
42
+ if (by === void 0) return false;
43
+ return AUTOMATION_ACTORS.has(by) || by.startsWith("agent:") || by.startsWith("github:");
24
44
  }
25
- var WORK_ITEMS_SCHEMA = {
26
- name: "work_items",
27
- columns: {
28
- id: { type: "uuid-pk" },
29
- org_id: { type: "text" },
30
- factory_project_id: { type: "text" },
31
- external_source: { type: "json", nullable: true },
32
- source_key: { type: "text", nullable: true },
33
- parent_work_item_id: { type: "text", nullable: true },
34
- title: { type: "text" },
35
- stages: { type: "json" },
36
- stage_history: { type: "json" },
37
- sessions: { type: "json" },
38
- metadata: { type: "json", nullable: true },
39
- revision: { type: "integer", default: 1 },
40
- created_by: { type: "text" },
41
- created_at: { type: "timestamp" },
42
- updated_at: { type: "timestamp" }
43
- },
44
- uniqueIndexes: [
45
- {
46
- name: "work_items_project_source_key_unique",
47
- columns: ["factory_project_id", "source_key"]
48
- }
49
- ],
50
- indexes: [
51
- {
52
- name: "work_items_org_project_updated_at_idx",
53
- columns: ["org_id", "factory_project_id", "updated_at"]
54
- },
55
- {
56
- name: "work_items_project_parent_idx",
57
- columns: ["org_id", "factory_project_id", "parent_work_item_id"]
58
- }
59
- ]
45
+ const WORK_ITEMS_SCHEMA = {
46
+ name: "work_items",
47
+ columns: {
48
+ id: { type: "uuid-pk" },
49
+ org_id: { type: "text" },
50
+ factory_project_id: { type: "text" },
51
+ external_source: {
52
+ type: "json",
53
+ nullable: true
54
+ },
55
+ source_key: {
56
+ type: "text",
57
+ nullable: true
58
+ },
59
+ parent_work_item_id: {
60
+ type: "text",
61
+ nullable: true
62
+ },
63
+ title: { type: "text" },
64
+ stages: { type: "json" },
65
+ stage_history: { type: "json" },
66
+ sessions: { type: "json" },
67
+ metadata: {
68
+ type: "json",
69
+ nullable: true
70
+ },
71
+ revision: {
72
+ type: "integer",
73
+ default: 1
74
+ },
75
+ created_by: { type: "text" },
76
+ created_at: { type: "timestamp" },
77
+ updated_at: { type: "timestamp" }
78
+ },
79
+ uniqueIndexes: [{
80
+ name: "work_items_project_source_key_unique",
81
+ columns: ["factory_project_id", "source_key"]
82
+ }],
83
+ indexes: [{
84
+ name: "work_items_org_project_updated_at_idx",
85
+ columns: [
86
+ "org_id",
87
+ "factory_project_id",
88
+ "updated_at"
89
+ ]
90
+ }, {
91
+ name: "work_items_project_parent_idx",
92
+ columns: [
93
+ "org_id",
94
+ "factory_project_id",
95
+ "parent_work_item_id"
96
+ ]
97
+ }]
60
98
  };
61
99
  function sourceKey(source) {
62
- return source ? `${source.integrationId}:${source.type}:${source.externalId}` : null;
100
+ return source ? `${source.integrationId}:${source.type}:${source.externalId}` : null;
63
101
  }
64
102
  function toWorkItem(row) {
65
- return {
66
- id: row.id,
67
- orgId: row.org_id,
68
- factoryProjectId: String(row.factory_project_id),
69
- externalSource: row.external_source,
70
- parentWorkItemId: row.parent_work_item_id,
71
- title: row.title,
72
- stages: row.stages,
73
- stageHistory: row.stage_history,
74
- sessions: row.sessions,
75
- metadata: row.metadata,
76
- revision: row.revision,
77
- createdBy: row.created_by,
78
- createdAt: row.created_at,
79
- updatedAt: row.updated_at
80
- };
103
+ return {
104
+ id: row.id,
105
+ orgId: row.org_id,
106
+ factoryProjectId: String(row.factory_project_id),
107
+ externalSource: row.external_source,
108
+ parentWorkItemId: row.parent_work_item_id,
109
+ title: row.title,
110
+ stages: row.stages,
111
+ stageHistory: row.stage_history,
112
+ sessions: row.sessions,
113
+ metadata: row.metadata,
114
+ revision: row.revision,
115
+ createdBy: row.created_by,
116
+ createdAt: row.created_at,
117
+ updatedAt: row.updated_at
118
+ };
81
119
  }
82
- var toRow = toWorkItem;
120
+ const toRow = toWorkItem;
83
121
  function patchColumns(changes) {
84
- return {
85
- ...changes.parentWorkItemId !== void 0 ? { parent_work_item_id: changes.parentWorkItemId } : {},
86
- ...changes.title !== void 0 ? { title: changes.title } : {},
87
- ...changes.stages !== void 0 ? { stages: changes.stages } : {},
88
- ...changes.stageHistory !== void 0 ? { stage_history: changes.stageHistory } : {},
89
- ...changes.sessions !== void 0 ? { sessions: changes.sessions } : {},
90
- ...changes.metadata !== void 0 ? { metadata: changes.metadata } : {},
91
- ...changes.revision !== void 0 ? { revision: changes.revision } : {},
92
- ...changes.updatedAt !== void 0 ? { updated_at: changes.updatedAt } : {}
93
- };
122
+ return {
123
+ ...changes.parentWorkItemId !== void 0 ? { parent_work_item_id: changes.parentWorkItemId } : {},
124
+ ...changes.title !== void 0 ? { title: changes.title } : {},
125
+ ...changes.stages !== void 0 ? { stages: changes.stages } : {},
126
+ ...changes.stageHistory !== void 0 ? { stage_history: changes.stageHistory } : {},
127
+ ...changes.sessions !== void 0 ? { sessions: changes.sessions } : {},
128
+ ...changes.metadata !== void 0 ? { metadata: changes.metadata } : {},
129
+ ...changes.revision !== void 0 ? { revision: changes.revision } : {},
130
+ ...changes.updatedAt !== void 0 ? { updated_at: changes.updatedAt } : {}
131
+ };
94
132
  }
95
133
  function emptyPrior() {
96
- return { stages: [], sessionRoles: [] };
134
+ return {
135
+ stages: [],
136
+ sessionRoles: []
137
+ };
97
138
  }
98
139
  function priorState(row) {
99
- return { stages: row.stages, sessionRoles: Object.keys(row.sessions) };
140
+ return {
141
+ stages: row.stages,
142
+ sessionRoles: Object.keys(row.sessions)
143
+ };
100
144
  }
101
145
  var WorkItemRelationError = class extends Error {
102
- code = "invalid_work_item_relation";
146
+ code = "invalid_work_item_relation";
103
147
  };
104
148
  function validateParentRelation(projectItems, itemId, parentWorkItemId) {
105
- if (parentWorkItemId === null) return;
106
- const byId = new Map(projectItems.map((item) => [item.id, item]));
107
- const parent = byId.get(parentWorkItemId);
108
- if (!parent) throw new WorkItemRelationError("Related work item not found in this project.");
109
- if (itemId === parentWorkItemId) throw new WorkItemRelationError("A work item cannot relate to itself.");
110
- const visited = /* @__PURE__ */ new Set();
111
- let cursor = parent;
112
- while (cursor?.parentWorkItemId) {
113
- if (cursor.parentWorkItemId === itemId) {
114
- throw new WorkItemRelationError("This relationship would create a cycle.");
115
- }
116
- if (visited.has(cursor.id)) throw new WorkItemRelationError("The related work item chain contains a cycle.");
117
- visited.add(cursor.id);
118
- cursor = byId.get(cursor.parentWorkItemId);
119
- }
149
+ if (parentWorkItemId === null) return;
150
+ const byId = new Map(projectItems.map((item) => [item.id, item]));
151
+ const parent = byId.get(parentWorkItemId);
152
+ if (!parent) throw new WorkItemRelationError("Related work item not found in this project.");
153
+ if (itemId === parentWorkItemId) throw new WorkItemRelationError("A work item cannot relate to itself.");
154
+ const visited = /* @__PURE__ */ new Set();
155
+ let cursor = parent;
156
+ while (cursor?.parentWorkItemId) {
157
+ if (cursor.parentWorkItemId === itemId) throw new WorkItemRelationError("This relationship would create a cycle.");
158
+ if (visited.has(cursor.id)) throw new WorkItemRelationError("The related work item chain contains a cycle.");
159
+ visited.add(cursor.id);
160
+ cursor = byId.get(cursor.parentWorkItemId);
161
+ }
120
162
  }
163
+ /**
164
+ * Diff `oldStages` → `newStages` and return the updated history: exited stages
165
+ * get `exitedAt` + `exitedBy` stamped on their open entry, entered stages get
166
+ * a new entry.
167
+ */
121
168
  function applyStageTransition(history, oldStages, newStages, by, now) {
122
- const timestamp = now.toISOString();
123
- const next = history.map((entry) => ({ ...entry }));
124
- for (const stage of oldStages) {
125
- if (newStages.includes(stage)) continue;
126
- for (let i = next.length - 1; i >= 0; i--) {
127
- const entry = next[i];
128
- if (entry.stage === stage && entry.exitedAt === void 0) {
129
- entry.exitedAt = timestamp;
130
- entry.exitedBy = by;
131
- break;
132
- }
133
- }
134
- }
135
- for (const stage of newStages) {
136
- if (!oldStages.includes(stage)) next.push({ stage, enteredAt: timestamp, by });
137
- }
138
- return next;
169
+ const timestamp = now.toISOString();
170
+ const next = history.map((entry) => ({ ...entry }));
171
+ for (const stage of oldStages) {
172
+ if (newStages.includes(stage)) continue;
173
+ for (let i = next.length - 1; i >= 0; i--) {
174
+ const entry = next[i];
175
+ if (entry.stage === stage && entry.exitedAt === void 0) {
176
+ entry.exitedAt = timestamp;
177
+ entry.exitedBy = by;
178
+ break;
179
+ }
180
+ }
181
+ }
182
+ for (const stage of newStages) if (!oldStages.includes(stage)) next.push({
183
+ stage,
184
+ enteredAt: timestamp,
185
+ by
186
+ });
187
+ return next;
139
188
  }
140
189
  function stampSessions(sessions, by) {
141
- return Object.fromEntries(Object.entries(sessions).map(([role, session]) => [role, { ...session, startedBy: by }]));
190
+ return Object.fromEntries(Object.entries(sessions).map(([role, session]) => [role, {
191
+ ...session,
192
+ startedBy: by
193
+ }]));
142
194
  }
143
- function applyUpdate({
144
- current,
145
- userId,
146
- input
147
- }) {
148
- const now = /* @__PURE__ */ new Date();
149
- return {
150
- ...input.parentWorkItemId !== void 0 ? { parent_work_item_id: input.parentWorkItemId } : {},
151
- ...input.title !== void 0 ? { title: input.title } : {},
152
- ...input.stages !== void 0 ? {
153
- stages: input.stages,
154
- stage_history: applyStageTransition(current.stage_history, current.stages, input.stages, userId, now)
155
- } : {},
156
- ...input.sessions !== void 0 ? { sessions: { ...current.sessions, ...stampSessions(input.sessions, userId) } } : {},
157
- ...input.metadata !== void 0 ? { metadata: input.metadata === null ? null : { ...current.metadata ?? {}, ...input.metadata } } : {},
158
- revision: current.revision + 1,
159
- updated_at: now
160
- };
195
+ function applyUpdate({ current, userId, input }) {
196
+ const now = /* @__PURE__ */ new Date();
197
+ return {
198
+ ...input.parentWorkItemId !== void 0 ? { parent_work_item_id: input.parentWorkItemId } : {},
199
+ ...input.title !== void 0 ? { title: input.title } : {},
200
+ ...input.stages !== void 0 ? {
201
+ stages: input.stages,
202
+ stage_history: applyStageTransition(current.stage_history, current.stages, input.stages, userId, now)
203
+ } : {},
204
+ ...input.sessions !== void 0 ? { sessions: {
205
+ ...current.sessions,
206
+ ...stampSessions(input.sessions, userId)
207
+ } } : {},
208
+ ...input.metadata !== void 0 ? { metadata: input.metadata === null ? null : {
209
+ ...current.metadata ?? {},
210
+ ...input.metadata
211
+ } } : {},
212
+ revision: current.revision + 1,
213
+ updated_at: now
214
+ };
161
215
  }
162
- var projectRelationLocks = /* @__PURE__ */ new Map();
216
+ const projectRelationLocks = /* @__PURE__ */ new Map();
163
217
  function withInProcessProjectLock(key, fn) {
164
- const previous = projectRelationLocks.get(key) ?? Promise.resolve();
165
- const result = previous.then(fn, fn);
166
- const tail = result.then(
167
- () => void 0,
168
- () => void 0
169
- );
170
- projectRelationLocks.set(key, tail);
171
- void tail.then(() => {
172
- if (projectRelationLocks.get(key) === tail) projectRelationLocks.delete(key);
173
- });
174
- return result;
218
+ const result = (projectRelationLocks.get(key) ?? Promise.resolve()).then(fn, fn);
219
+ const tail = result.then(() => void 0, () => void 0);
220
+ projectRelationLocks.set(key, tail);
221
+ tail.then(() => {
222
+ if (projectRelationLocks.get(key) === tail) projectRelationLocks.delete(key);
223
+ });
224
+ return result;
175
225
  }
176
- var FACTORY_GOVERNANCE_SCHEMAS = [
177
- {
178
- name: "factory_rule_ingress",
179
- columns: {
180
- id: { type: "uuid-pk" },
181
- org_id: { type: "text" },
182
- factory_project_id: { type: "text" },
183
- identity: { type: "text" },
184
- trigger_type: { type: "text" },
185
- transition_id: { type: "text" },
186
- result: { type: "json" },
187
- created_at: { type: "timestamp" }
188
- },
189
- uniqueIndexes: [
190
- { name: "factory_rule_ingress_tenant_identity_unique", columns: ["org_id", "factory_project_id", "identity"] }
191
- ]
192
- },
193
- {
194
- name: "factory_rule_evaluations",
195
- columns: {
196
- id: { type: "uuid-pk" },
197
- ingress_id: { type: "text" },
198
- work_item_id: { type: "text", nullable: true },
199
- rule_set_version: { type: "text" },
200
- expected_revision: { type: "integer", nullable: true },
201
- outcome: { type: "text" },
202
- code: { type: "text", nullable: true },
203
- reason: { type: "text", nullable: true },
204
- causal_chain: { type: "json" },
205
- created_at: { type: "timestamp" }
206
- }
207
- },
208
- {
209
- name: "factory_deferred_decisions",
210
- columns: {
211
- id: { type: "uuid-pk" },
212
- org_id: { type: "text" },
213
- factory_project_id: { type: "text" },
214
- evaluation_id: { type: "text" },
215
- work_item_id: { type: "text", nullable: true },
216
- idempotency_key: { type: "text" },
217
- effect_ordinal: { type: "integer" },
218
- effect_hash: { type: "text" },
219
- causal_chain: { type: "json" },
220
- actor: { type: "json", nullable: true },
221
- decision: { type: "json" },
222
- status: { type: "text" },
223
- attempts: { type: "integer" },
224
- available_at: { type: "timestamp" },
225
- lease_owner: { type: "text", nullable: true },
226
- lease_expires_at: { type: "timestamp", nullable: true },
227
- last_error: { type: "text", nullable: true },
228
- completed_at: { type: "timestamp", nullable: true },
229
- created_at: { type: "timestamp" },
230
- updated_at: { type: "timestamp" }
231
- },
232
- uniqueIndexes: [
233
- {
234
- name: "factory_deferred_decisions_tenant_key_unique",
235
- columns: ["org_id", "factory_project_id", "idempotency_key"]
236
- }
237
- ]
238
- },
239
- {
240
- name: "factory_run_bindings",
241
- columns: {
242
- id: { type: "uuid-pk" },
243
- org_id: { type: "text" },
244
- factory_project_id: { type: "text" },
245
- work_item_id: { type: "text" },
246
- role: { type: "text" },
247
- thread_id: { type: "text" },
248
- resource_id: { type: "text" },
249
- session_id: { type: "text" },
250
- branch: { type: "text" },
251
- status: { type: "text" },
252
- created_at: { type: "timestamp" },
253
- revoked_at: { type: "timestamp", nullable: true }
254
- }
255
- },
256
- {
257
- name: "factory_tool_result_cursors",
258
- columns: {
259
- binding_id: { type: "text", primaryKey: true },
260
- org_id: { type: "text" },
261
- factory_project_id: { type: "text" },
262
- last_message_id: { type: "text" },
263
- last_message_created_at: { type: "timestamp" },
264
- updated_at: { type: "timestamp" }
265
- }
266
- },
267
- {
268
- name: "factory_pending_starts",
269
- columns: {
270
- id: { type: "uuid-pk" },
271
- org_id: { type: "text" },
272
- factory_project_id: { type: "text" },
273
- binding_id: { type: "text" },
274
- kickoff_key: { type: "text" },
275
- message: { type: "text", nullable: true },
276
- status: { type: "text" },
277
- attempts: { type: "integer" },
278
- available_at: { type: "timestamp" },
279
- lease_owner: { type: "text", nullable: true },
280
- lease_expires_at: { type: "timestamp", nullable: true },
281
- last_error: { type: "text", nullable: true },
282
- completed_at: { type: "timestamp", nullable: true },
283
- created_at: { type: "timestamp" },
284
- updated_at: { type: "timestamp" }
285
- },
286
- uniqueIndexes: [
287
- {
288
- name: "factory_pending_starts_tenant_kickoff_unique",
289
- columns: ["org_id", "factory_project_id", "kickoff_key"]
290
- }
291
- ]
292
- }
226
+ const FACTORY_GOVERNANCE_SCHEMAS = [
227
+ {
228
+ name: "factory_rule_ingress",
229
+ columns: {
230
+ id: { type: "uuid-pk" },
231
+ org_id: { type: "text" },
232
+ factory_project_id: { type: "text" },
233
+ identity: { type: "text" },
234
+ trigger_type: { type: "text" },
235
+ transition_id: { type: "text" },
236
+ result: { type: "json" },
237
+ created_at: { type: "timestamp" }
238
+ },
239
+ uniqueIndexes: [{
240
+ name: "factory_rule_ingress_tenant_identity_unique",
241
+ columns: [
242
+ "org_id",
243
+ "factory_project_id",
244
+ "identity"
245
+ ]
246
+ }]
247
+ },
248
+ {
249
+ name: "factory_rule_evaluations",
250
+ columns: {
251
+ id: { type: "uuid-pk" },
252
+ ingress_id: { type: "text" },
253
+ work_item_id: {
254
+ type: "text",
255
+ nullable: true
256
+ },
257
+ rule_set_version: { type: "text" },
258
+ expected_revision: {
259
+ type: "integer",
260
+ nullable: true
261
+ },
262
+ outcome: { type: "text" },
263
+ code: {
264
+ type: "text",
265
+ nullable: true
266
+ },
267
+ reason: {
268
+ type: "text",
269
+ nullable: true
270
+ },
271
+ causal_chain: { type: "json" },
272
+ created_at: { type: "timestamp" }
273
+ }
274
+ },
275
+ {
276
+ name: "factory_deferred_decisions",
277
+ columns: {
278
+ id: { type: "uuid-pk" },
279
+ org_id: { type: "text" },
280
+ factory_project_id: { type: "text" },
281
+ evaluation_id: { type: "text" },
282
+ work_item_id: {
283
+ type: "text",
284
+ nullable: true
285
+ },
286
+ idempotency_key: { type: "text" },
287
+ effect_ordinal: { type: "integer" },
288
+ effect_hash: { type: "text" },
289
+ causal_chain: { type: "json" },
290
+ actor: {
291
+ type: "json",
292
+ nullable: true
293
+ },
294
+ decision: { type: "json" },
295
+ status: { type: "text" },
296
+ attempts: { type: "integer" },
297
+ available_at: { type: "timestamp" },
298
+ lease_owner: {
299
+ type: "text",
300
+ nullable: true
301
+ },
302
+ lease_expires_at: {
303
+ type: "timestamp",
304
+ nullable: true
305
+ },
306
+ last_error: {
307
+ type: "text",
308
+ nullable: true
309
+ },
310
+ completed_at: {
311
+ type: "timestamp",
312
+ nullable: true
313
+ },
314
+ created_at: { type: "timestamp" },
315
+ updated_at: { type: "timestamp" }
316
+ },
317
+ uniqueIndexes: [{
318
+ name: "factory_deferred_decisions_tenant_key_unique",
319
+ columns: [
320
+ "org_id",
321
+ "factory_project_id",
322
+ "idempotency_key"
323
+ ]
324
+ }],
325
+ indexes: [{
326
+ name: "factory_deferred_decisions_claim_idx",
327
+ columns: ["status", "created_at"]
328
+ }]
329
+ },
330
+ {
331
+ name: "factory_run_bindings",
332
+ columns: {
333
+ id: { type: "uuid-pk" },
334
+ org_id: { type: "text" },
335
+ factory_project_id: { type: "text" },
336
+ work_item_id: { type: "text" },
337
+ role: { type: "text" },
338
+ thread_id: { type: "text" },
339
+ resource_id: { type: "text" },
340
+ session_id: { type: "text" },
341
+ branch: { type: "text" },
342
+ status: { type: "text" },
343
+ created_at: { type: "timestamp" },
344
+ revoked_at: {
345
+ type: "timestamp",
346
+ nullable: true
347
+ }
348
+ },
349
+ indexes: [{
350
+ name: "factory_run_bindings_session_idx",
351
+ columns: [
352
+ "factory_project_id",
353
+ "thread_id",
354
+ "resource_id",
355
+ "session_id"
356
+ ]
357
+ }, {
358
+ name: "factory_run_bindings_status_idx",
359
+ columns: ["status"]
360
+ }]
361
+ },
362
+ {
363
+ name: "factory_tool_result_cursors",
364
+ columns: {
365
+ binding_id: {
366
+ type: "text",
367
+ primaryKey: true
368
+ },
369
+ org_id: { type: "text" },
370
+ factory_project_id: { type: "text" },
371
+ last_message_id: { type: "text" },
372
+ last_message_created_at: { type: "timestamp" },
373
+ updated_at: { type: "timestamp" }
374
+ }
375
+ },
376
+ {
377
+ name: "factory_pending_starts",
378
+ columns: {
379
+ id: { type: "uuid-pk" },
380
+ org_id: { type: "text" },
381
+ factory_project_id: { type: "text" },
382
+ binding_id: { type: "text" },
383
+ kickoff_key: { type: "text" },
384
+ message: {
385
+ type: "text",
386
+ nullable: true
387
+ },
388
+ status: { type: "text" },
389
+ attempts: { type: "integer" },
390
+ available_at: { type: "timestamp" },
391
+ lease_owner: {
392
+ type: "text",
393
+ nullable: true
394
+ },
395
+ lease_expires_at: {
396
+ type: "timestamp",
397
+ nullable: true
398
+ },
399
+ last_error: {
400
+ type: "text",
401
+ nullable: true
402
+ },
403
+ completed_at: {
404
+ type: "timestamp",
405
+ nullable: true
406
+ },
407
+ created_at: { type: "timestamp" },
408
+ updated_at: { type: "timestamp" }
409
+ },
410
+ uniqueIndexes: [{
411
+ name: "factory_pending_starts_tenant_kickoff_unique",
412
+ columns: [
413
+ "org_id",
414
+ "factory_project_id",
415
+ "kickoff_key"
416
+ ]
417
+ }],
418
+ indexes: [{
419
+ name: "factory_pending_starts_claim_idx",
420
+ columns: ["status", "created_at"]
421
+ }]
422
+ }
293
423
  ];
294
424
  function toBinding(row) {
295
- return {
296
- id: row.id,
297
- orgId: row.org_id,
298
- factoryProjectId: String(row.factory_project_id),
299
- workItemId: String(row.work_item_id),
300
- role: String(row.role),
301
- threadId: String(row.thread_id),
302
- resourceId: String(row.resource_id),
303
- sessionId: String(row.session_id),
304
- branch: String(row.branch),
305
- status: row.status,
306
- createdAt: row.created_at,
307
- revokedAt: row.revoked_at ?? null
308
- };
425
+ return {
426
+ id: row.id,
427
+ orgId: row.org_id,
428
+ factoryProjectId: String(row.factory_project_id),
429
+ workItemId: String(row.work_item_id),
430
+ role: String(row.role),
431
+ threadId: String(row.thread_id),
432
+ resourceId: String(row.resource_id),
433
+ sessionId: String(row.session_id),
434
+ branch: String(row.branch),
435
+ status: row.status,
436
+ createdAt: row.created_at,
437
+ revokedAt: row.revoked_at ?? null
438
+ };
309
439
  }
310
440
  function toDeferredDecision(row) {
311
- return {
312
- id: row.id,
313
- orgId: row.org_id,
314
- factoryProjectId: String(row.factory_project_id),
315
- evaluationId: String(row.evaluation_id),
316
- workItemId: row.work_item_id === null || row.work_item_id === void 0 ? null : String(row.work_item_id),
317
- idempotencyKey: String(row.idempotency_key),
318
- effectOrdinal: Number(row.effect_ordinal),
319
- effectHash: String(row.effect_hash),
320
- causalChain: row.causal_chain ?? [],
321
- actor: row.actor ?? null,
322
- decision: row.decision,
323
- status: row.status,
324
- attempts: Number(row.attempts),
325
- availableAt: row.available_at,
326
- leaseOwner: row.lease_owner ?? null,
327
- leaseExpiresAt: row.lease_expires_at ?? null,
328
- lastError: row.last_error ?? null,
329
- completedAt: row.completed_at ?? null,
330
- createdAt: row.created_at,
331
- updatedAt: row.updated_at
332
- };
441
+ return {
442
+ id: row.id,
443
+ orgId: row.org_id,
444
+ factoryProjectId: String(row.factory_project_id),
445
+ evaluationId: String(row.evaluation_id),
446
+ workItemId: row.work_item_id === null || row.work_item_id === void 0 ? null : String(row.work_item_id),
447
+ idempotencyKey: String(row.idempotency_key),
448
+ effectOrdinal: Number(row.effect_ordinal),
449
+ effectHash: String(row.effect_hash),
450
+ causalChain: row.causal_chain ?? [],
451
+ actor: row.actor ?? null,
452
+ decision: row.decision,
453
+ status: row.status,
454
+ attempts: Number(row.attempts),
455
+ availableAt: row.available_at,
456
+ leaseOwner: row.lease_owner ?? null,
457
+ leaseExpiresAt: row.lease_expires_at ?? null,
458
+ lastError: row.last_error ?? null,
459
+ completedAt: row.completed_at ?? null,
460
+ createdAt: row.created_at,
461
+ updatedAt: row.updated_at
462
+ };
333
463
  }
334
464
  function toPendingStart(row) {
335
- return {
336
- id: row.id,
337
- orgId: row.org_id,
338
- factoryProjectId: String(row.factory_project_id),
339
- bindingId: String(row.binding_id),
340
- kickoffKey: String(row.kickoff_key),
341
- message: row.message ?? null,
342
- status: row.status,
343
- attempts: Number(row.attempts),
344
- availableAt: row.available_at,
345
- leaseOwner: row.lease_owner ?? null,
346
- leaseExpiresAt: row.lease_expires_at ?? null,
347
- lastError: row.last_error ?? null,
348
- completedAt: row.completed_at ?? null,
349
- createdAt: row.created_at,
350
- updatedAt: row.updated_at
351
- };
465
+ return {
466
+ id: row.id,
467
+ orgId: row.org_id,
468
+ factoryProjectId: String(row.factory_project_id),
469
+ bindingId: String(row.binding_id),
470
+ kickoffKey: String(row.kickoff_key),
471
+ message: row.message ?? null,
472
+ status: row.status,
473
+ attempts: Number(row.attempts),
474
+ availableAt: row.available_at,
475
+ leaseOwner: row.lease_owner ?? null,
476
+ leaseExpiresAt: row.lease_expires_at ?? null,
477
+ lastError: row.last_error ?? null,
478
+ completedAt: row.completed_at ?? null,
479
+ createdAt: row.created_at,
480
+ updatedAt: row.updated_at
481
+ };
352
482
  }
353
483
  var WorkItemsStorage = class extends FactoryStorageDomain {
354
- constructor() {
355
- super("work-items");
356
- }
357
- async init() {
358
- await this.ensureCollections([WORK_ITEMS_SCHEMA, ...FACTORY_GOVERNANCE_SCHEMAS]);
359
- }
360
- async dangerouslyClearAll() {
361
- await this.ops.deleteMany("work_items", {});
362
- }
363
- get #db() {
364
- return this.ops;
365
- }
366
- async #withProjectRelationTransaction(orgId, factoryProjectId, fn) {
367
- const key = `work-items:${orgId}:${factoryProjectId}`;
368
- return withInProcessProjectLock(key, () => this.storage.withTransaction(fn, { isolationLevel: "serializable" }));
369
- }
370
- async #claimLeases(table, input, map) {
371
- const claim = () => this.storage.withTransaction(async (ops) => {
372
- const candidates = await ops.findMany(table, {}, { orderBy: [["created_at", "asc"]] });
373
- const claimed = [];
374
- for (const candidate of candidates) {
375
- if (claimed.length >= input.limit) break;
376
- const availableAt = new Date(candidate.available_at).getTime();
377
- const leaseExpiresAt = candidate.lease_expires_at ? new Date(candidate.lease_expires_at).getTime() : 0;
378
- const claimable = (candidate.status === "pending" || candidate.status === "retry") && availableAt <= input.now.getTime();
379
- const expired = candidate.status === "leased" && leaseExpiresAt <= input.now.getTime();
380
- if (!claimable && !expired) continue;
381
- let didClaim = false;
382
- const row = await ops.updateAtomic(table, { id: candidate.id }, (current) => {
383
- const currentAvailable = new Date(current.available_at).getTime();
384
- const currentExpiry = current.lease_expires_at ? new Date(current.lease_expires_at).getTime() : 0;
385
- const currentClaimable = (current.status === "pending" || current.status === "retry") && currentAvailable <= input.now.getTime();
386
- const currentExpired = current.status === "leased" && currentExpiry <= input.now.getTime();
387
- if (!currentClaimable && !currentExpired) return null;
388
- didClaim = true;
389
- return {
390
- status: "leased",
391
- attempts: Number(current.attempts) + 1,
392
- lease_owner: input.ownerId,
393
- lease_expires_at: input.leaseExpiresAt,
394
- updated_at: input.now
395
- };
396
- });
397
- if (didClaim && row) claimed.push(map(row));
398
- }
399
- return claimed;
400
- });
401
- return claim();
402
- }
403
- async #renewLease(table, identity, leaseExpiresAt) {
404
- let renewed = false;
405
- await this.#db.updateAtomic(
406
- table,
407
- { id: identity.id, org_id: identity.orgId, factory_project_id: identity.factoryProjectId },
408
- (current) => {
409
- if (current.status !== "leased" || current.lease_owner !== identity.ownerId) return null;
410
- renewed = true;
411
- return { lease_expires_at: leaseExpiresAt, updated_at: /* @__PURE__ */ new Date() };
412
- }
413
- );
414
- return renewed;
415
- }
416
- async #completeLease(table, identity, now) {
417
- let completed = false;
418
- const row = await this.#db.updateAtomic(
419
- table,
420
- { id: identity.id, org_id: identity.orgId, factory_project_id: identity.factoryProjectId },
421
- (current) => {
422
- if (current.status !== "leased" || current.lease_owner !== identity.ownerId) return null;
423
- completed = true;
424
- return {
425
- status: table === "factory_pending_starts" ? "sent" : "succeeded",
426
- lease_owner: null,
427
- lease_expires_at: null,
428
- completed_at: now,
429
- updated_at: now
430
- };
431
- }
432
- );
433
- return completed ? row : null;
434
- }
435
- async #failLease(table, input) {
436
- let failed = false;
437
- const row = await this.#db.updateAtomic(
438
- table,
439
- { id: input.id, org_id: input.orgId, factory_project_id: input.factoryProjectId },
440
- (current) => {
441
- if (current.status !== "leased" || current.lease_owner !== input.ownerId) return null;
442
- failed = true;
443
- return {
444
- status: input.terminal ? "failed" : "retry",
445
- available_at: input.availableAt,
446
- lease_owner: null,
447
- lease_expires_at: null,
448
- last_error: input.lastError,
449
- completed_at: input.terminal ? input.now : null,
450
- updated_at: input.now
451
- };
452
- }
453
- );
454
- return failed ? row : null;
455
- }
456
- async #listWithOps(ops, orgId, factoryProjectId) {
457
- const rows = await ops.findMany(
458
- "work_items",
459
- { org_id: orgId, factory_project_id: factoryProjectId },
460
- { orderBy: [["updated_at", "desc"]] }
461
- );
462
- return rows.map(toWorkItem);
463
- }
464
- /** List the org's work items for a project, newest first. */
465
- async list({ orgId, factoryProjectId }) {
466
- return this.#listWithOps(this.#db, orgId, factoryProjectId);
467
- }
468
- async get({ orgId, id }) {
469
- const row = await this.#db.findOne("work_items", { org_id: orgId, id });
470
- return row ? toWorkItem(row) : null;
471
- }
472
- async getForProject(orgId, factoryProjectId, id) {
473
- const row = await this.#db.findOne("work_items", {
474
- id,
475
- org_id: orgId,
476
- factory_project_id: factoryProjectId
477
- });
478
- return row ? toRow(row) : null;
479
- }
480
- async getTransitionResultByIngress(orgId, factoryProjectId, identity) {
481
- const row = await this.#db.findOne("factory_rule_ingress", {
482
- org_id: orgId,
483
- factory_project_id: factoryProjectId,
484
- identity
485
- });
486
- return row?.result ?? null;
487
- }
488
- async commitTransition(input) {
489
- const commit = () => this.storage.withTransaction(async (ops) => {
490
- const prior = await ops.findOne("factory_rule_ingress", {
491
- org_id: input.orgId,
492
- factory_project_id: input.factoryProjectId,
493
- identity: input.ingress.identity
494
- });
495
- if (prior)
496
- return {
497
- status: "replayed",
498
- item: await this.getForProject(input.orgId, input.factoryProjectId, input.workItemId),
499
- result: prior.result
500
- };
501
- const now = /* @__PURE__ */ new Date();
502
- let item = null;
503
- let code = null;
504
- let reason = null;
505
- let result;
506
- const updated = await ops.updateAtomic(
507
- "work_items",
508
- {
509
- id: input.workItemId,
510
- org_id: input.orgId,
511
- factory_project_id: input.factoryProjectId
512
- },
513
- (row) => {
514
- const existing = toRow(row);
515
- item = existing;
516
- if (existing.revision !== input.expectedRevision) {
517
- code = "stale";
518
- reason = "The work item changed before this transition committed.";
519
- return null;
520
- }
521
- if (input.evaluation.outcome === "rejected") {
522
- code = input.evaluation.code;
523
- reason = input.evaluation.reason;
524
- return null;
525
- }
526
- if (existing.stages.length === 1 && existing.stages[0] === input.destinationStage) return null;
527
- return patchColumns({
528
- stages: [input.destinationStage],
529
- stageHistory: applyStageTransition(
530
- existing.stageHistory,
531
- existing.stages,
532
- [input.destinationStage],
533
- input.actorId,
534
- now
535
- ),
536
- revision: existing.revision + 1,
537
- updatedAt: now
538
- });
539
- }
540
- );
541
- if (updated) item = toRow(updated);
542
- if (!item) {
543
- code = input.evaluation.outcome === "rejected" ? input.evaluation.code : "invalid_transition";
544
- reason = input.evaluation.outcome === "rejected" ? input.evaluation.reason : "Work item not found.";
545
- }
546
- const outcome = item && code === null && input.evaluation.outcome === "accepted" ? "accepted" : "rejected";
547
- result = outcome === "accepted" ? {
548
- status: "accepted",
549
- transitionId: input.ingress.transitionId,
550
- itemId: item.id,
551
- revision: item.revision,
552
- stage: input.destinationStage,
553
- decisions: input.evaluation.outcome === "accepted" ? input.evaluation.decisions : []
554
- } : { status: "rejected", transitionId: input.ingress.transitionId, itemId: input.workItemId, code, reason };
555
- const ingress = await ops.insertOne("factory_rule_ingress", {
556
- org_id: input.orgId,
557
- factory_project_id: input.factoryProjectId,
558
- identity: input.ingress.identity,
559
- trigger_type: input.ingress.triggerType,
560
- transition_id: input.ingress.transitionId,
561
- result,
562
- created_at: now
563
- });
564
- if (item) {
565
- const evaluation = await ops.insertOne("factory_rule_evaluations", {
566
- ingress_id: ingress.id,
567
- work_item_id: item.id,
568
- rule_set_version: input.ruleSetVersion,
569
- expected_revision: input.expectedRevision,
570
- outcome,
571
- code,
572
- reason,
573
- causal_chain: input.causalChain,
574
- created_at: now
575
- });
576
- if (outcome === "accepted" && input.evaluation.outcome === "accepted") {
577
- for (const [index, decision] of input.evaluation.decisions.entries()) {
578
- await ops.insertOne("factory_deferred_decisions", {
579
- org_id: input.orgId,
580
- factory_project_id: input.factoryProjectId,
581
- evaluation_id: evaluation.id,
582
- work_item_id: item.id,
583
- idempotency_key: String(decision.idempotencyKey),
584
- effect_ordinal: index,
585
- effect_hash: factoryDecisionHash(decision),
586
- causal_chain: input.causalChain,
587
- actor: null,
588
- decision,
589
- status: "pending",
590
- attempts: 0,
591
- available_at: now,
592
- lease_owner: null,
593
- lease_expires_at: null,
594
- last_error: null,
595
- completed_at: null,
596
- created_at: new Date(now.getTime() + index),
597
- updated_at: now
598
- });
599
- }
600
- }
601
- }
602
- return { status: "committed", item, result };
603
- });
604
- try {
605
- return await commit();
606
- } catch (error) {
607
- if (!(error instanceof UniqueViolationError)) throw error;
608
- return commit();
609
- }
610
- }
611
- async commitRuleEvaluation(input) {
612
- const commit = () => this.storage.withTransaction(async (ops) => {
613
- const prior = await ops.findOne("factory_rule_ingress", {
614
- org_id: input.orgId,
615
- factory_project_id: input.factoryProjectId,
616
- identity: input.ingress.identity
617
- });
618
- if (prior) {
619
- const result2 = prior.result;
620
- const decisions2 = Array.isArray(result2.decisions) ? result2.decisions : [];
621
- const evaluation2 = await ops.findOne("factory_rule_evaluations", { ingress_id: prior.id });
622
- for (const decision of decisions2) {
623
- if (!evaluation2 || !decision || typeof decision !== "object" || decision.type !== "upsertLinkedWorkItem" || typeof decision.sourceKey !== "string" || typeof decision.idempotencyKey !== "string") {
624
- continue;
625
- }
626
- const materialization = decision;
627
- const item2 = await ops.findOne("work_items", {
628
- org_id: input.orgId,
629
- factory_project_id: input.factoryProjectId,
630
- source_key: materialization.sourceKey
631
- });
632
- if (item2) continue;
633
- await ops.updateAtomic(
634
- "factory_deferred_decisions",
635
- {
636
- org_id: input.orgId,
637
- factory_project_id: input.factoryProjectId,
638
- evaluation_id: evaluation2.id,
639
- idempotency_key: materialization.idempotencyKey
640
- },
641
- (current) => current.status === "succeeded" ? {
642
- status: "retry",
643
- attempts: 0,
644
- available_at: input.now,
645
- lease_owner: null,
646
- lease_expires_at: null,
647
- last_error: null,
648
- completed_at: null,
649
- updated_at: input.now
650
- } : null
651
- );
652
- }
653
- return { status: "replayed", result: result2 };
654
- }
655
- const itemRow = input.workItemId ? await ops.findOne("work_items", {
656
- id: input.workItemId,
657
- org_id: input.orgId,
658
- factory_project_id: input.factoryProjectId
659
- }) : null;
660
- if (input.workItemId !== null && !itemRow) return { status: "missing" };
661
- const item = itemRow ? toRow(itemRow) : null;
662
- const stale = item !== null && item.revision !== input.expectedRevision;
663
- const outcome = stale ? "rejected" : input.outcome.status;
664
- const code = stale ? "stale" : input.outcome.code ?? null;
665
- const reason = stale ? "The work item changed before this rule evaluation committed." : input.outcome.reason ?? null;
666
- const decisions = outcome === "accepted" ? input.decisions : [];
667
- const result = {
668
- status: outcome,
669
- itemId: item?.id ?? null,
670
- revision: item?.revision ?? null,
671
- code,
672
- reason,
673
- decisions
674
- };
675
- const ingress = await ops.insertOne("factory_rule_ingress", {
676
- org_id: input.orgId,
677
- factory_project_id: input.factoryProjectId,
678
- identity: input.ingress.identity,
679
- trigger_type: input.ingress.triggerType,
680
- transition_id: input.ingress.identity,
681
- result,
682
- created_at: input.now
683
- });
684
- const evaluation = await ops.insertOne("factory_rule_evaluations", {
685
- ingress_id: ingress.id,
686
- work_item_id: item?.id ?? null,
687
- rule_set_version: input.ruleSetVersion,
688
- expected_revision: input.expectedRevision,
689
- outcome,
690
- code,
691
- reason,
692
- causal_chain: input.causalChain,
693
- created_at: input.now
694
- });
695
- for (const [effectOrdinal, decision] of decisions.entries()) {
696
- await ops.insertOne("factory_deferred_decisions", {
697
- org_id: input.orgId,
698
- factory_project_id: input.factoryProjectId,
699
- evaluation_id: evaluation.id,
700
- work_item_id: item?.id ?? null,
701
- idempotency_key: String(decision.idempotencyKey),
702
- effect_ordinal: effectOrdinal,
703
- effect_hash: factoryDecisionHash(decision),
704
- causal_chain: input.causalChain,
705
- actor: input.actor,
706
- decision,
707
- status: "pending",
708
- attempts: 0,
709
- available_at: input.now,
710
- lease_owner: null,
711
- lease_expires_at: null,
712
- last_error: null,
713
- completed_at: null,
714
- created_at: new Date(input.now.getTime() + effectOrdinal),
715
- updated_at: input.now
716
- });
717
- }
718
- return { status: "committed", result };
719
- });
720
- try {
721
- return await commit();
722
- } catch (error) {
723
- if (!(error instanceof UniqueViolationError)) throw error;
724
- return commit();
725
- }
726
- }
727
- async getToolResultCursor(orgId, factoryProjectId, bindingId) {
728
- const row = await this.#db.findOne("factory_tool_result_cursors", {
729
- org_id: orgId,
730
- factory_project_id: factoryProjectId,
731
- binding_id: bindingId
732
- });
733
- return row ? {
734
- bindingId: String(row.binding_id),
735
- orgId: row.org_id,
736
- factoryProjectId: String(row.factory_project_id),
737
- lastMessageId: String(row.last_message_id),
738
- lastMessageCreatedAt: row.last_message_created_at,
739
- updatedAt: row.updated_at
740
- } : null;
741
- }
742
- async advanceToolResultCursor(cursor) {
743
- const current = await this.getToolResultCursor(cursor.orgId, cursor.factoryProjectId, cursor.bindingId);
744
- if (current && current.lastMessageCreatedAt > cursor.lastMessageCreatedAt) return;
745
- await this.#db.upsertOne("factory_tool_result_cursors", ["binding_id"], {
746
- binding_id: cursor.bindingId,
747
- org_id: cursor.orgId,
748
- factory_project_id: cursor.factoryProjectId,
749
- last_message_id: cursor.lastMessageId,
750
- last_message_created_at: cursor.lastMessageCreatedAt,
751
- updated_at: cursor.updatedAt
752
- });
753
- }
754
- async listDeferredDecisions(orgId, factoryProjectId) {
755
- return (await this.#db.findMany(
756
- "factory_deferred_decisions",
757
- { org_id: orgId, factory_project_id: factoryProjectId },
758
- { orderBy: [["created_at", "asc"]] }
759
- )).map(toDeferredDecision);
760
- }
761
- /** Read a bounded newest-first status page without exposing another tenant. */
762
- async listDeferredDecisionPage(input) {
763
- const rows = await this.#db.findMany(
764
- "factory_deferred_decisions",
765
- {
766
- org_id: input.orgId,
767
- factory_project_id: input.factoryProjectId,
768
- ...input.statuses ? { status: { in: input.statuses } } : {}
769
- },
770
- {
771
- orderBy: [
772
- ["created_at", "desc"],
773
- ["id", "desc"]
774
- ],
775
- limit: input.limit + 1,
776
- ...input.before ? { cursor: { values: [input.before.createdAt, input.before.id] } } : {}
777
- }
778
- );
779
- return { decisions: rows.slice(0, input.limit).map(toDeferredDecision), hasMore: rows.length > input.limit };
780
- }
781
- async claimDeferredDecisions(input) {
782
- return this.#claimLeases("factory_deferred_decisions", input, toDeferredDecision);
783
- }
784
- async renewDeferredDecisionLease(identity, leaseExpiresAt) {
785
- return this.#renewLease("factory_deferred_decisions", identity, leaseExpiresAt);
786
- }
787
- async completeDeferredDecision(identity, now) {
788
- const row = await this.#completeLease("factory_deferred_decisions", identity, now);
789
- return row ? toDeferredDecision(row) : null;
790
- }
791
- async failDeferredDecision(input) {
792
- const row = await this.#failLease("factory_deferred_decisions", input);
793
- return row ? toDeferredDecision(row) : null;
794
- }
795
- /** Requeue the same idempotent terminal effect; non-failed decisions are never rerun. */
796
- async retryDeferredDecision(orgId, factoryProjectId, decisionId, now) {
797
- let retried = false;
798
- const row = await this.#db.updateAtomic(
799
- "factory_deferred_decisions",
800
- { id: decisionId, org_id: orgId, factory_project_id: factoryProjectId },
801
- (current) => {
802
- if (current.status !== "failed") return null;
803
- retried = true;
804
- return {
805
- status: "retry",
806
- attempts: 0,
807
- available_at: now,
808
- lease_owner: null,
809
- lease_expires_at: null,
810
- last_error: null,
811
- completed_at: null,
812
- updated_at: now
813
- };
814
- }
815
- );
816
- return retried && row ? toDeferredDecision(row) : null;
817
- }
818
- /** Resolve exact active agent authority; partial session matches never authorize. */
819
- async findActiveRunBinding(address) {
820
- const row = await this.#db.findOne("factory_run_bindings", {
821
- org_id: address.orgId,
822
- factory_project_id: address.factoryProjectId,
823
- thread_id: address.threadId,
824
- resource_id: address.resourceId,
825
- session_id: address.sessionId,
826
- status: "active"
827
- });
828
- return row ? toBinding(row) : null;
829
- }
830
- /** Resolve exact bound-session state for processor awareness; ambiguous cross-tenant matches return null. */
831
- async findRunBindingBySession(address) {
832
- const rows = await this.#db.findMany("factory_run_bindings", {
833
- factory_project_id: address.factoryProjectId,
834
- thread_id: address.threadId,
835
- resource_id: address.resourceId,
836
- session_id: address.sessionId
837
- });
838
- if (new Set(rows.map((row2) => row2.org_id)).size !== 1) return null;
839
- const row = rows.sort((left, right) => {
840
- if (left.status === "active" && right.status !== "active") return -1;
841
- if (right.status === "active" && left.status !== "active") return 1;
842
- return right.created_at.getTime() - left.created_at.getTime();
843
- })[0];
844
- return row ? toBinding(row) : null;
845
- }
846
- /** Revoke one exact tenant-scoped binding. */
847
- async revokeRunBinding(input) {
848
- let revoked = false;
849
- const row = await this.#db.updateAtomic(
850
- "factory_run_bindings",
851
- { id: input.bindingId, org_id: input.orgId, factory_project_id: input.factoryProjectId },
852
- (current) => {
853
- if (current.status !== "active") return null;
854
- revoked = true;
855
- return { status: "revoked", revoked_at: input.revokedAt };
856
- }
857
- );
858
- return revoked && row ? toBinding(row) : null;
859
- }
860
- /** Enumerate active bindings for the server-owned restart reconciler. */
861
- async listActiveRunBindings() {
862
- return (await this.#db.findMany("factory_run_bindings", { status: "active" })).map(toBinding);
863
- }
864
- /** List binding history, optionally narrowed to one work item. */
865
- async listRunBindings(orgId, factoryProjectId, workItemId) {
866
- return (await this.#db.findMany(
867
- "factory_run_bindings",
868
- {
869
- org_id: orgId,
870
- factory_project_id: factoryProjectId,
871
- ...workItemId ? { work_item_id: workItemId } : {}
872
- },
873
- { orderBy: [["created_at", "asc"]] }
874
- )).map(toBinding);
875
- }
876
- async listPendingStarts(orgId, factoryProjectId) {
877
- return (await this.#db.findMany(
878
- "factory_pending_starts",
879
- { org_id: orgId, factory_project_id: factoryProjectId },
880
- { orderBy: [["created_at", "asc"]] }
881
- )).map(toPendingStart);
882
- }
883
- async claimPendingStarts(input) {
884
- return this.#claimLeases("factory_pending_starts", input, toPendingStart);
885
- }
886
- async renewPendingStartLease(identity, leaseExpiresAt) {
887
- return this.#renewLease("factory_pending_starts", identity, leaseExpiresAt);
888
- }
889
- async completePendingStart(identity, now) {
890
- const row = await this.#completeLease("factory_pending_starts", identity, now);
891
- return row ? toPendingStart(row) : null;
892
- }
893
- async failPendingStart(input) {
894
- const row = await this.#failLease("factory_pending_starts", input);
895
- return row ? toPendingStart(row) : null;
896
- }
897
- async prepareRunStart(input) {
898
- const prepare = () => this.storage.withTransaction(async (ops) => {
899
- const prior = await ops.findOne("factory_pending_starts", {
900
- org_id: input.orgId,
901
- factory_project_id: input.factoryProjectId,
902
- kickoff_key: input.kickoffKey
903
- });
904
- if (prior) {
905
- const bindingRow2 = await ops.findOne("factory_run_bindings", {
906
- id: String(prior.binding_id),
907
- org_id: input.orgId,
908
- factory_project_id: input.factoryProjectId
909
- });
910
- const itemRow = bindingRow2 && await ops.findOne("work_items", {
911
- id: String(bindingRow2.work_item_id),
912
- org_id: input.orgId,
913
- factory_project_id: input.factoryProjectId
914
- });
915
- if (!bindingRow2 || !itemRow) throw new Error("Factory start replay references missing state.");
916
- return {
917
- item: toRow(itemRow),
918
- binding: toBinding(bindingRow2),
919
- pendingStart: toPendingStart(prior),
920
- replayed: true
921
- };
922
- }
923
- const now = /* @__PURE__ */ new Date();
924
- const create = input.workItem.input;
925
- let row = input.workItem.id ? await ops.findOne("work_items", {
926
- id: input.workItem.id,
927
- org_id: input.orgId,
928
- factory_project_id: input.factoryProjectId
929
- }) : sourceKey(create.externalSource) ? await ops.findOne("work_items", {
930
- org_id: input.orgId,
931
- factory_project_id: input.factoryProjectId,
932
- source_key: sourceKey(create.externalSource)
933
- }) : null;
934
- let item;
935
- if (row) {
936
- row = await ops.updateAtomic("work_items", { id: row.id }, (current) => {
937
- const roles = /* @__PURE__ */ new Set([...Object.keys(current.sessions), input.role]);
938
- const sessions = Object.fromEntries([...roles].map((role) => [role, input.session]));
939
- return applyUpdate({ current, userId: input.userId, input: { sessions } });
940
- });
941
- item = toRow(row);
942
- } else {
943
- if (create.parentWorkItemId)
944
- validateParentRelation(
945
- (await ops.findMany("work_items", {
946
- org_id: input.orgId,
947
- factory_project_id: input.factoryProjectId
948
- })).map(toRow),
949
- void 0,
950
- create.parentWorkItemId
951
- );
952
- row = await ops.insertOne("work_items", {
953
- org_id: input.orgId,
954
- created_by: input.userId,
955
- factory_project_id: input.factoryProjectId,
956
- external_source: create.externalSource ?? null,
957
- source_key: sourceKey(create.externalSource),
958
- parent_work_item_id: create.parentWorkItemId ?? null,
959
- title: create.title,
960
- stages: create.stages ?? [],
961
- stage_history: applyStageTransition([], [], create.stages ?? [], input.userId, now),
962
- sessions: stampSessions({ [input.role]: input.session }, input.userId),
963
- metadata: create.metadata ?? null,
964
- revision: 1,
965
- created_at: now,
966
- updated_at: now
967
- });
968
- item = toRow(row);
969
- }
970
- await ops.updateMany(
971
- "factory_run_bindings",
972
- {
973
- org_id: input.orgId,
974
- factory_project_id: input.factoryProjectId,
975
- work_item_id: item.id,
976
- role: input.role,
977
- status: "active"
978
- },
979
- { status: "revoked", revoked_at: now }
980
- );
981
- const bindingRow = await ops.insertOne("factory_run_bindings", {
982
- org_id: input.orgId,
983
- factory_project_id: input.factoryProjectId,
984
- work_item_id: item.id,
985
- role: input.role,
986
- thread_id: input.session.threadId,
987
- resource_id: input.resourceId,
988
- session_id: input.session.sessionId,
989
- branch: input.session.branch,
990
- status: "active",
991
- created_at: now,
992
- revoked_at: null
993
- });
994
- const pendingRow = await ops.insertOne("factory_pending_starts", {
995
- org_id: input.orgId,
996
- factory_project_id: input.factoryProjectId,
997
- binding_id: bindingRow.id,
998
- kickoff_key: input.kickoffKey,
999
- message: input.kickoffMessage,
1000
- status: "pending",
1001
- attempts: 0,
1002
- available_at: now,
1003
- lease_owner: null,
1004
- lease_expires_at: null,
1005
- last_error: null,
1006
- completed_at: null,
1007
- created_at: now,
1008
- updated_at: now
1009
- });
1010
- return { item, binding: toBinding(bindingRow), pendingStart: toPendingStart(pendingRow), replayed: false };
1011
- });
1012
- try {
1013
- return await prepare();
1014
- } catch (error) {
1015
- if (!(error instanceof UniqueViolationError)) throw error;
1016
- return prepare();
1017
- }
1018
- }
1019
- async markPendingStart(bindingId, status, lastError) {
1020
- const row = await this.#db.updateAtomic(
1021
- "factory_pending_starts",
1022
- { binding_id: bindingId },
1023
- () => ({ status, last_error: lastError ?? null, updated_at: /* @__PURE__ */ new Date() })
1024
- );
1025
- return row ? toPendingStart(row) : null;
1026
- }
1027
- /**
1028
- * Create a work item, reusing the existing record when `sourceKey` already
1029
- * has one for the project (acting twice on the same issue must not duplicate
1030
- * the card). On reuse the provided stages replace the current ones (with the
1031
- * transition recorded in history) and sessions/metadata are merged in. The
1032
- * result discriminates insert from reuse so callers can audit the actual
1033
- * outcome.
1034
- */
1035
- async upsert(params) {
1036
- const run = (ops) => this.#upsert(params, ops);
1037
- const execute = () => params.input.parentWorkItemId ? this.#withProjectRelationTransaction(params.orgId, params.factoryProjectId, run) : run(this.#db);
1038
- try {
1039
- return await execute();
1040
- } catch (error) {
1041
- if (!(error instanceof UniqueViolationError)) throw error;
1042
- return execute();
1043
- }
1044
- }
1045
- async #upsert({
1046
- orgId,
1047
- userId,
1048
- factoryProjectId,
1049
- input,
1050
- reuseMode = "update"
1051
- }, ops) {
1052
- const key = sourceKey(input.externalSource);
1053
- const reuse = async () => {
1054
- if (!key) return null;
1055
- const existing = await ops.findOne("work_items", {
1056
- org_id: orgId,
1057
- factory_project_id: factoryProjectId,
1058
- source_key: key
1059
- });
1060
- if (!existing) return null;
1061
- if (reuseMode === "preserve") {
1062
- const item = toWorkItem(existing);
1063
- return { created: false, item, previous: priorState(existing) };
1064
- }
1065
- let previous = emptyPrior();
1066
- const updated = await ops.updateAtomic(
1067
- "work_items",
1068
- { org_id: orgId, factory_project_id: factoryProjectId, source_key: key },
1069
- async (current) => {
1070
- previous = priorState(current);
1071
- const fullPatch = input.parentWorkItemId === null ? { ...input, parentWorkItemId: void 0 } : input;
1072
- const patch = reuseMode === "non-stage" ? {
1073
- title: input.title,
1074
- parentWorkItemId: input.parentWorkItemId ?? void 0,
1075
- metadata: input.metadata
1076
- } : fullPatch;
1077
- if (patch.parentWorkItemId !== void 0) {
1078
- validateParentRelation(
1079
- await this.#listWithOps(ops, orgId, factoryProjectId),
1080
- current.id,
1081
- patch.parentWorkItemId
1082
- );
1083
- }
1084
- return {
1085
- external_source: input.externalSource ?? null,
1086
- ...applyUpdate({ current, userId, input: patch })
1087
- };
1088
- }
1089
- );
1090
- return updated ? { item: toWorkItem(updated), created: false, previous } : null;
1091
- };
1092
- const reused = await reuse();
1093
- if (reused) return reused;
1094
- const now = /* @__PURE__ */ new Date();
1095
- const stages = input.stages ?? ["intake"];
1096
- validateParentRelation(
1097
- await this.#listWithOps(ops, orgId, factoryProjectId),
1098
- void 0,
1099
- input.parentWorkItemId ?? null
1100
- );
1101
- const row = await ops.insertOne("work_items", {
1102
- org_id: orgId,
1103
- factory_project_id: factoryProjectId,
1104
- external_source: input.externalSource ?? null,
1105
- source_key: key,
1106
- parent_work_item_id: input.parentWorkItemId ?? null,
1107
- title: input.title,
1108
- stages,
1109
- stage_history: stages.map((stage) => ({ stage, enteredAt: now.toISOString(), by: userId })),
1110
- sessions: stampSessions(input.sessions ?? {}, userId),
1111
- metadata: input.metadata ?? null,
1112
- revision: 1,
1113
- created_by: userId,
1114
- created_at: now,
1115
- updated_at: now
1116
- });
1117
- return { item: toWorkItem(row), created: true, previous: emptyPrior() };
1118
- }
1119
- async update({
1120
- orgId,
1121
- id,
1122
- userId,
1123
- patch
1124
- }) {
1125
- const run = async (ops) => {
1126
- let previous = emptyPrior();
1127
- const row = await ops.updateAtomic("work_items", { org_id: orgId, id }, async (current) => {
1128
- previous = priorState(current);
1129
- if (patch.parentWorkItemId !== void 0) {
1130
- validateParentRelation(
1131
- await this.#listWithOps(ops, orgId, current.factory_project_id),
1132
- current.id,
1133
- patch.parentWorkItemId
1134
- );
1135
- }
1136
- return applyUpdate({ current, userId, input: patch });
1137
- });
1138
- return row ? { item: toWorkItem(row), previous } : null;
1139
- };
1140
- if (patch.parentWorkItemId === void 0) return run(this.#db);
1141
- const candidate = await this.#db.findOne("work_items", { org_id: orgId, id });
1142
- if (!candidate) return null;
1143
- return this.#withProjectRelationTransaction(orgId, candidate.factory_project_id, run);
1144
- }
1145
- async delete({ orgId, id }) {
1146
- const candidate = await this.#db.findOne("work_items", { org_id: orgId, id });
1147
- if (!candidate) return null;
1148
- return this.#withProjectRelationTransaction(orgId, candidate.factory_project_id, async (ops) => {
1149
- const existing = await ops.findOne("work_items", { org_id: orgId, id });
1150
- if (!existing) return null;
1151
- const deleted = await ops.deleteMany("work_items", { org_id: orgId, id });
1152
- if (deleted === 0) return null;
1153
- await ops.updateMany(
1154
- "work_items",
1155
- { org_id: orgId, parent_work_item_id: id },
1156
- { parent_work_item_id: null, updated_at: /* @__PURE__ */ new Date() }
1157
- );
1158
- return toWorkItem(existing);
1159
- });
1160
- }
1161
- };
1162
- export {
1163
- AUTOMATION_ACTORS,
1164
- WORK_ITEMS_SCHEMA,
1165
- WorkItemRelationError,
1166
- WorkItemsStorage,
1167
- applyStageTransition,
1168
- factoryDecisionHash,
1169
- isAutomationActor,
1170
- stampSessions,
1171
- validateParentRelation
484
+ constructor() {
485
+ super("work-items");
486
+ }
487
+ async init() {
488
+ await this.ensureCollections([WORK_ITEMS_SCHEMA, ...FACTORY_GOVERNANCE_SCHEMAS]);
489
+ }
490
+ async dangerouslyClearAll() {
491
+ await this.ops.deleteMany("work_items", {});
492
+ }
493
+ get #db() {
494
+ return this.ops;
495
+ }
496
+ async #withProjectRelationTransaction(orgId, factoryProjectId, fn) {
497
+ return withInProcessProjectLock(`work-items:${orgId}:${factoryProjectId}`, () => this.storage.withTransaction(fn, { isolationLevel: "serializable" }));
498
+ }
499
+ async #claimLeases(table, input, map) {
500
+ const claim = () => this.storage.withTransaction(async (ops) => {
501
+ const candidates = await ops.findMany(table, { status: { in: [
502
+ "pending",
503
+ "retry",
504
+ "leased"
505
+ ] } }, {
506
+ orderBy: [["created_at", "asc"]],
507
+ limit: Math.max(input.limit * 5, 50)
508
+ });
509
+ const claimed = [];
510
+ for (const candidate of candidates) {
511
+ if (claimed.length >= input.limit) break;
512
+ const availableAt = new Date(candidate.available_at).getTime();
513
+ const leaseExpiresAt = candidate.lease_expires_at ? new Date(candidate.lease_expires_at).getTime() : 0;
514
+ const claimable = (candidate.status === "pending" || candidate.status === "retry") && availableAt <= input.now.getTime();
515
+ const expired = candidate.status === "leased" && leaseExpiresAt <= input.now.getTime();
516
+ if (!claimable && !expired) continue;
517
+ let didClaim = false;
518
+ const row = await ops.updateAtomic(table, { id: candidate.id }, (current) => {
519
+ const currentAvailable = new Date(current.available_at).getTime();
520
+ const currentExpiry = current.lease_expires_at ? new Date(current.lease_expires_at).getTime() : 0;
521
+ const currentClaimable = (current.status === "pending" || current.status === "retry") && currentAvailable <= input.now.getTime();
522
+ const currentExpired = current.status === "leased" && currentExpiry <= input.now.getTime();
523
+ if (!currentClaimable && !currentExpired) return null;
524
+ didClaim = true;
525
+ return {
526
+ status: "leased",
527
+ attempts: Number(current.attempts) + 1,
528
+ lease_owner: input.ownerId,
529
+ lease_expires_at: input.leaseExpiresAt,
530
+ updated_at: input.now
531
+ };
532
+ });
533
+ if (didClaim && row) claimed.push(map(row));
534
+ }
535
+ return claimed;
536
+ });
537
+ return claim();
538
+ }
539
+ async #renewLease(table, identity, leaseExpiresAt) {
540
+ let renewed = false;
541
+ await this.#db.updateAtomic(table, {
542
+ id: identity.id,
543
+ org_id: identity.orgId,
544
+ factory_project_id: identity.factoryProjectId
545
+ }, (current) => {
546
+ if (current.status !== "leased" || current.lease_owner !== identity.ownerId) return null;
547
+ renewed = true;
548
+ return {
549
+ lease_expires_at: leaseExpiresAt,
550
+ updated_at: /* @__PURE__ */ new Date()
551
+ };
552
+ });
553
+ return renewed;
554
+ }
555
+ async #completeLease(table, identity, now) {
556
+ let completed = false;
557
+ const row = await this.#db.updateAtomic(table, {
558
+ id: identity.id,
559
+ org_id: identity.orgId,
560
+ factory_project_id: identity.factoryProjectId
561
+ }, (current) => {
562
+ if (current.status !== "leased" || current.lease_owner !== identity.ownerId) return null;
563
+ completed = true;
564
+ return {
565
+ status: table === "factory_pending_starts" ? "sent" : "succeeded",
566
+ lease_owner: null,
567
+ lease_expires_at: null,
568
+ completed_at: now,
569
+ updated_at: now
570
+ };
571
+ });
572
+ return completed ? row : null;
573
+ }
574
+ async #failLease(table, input) {
575
+ let failed = false;
576
+ const row = await this.#db.updateAtomic(table, {
577
+ id: input.id,
578
+ org_id: input.orgId,
579
+ factory_project_id: input.factoryProjectId
580
+ }, (current) => {
581
+ if (current.status !== "leased" || current.lease_owner !== input.ownerId) return null;
582
+ failed = true;
583
+ return {
584
+ status: input.terminal ? "failed" : "retry",
585
+ available_at: input.availableAt,
586
+ lease_owner: null,
587
+ lease_expires_at: null,
588
+ last_error: input.lastError,
589
+ completed_at: input.terminal ? input.now : null,
590
+ updated_at: input.now
591
+ };
592
+ });
593
+ return failed ? row : null;
594
+ }
595
+ async #listWithOps(ops, orgId, factoryProjectId) {
596
+ return (await ops.findMany("work_items", {
597
+ org_id: orgId,
598
+ factory_project_id: factoryProjectId
599
+ }, { orderBy: [["updated_at", "desc"]] })).map(toWorkItem);
600
+ }
601
+ /** List the org's work items for a project, newest first. */
602
+ async list({ orgId, factoryProjectId }) {
603
+ return this.#listWithOps(this.#db, orgId, factoryProjectId);
604
+ }
605
+ async get({ orgId, id }) {
606
+ const row = await this.#db.findOne("work_items", {
607
+ org_id: orgId,
608
+ id
609
+ });
610
+ return row ? toWorkItem(row) : null;
611
+ }
612
+ async getForProject(orgId, factoryProjectId, id) {
613
+ const row = await this.#db.findOne("work_items", {
614
+ id,
615
+ org_id: orgId,
616
+ factory_project_id: factoryProjectId
617
+ });
618
+ return row ? toRow(row) : null;
619
+ }
620
+ async getTransitionResultByIngress(orgId, factoryProjectId, identity) {
621
+ return (await this.#db.findOne("factory_rule_ingress", {
622
+ org_id: orgId,
623
+ factory_project_id: factoryProjectId,
624
+ identity
625
+ }))?.result ?? null;
626
+ }
627
+ async commitTransition(input) {
628
+ const commit = () => this.storage.withTransaction(async (ops) => {
629
+ const prior = await ops.findOne("factory_rule_ingress", {
630
+ org_id: input.orgId,
631
+ factory_project_id: input.factoryProjectId,
632
+ identity: input.ingress.identity
633
+ });
634
+ if (prior) return {
635
+ status: "replayed",
636
+ item: await this.getForProject(input.orgId, input.factoryProjectId, input.workItemId),
637
+ result: prior.result
638
+ };
639
+ const now = /* @__PURE__ */ new Date();
640
+ let item = null;
641
+ let code = null;
642
+ let reason = null;
643
+ let result;
644
+ const updated = await ops.updateAtomic("work_items", {
645
+ id: input.workItemId,
646
+ org_id: input.orgId,
647
+ factory_project_id: input.factoryProjectId
648
+ }, (row) => {
649
+ const existing = toRow(row);
650
+ item = existing;
651
+ if (existing.revision !== input.expectedRevision) {
652
+ code = "stale";
653
+ reason = "The work item changed before this transition committed.";
654
+ return null;
655
+ }
656
+ if (input.evaluation.outcome === "rejected") {
657
+ code = input.evaluation.code;
658
+ reason = input.evaluation.reason;
659
+ return null;
660
+ }
661
+ if (existing.stages.length === 1 && existing.stages[0] === input.destinationStage) return null;
662
+ return patchColumns({
663
+ stages: [input.destinationStage],
664
+ stageHistory: applyStageTransition(existing.stageHistory, existing.stages, [input.destinationStage], input.actorId, now),
665
+ revision: existing.revision + 1,
666
+ updatedAt: now
667
+ });
668
+ });
669
+ if (updated) item = toRow(updated);
670
+ if (!item) {
671
+ code = input.evaluation.outcome === "rejected" ? input.evaluation.code : "invalid_transition";
672
+ reason = input.evaluation.outcome === "rejected" ? input.evaluation.reason : "Work item not found.";
673
+ }
674
+ const outcome = item && code === null && input.evaluation.outcome === "accepted" ? "accepted" : "rejected";
675
+ result = outcome === "accepted" ? {
676
+ status: "accepted",
677
+ transitionId: input.ingress.transitionId,
678
+ itemId: item.id,
679
+ revision: item.revision,
680
+ stage: input.destinationStage,
681
+ decisions: input.evaluation.outcome === "accepted" ? input.evaluation.decisions : []
682
+ } : {
683
+ status: "rejected",
684
+ transitionId: input.ingress.transitionId,
685
+ itemId: input.workItemId,
686
+ code,
687
+ reason
688
+ };
689
+ const ingress = await ops.insertOne("factory_rule_ingress", {
690
+ org_id: input.orgId,
691
+ factory_project_id: input.factoryProjectId,
692
+ identity: input.ingress.identity,
693
+ trigger_type: input.ingress.triggerType,
694
+ transition_id: input.ingress.transitionId,
695
+ result,
696
+ created_at: now
697
+ });
698
+ if (item) {
699
+ const evaluation = await ops.insertOne("factory_rule_evaluations", {
700
+ ingress_id: ingress.id,
701
+ work_item_id: item.id,
702
+ rule_set_version: input.ruleSetVersion,
703
+ expected_revision: input.expectedRevision,
704
+ outcome,
705
+ code,
706
+ reason,
707
+ causal_chain: input.causalChain,
708
+ created_at: now
709
+ });
710
+ if (outcome === "accepted" && input.evaluation.outcome === "accepted") for (const [index, decision] of input.evaluation.decisions.entries()) await ops.insertOne("factory_deferred_decisions", {
711
+ org_id: input.orgId,
712
+ factory_project_id: input.factoryProjectId,
713
+ evaluation_id: evaluation.id,
714
+ work_item_id: item.id,
715
+ idempotency_key: String(decision.idempotencyKey),
716
+ effect_ordinal: index,
717
+ effect_hash: factoryDecisionHash(decision),
718
+ causal_chain: input.causalChain,
719
+ actor: null,
720
+ decision,
721
+ status: "pending",
722
+ attempts: 0,
723
+ available_at: now,
724
+ lease_owner: null,
725
+ lease_expires_at: null,
726
+ last_error: null,
727
+ completed_at: null,
728
+ created_at: new Date(now.getTime() + index),
729
+ updated_at: now
730
+ });
731
+ }
732
+ return {
733
+ status: "committed",
734
+ item,
735
+ result
736
+ };
737
+ });
738
+ try {
739
+ return await commit();
740
+ } catch (error) {
741
+ if (!(error instanceof UniqueViolationError)) throw error;
742
+ return commit();
743
+ }
744
+ }
745
+ async commitRuleEvaluation(input) {
746
+ const commit = () => this.storage.withTransaction(async (ops) => {
747
+ const prior = await ops.findOne("factory_rule_ingress", {
748
+ org_id: input.orgId,
749
+ factory_project_id: input.factoryProjectId,
750
+ identity: input.ingress.identity
751
+ });
752
+ if (prior) {
753
+ const result = prior.result;
754
+ const decisions = Array.isArray(result.decisions) ? result.decisions : [];
755
+ const evaluation = await ops.findOne("factory_rule_evaluations", { ingress_id: prior.id });
756
+ for (const decision of decisions) {
757
+ if (!evaluation || !decision || typeof decision !== "object" || decision.type !== "upsertLinkedWorkItem" || typeof decision.sourceKey !== "string" || typeof decision.idempotencyKey !== "string") continue;
758
+ const materialization = decision;
759
+ if (await ops.findOne("work_items", {
760
+ org_id: input.orgId,
761
+ factory_project_id: input.factoryProjectId,
762
+ source_key: materialization.sourceKey
763
+ })) continue;
764
+ await ops.updateAtomic("factory_deferred_decisions", {
765
+ org_id: input.orgId,
766
+ factory_project_id: input.factoryProjectId,
767
+ evaluation_id: evaluation.id,
768
+ idempotency_key: materialization.idempotencyKey
769
+ }, (current) => current.status === "succeeded" ? {
770
+ status: "retry",
771
+ attempts: 0,
772
+ available_at: input.now,
773
+ lease_owner: null,
774
+ lease_expires_at: null,
775
+ last_error: null,
776
+ completed_at: null,
777
+ updated_at: input.now
778
+ } : null);
779
+ }
780
+ return {
781
+ status: "replayed",
782
+ result
783
+ };
784
+ }
785
+ const itemRow = input.workItemId ? await ops.findOne("work_items", {
786
+ id: input.workItemId,
787
+ org_id: input.orgId,
788
+ factory_project_id: input.factoryProjectId
789
+ }) : null;
790
+ if (input.workItemId !== null && !itemRow) return { status: "missing" };
791
+ const item = itemRow ? toRow(itemRow) : null;
792
+ const stale = item !== null && item.revision !== input.expectedRevision;
793
+ const outcome = stale ? "rejected" : input.outcome.status;
794
+ const code = stale ? "stale" : input.outcome.code ?? null;
795
+ const reason = stale ? "The work item changed before this rule evaluation committed." : input.outcome.reason ?? null;
796
+ const decisions = outcome === "accepted" ? input.decisions : [];
797
+ const result = {
798
+ status: outcome,
799
+ itemId: item?.id ?? null,
800
+ revision: item?.revision ?? null,
801
+ code,
802
+ reason,
803
+ decisions
804
+ };
805
+ const ingress = await ops.insertOne("factory_rule_ingress", {
806
+ org_id: input.orgId,
807
+ factory_project_id: input.factoryProjectId,
808
+ identity: input.ingress.identity,
809
+ trigger_type: input.ingress.triggerType,
810
+ transition_id: input.ingress.identity,
811
+ result,
812
+ created_at: input.now
813
+ });
814
+ const evaluation = await ops.insertOne("factory_rule_evaluations", {
815
+ ingress_id: ingress.id,
816
+ work_item_id: item?.id ?? null,
817
+ rule_set_version: input.ruleSetVersion,
818
+ expected_revision: input.expectedRevision,
819
+ outcome,
820
+ code,
821
+ reason,
822
+ causal_chain: input.causalChain,
823
+ created_at: input.now
824
+ });
825
+ for (const [effectOrdinal, decision] of decisions.entries()) await ops.insertOne("factory_deferred_decisions", {
826
+ org_id: input.orgId,
827
+ factory_project_id: input.factoryProjectId,
828
+ evaluation_id: evaluation.id,
829
+ work_item_id: item?.id ?? null,
830
+ idempotency_key: String(decision.idempotencyKey),
831
+ effect_ordinal: effectOrdinal,
832
+ effect_hash: factoryDecisionHash(decision),
833
+ causal_chain: input.causalChain,
834
+ actor: input.actor,
835
+ decision,
836
+ status: "pending",
837
+ attempts: 0,
838
+ available_at: input.now,
839
+ lease_owner: null,
840
+ lease_expires_at: null,
841
+ last_error: null,
842
+ completed_at: null,
843
+ created_at: new Date(input.now.getTime() + effectOrdinal),
844
+ updated_at: input.now
845
+ });
846
+ return {
847
+ status: "committed",
848
+ result
849
+ };
850
+ });
851
+ try {
852
+ return await commit();
853
+ } catch (error) {
854
+ if (!(error instanceof UniqueViolationError)) throw error;
855
+ return commit();
856
+ }
857
+ }
858
+ async getToolResultCursor(orgId, factoryProjectId, bindingId) {
859
+ const row = await this.#db.findOne("factory_tool_result_cursors", {
860
+ org_id: orgId,
861
+ factory_project_id: factoryProjectId,
862
+ binding_id: bindingId
863
+ });
864
+ return row ? {
865
+ bindingId: String(row.binding_id),
866
+ orgId: row.org_id,
867
+ factoryProjectId: String(row.factory_project_id),
868
+ lastMessageId: String(row.last_message_id),
869
+ lastMessageCreatedAt: row.last_message_created_at,
870
+ updatedAt: row.updated_at
871
+ } : null;
872
+ }
873
+ async advanceToolResultCursor(cursor) {
874
+ const current = await this.getToolResultCursor(cursor.orgId, cursor.factoryProjectId, cursor.bindingId);
875
+ if (current && current.lastMessageCreatedAt > cursor.lastMessageCreatedAt) return;
876
+ await this.#db.upsertOne("factory_tool_result_cursors", ["binding_id"], {
877
+ binding_id: cursor.bindingId,
878
+ org_id: cursor.orgId,
879
+ factory_project_id: cursor.factoryProjectId,
880
+ last_message_id: cursor.lastMessageId,
881
+ last_message_created_at: cursor.lastMessageCreatedAt,
882
+ updated_at: cursor.updatedAt
883
+ });
884
+ }
885
+ async listDeferredDecisions(orgId, factoryProjectId) {
886
+ return (await this.#db.findMany("factory_deferred_decisions", {
887
+ org_id: orgId,
888
+ factory_project_id: factoryProjectId
889
+ }, { orderBy: [["created_at", "asc"]] })).map(toDeferredDecision);
890
+ }
891
+ /** Read a bounded newest-first status page without exposing another tenant. */
892
+ async listDeferredDecisionPage(input) {
893
+ const rows = await this.#db.findMany("factory_deferred_decisions", {
894
+ org_id: input.orgId,
895
+ factory_project_id: input.factoryProjectId,
896
+ ...input.statuses ? { status: { in: input.statuses } } : {}
897
+ }, {
898
+ orderBy: [["created_at", "desc"], ["id", "desc"]],
899
+ limit: input.limit + 1,
900
+ ...input.before ? { cursor: { values: [input.before.createdAt, input.before.id] } } : {}
901
+ });
902
+ return {
903
+ decisions: rows.slice(0, input.limit).map(toDeferredDecision),
904
+ hasMore: rows.length > input.limit
905
+ };
906
+ }
907
+ async claimDeferredDecisions(input) {
908
+ return this.#claimLeases("factory_deferred_decisions", input, toDeferredDecision);
909
+ }
910
+ async renewDeferredDecisionLease(identity, leaseExpiresAt) {
911
+ return this.#renewLease("factory_deferred_decisions", identity, leaseExpiresAt);
912
+ }
913
+ async completeDeferredDecision(identity, now) {
914
+ const row = await this.#completeLease("factory_deferred_decisions", identity, now);
915
+ return row ? toDeferredDecision(row) : null;
916
+ }
917
+ async failDeferredDecision(input) {
918
+ const row = await this.#failLease("factory_deferred_decisions", input);
919
+ return row ? toDeferredDecision(row) : null;
920
+ }
921
+ /** Requeue the same idempotent terminal effect; non-failed decisions are never rerun. */
922
+ async retryDeferredDecision(orgId, factoryProjectId, decisionId, now) {
923
+ let retried = false;
924
+ const row = await this.#db.updateAtomic("factory_deferred_decisions", {
925
+ id: decisionId,
926
+ org_id: orgId,
927
+ factory_project_id: factoryProjectId
928
+ }, (current) => {
929
+ if (current.status !== "failed") return null;
930
+ retried = true;
931
+ return {
932
+ status: "retry",
933
+ attempts: 0,
934
+ available_at: now,
935
+ lease_owner: null,
936
+ lease_expires_at: null,
937
+ last_error: null,
938
+ completed_at: null,
939
+ updated_at: now
940
+ };
941
+ });
942
+ return retried && row ? toDeferredDecision(row) : null;
943
+ }
944
+ /** Resolve exact active agent authority; partial session matches never authorize. */
945
+ async findActiveRunBinding(address) {
946
+ const row = await this.#db.findOne("factory_run_bindings", {
947
+ org_id: address.orgId,
948
+ factory_project_id: address.factoryProjectId,
949
+ thread_id: address.threadId,
950
+ resource_id: address.resourceId,
951
+ session_id: address.sessionId,
952
+ status: "active"
953
+ });
954
+ return row ? toBinding(row) : null;
955
+ }
956
+ /** Resolve exact bound-session state for processor awareness; ambiguous cross-tenant matches return null. */
957
+ async findRunBindingBySession(address) {
958
+ const rows = await this.#db.findMany("factory_run_bindings", {
959
+ factory_project_id: address.factoryProjectId,
960
+ thread_id: address.threadId,
961
+ resource_id: address.resourceId,
962
+ session_id: address.sessionId
963
+ });
964
+ if (new Set(rows.map((row) => row.org_id)).size !== 1) return null;
965
+ const row = rows.sort((left, right) => {
966
+ if (left.status === "active" && right.status !== "active") return -1;
967
+ if (right.status === "active" && left.status !== "active") return 1;
968
+ return right.created_at.getTime() - left.created_at.getTime();
969
+ })[0];
970
+ return row ? toBinding(row) : null;
971
+ }
972
+ /** Revoke one exact tenant-scoped binding. */
973
+ async revokeRunBinding(input) {
974
+ let revoked = false;
975
+ const row = await this.#db.updateAtomic("factory_run_bindings", {
976
+ id: input.bindingId,
977
+ org_id: input.orgId,
978
+ factory_project_id: input.factoryProjectId
979
+ }, (current) => {
980
+ if (current.status !== "active") return null;
981
+ revoked = true;
982
+ return {
983
+ status: "revoked",
984
+ revoked_at: input.revokedAt
985
+ };
986
+ });
987
+ return revoked && row ? toBinding(row) : null;
988
+ }
989
+ /** Enumerate active bindings for the server-owned restart reconciler. */
990
+ async listActiveRunBindings() {
991
+ return (await this.#db.findMany("factory_run_bindings", { status: "active" })).map(toBinding);
992
+ }
993
+ /** List binding history, optionally narrowed to one work item. */
994
+ async listRunBindings(orgId, factoryProjectId, workItemId) {
995
+ return (await this.#db.findMany("factory_run_bindings", {
996
+ org_id: orgId,
997
+ factory_project_id: factoryProjectId,
998
+ ...workItemId ? { work_item_id: workItemId } : {}
999
+ }, { orderBy: [["created_at", "asc"]] })).map(toBinding);
1000
+ }
1001
+ async listPendingStarts(orgId, factoryProjectId) {
1002
+ return (await this.#db.findMany("factory_pending_starts", {
1003
+ org_id: orgId,
1004
+ factory_project_id: factoryProjectId
1005
+ }, { orderBy: [["created_at", "asc"]] })).map(toPendingStart);
1006
+ }
1007
+ async claimPendingStarts(input) {
1008
+ return this.#claimLeases("factory_pending_starts", input, toPendingStart);
1009
+ }
1010
+ async renewPendingStartLease(identity, leaseExpiresAt) {
1011
+ return this.#renewLease("factory_pending_starts", identity, leaseExpiresAt);
1012
+ }
1013
+ async completePendingStart(identity, now) {
1014
+ const row = await this.#completeLease("factory_pending_starts", identity, now);
1015
+ return row ? toPendingStart(row) : null;
1016
+ }
1017
+ async failPendingStart(input) {
1018
+ const row = await this.#failLease("factory_pending_starts", input);
1019
+ return row ? toPendingStart(row) : null;
1020
+ }
1021
+ async prepareRunStart(input) {
1022
+ const prepare = () => this.storage.withTransaction(async (ops) => {
1023
+ const prior = await ops.findOne("factory_pending_starts", {
1024
+ org_id: input.orgId,
1025
+ factory_project_id: input.factoryProjectId,
1026
+ kickoff_key: input.kickoffKey
1027
+ });
1028
+ if (prior) {
1029
+ const bindingRow = await ops.findOne("factory_run_bindings", {
1030
+ id: String(prior.binding_id),
1031
+ org_id: input.orgId,
1032
+ factory_project_id: input.factoryProjectId
1033
+ });
1034
+ const itemRow = bindingRow && await ops.findOne("work_items", {
1035
+ id: String(bindingRow.work_item_id),
1036
+ org_id: input.orgId,
1037
+ factory_project_id: input.factoryProjectId
1038
+ });
1039
+ if (!bindingRow || !itemRow) throw new Error("Factory start replay references missing state.");
1040
+ return {
1041
+ item: toRow(itemRow),
1042
+ binding: toBinding(bindingRow),
1043
+ pendingStart: toPendingStart(prior),
1044
+ replayed: true
1045
+ };
1046
+ }
1047
+ const now = /* @__PURE__ */ new Date();
1048
+ const create = input.workItem.input;
1049
+ let row = input.workItem.id ? await ops.findOne("work_items", {
1050
+ id: input.workItem.id,
1051
+ org_id: input.orgId,
1052
+ factory_project_id: input.factoryProjectId
1053
+ }) : sourceKey(create.externalSource) ? await ops.findOne("work_items", {
1054
+ org_id: input.orgId,
1055
+ factory_project_id: input.factoryProjectId,
1056
+ source_key: sourceKey(create.externalSource)
1057
+ }) : null;
1058
+ let item;
1059
+ if (row) {
1060
+ row = await ops.updateAtomic("work_items", { id: row.id }, (current) => {
1061
+ const roles = /* @__PURE__ */ new Set([...Object.keys(current.sessions), input.role]);
1062
+ const sessions = Object.fromEntries([...roles].map((role) => [role, input.session]));
1063
+ return applyUpdate({
1064
+ current,
1065
+ userId: input.userId,
1066
+ input: { sessions }
1067
+ });
1068
+ });
1069
+ item = toRow(row);
1070
+ } else {
1071
+ if (create.parentWorkItemId) validateParentRelation((await ops.findMany("work_items", {
1072
+ org_id: input.orgId,
1073
+ factory_project_id: input.factoryProjectId
1074
+ })).map(toRow), void 0, create.parentWorkItemId);
1075
+ row = await ops.insertOne("work_items", {
1076
+ org_id: input.orgId,
1077
+ created_by: input.userId,
1078
+ factory_project_id: input.factoryProjectId,
1079
+ external_source: create.externalSource ?? null,
1080
+ source_key: sourceKey(create.externalSource),
1081
+ parent_work_item_id: create.parentWorkItemId ?? null,
1082
+ title: create.title,
1083
+ stages: create.stages ?? [],
1084
+ stage_history: applyStageTransition([], [], create.stages ?? [], input.userId, now),
1085
+ sessions: stampSessions({ [input.role]: input.session }, input.userId),
1086
+ metadata: create.metadata ?? null,
1087
+ revision: 1,
1088
+ created_at: now,
1089
+ updated_at: now
1090
+ });
1091
+ item = toRow(row);
1092
+ }
1093
+ await ops.updateMany("factory_run_bindings", {
1094
+ org_id: input.orgId,
1095
+ factory_project_id: input.factoryProjectId,
1096
+ work_item_id: item.id,
1097
+ role: input.role,
1098
+ status: "active"
1099
+ }, {
1100
+ status: "revoked",
1101
+ revoked_at: now
1102
+ });
1103
+ const bindingRow = await ops.insertOne("factory_run_bindings", {
1104
+ org_id: input.orgId,
1105
+ factory_project_id: input.factoryProjectId,
1106
+ work_item_id: item.id,
1107
+ role: input.role,
1108
+ thread_id: input.session.threadId,
1109
+ resource_id: input.resourceId,
1110
+ session_id: input.session.sessionId,
1111
+ branch: input.session.branch,
1112
+ status: "active",
1113
+ created_at: now,
1114
+ revoked_at: null
1115
+ });
1116
+ const pendingRow = await ops.insertOne("factory_pending_starts", {
1117
+ org_id: input.orgId,
1118
+ factory_project_id: input.factoryProjectId,
1119
+ binding_id: bindingRow.id,
1120
+ kickoff_key: input.kickoffKey,
1121
+ message: input.kickoffMessage,
1122
+ status: "pending",
1123
+ attempts: 0,
1124
+ available_at: now,
1125
+ lease_owner: null,
1126
+ lease_expires_at: null,
1127
+ last_error: null,
1128
+ completed_at: null,
1129
+ created_at: now,
1130
+ updated_at: now
1131
+ });
1132
+ return {
1133
+ item,
1134
+ binding: toBinding(bindingRow),
1135
+ pendingStart: toPendingStart(pendingRow),
1136
+ replayed: false
1137
+ };
1138
+ });
1139
+ try {
1140
+ return await prepare();
1141
+ } catch (error) {
1142
+ if (!(error instanceof UniqueViolationError)) throw error;
1143
+ return prepare();
1144
+ }
1145
+ }
1146
+ async markPendingStart(bindingId, status, lastError) {
1147
+ const row = await this.#db.updateAtomic("factory_pending_starts", { binding_id: bindingId }, () => ({
1148
+ status,
1149
+ last_error: lastError ?? null,
1150
+ updated_at: /* @__PURE__ */ new Date()
1151
+ }));
1152
+ return row ? toPendingStart(row) : null;
1153
+ }
1154
+ /**
1155
+ * Create a work item, reusing the existing record when `sourceKey` already
1156
+ * has one for the project (acting twice on the same issue must not duplicate
1157
+ * the card). On reuse the provided stages replace the current ones (with the
1158
+ * transition recorded in history) and sessions/metadata are merged in. The
1159
+ * result discriminates insert from reuse so callers can audit the actual
1160
+ * outcome.
1161
+ */
1162
+ async upsert(params) {
1163
+ const run = (ops) => this.#upsert(params, ops);
1164
+ const execute = () => params.input.parentWorkItemId ? this.#withProjectRelationTransaction(params.orgId, params.factoryProjectId, run) : run(this.#db);
1165
+ try {
1166
+ return await execute();
1167
+ } catch (error) {
1168
+ if (!(error instanceof UniqueViolationError)) throw error;
1169
+ return execute();
1170
+ }
1171
+ }
1172
+ async #upsert({ orgId, userId, factoryProjectId, input, reuseMode = "update" }, ops) {
1173
+ const key = sourceKey(input.externalSource);
1174
+ const reuse = async () => {
1175
+ if (!key) return null;
1176
+ const existing = await ops.findOne("work_items", {
1177
+ org_id: orgId,
1178
+ factory_project_id: factoryProjectId,
1179
+ source_key: key
1180
+ });
1181
+ if (!existing) return null;
1182
+ if (reuseMode === "preserve") return {
1183
+ created: false,
1184
+ item: toWorkItem(existing),
1185
+ previous: priorState(existing)
1186
+ };
1187
+ let previous = emptyPrior();
1188
+ const updated = await ops.updateAtomic("work_items", {
1189
+ org_id: orgId,
1190
+ factory_project_id: factoryProjectId,
1191
+ source_key: key
1192
+ }, async (current) => {
1193
+ previous = priorState(current);
1194
+ const fullPatch = input.parentWorkItemId === null ? {
1195
+ ...input,
1196
+ parentWorkItemId: void 0
1197
+ } : input;
1198
+ const patch = reuseMode === "non-stage" ? {
1199
+ title: input.title,
1200
+ parentWorkItemId: input.parentWorkItemId ?? void 0,
1201
+ metadata: input.metadata
1202
+ } : fullPatch;
1203
+ if (patch.parentWorkItemId !== void 0) validateParentRelation(await this.#listWithOps(ops, orgId, factoryProjectId), current.id, patch.parentWorkItemId);
1204
+ return {
1205
+ external_source: input.externalSource ?? null,
1206
+ ...applyUpdate({
1207
+ current,
1208
+ userId,
1209
+ input: patch
1210
+ })
1211
+ };
1212
+ });
1213
+ return updated ? {
1214
+ item: toWorkItem(updated),
1215
+ created: false,
1216
+ previous
1217
+ } : null;
1218
+ };
1219
+ const reused = await reuse();
1220
+ if (reused) return reused;
1221
+ const now = /* @__PURE__ */ new Date();
1222
+ const stages = input.stages ?? ["intake"];
1223
+ validateParentRelation(await this.#listWithOps(ops, orgId, factoryProjectId), void 0, input.parentWorkItemId ?? null);
1224
+ return {
1225
+ item: toWorkItem(await ops.insertOne("work_items", {
1226
+ org_id: orgId,
1227
+ factory_project_id: factoryProjectId,
1228
+ external_source: input.externalSource ?? null,
1229
+ source_key: key,
1230
+ parent_work_item_id: input.parentWorkItemId ?? null,
1231
+ title: input.title,
1232
+ stages,
1233
+ stage_history: stages.map((stage) => ({
1234
+ stage,
1235
+ enteredAt: now.toISOString(),
1236
+ by: userId
1237
+ })),
1238
+ sessions: stampSessions(input.sessions ?? {}, userId),
1239
+ metadata: input.metadata ?? null,
1240
+ revision: 1,
1241
+ created_by: userId,
1242
+ created_at: now,
1243
+ updated_at: now
1244
+ })),
1245
+ created: true,
1246
+ previous: emptyPrior()
1247
+ };
1248
+ }
1249
+ async update({ orgId, id, userId, patch }) {
1250
+ const run = async (ops) => {
1251
+ let previous = emptyPrior();
1252
+ const row = await ops.updateAtomic("work_items", {
1253
+ org_id: orgId,
1254
+ id
1255
+ }, async (current) => {
1256
+ previous = priorState(current);
1257
+ if (patch.parentWorkItemId !== void 0) validateParentRelation(await this.#listWithOps(ops, orgId, current.factory_project_id), current.id, patch.parentWorkItemId);
1258
+ return applyUpdate({
1259
+ current,
1260
+ userId,
1261
+ input: patch
1262
+ });
1263
+ });
1264
+ return row ? {
1265
+ item: toWorkItem(row),
1266
+ previous
1267
+ } : null;
1268
+ };
1269
+ if (patch.parentWorkItemId === void 0) return run(this.#db);
1270
+ const candidate = await this.#db.findOne("work_items", {
1271
+ org_id: orgId,
1272
+ id
1273
+ });
1274
+ if (!candidate) return null;
1275
+ return this.#withProjectRelationTransaction(orgId, candidate.factory_project_id, run);
1276
+ }
1277
+ async delete({ orgId, id }) {
1278
+ const candidate = await this.#db.findOne("work_items", {
1279
+ org_id: orgId,
1280
+ id
1281
+ });
1282
+ if (!candidate) return null;
1283
+ return this.#withProjectRelationTransaction(orgId, candidate.factory_project_id, async (ops) => {
1284
+ const existing = await ops.findOne("work_items", {
1285
+ org_id: orgId,
1286
+ id
1287
+ });
1288
+ if (!existing) return null;
1289
+ if (await ops.deleteMany("work_items", {
1290
+ org_id: orgId,
1291
+ id
1292
+ }) === 0) return null;
1293
+ await ops.updateMany("work_items", {
1294
+ org_id: orgId,
1295
+ parent_work_item_id: id
1296
+ }, {
1297
+ parent_work_item_id: null,
1298
+ updated_at: /* @__PURE__ */ new Date()
1299
+ });
1300
+ return toWorkItem(existing);
1301
+ });
1302
+ }
1172
1303
  };
1304
+ //#endregion
1305
+ export { AUTOMATION_ACTORS, WORK_ITEMS_SCHEMA, WorkItemRelationError, WorkItemsStorage, applyStageTransition, factoryDecisionHash, isAutomationActor, stampSessions, validateParentRelation };
1306
+
1173
1307
  //# sourceMappingURL=base.js.map