@mastra/factory 0.0.0-fix-thread-subscription-multi-instance-redis-streams-20260730204150
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +695 -0
- package/LICENSE.md +30 -0
- package/README.md +27 -0
- package/dist/auth.d.ts +181 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +505 -0
- package/dist/auth.js.map +1 -0
- package/dist/capabilities/connection.d.ts +9 -0
- package/dist/capabilities/connection.d.ts.map +1 -0
- package/dist/capabilities/connection.js +1 -0
- package/dist/capabilities/intake.d.ts +140 -0
- package/dist/capabilities/intake.d.ts.map +1 -0
- package/dist/capabilities/intake.js +1 -0
- package/dist/capabilities/version-control.d.ts +245 -0
- package/dist/capabilities/version-control.d.ts.map +1 -0
- package/dist/capabilities/version-control.js +1 -0
- package/dist/factory.d.ts +156 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +483 -0
- package/dist/factory.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/integrations/base.d.ts +210 -0
- package/dist/integrations/base.d.ts.map +1 -0
- package/dist/integrations/base.js +1 -0
- package/dist/integrations/github/config.d.ts +60 -0
- package/dist/integrations/github/config.d.ts.map +1 -0
- package/dist/integrations/github/config.js +42 -0
- package/dist/integrations/github/config.js.map +1 -0
- package/dist/integrations/github/factory-session.d.ts +12 -0
- package/dist/integrations/github/factory-session.d.ts.map +1 -0
- package/dist/integrations/github/factory-session.js +37 -0
- package/dist/integrations/github/factory-session.js.map +1 -0
- package/dist/integrations/github/integration.d.ts +191 -0
- package/dist/integrations/github/integration.d.ts.map +1 -0
- package/dist/integrations/github/integration.js +1077 -0
- package/dist/integrations/github/integration.js.map +1 -0
- package/dist/integrations/github/issue-triage.d.ts +27 -0
- package/dist/integrations/github/issue-triage.d.ts.map +1 -0
- package/dist/integrations/github/issue-triage.js +82 -0
- package/dist/integrations/github/issue-triage.js.map +1 -0
- package/dist/integrations/github/pat.d.ts +38 -0
- package/dist/integrations/github/pat.d.ts.map +1 -0
- package/dist/integrations/github/pat.js +59 -0
- package/dist/integrations/github/pat.js.map +1 -0
- package/dist/integrations/github/provenance.d.ts +27 -0
- package/dist/integrations/github/provenance.d.ts.map +1 -0
- package/dist/integrations/github/provenance.js +75 -0
- package/dist/integrations/github/provenance.js.map +1 -0
- package/dist/integrations/github/routes.d.ts +69 -0
- package/dist/integrations/github/routes.d.ts.map +1 -0
- package/dist/integrations/github/routes.js +1324 -0
- package/dist/integrations/github/routes.js.map +1 -0
- package/dist/integrations/github/rules.d.ts +78 -0
- package/dist/integrations/github/rules.d.ts.map +1 -0
- package/dist/integrations/github/rules.js +362 -0
- package/dist/integrations/github/rules.js.map +1 -0
- package/dist/integrations/github/sandbox-release.d.ts +38 -0
- package/dist/integrations/github/sandbox-release.d.ts.map +1 -0
- package/dist/integrations/github/sandbox-release.js +74 -0
- package/dist/integrations/github/sandbox-release.js.map +1 -0
- package/dist/integrations/github/sandbox.d.ts +281 -0
- package/dist/integrations/github/sandbox.d.ts.map +1 -0
- package/dist/integrations/github/sandbox.js +547 -0
- package/dist/integrations/github/sandbox.js.map +1 -0
- package/dist/integrations/github/session-subscriptions.d.ts +26 -0
- package/dist/integrations/github/session-subscriptions.d.ts.map +1 -0
- package/dist/integrations/github/session-subscriptions.js +192 -0
- package/dist/integrations/github/session-subscriptions.js.map +1 -0
- package/dist/integrations/github/subscriptions.d.ts +54 -0
- package/dist/integrations/github/subscriptions.d.ts.map +1 -0
- package/dist/integrations/github/subscriptions.js +62 -0
- package/dist/integrations/github/subscriptions.js.map +1 -0
- package/dist/integrations/github/token-refresh.d.ts +12 -0
- package/dist/integrations/github/token-refresh.d.ts.map +1 -0
- package/dist/integrations/github/token-refresh.js +24 -0
- package/dist/integrations/github/token-refresh.js.map +1 -0
- package/dist/integrations/github/webhook.d.ts +87 -0
- package/dist/integrations/github/webhook.d.ts.map +1 -0
- package/dist/integrations/github/webhook.js +397 -0
- package/dist/integrations/github/webhook.js.map +1 -0
- package/dist/integrations/linear/agent-tools.d.ts +34 -0
- package/dist/integrations/linear/agent-tools.d.ts.map +1 -0
- package/dist/integrations/linear/agent-tools.js +85 -0
- package/dist/integrations/linear/agent-tools.js.map +1 -0
- package/dist/integrations/linear/integration.d.ts +233 -0
- package/dist/integrations/linear/integration.d.ts.map +1 -0
- package/dist/integrations/linear/integration.js +672 -0
- package/dist/integrations/linear/integration.js.map +1 -0
- package/dist/integrations/linear/routes.d.ts +61 -0
- package/dist/integrations/linear/routes.d.ts.map +1 -0
- package/dist/integrations/linear/routes.js +239 -0
- package/dist/integrations/linear/routes.js.map +1 -0
- package/dist/integrations/linear/rules.d.ts +42 -0
- package/dist/integrations/linear/rules.d.ts.map +1 -0
- package/dist/integrations/linear/rules.js +133 -0
- package/dist/integrations/linear/rules.js.map +1 -0
- package/dist/integrations/linear/storage.d.ts +65 -0
- package/dist/integrations/linear/storage.d.ts.map +1 -0
- package/dist/integrations/linear/storage.js +1 -0
- package/dist/integrations/platform/api-client.d.ts +26 -0
- package/dist/integrations/platform/api-client.d.ts.map +1 -0
- package/dist/integrations/platform/api-client.js +156 -0
- package/dist/integrations/platform/api-client.js.map +1 -0
- package/dist/integrations/platform/github/event-worker.d.ts +49 -0
- package/dist/integrations/platform/github/event-worker.d.ts.map +1 -0
- package/dist/integrations/platform/github/event-worker.js +348 -0
- package/dist/integrations/platform/github/event-worker.js.map +1 -0
- package/dist/integrations/platform/github/integration.d.ts +50 -0
- package/dist/integrations/platform/github/integration.d.ts.map +1 -0
- package/dist/integrations/platform/github/integration.js +961 -0
- package/dist/integrations/platform/github/integration.js.map +1 -0
- package/dist/integrations/platform/linear/integration.d.ts +46 -0
- package/dist/integrations/platform/linear/integration.d.ts.map +1 -0
- package/dist/integrations/platform/linear/integration.js +483 -0
- package/dist/integrations/platform/linear/integration.js.map +1 -0
- package/dist/integrations/workos/integration.d.ts +39 -0
- package/dist/integrations/workos/integration.d.ts.map +1 -0
- package/dist/integrations/workos/integration.js +91 -0
- package/dist/integrations/workos/integration.js.map +1 -0
- package/dist/routes/config.d.ts +235 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +972 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/custom-provider-source.d.ts +52 -0
- package/dist/routes/custom-provider-source.d.ts.map +1 -0
- package/dist/routes/custom-provider-source.js +124 -0
- package/dist/routes/custom-provider-source.js.map +1 -0
- package/dist/routes/fs.d.ts +151 -0
- package/dist/routes/fs.d.ts.map +1 -0
- package/dist/routes/fs.js +708 -0
- package/dist/routes/fs.js.map +1 -0
- package/dist/routes/intake.d.ts +23 -0
- package/dist/routes/intake.d.ts.map +1 -0
- package/dist/routes/intake.js +179 -0
- package/dist/routes/intake.js.map +1 -0
- package/dist/routes/oauth.d.ts +43 -0
- package/dist/routes/oauth.d.ts.map +1 -0
- package/dist/routes/oauth.js +463 -0
- package/dist/routes/oauth.js.map +1 -0
- package/dist/routes/projects.d.ts +19 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +461 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/provider-credentials.d.ts +81 -0
- package/dist/routes/provider-credentials.d.ts.map +1 -0
- package/dist/routes/provider-credentials.js +86 -0
- package/dist/routes/provider-credentials.js.map +1 -0
- package/dist/routes/route.d.ts +48 -0
- package/dist/routes/route.d.ts.map +1 -0
- package/dist/routes/route.js +15 -0
- package/dist/routes/route.js.map +1 -0
- package/dist/routes/skills.d.ts +30 -0
- package/dist/routes/skills.d.ts.map +1 -0
- package/dist/routes/skills.js +168 -0
- package/dist/routes/skills.js.map +1 -0
- package/dist/routes/surface.d.ts +91 -0
- package/dist/routes/surface.d.ts.map +1 -0
- package/dist/routes/surface.js +260 -0
- package/dist/routes/surface.js.map +1 -0
- package/dist/routes/tenant-credentials.d.ts +74 -0
- package/dist/routes/tenant-credentials.d.ts.map +1 -0
- package/dist/routes/tenant-credentials.js +156 -0
- package/dist/routes/tenant-credentials.js.map +1 -0
- package/dist/routes/work-items.d.ts +40 -0
- package/dist/routes/work-items.d.ts.map +1 -0
- package/dist/routes/work-items.js +695 -0
- package/dist/routes/work-items.js.map +1 -0
- package/dist/rules/binding-context.d.ts +5 -0
- package/dist/rules/binding-context.d.ts.map +1 -0
- package/dist/rules/binding-context.js +28 -0
- package/dist/rules/binding-context.js.map +1 -0
- package/dist/rules/defaults.d.ts +9 -0
- package/dist/rules/defaults.d.ts.map +1 -0
- package/dist/rules/defaults.js +260 -0
- package/dist/rules/defaults.js.map +1 -0
- package/dist/rules/dispatcher.d.ts +42 -0
- package/dist/rules/dispatcher.d.ts.map +1 -0
- package/dist/rules/dispatcher.js +518 -0
- package/dist/rules/dispatcher.js.map +1 -0
- package/dist/rules/index.d.ts +7 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +5 -0
- package/dist/rules/processor.d.ts +54 -0
- package/dist/rules/processor.d.ts.map +1 -0
- package/dist/rules/processor.js +378 -0
- package/dist/rules/processor.js.map +1 -0
- package/dist/rules/resolve.d.ts +17 -0
- package/dist/rules/resolve.d.ts.map +1 -0
- package/dist/rules/resolve.js +30 -0
- package/dist/rules/resolve.js.map +1 -0
- package/dist/rules/start-coordinator.d.ts +60 -0
- package/dist/rules/start-coordinator.d.ts.map +1 -0
- package/dist/rules/start-coordinator.js +194 -0
- package/dist/rules/start-coordinator.js.map +1 -0
- package/dist/rules/tools.d.ts +10 -0
- package/dist/rules/tools.d.ts.map +1 -0
- package/dist/rules/tools.js +58 -0
- package/dist/rules/tools.js.map +1 -0
- package/dist/rules/transition-service.d.ts +49 -0
- package/dist/rules/transition-service.d.ts.map +1 -0
- package/dist/rules/transition-service.js +232 -0
- package/dist/rules/transition-service.js.map +1 -0
- package/dist/rules/types.d.ts +234 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/rules/types.js +38 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/rules/validation.d.ts +11 -0
- package/dist/rules/validation.d.ts.map +1 -0
- package/dist/rules/validation.js +308 -0
- package/dist/rules/validation.js.map +1 -0
- package/dist/sandbox/fleet.d.ts +222 -0
- package/dist/sandbox/fleet.d.ts.map +1 -0
- package/dist/sandbox/fleet.js +322 -0
- package/dist/sandbox/fleet.js.map +1 -0
- package/dist/sandbox/reattach.d.ts +3 -0
- package/dist/sandbox/reattach.d.ts.map +1 -0
- package/dist/sandbox/reattach.js +16 -0
- package/dist/sandbox/reattach.js.map +1 -0
- package/dist/server-error.d.ts +12 -0
- package/dist/server-error.d.ts.map +1 -0
- package/dist/server-error.js +16 -0
- package/dist/server-error.js.map +1 -0
- package/dist/skills/service.d.ts +48 -0
- package/dist/skills/service.d.ts.map +1 -0
- package/dist/skills/service.js +40 -0
- package/dist/skills/service.js.map +1 -0
- package/dist/spa-static.d.ts +33 -0
- package/dist/spa-static.d.ts.map +1 -0
- package/dist/spa-static.js +104 -0
- package/dist/spa-static.js.map +1 -0
- package/dist/state-signing.d.ts +59 -0
- package/dist/state-signing.d.ts.map +1 -0
- package/dist/state-signing.js +83 -0
- package/dist/state-signing.js.map +1 -0
- package/dist/storage/domains/audit/agent-audit.d.ts +27 -0
- package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -0
- package/dist/storage/domains/audit/agent-audit.js +66 -0
- package/dist/storage/domains/audit/agent-audit.js.map +1 -0
- package/dist/storage/domains/audit/base.d.ts +140 -0
- package/dist/storage/domains/audit/base.d.ts.map +1 -0
- package/dist/storage/domains/audit/base.js +184 -0
- package/dist/storage/domains/audit/base.js.map +1 -0
- package/dist/storage/domains/audit/domain.d.ts +69 -0
- package/dist/storage/domains/audit/domain.d.ts.map +1 -0
- package/dist/storage/domains/audit/domain.js +167 -0
- package/dist/storage/domains/audit/domain.js.map +1 -0
- package/dist/storage/domains/channel-identity/base.d.ts +96 -0
- package/dist/storage/domains/channel-identity/base.d.ts.map +1 -0
- package/dist/storage/domains/channel-identity/base.js +149 -0
- package/dist/storage/domains/channel-identity/base.js.map +1 -0
- package/dist/storage/domains/credentials/base.d.ts +134 -0
- package/dist/storage/domains/credentials/base.d.ts.map +1 -0
- package/dist/storage/domains/credentials/base.js +273 -0
- package/dist/storage/domains/credentials/base.js.map +1 -0
- package/dist/storage/domains/custom-providers/base.d.ts +57 -0
- package/dist/storage/domains/custom-providers/base.d.ts.map +1 -0
- package/dist/storage/domains/custom-providers/base.js +145 -0
- package/dist/storage/domains/custom-providers/base.js.map +1 -0
- package/dist/storage/domains/intake/base.d.ts +38 -0
- package/dist/storage/domains/intake/base.d.ts.map +1 -0
- package/dist/storage/domains/intake/base.js +80 -0
- package/dist/storage/domains/intake/base.js.map +1 -0
- package/dist/storage/domains/integrations/base.d.ts +126 -0
- package/dist/storage/domains/integrations/base.d.ts.map +1 -0
- package/dist/storage/domains/integrations/base.js +317 -0
- package/dist/storage/domains/integrations/base.js.map +1 -0
- package/dist/storage/domains/memory-settings/base.d.ts +62 -0
- package/dist/storage/domains/memory-settings/base.d.ts.map +1 -0
- package/dist/storage/domains/memory-settings/base.js +126 -0
- package/dist/storage/domains/memory-settings/base.js.map +1 -0
- package/dist/storage/domains/model-packs/base.d.ts +49 -0
- package/dist/storage/domains/model-packs/base.d.ts.map +1 -0
- package/dist/storage/domains/model-packs/base.js +96 -0
- package/dist/storage/domains/model-packs/base.js.map +1 -0
- package/dist/storage/domains/projects/base.d.ts +58 -0
- package/dist/storage/domains/projects/base.d.ts.map +1 -0
- package/dist/storage/domains/projects/base.js +111 -0
- package/dist/storage/domains/projects/base.js.map +1 -0
- package/dist/storage/domains/queue-health/base.d.ts +52 -0
- package/dist/storage/domains/queue-health/base.d.ts.map +1 -0
- package/dist/storage/domains/queue-health/base.js +122 -0
- package/dist/storage/domains/queue-health/base.js.map +1 -0
- package/dist/storage/domains/source-control/base.d.ts +358 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -0
- package/dist/storage/domains/source-control/base.js +908 -0
- package/dist/storage/domains/source-control/base.js.map +1 -0
- package/dist/storage/domains/source-control/inmemory.d.ts +175 -0
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -0
- package/dist/storage/domains/source-control/inmemory.js +377 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -0
- package/dist/storage/domains/work-items/base.d.ts +412 -0
- package/dist/storage/domains/work-items/base.d.ts.map +1 -0
- package/dist/storage/domains/work-items/base.js +1309 -0
- package/dist/storage/domains/work-items/base.js.map +1 -0
- package/dist/storage/domains/work-items/metrics.d.ts +101 -0
- package/dist/storage/domains/work-items/metrics.d.ts.map +1 -0
- package/dist/storage/domains/work-items/metrics.js +221 -0
- package/dist/storage/domains/work-items/metrics.js.map +1 -0
- package/dist/timing.d.ts +15 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +34 -0
- package/dist/timing.js.map +1 -0
- package/dist/workspace.d.ts +24 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +286 -0
- package/dist/workspace.js.map +1 -0
- package/factory-skills/configure-factory-rules/SKILL.md +67 -0
- package/factory-skills/factory-plan/SKILL.md +61 -0
- package/factory-skills/factory-review/SKILL.md +155 -0
- package/factory-skills/factory-triage/SKILL.md +77 -0
- package/package.json +81 -0
|
@@ -0,0 +1,1309 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
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
|
+
*/
|
|
13
|
+
function stableJson(value) {
|
|
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";
|
|
17
|
+
}
|
|
18
|
+
function factoryDecisionHash(decision) {
|
|
19
|
+
return createHash("sha256").update(stableJson(decision)).digest("hex");
|
|
20
|
+
}
|
|
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"
|
|
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
|
+
*/
|
|
41
|
+
function isAutomationActor(by) {
|
|
42
|
+
if (by === void 0) return false;
|
|
43
|
+
return AUTOMATION_ACTORS.has(by) || by.startsWith("agent:") || by.startsWith("github:");
|
|
44
|
+
}
|
|
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
|
+
}]
|
|
98
|
+
};
|
|
99
|
+
function sourceKey(source) {
|
|
100
|
+
return source ? `${source.integrationId}:${source.type}:${source.externalId}` : null;
|
|
101
|
+
}
|
|
102
|
+
function toWorkItem(row) {
|
|
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
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const toRow = toWorkItem;
|
|
121
|
+
function patchColumns(changes) {
|
|
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
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function emptyPrior() {
|
|
134
|
+
return {
|
|
135
|
+
stages: [],
|
|
136
|
+
sessionRoles: []
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function priorState(row) {
|
|
140
|
+
return {
|
|
141
|
+
stages: row.stages,
|
|
142
|
+
sessionRoles: Object.keys(row.sessions)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
var WorkItemRelationError = class extends Error {
|
|
146
|
+
code = "invalid_work_item_relation";
|
|
147
|
+
};
|
|
148
|
+
function validateParentRelation(projectItems, itemId, parentWorkItemId) {
|
|
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
|
+
}
|
|
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
|
+
*/
|
|
168
|
+
function applyStageTransition(history, oldStages, newStages, by, now) {
|
|
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;
|
|
188
|
+
}
|
|
189
|
+
function stampSessions(sessions, by) {
|
|
190
|
+
return Object.fromEntries(Object.entries(sessions).map(([role, session]) => [role, {
|
|
191
|
+
...session,
|
|
192
|
+
startedBy: by
|
|
193
|
+
}]));
|
|
194
|
+
}
|
|
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
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const projectRelationLocks = /* @__PURE__ */ new Map();
|
|
217
|
+
function withInProcessProjectLock(key, fn) {
|
|
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;
|
|
225
|
+
}
|
|
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
|
+
}
|
|
423
|
+
];
|
|
424
|
+
function toBinding(row) {
|
|
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
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function toDeferredDecision(row) {
|
|
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
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function toPendingStart(row) {
|
|
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
|
+
};
|
|
482
|
+
}
|
|
483
|
+
var WorkItemsStorage = class extends FactoryStorageDomain {
|
|
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
|
+
let lastError;
|
|
1140
|
+
for (let attempt = 0; attempt < 3; attempt++) try {
|
|
1141
|
+
return await prepare();
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
if (!(error instanceof UniqueViolationError)) throw error;
|
|
1144
|
+
lastError = error;
|
|
1145
|
+
}
|
|
1146
|
+
throw lastError;
|
|
1147
|
+
}
|
|
1148
|
+
async markPendingStart(bindingId, status, lastError) {
|
|
1149
|
+
const row = await this.#db.updateAtomic("factory_pending_starts", { binding_id: bindingId }, () => ({
|
|
1150
|
+
status,
|
|
1151
|
+
last_error: lastError ?? null,
|
|
1152
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
1153
|
+
}));
|
|
1154
|
+
return row ? toPendingStart(row) : null;
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Create a work item, reusing the existing record when `sourceKey` already
|
|
1158
|
+
* has one for the project (acting twice on the same issue must not duplicate
|
|
1159
|
+
* the card). On reuse the provided stages replace the current ones (with the
|
|
1160
|
+
* transition recorded in history) and sessions/metadata are merged in. The
|
|
1161
|
+
* result discriminates insert from reuse so callers can audit the actual
|
|
1162
|
+
* outcome.
|
|
1163
|
+
*/
|
|
1164
|
+
async upsert(params) {
|
|
1165
|
+
const run = (ops) => this.#upsert(params, ops);
|
|
1166
|
+
const execute = () => params.input.parentWorkItemId ? this.#withProjectRelationTransaction(params.orgId, params.factoryProjectId, run) : run(this.#db);
|
|
1167
|
+
try {
|
|
1168
|
+
return await execute();
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
if (!(error instanceof UniqueViolationError)) throw error;
|
|
1171
|
+
return execute();
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
async #upsert({ orgId, userId, factoryProjectId, input, reuseMode = "update" }, ops) {
|
|
1175
|
+
const key = sourceKey(input.externalSource);
|
|
1176
|
+
const reuse = async () => {
|
|
1177
|
+
if (!key) return null;
|
|
1178
|
+
const existing = await ops.findOne("work_items", {
|
|
1179
|
+
org_id: orgId,
|
|
1180
|
+
factory_project_id: factoryProjectId,
|
|
1181
|
+
source_key: key
|
|
1182
|
+
});
|
|
1183
|
+
if (!existing) return null;
|
|
1184
|
+
if (reuseMode === "preserve") return {
|
|
1185
|
+
created: false,
|
|
1186
|
+
item: toWorkItem(existing),
|
|
1187
|
+
previous: priorState(existing)
|
|
1188
|
+
};
|
|
1189
|
+
let previous = emptyPrior();
|
|
1190
|
+
const updated = await ops.updateAtomic("work_items", {
|
|
1191
|
+
org_id: orgId,
|
|
1192
|
+
factory_project_id: factoryProjectId,
|
|
1193
|
+
source_key: key
|
|
1194
|
+
}, async (current) => {
|
|
1195
|
+
previous = priorState(current);
|
|
1196
|
+
const fullPatch = input.parentWorkItemId === null ? {
|
|
1197
|
+
...input,
|
|
1198
|
+
parentWorkItemId: void 0
|
|
1199
|
+
} : input;
|
|
1200
|
+
const patch = reuseMode === "non-stage" ? {
|
|
1201
|
+
title: input.title,
|
|
1202
|
+
parentWorkItemId: input.parentWorkItemId ?? void 0,
|
|
1203
|
+
metadata: input.metadata
|
|
1204
|
+
} : fullPatch;
|
|
1205
|
+
if (patch.parentWorkItemId !== void 0) validateParentRelation(await this.#listWithOps(ops, orgId, factoryProjectId), current.id, patch.parentWorkItemId);
|
|
1206
|
+
return {
|
|
1207
|
+
external_source: input.externalSource ?? null,
|
|
1208
|
+
...applyUpdate({
|
|
1209
|
+
current,
|
|
1210
|
+
userId,
|
|
1211
|
+
input: patch
|
|
1212
|
+
})
|
|
1213
|
+
};
|
|
1214
|
+
});
|
|
1215
|
+
return updated ? {
|
|
1216
|
+
item: toWorkItem(updated),
|
|
1217
|
+
created: false,
|
|
1218
|
+
previous
|
|
1219
|
+
} : null;
|
|
1220
|
+
};
|
|
1221
|
+
const reused = await reuse();
|
|
1222
|
+
if (reused) return reused;
|
|
1223
|
+
const now = /* @__PURE__ */ new Date();
|
|
1224
|
+
const stages = input.stages ?? ["intake"];
|
|
1225
|
+
validateParentRelation(await this.#listWithOps(ops, orgId, factoryProjectId), void 0, input.parentWorkItemId ?? null);
|
|
1226
|
+
return {
|
|
1227
|
+
item: toWorkItem(await ops.insertOne("work_items", {
|
|
1228
|
+
org_id: orgId,
|
|
1229
|
+
factory_project_id: factoryProjectId,
|
|
1230
|
+
external_source: input.externalSource ?? null,
|
|
1231
|
+
source_key: key,
|
|
1232
|
+
parent_work_item_id: input.parentWorkItemId ?? null,
|
|
1233
|
+
title: input.title,
|
|
1234
|
+
stages,
|
|
1235
|
+
stage_history: stages.map((stage) => ({
|
|
1236
|
+
stage,
|
|
1237
|
+
enteredAt: now.toISOString(),
|
|
1238
|
+
by: userId
|
|
1239
|
+
})),
|
|
1240
|
+
sessions: stampSessions(input.sessions ?? {}, userId),
|
|
1241
|
+
metadata: input.metadata ?? null,
|
|
1242
|
+
revision: 1,
|
|
1243
|
+
created_by: userId,
|
|
1244
|
+
created_at: now,
|
|
1245
|
+
updated_at: now
|
|
1246
|
+
})),
|
|
1247
|
+
created: true,
|
|
1248
|
+
previous: emptyPrior()
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
async update({ orgId, id, userId, patch }) {
|
|
1252
|
+
const run = async (ops) => {
|
|
1253
|
+
let previous = emptyPrior();
|
|
1254
|
+
const row = await ops.updateAtomic("work_items", {
|
|
1255
|
+
org_id: orgId,
|
|
1256
|
+
id
|
|
1257
|
+
}, async (current) => {
|
|
1258
|
+
previous = priorState(current);
|
|
1259
|
+
if (patch.parentWorkItemId !== void 0) validateParentRelation(await this.#listWithOps(ops, orgId, current.factory_project_id), current.id, patch.parentWorkItemId);
|
|
1260
|
+
return applyUpdate({
|
|
1261
|
+
current,
|
|
1262
|
+
userId,
|
|
1263
|
+
input: patch
|
|
1264
|
+
});
|
|
1265
|
+
});
|
|
1266
|
+
return row ? {
|
|
1267
|
+
item: toWorkItem(row),
|
|
1268
|
+
previous
|
|
1269
|
+
} : null;
|
|
1270
|
+
};
|
|
1271
|
+
if (patch.parentWorkItemId === void 0) return run(this.#db);
|
|
1272
|
+
const candidate = await this.#db.findOne("work_items", {
|
|
1273
|
+
org_id: orgId,
|
|
1274
|
+
id
|
|
1275
|
+
});
|
|
1276
|
+
if (!candidate) return null;
|
|
1277
|
+
return this.#withProjectRelationTransaction(orgId, candidate.factory_project_id, run);
|
|
1278
|
+
}
|
|
1279
|
+
async delete({ orgId, id }) {
|
|
1280
|
+
const candidate = await this.#db.findOne("work_items", {
|
|
1281
|
+
org_id: orgId,
|
|
1282
|
+
id
|
|
1283
|
+
});
|
|
1284
|
+
if (!candidate) return null;
|
|
1285
|
+
return this.#withProjectRelationTransaction(orgId, candidate.factory_project_id, async (ops) => {
|
|
1286
|
+
const existing = await ops.findOne("work_items", {
|
|
1287
|
+
org_id: orgId,
|
|
1288
|
+
id
|
|
1289
|
+
});
|
|
1290
|
+
if (!existing) return null;
|
|
1291
|
+
if (await ops.deleteMany("work_items", {
|
|
1292
|
+
org_id: orgId,
|
|
1293
|
+
id
|
|
1294
|
+
}) === 0) return null;
|
|
1295
|
+
await ops.updateMany("work_items", {
|
|
1296
|
+
org_id: orgId,
|
|
1297
|
+
parent_work_item_id: id
|
|
1298
|
+
}, {
|
|
1299
|
+
parent_work_item_id: null,
|
|
1300
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
1301
|
+
});
|
|
1302
|
+
return toWorkItem(existing);
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
//#endregion
|
|
1307
|
+
export { AUTOMATION_ACTORS, WORK_ITEMS_SCHEMA, WorkItemRelationError, WorkItemsStorage, applyStageTransition, factoryDecisionHash, isAutomationActor, stampSessions, validateParentRelation };
|
|
1308
|
+
|
|
1309
|
+
//# sourceMappingURL=base.js.map
|