@osolmaz/pi-workflows 0.12.0 → 0.13.0
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/README.md +56 -31
- package/dist/builtins/autodoc.workflow.d.ts +4 -4
- package/dist/builtins/autoimplement.workflow.d.ts +797 -142
- package/dist/builtins/autoimplement.workflow.js +17 -105
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/autoplan.workflow.d.ts +126 -13
- package/dist/builtins/autoplan.workflow.js +200 -29
- package/dist/builtins/autoplan.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +7 -5
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +4 -2
- package/dist/builtins/index.js +3 -1
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +56 -23
- package/dist/builtins/monitor.workflow.js +508 -329
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/pi-agent-group.d.ts +72 -0
- package/dist/builtins/pi-agent-group.js +1087 -0
- package/dist/builtins/pi-agent-group.js.map +1 -0
- package/dist/builtins/plain-summary.workflow.d.ts +32 -0
- package/dist/builtins/plain-summary.workflow.js +134 -0
- package/dist/builtins/plain-summary.workflow.js.map +1 -0
- package/dist/builtins/plan-approval.workflow.d.ts +39 -5
- package/dist/builtins/plan-approval.workflow.js +92 -14
- package/dist/builtins/plan-approval.workflow.js.map +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +449 -0
- package/dist/builtins/plan-change.workflow.js +256 -0
- package/dist/builtins/plan-change.workflow.js.map +1 -0
- package/dist/builtins/plan-presentation.js +2 -2
- package/dist/builtins/plan-presentation.js.map +1 -1
- package/dist/builtins/sanity-check.workflow.d.ts +50 -6
- package/dist/builtins/sanity-check.workflow.js +150 -28
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/controllers/effects.d.ts +3 -2
- package/dist/controllers/effects.js +8 -1
- package/dist/controllers/effects.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/manager.d.ts +1 -0
- package/dist/controllers/manager.js +41 -17
- package/dist/controllers/manager.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +107 -85
- package/dist/controllers/sqlite.js +1611 -1486
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/store.d.ts +9 -8
- package/dist/controllers/store.js +1 -31
- package/dist/controllers/store.js.map +1 -1
- package/dist/controllers/types.d.ts +3 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +1 -1
- package/dist/controllers/workflow-engine-scheduler.js +3 -5
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
- package/dist/controllers/workflows.d.ts +2 -2
- package/dist/controllers/workflows.js +11 -10
- package/dist/controllers/workflows.js.map +1 -1
- package/dist/extension/controller-host.js +2 -2
- package/dist/extension/controller-host.js.map +1 -1
- package/dist/extension/decision-channels.d.ts +4 -2
- package/dist/extension/decision-channels.js +333 -146
- package/dist/extension/decision-channels.js.map +1 -1
- package/dist/extension/executor.d.ts +9 -2
- package/dist/extension/executor.js +89 -2
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/herdr-viewer.d.ts +0 -1
- package/dist/extension/herdr-viewer.js +0 -4
- package/dist/extension/herdr-viewer.js.map +1 -1
- package/dist/extension/index.js +209 -127
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +3 -10
- package/dist/extension/recorder.js +13 -22
- package/dist/extension/recorder.js.map +1 -1
- package/dist/extension/session-events.d.ts +2 -2
- package/dist/extension/step-message.d.ts +7 -2
- package/dist/extension/step-message.js +94 -2
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.js +29 -3
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/rpc-executor.d.ts +3 -0
- package/dist/host/rpc-executor.js +2 -0
- package/dist/host/rpc-executor.js.map +1 -1
- package/dist/host/runner.d.ts +3 -6
- package/dist/host/runner.js +83 -32
- package/dist/host/runner.js.map +1 -1
- package/dist/render/graph-render.js +15 -10
- package/dist/render/graph-render.js.map +1 -1
- package/dist/state/database.d.ts +35 -0
- package/dist/state/database.js +287 -0
- package/dist/state/database.js.map +1 -0
- package/dist/state/index.d.ts +4 -0
- package/dist/state/index.js +5 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/json.d.ts +6 -0
- package/dist/state/json.js +38 -0
- package/dist/state/json.js.map +1 -0
- package/dist/state/mutation.d.ts +57 -0
- package/dist/state/mutation.js +255 -0
- package/dist/state/mutation.js.map +1 -0
- package/dist/state/schema.d.ts +6 -0
- package/dist/state/schema.js +473 -0
- package/dist/state/schema.js.map +1 -0
- package/dist/viewer/cli.d.ts +2 -2
- package/dist/viewer/cli.js +142 -122
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.d.ts +4 -4
- package/dist/viewer/render.js +9 -23
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/session-reducer.d.ts +1 -1
- package/dist/viewer/session-reducer.js +1 -1
- package/dist/viewer/tui.d.ts +3 -3
- package/dist/viewer/tui.js +15 -13
- package/dist/viewer/tui.js.map +1 -1
- package/dist/viewer/watch.d.ts +2 -5
- package/dist/viewer/watch.js +13 -13
- package/dist/viewer/watch.js.map +1 -1
- package/dist/workflows/catalog.js +7 -2
- package/dist/workflows/catalog.js.map +1 -1
- package/dist/workflows/composition.js +20 -2
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/decision-presentation.d.ts +1 -1
- package/dist/workflows/decision-presentation.js +51 -38
- package/dist/workflows/decision-presentation.js.map +1 -1
- package/dist/workflows/definition.d.ts +6 -2
- package/dist/workflows/definition.js +17 -0
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +7 -7
- package/dist/workflows/engine.js +196 -115
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +16 -3
- package/dist/workflows/errors.js +18 -3
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +45 -8
- package/dist/workflows/human-decision.js +641 -274
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +5 -6
- package/dist/workflows/index.js +4 -5
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/progress.d.ts +1 -0
- package/dist/workflows/progress.js +15 -3
- package/dist/workflows/progress.js.map +1 -1
- package/dist/workflows/schema.js +27 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +83 -103
- package/dist/workflows/store.js +907 -951
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +88 -126
- package/docs/CONTROLLERS.md +10 -8
- package/docs/DEFERRED_TURNS.md +2 -2
- package/docs/DESIGN_PHILOSOPHY.md +4 -2
- package/docs/HUMAN_DECISIONS.md +36 -47
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +14 -24
- package/docs/MONITOR.md +179 -237
- package/docs/SQLITE_STATE.md +219 -0
- package/docs/WORKFLOW_COMPOSITION.md +27 -26
- package/docs/WORKFLOW_STEP_MESSAGES.md +20 -18
- package/docs/WORKFLOW_UPDATES.md +10 -9
- package/docs/development.md +9 -9
- package/docs/live-replay-protocol.md +28 -37
- package/docs/plans/2026-08-16-workflow-updates-plan.md +1 -1
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
- package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +1 -1
- package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
- package/docs/plans/2026-08-21-sanity-check-plan.md +217 -95
- package/docs/plans/2026-08-22-goal-finishing-monitor-plan.md +200 -0
- package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +701 -0
- package/docs/plans/2026-08-23-sqlite-state-plan.md +970 -0
- package/docs/plans/replayable-run-bundles-implementation-plan.md +2 -2
- package/docs/plans/session-event-replay-implementation-plan.md +1 -1
- package/docs/plans/tui-viewer-implementation-plan.md +1 -1
- package/docs/session-event-journal.md +63 -434
- package/docs/tui-viewer.md +13 -15
- package/docs/workflows.md +127 -53
- package/examples/workflows/approved-plan.workflow.ts +19 -46
- package/examples/workflows/plain-summary.workflow.ts +1 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +7 -7
- package/plugins/herdr/viewer.mjs +1 -11
- package/schemas/human-decision-accepted-v1.schema.json +15 -3
- package/schemas/human-decision-continuation-v1.schema.json +10 -1
- package/schemas/human-decision-delivery-v1.schema.json +8 -0
- package/schemas/human-decision-receipt-v1.schema.json +8 -0
- package/schemas/human-decision-request-v1.schema.json +24 -4
- package/skills/autoimplement/SKILL.md +27 -0
- package/skills/autoplan/SKILL.md +5 -4
- package/skills/monitor/SKILL.md +91 -135
- package/skills/pi-workflows/SKILL.md +9 -8
- package/skills/sanity-check/SKILL.md +44 -0
- package/src/builtins/autoimplement.workflow.ts +19 -118
- package/src/builtins/autoplan.workflow.ts +290 -40
- package/src/builtins/catalog.ts +7 -5
- package/src/builtins/index.ts +18 -1
- package/src/builtins/monitor.workflow.ts +673 -382
- package/src/builtins/pi-agent-group.ts +1407 -0
- package/src/builtins/plain-summary.workflow.ts +185 -0
- package/src/builtins/plan-approval.workflow.ts +157 -24
- package/src/builtins/plan-change.workflow.ts +321 -0
- package/src/builtins/plan-presentation.ts +2 -2
- package/src/builtins/sanity-check.workflow.ts +248 -48
- package/src/controllers/effects.ts +7 -0
- package/src/controllers/index.ts +0 -6
- package/src/controllers/manager.ts +72 -32
- package/src/controllers/sqlite.ts +2420 -1948
- package/src/controllers/store.ts +17 -38
- package/src/controllers/types.ts +3 -0
- package/src/controllers/workflow-engine-scheduler.ts +4 -8
- package/src/controllers/workflows.ts +45 -21
- package/src/extension/controller-host.ts +1 -2
- package/src/extension/decision-channels.ts +456 -203
- package/src/extension/executor.ts +105 -2
- package/src/extension/herdr-viewer.ts +0 -5
- package/src/extension/index.ts +251 -151
- package/src/extension/recorder.ts +13 -22
- package/src/extension/session-events.ts +2 -2
- package/src/extension/step-message.ts +106 -4
- package/src/extension/widget.ts +32 -5
- package/src/host/rpc-executor.ts +4 -0
- package/src/host/runner.ts +93 -41
- package/src/render/graph-render.ts +12 -6
- package/src/state/database.ts +375 -0
- package/src/state/index.ts +29 -0
- package/src/state/json.ts +42 -0
- package/src/state/mutation.ts +435 -0
- package/src/state/schema.ts +475 -0
- package/src/viewer/cli.ts +151 -144
- package/src/viewer/render.ts +19 -30
- package/src/viewer/session-reducer.ts +1 -1
- package/src/viewer/tui.ts +18 -17
- package/src/viewer/watch.ts +13 -17
- package/src/workflows/catalog.ts +7 -2
- package/src/workflows/composition.ts +21 -3
- package/src/workflows/decision-presentation.ts +56 -43
- package/src/workflows/definition.ts +36 -3
- package/src/workflows/engine.ts +228 -130
- package/src/workflows/errors.ts +27 -3
- package/src/workflows/human-decision.ts +921 -349
- package/src/workflows/index.ts +20 -30
- package/src/workflows/progress.ts +18 -3
- package/src/workflows/schema.ts +36 -1
- package/src/workflows/store.ts +1300 -1163
- package/src/workflows/types.ts +100 -137
- package/dist/builtins/sanity-check-session.d.ts +0 -17
- package/dist/builtins/sanity-check-session.js +0 -168
- package/dist/builtins/sanity-check-session.js.map +0 -1
- package/dist/workflows/artifacts.d.ts +0 -40
- package/dist/workflows/artifacts.js +0 -155
- package/dist/workflows/artifacts.js.map +0 -1
- package/dist/workflows/migrate-sources.d.ts +0 -42
- package/dist/workflows/migrate-sources.js +0 -133
- package/dist/workflows/migrate-sources.js.map +0 -1
- package/docs/run-bundles.md +0 -481
- package/schemas/human-decision-accepted-v2.schema.json +0 -50
- package/schemas/human-decision-delivery-v2.schema.json +0 -36
- package/schemas/human-decision-receipt-v2.schema.json +0 -39
- package/schemas/human-decision-request-v2.schema.json +0 -69
- package/schemas/human-decision-resolution-v2.schema.json +0 -27
- package/src/builtins/sanity-check-session.ts +0 -205
- package/src/workflows/artifacts.ts +0 -188
- package/src/workflows/migrate-sources.ts +0 -178
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import { StateDatabase, workflowStatePath } from "../state/database.js";
|
|
3
|
+
import { resourceIdFor, tokenHash } from "../state/mutation.js";
|
|
4
4
|
import { digestCanonical, normalizeDecisionPresentation, validateHumanDecisionRequestIntegrity, } from "./decision-presentation.js";
|
|
5
5
|
import { checkpoint } from "./definition.js";
|
|
6
6
|
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
|
|
@@ -38,11 +38,23 @@ export function humanDecision(definition) {
|
|
|
38
38
|
if (typeof definition.request !== "function") {
|
|
39
39
|
throw new Error("Human decision request must be a function");
|
|
40
40
|
}
|
|
41
|
-
const { choices, request, ...common } = definition;
|
|
41
|
+
const { choices, request, onTimeout, ...common } = definition;
|
|
42
|
+
if (onTimeout !== undefined &&
|
|
43
|
+
typeof onTimeout !== "function" &&
|
|
44
|
+
(typeof onTimeout.afterMs !== "number" ||
|
|
45
|
+
!Number.isFinite(onTimeout.afterMs) ||
|
|
46
|
+
onTimeout.afterMs <= 0)) {
|
|
47
|
+
throw new Error("Human decision onTimeout afterMs must be a finite positive number");
|
|
48
|
+
}
|
|
42
49
|
return checkpoint({
|
|
43
50
|
...common,
|
|
44
51
|
summary: typeof audience === "string" ? `human decision for ${audience}` : "human decision",
|
|
45
|
-
humanDecision: {
|
|
52
|
+
humanDecision: {
|
|
53
|
+
audience,
|
|
54
|
+
choices,
|
|
55
|
+
request,
|
|
56
|
+
...(onTimeout !== undefined ? { onTimeout } : {}),
|
|
57
|
+
},
|
|
46
58
|
});
|
|
47
59
|
}
|
|
48
60
|
export function humanDecisionEdge(args) {
|
|
@@ -61,8 +73,23 @@ export function humanDecisionEdge(args) {
|
|
|
61
73
|
}
|
|
62
74
|
export function createHumanDecisionRequest(input) {
|
|
63
75
|
validateChoices(input.contract.choices);
|
|
76
|
+
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
64
77
|
const title = requireString(input.prompt.title, "Human decision title");
|
|
65
|
-
|
|
78
|
+
if (input.timeout !== undefined && input.prompt.expiresAt !== undefined) {
|
|
79
|
+
throw new Error("Human decision request cannot combine expiresAt with onTimeout");
|
|
80
|
+
}
|
|
81
|
+
if (input.timeout !== undefined &&
|
|
82
|
+
(typeof input.timeout.afterMs !== "number" ||
|
|
83
|
+
!Number.isFinite(input.timeout.afterMs) ||
|
|
84
|
+
input.timeout.afterMs <= 0)) {
|
|
85
|
+
throw new Error("Human decision onTimeout afterMs must be a finite positive number");
|
|
86
|
+
}
|
|
87
|
+
const defaultResponse = input.timeout === undefined
|
|
88
|
+
? undefined
|
|
89
|
+
: validateResponseForChoices(input.contract.choices, input.timeout.response);
|
|
90
|
+
const expiresAt = input.timeout === undefined
|
|
91
|
+
? validateExpiry(input.prompt.expiresAt, createdAt)
|
|
92
|
+
: new Date(Date.parse(createdAt) + input.timeout.afterMs).toISOString();
|
|
66
93
|
const common = {
|
|
67
94
|
runId: input.runId,
|
|
68
95
|
workflowName: input.workflowName,
|
|
@@ -72,57 +99,35 @@ export function createHumanDecisionRequest(input) {
|
|
|
72
99
|
title,
|
|
73
100
|
choices: input.contract.choices,
|
|
74
101
|
...(expiresAt !== undefined ? { expiresAt } : {}),
|
|
102
|
+
...(defaultResponse !== undefined ? { defaultResponse } : {}),
|
|
75
103
|
};
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
validatePresentedText(title, "Human decision title");
|
|
79
|
-
validatePresentedChoices(input.contract.choices);
|
|
80
|
-
if (Object.hasOwn(input.prompt, "body")) {
|
|
81
|
-
throw new Error("Presented human decision requests must not contain a legacy body");
|
|
82
|
-
}
|
|
83
|
-
if (!Object.hasOwn(input.prompt, "subject") || !Object.hasOwn(input.prompt, "presentation")) {
|
|
84
|
-
throw new Error("Presented human decision requests require subject and presentation");
|
|
85
|
-
}
|
|
86
|
-
const prompt = input.prompt;
|
|
87
|
-
assertJsonValue(prompt.subject, "Human decision subject");
|
|
88
|
-
const presentation = normalizeDecisionPresentation(prompt.presentation);
|
|
89
|
-
const revision = prompt.revision ?? 1;
|
|
90
|
-
if (!Number.isInteger(revision) || revision < 1) {
|
|
91
|
-
throw new Error("Human decision revision must be a positive integer");
|
|
92
|
-
}
|
|
93
|
-
const basis = {
|
|
94
|
-
schema: "pi-workflows.human-decision-request.v2",
|
|
95
|
-
...common,
|
|
96
|
-
subject: prompt.subject,
|
|
97
|
-
presentation,
|
|
98
|
-
revision,
|
|
99
|
-
};
|
|
100
|
-
const subjectDigest = digestCanonical(prompt.subject);
|
|
101
|
-
const presentationDigest = digestCanonical(presentation);
|
|
102
|
-
const requestDigest = digestCanonical(basis);
|
|
103
|
-
const decisionId = decisionIdFor(input, requestDigest);
|
|
104
|
-
return {
|
|
105
|
-
...basis,
|
|
106
|
-
decisionId,
|
|
107
|
-
requestDigest,
|
|
108
|
-
subjectDigest,
|
|
109
|
-
presentationDigest,
|
|
110
|
-
createdAt: input.createdAt ?? new Date().toISOString(),
|
|
111
|
-
};
|
|
112
|
-
}
|
|
104
|
+
validatePresentedText(title, "Human decision title");
|
|
105
|
+
validatePresentedChoices(input.contract.choices);
|
|
113
106
|
const prompt = input.prompt;
|
|
114
|
-
assertJsonValue(prompt.
|
|
107
|
+
assertJsonValue(prompt.subject, "Human decision subject");
|
|
108
|
+
const presentation = normalizeDecisionPresentation(prompt.presentation);
|
|
109
|
+
const revision = prompt.revision ?? 1;
|
|
110
|
+
if (!Number.isInteger(revision) || revision < 1) {
|
|
111
|
+
throw new Error("Human decision revision must be a positive integer");
|
|
112
|
+
}
|
|
115
113
|
const basis = {
|
|
116
114
|
schema: "pi-workflows.human-decision-request.v1",
|
|
117
115
|
...common,
|
|
118
|
-
|
|
116
|
+
subject: prompt.subject,
|
|
117
|
+
presentation,
|
|
118
|
+
revision,
|
|
119
119
|
};
|
|
120
|
-
const
|
|
120
|
+
const subjectDigest = digestCanonical(prompt.subject);
|
|
121
|
+
const presentationDigest = digestCanonical(presentation);
|
|
122
|
+
const requestDigest = digestCanonical(basis);
|
|
123
|
+
const decisionId = decisionIdFor(input, requestDigest);
|
|
121
124
|
return {
|
|
122
125
|
...basis,
|
|
123
|
-
decisionId
|
|
126
|
+
decisionId,
|
|
124
127
|
requestDigest,
|
|
125
|
-
|
|
128
|
+
subjectDigest,
|
|
129
|
+
presentationDigest,
|
|
130
|
+
createdAt,
|
|
126
131
|
};
|
|
127
132
|
}
|
|
128
133
|
function decisionIdFor(input, requestDigest) {
|
|
@@ -134,12 +139,15 @@ function decisionIdFor(input, requestDigest) {
|
|
|
134
139
|
}).slice(0, 40)}`;
|
|
135
140
|
}
|
|
136
141
|
export function validateHumanDecisionResponse(request, value) {
|
|
142
|
+
return validateResponseForChoices(request.choices, value);
|
|
143
|
+
}
|
|
144
|
+
function validateResponseForChoices(choices, value) {
|
|
137
145
|
const response = requireRecord(value, "Human decision response");
|
|
138
146
|
const selected = response.choice;
|
|
139
|
-
if (typeof selected !== "string" || !Object.hasOwn(
|
|
147
|
+
if (typeof selected !== "string" || !Object.hasOwn(choices, selected)) {
|
|
140
148
|
throw new Error(`Human decision choice ${JSON.stringify(selected)} is not available`);
|
|
141
149
|
}
|
|
142
|
-
const selectedChoice =
|
|
150
|
+
const selectedChoice = choices[selected];
|
|
143
151
|
if (!selectedChoice)
|
|
144
152
|
throw new Error("Human decision choice contract is missing");
|
|
145
153
|
if (selectedChoice.input === undefined) {
|
|
@@ -184,214 +192,636 @@ export function validateHumanDecisionSubmission(request, value) {
|
|
|
184
192
|
idempotencyKey,
|
|
185
193
|
};
|
|
186
194
|
}
|
|
187
|
-
export function
|
|
188
|
-
return
|
|
189
|
-
? path.join(path.dirname(runsRoot), "decisions")
|
|
190
|
-
: `${runsRoot}.decisions`;
|
|
195
|
+
export function humanDecisionDatabasePath(homeDir) {
|
|
196
|
+
return workflowStatePath(homeDir);
|
|
191
197
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
198
|
+
const INCOMPATIBLE_HUMAN_DECISION_STATE = "Human decision state uses an incompatible alpha contract; reset the affected workflow run and decision state.";
|
|
199
|
+
function requireCurrentDecision(value) {
|
|
200
|
+
const decision = requireRecord(value, "Resolved human decision");
|
|
201
|
+
if (decision.schema !== "pi-workflows.human-decision-accepted.v1" ||
|
|
202
|
+
!Object.hasOwn(decision, "subjectDigest") ||
|
|
203
|
+
!Object.hasOwn(decision, "presentationDigest") ||
|
|
204
|
+
!Object.hasOwn(decision, "revision")) {
|
|
205
|
+
throw new Error(INCOMPATIBLE_HUMAN_DECISION_STATE);
|
|
196
206
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
export class HumanDecisionStore {
|
|
210
|
+
databasePath;
|
|
211
|
+
state;
|
|
212
|
+
ownsState;
|
|
213
|
+
authorityProvider;
|
|
214
|
+
constructor(databasePath = workflowStatePath(), options = {}) {
|
|
215
|
+
this.ownsState = options.state === undefined;
|
|
216
|
+
this.state =
|
|
217
|
+
options.state ??
|
|
218
|
+
new StateDatabase({
|
|
219
|
+
filePath: databasePath,
|
|
220
|
+
mode: options.readOnly === true ? "read-only" : "read-write",
|
|
221
|
+
checkLegacyState: databasePath === workflowStatePath(),
|
|
222
|
+
});
|
|
223
|
+
this.databasePath = this.state.filePath;
|
|
224
|
+
this.authorityProvider = options.authorityProvider;
|
|
225
|
+
}
|
|
226
|
+
close() {
|
|
227
|
+
if (this.ownsState)
|
|
228
|
+
this.state.close();
|
|
200
229
|
}
|
|
201
230
|
async createRequest(request) {
|
|
202
231
|
validateHumanDecisionRequestIntegrity(request);
|
|
203
|
-
return
|
|
232
|
+
return this.state.transaction(() => {
|
|
233
|
+
const existing = this.readRequestRow(request.decisionId);
|
|
234
|
+
if (existing !== undefined) {
|
|
235
|
+
const stored = this.readRequestFromHash(existing.requestHash);
|
|
236
|
+
if (canonicalJson(stored) !== canonicalJson(request)) {
|
|
237
|
+
throw new Error("Immutable human decision request conflicts");
|
|
238
|
+
}
|
|
239
|
+
return "adopted";
|
|
240
|
+
}
|
|
241
|
+
const run = this.state.connection
|
|
242
|
+
.prepare("SELECT resource_id AS resourceId FROM runs WHERE run_id = ?")
|
|
243
|
+
.get(request.runId);
|
|
244
|
+
if (!isResourceIdRow(run))
|
|
245
|
+
throw new Error(`Human decision run is missing: ${request.runId}`);
|
|
246
|
+
const attempt = this.state.connection
|
|
247
|
+
.prepare("SELECT 1 AS present FROM node_attempts WHERE attempt_id = ? AND run_id = ?")
|
|
248
|
+
.get(request.attemptId, request.runId);
|
|
249
|
+
if (!isPresentRow(attempt)) {
|
|
250
|
+
throw new Error(`Human decision attempt is missing: ${request.attemptId}`);
|
|
251
|
+
}
|
|
252
|
+
const now = Date.parse(request.createdAt);
|
|
253
|
+
const resourceId = resourceIdFor("decision", request.decisionId);
|
|
254
|
+
const subjectHash = this.state.putJson(request.subject, now);
|
|
255
|
+
const presentationHash = this.state.putJson(request.presentation, now);
|
|
256
|
+
const choicesHash = this.state.putJson(request.choices, now);
|
|
257
|
+
const defaultHash = request.defaultResponse === undefined
|
|
258
|
+
? null
|
|
259
|
+
: this.state.putJson(request.defaultResponse, now);
|
|
260
|
+
const requestHash = this.state.putJson(request, now);
|
|
261
|
+
this.state.connection
|
|
262
|
+
.prepare(`INSERT INTO resources(
|
|
263
|
+
resource_id, resource_type, aggregate_key, revision, created_at, updated_at
|
|
264
|
+
) VALUES (?, 'decision', ?, 1, ?, ?)`)
|
|
265
|
+
.run(resourceId, request.decisionId, now, now);
|
|
266
|
+
this.state.connection
|
|
267
|
+
.prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
|
|
268
|
+
.run(resourceId);
|
|
269
|
+
this.state.connection
|
|
270
|
+
.prepare(`INSERT INTO human_decisions(
|
|
271
|
+
decision_id, resource_id, run_id, attempt_id, audience, title,
|
|
272
|
+
subject_hash, presentation_hash, choices_hash, request_digest,
|
|
273
|
+
presentation_revision, deadline_at, default_response_hash, request_hash, created_at
|
|
274
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
275
|
+
.run(request.decisionId, resourceId, request.runId, request.attemptId, request.audience, request.title, subjectHash, presentationHash, choicesHash, digestBuffer(request.requestDigest), request.revision, request.expiresAt === undefined ? null : Date.parse(request.expiresAt), defaultHash, requestHash, now);
|
|
276
|
+
this.insertDecisionEvent(resourceId, 1, "decision.requested", "system", null, requestHash, now);
|
|
277
|
+
return "created";
|
|
278
|
+
});
|
|
204
279
|
}
|
|
205
280
|
async readRequest(decisionId) {
|
|
206
|
-
|
|
207
|
-
|
|
281
|
+
assertId(decisionId, "decision id");
|
|
282
|
+
const row = this.readRequestRow(decisionId);
|
|
283
|
+
return row === undefined ? null : this.readRequestFromHash(row.requestHash);
|
|
208
284
|
}
|
|
209
285
|
async recordDelivery(request, channel, value) {
|
|
210
|
-
|
|
211
|
-
const attemptId = requireString(value.attemptId, "Human decision delivery attempt");
|
|
212
|
-
assertId(attemptId, "delivery attempt id");
|
|
213
|
-
return await writeImmutableJson(path.join(this.decisionDir(request.decisionId), "deliveries", safeChannel, `${attemptId}.json`), value);
|
|
286
|
+
return this.recordChannelMessage(request.decisionId, channel, "delivery", value);
|
|
214
287
|
}
|
|
215
288
|
async accept(request, submission) {
|
|
216
289
|
validateHumanDecisionRequestIntegrity(request);
|
|
217
|
-
const cancellation = await this.readCancellation(request.decisionId);
|
|
218
|
-
if (cancellation !== null) {
|
|
219
|
-
throw new Error(`Human decision request was ${cancellation.reason}`);
|
|
220
|
-
}
|
|
221
290
|
const normalized = validateHumanDecisionSubmission(request, submission);
|
|
222
|
-
|
|
291
|
+
const attemptedAt = new Date().toISOString();
|
|
223
292
|
const attemptId = digestHex({
|
|
224
293
|
decisionId: request.decisionId,
|
|
225
294
|
idempotencyKey: normalized.idempotencyKey,
|
|
226
295
|
}).slice(0, 40);
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
schema: "pi-workflows.human-decision-answer-attempt.v1",
|
|
230
|
-
attemptId,
|
|
231
|
-
attemptedAt,
|
|
232
|
-
...normalized,
|
|
233
|
-
};
|
|
234
|
-
const attemptWrite = await writeImmutableJson(attemptPath, attempt, false);
|
|
235
|
-
if (attemptWrite === "adopted") {
|
|
236
|
-
const existingAttempt = requireRecord(await readJson(attemptPath), "Existing human decision answer attempt");
|
|
237
|
-
const existingSubmission = {
|
|
238
|
-
decisionId: existingAttempt.decisionId,
|
|
239
|
-
requestDigest: existingAttempt.requestDigest,
|
|
240
|
-
choice: existingAttempt.choice,
|
|
241
|
-
...(existingAttempt.input !== undefined ? { input: existingAttempt.input } : {}),
|
|
242
|
-
source: existingAttempt.source,
|
|
243
|
-
idempotencyKey: existingAttempt.idempotencyKey,
|
|
244
|
-
};
|
|
245
|
-
if (canonicalJson(existingSubmission) !== canonicalJson(normalized)) {
|
|
246
|
-
throw new Error("Human decision idempotency key was reused for a different answer");
|
|
247
|
-
}
|
|
248
|
-
attemptedAt = requireString(existingAttempt.attemptedAt, "answer attempt time");
|
|
249
|
-
}
|
|
250
|
-
const response = validateHumanDecisionResponse(request, normalized);
|
|
251
|
-
const commonDecision = {
|
|
296
|
+
const decision = {
|
|
297
|
+
schema: "pi-workflows.human-decision-accepted.v1",
|
|
252
298
|
decisionId: request.decisionId,
|
|
253
299
|
requestDigest: request.requestDigest,
|
|
254
|
-
response,
|
|
300
|
+
response: validateHumanDecisionResponse(request, normalized),
|
|
301
|
+
provenance: "human",
|
|
255
302
|
source: normalized.source,
|
|
256
303
|
idempotencyKey: normalized.idempotencyKey,
|
|
257
304
|
acceptedAt: attemptedAt,
|
|
258
|
-
answerDigest: digest({
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
revision: request.revision,
|
|
267
|
-
}
|
|
268
|
-
: { schema: "pi-workflows.human-decision-accepted.v1", ...commonDecision };
|
|
269
|
-
const resolutionPath = path.join(this.decisionDir(request.decisionId), "resolution.json");
|
|
270
|
-
const resolution = {
|
|
271
|
-
schema: decision.schema === "pi-workflows.human-decision-accepted.v2"
|
|
272
|
-
? "pi-workflows.human-decision-resolution.v2"
|
|
273
|
-
: "pi-workflows.human-decision-resolution.v1",
|
|
274
|
-
outcome: "accepted",
|
|
275
|
-
decision,
|
|
305
|
+
answerDigest: digest({
|
|
306
|
+
requestDigest: request.requestDigest,
|
|
307
|
+
response: validateHumanDecisionResponse(request, normalized),
|
|
308
|
+
source: normalized.source,
|
|
309
|
+
}),
|
|
310
|
+
subjectDigest: request.subjectDigest,
|
|
311
|
+
presentationDigest: request.presentationDigest,
|
|
312
|
+
revision: request.revision,
|
|
276
313
|
};
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
:
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return { status: "accepted", decision: existing };
|
|
290
|
-
if (canonicalJson(existing) === canonicalJson(decision)) {
|
|
291
|
-
return { status: "adopted", decision: existing };
|
|
314
|
+
return this.attemptAcceptance(request, decision, {
|
|
315
|
+
attemptId,
|
|
316
|
+
source: "human",
|
|
317
|
+
actorId: normalized.source.actorId,
|
|
318
|
+
channel: normalized.source.channel,
|
|
319
|
+
candidate: normalized,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
async resolveTimeout(request, now = new Date()) {
|
|
323
|
+
validateHumanDecisionRequestIntegrity(request);
|
|
324
|
+
if (request.expiresAt === undefined || request.defaultResponse === undefined) {
|
|
325
|
+
throw new Error("Human decision request has no timeout default");
|
|
292
326
|
}
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
canonicalJson(existing.source) === canonicalJson(decision.source)) {
|
|
296
|
-
return { status: "adopted", decision: existing };
|
|
327
|
+
if (Date.parse(request.expiresAt) > now.getTime()) {
|
|
328
|
+
throw new Error("Human decision timeout default is not eligible yet");
|
|
297
329
|
}
|
|
298
|
-
|
|
330
|
+
this.assertRunOwner(request.runId, now.getTime());
|
|
331
|
+
const response = validateHumanDecisionResponse(request, request.defaultResponse);
|
|
332
|
+
const decision = {
|
|
333
|
+
schema: "pi-workflows.human-decision-accepted.v1",
|
|
334
|
+
decisionId: request.decisionId,
|
|
335
|
+
requestDigest: request.requestDigest,
|
|
336
|
+
response,
|
|
337
|
+
provenance: "timeout",
|
|
338
|
+
acceptedAt: now.toISOString(),
|
|
339
|
+
answerDigest: digest({
|
|
340
|
+
provenance: "timeout",
|
|
341
|
+
requestDigest: request.requestDigest,
|
|
342
|
+
response,
|
|
343
|
+
}),
|
|
344
|
+
subjectDigest: request.subjectDigest,
|
|
345
|
+
presentationDigest: request.presentationDigest,
|
|
346
|
+
revision: request.revision,
|
|
347
|
+
};
|
|
348
|
+
const actorId = this.authorityProvider?.(request.runId)?.ownerId;
|
|
349
|
+
return this.attemptAcceptance(request, decision, {
|
|
350
|
+
attemptId: digestHex({ decisionId: request.decisionId, provenance: "timeout" }).slice(0, 40),
|
|
351
|
+
source: "policy",
|
|
352
|
+
...(actorId === undefined ? {} : { actorId }),
|
|
353
|
+
candidate: response,
|
|
354
|
+
});
|
|
299
355
|
}
|
|
300
356
|
async listDeliveries(decisionId, channel) {
|
|
301
|
-
|
|
302
|
-
const directory = path.join(this.decisionDir(decisionId), "deliveries", safeChannel);
|
|
303
|
-
return (await readJsonDirectory(directory));
|
|
357
|
+
return this.listChannelMessages(decisionId, channel, "delivery");
|
|
304
358
|
}
|
|
305
359
|
async recordSettlement(decisionId, channel, value) {
|
|
306
|
-
|
|
307
|
-
assertId(value.attemptId, "settlement attempt id");
|
|
308
|
-
return await writeImmutableJson(path.join(this.decisionDir(decisionId), "settlements", safeChannel, `${value.attemptId}.json`), value);
|
|
360
|
+
return this.recordChannelMessage(decisionId, channel, "settlement", value);
|
|
309
361
|
}
|
|
310
362
|
async listSettlements(decisionId, channel) {
|
|
311
|
-
|
|
312
|
-
const directory = path.join(this.decisionDir(decisionId), "settlements", safeChannel);
|
|
313
|
-
return (await readJsonDirectory(directory));
|
|
363
|
+
return this.listChannelMessages(decisionId, channel, "settlement");
|
|
314
364
|
}
|
|
315
365
|
async cancel(request, reason) {
|
|
316
366
|
validateHumanDecisionRequestIntegrity(request);
|
|
317
|
-
if (
|
|
318
|
-
throw new Error("
|
|
367
|
+
if (reason === "expired" && request.defaultResponse !== undefined) {
|
|
368
|
+
throw new Error("A defaulted human decision must resolve through its timeout policy");
|
|
319
369
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
370
|
+
return this.state.transaction(() => {
|
|
371
|
+
this.requireStoredRequest(request);
|
|
372
|
+
const winner = this.readResolution(request.decisionId);
|
|
373
|
+
if (winner !== null) {
|
|
374
|
+
if (winner.outcome === "cancelled") {
|
|
375
|
+
if (winner.cancellation.reason === reason)
|
|
376
|
+
return "adopted";
|
|
377
|
+
throw new Error(`Human decision was already cancelled with reason ${winner.cancellation.reason}`);
|
|
378
|
+
}
|
|
379
|
+
throw new Error(`Human decision was already accepted by ${winner.decision.provenance}`);
|
|
380
|
+
}
|
|
381
|
+
const now = Date.now();
|
|
382
|
+
if (reason === "expired") {
|
|
383
|
+
if (request.expiresAt === undefined) {
|
|
384
|
+
throw new Error("Human decision request has no expiry");
|
|
385
|
+
}
|
|
386
|
+
if (Date.parse(request.expiresAt) > now) {
|
|
387
|
+
throw new Error("Human decision expiry cancellation is not eligible yet");
|
|
388
|
+
}
|
|
389
|
+
this.assertRunOwner(request.runId, now);
|
|
390
|
+
}
|
|
391
|
+
const record = {
|
|
392
|
+
schema: "pi-workflows.human-decision-cancellation.v1",
|
|
393
|
+
decisionId: request.decisionId,
|
|
394
|
+
requestDigest: request.requestDigest,
|
|
395
|
+
cancelledAt: new Date(now).toISOString(),
|
|
396
|
+
reason,
|
|
397
|
+
};
|
|
398
|
+
const provenance = reason === "expired" ? "expired_no_default" : "explicit_cancel";
|
|
399
|
+
this.state.connection
|
|
400
|
+
.prepare(`INSERT INTO human_decision_resolutions(
|
|
401
|
+
decision_id, outcome, provenance, response_hash, reason, channel,
|
|
402
|
+
actor_id, request_digest, resolved_at
|
|
403
|
+
) VALUES (?, 'cancelled', ?, NULL, ?, NULL, NULL, ?, ?)`)
|
|
404
|
+
.run(request.decisionId, provenance, reason, digestBuffer(request.requestDigest), now);
|
|
405
|
+
const row = this.requireDecisionResource(request.decisionId);
|
|
406
|
+
this.bumpDecision(row.resourceId, row.revision, now);
|
|
407
|
+
const recordHash = this.state.putJson(record, now);
|
|
408
|
+
this.insertDecisionEvent(row.resourceId, row.revision + 1, "decision.cancelled", reason === "expired" ? "policy" : "control", null, recordHash, now);
|
|
409
|
+
this.enqueueResolutionEffects(row.resourceId, row.revision + 1, request, "cancelled", now);
|
|
410
|
+
return "created";
|
|
411
|
+
});
|
|
353
412
|
}
|
|
354
413
|
async readCancellation(decisionId) {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
await writeImmutableJson(path.join(this.decisionDir(decisionId), "cancelled.json"), resolution.cancellation);
|
|
362
|
-
return resolution.cancellation;
|
|
363
|
-
}
|
|
364
|
-
async readAccepted(decisionId) {
|
|
365
|
-
const stored = (await readJson(path.join(this.decisionDir(decisionId), "accepted.json")));
|
|
366
|
-
if (stored !== null)
|
|
367
|
-
return stored;
|
|
368
|
-
const resolution = (await readJson(path.join(this.decisionDir(decisionId), "resolution.json")));
|
|
369
|
-
if (resolution?.outcome !== "accepted")
|
|
370
|
-
return null;
|
|
371
|
-
await writeImmutableJson(path.join(this.decisionDir(decisionId), "accepted.json"), resolution.decision);
|
|
372
|
-
return resolution.decision;
|
|
414
|
+
const resolution = this.readResolution(decisionId);
|
|
415
|
+
return resolution?.outcome === "cancelled" ? resolution.cancellation : null;
|
|
416
|
+
}
|
|
417
|
+
async readResolved(decisionId) {
|
|
418
|
+
const resolution = this.readResolution(decisionId);
|
|
419
|
+
return resolution?.outcome === "accepted" ? resolution.decision : null;
|
|
373
420
|
}
|
|
374
421
|
async recordContinuation(decisionId, value) {
|
|
375
|
-
return
|
|
422
|
+
return this.state.transaction(() => {
|
|
423
|
+
const existing = this.readContinuationRow(decisionId);
|
|
424
|
+
if (existing !== undefined) {
|
|
425
|
+
const stored = this.state.readJson(existing.recordHash);
|
|
426
|
+
if (canonicalJson(stored) !== canonicalJson(value)) {
|
|
427
|
+
throw new Error("Immutable human decision continuation conflicts");
|
|
428
|
+
}
|
|
429
|
+
return "adopted";
|
|
430
|
+
}
|
|
431
|
+
const resolution = this.readResolution(decisionId);
|
|
432
|
+
if (resolution?.outcome !== "accepted") {
|
|
433
|
+
throw new Error("A continuation requires an accepted human decision");
|
|
434
|
+
}
|
|
435
|
+
const recordHash = this.state.putJson(value, Date.parse(value.createdAt));
|
|
436
|
+
this.state.connection
|
|
437
|
+
.prepare(`INSERT INTO continuations(
|
|
438
|
+
decision_id, parent_run_id, continuation_run_id, response_hash, created_at
|
|
439
|
+
) VALUES (?, ?, ?, ?, ?)`)
|
|
440
|
+
.run(decisionId, value.parentRunId, value.runId, recordHash, Date.parse(value.createdAt));
|
|
441
|
+
return "created";
|
|
442
|
+
});
|
|
376
443
|
}
|
|
377
444
|
async readContinuation(decisionId) {
|
|
378
|
-
|
|
445
|
+
const row = this.readContinuationRow(decisionId);
|
|
446
|
+
return row === undefined
|
|
447
|
+
? null
|
|
448
|
+
: this.state.readJson(row.recordHash);
|
|
449
|
+
}
|
|
450
|
+
markEffectApplied(decisionId, effectType) {
|
|
451
|
+
const rows = this.state.connection
|
|
452
|
+
.prepare(`SELECT e.effect_id AS effectId, e.resource_id AS resourceId
|
|
453
|
+
FROM effects e
|
|
454
|
+
JOIN human_decisions d ON d.resource_id = e.source_resource_id
|
|
455
|
+
WHERE d.decision_id = ? AND e.effect_type = ? AND e.status = 'pending'`)
|
|
456
|
+
.all(decisionId, effectType);
|
|
457
|
+
for (const row of rows) {
|
|
458
|
+
/* istanbul ignore if -- exact schema and internal query shape */
|
|
459
|
+
if (!isEffectIdentityRecord(row))
|
|
460
|
+
continue;
|
|
461
|
+
this.state.transaction(() => {
|
|
462
|
+
const now = Date.now();
|
|
463
|
+
const revisionRow = this.state.connection
|
|
464
|
+
.prepare("SELECT revision FROM resources WHERE resource_id = ?")
|
|
465
|
+
.get(row.resourceId);
|
|
466
|
+
/* istanbul ignore if -- exact schema and internal query shape */
|
|
467
|
+
if (!isRevisionRecord(revisionRow))
|
|
468
|
+
throw new Error("Decision effect resource is missing");
|
|
469
|
+
this.state.connection
|
|
470
|
+
.prepare(`UPDATE effects SET status = 'applied', updated_at = ?, settled_at = ?
|
|
471
|
+
WHERE effect_id = ? AND status = 'pending'`)
|
|
472
|
+
.run(now, now, row.effectId);
|
|
473
|
+
this.state.connection
|
|
474
|
+
.prepare(`UPDATE resources SET revision = revision + 1, updated_at = ?
|
|
475
|
+
WHERE resource_id = ? AND revision = ?`)
|
|
476
|
+
.run(now, row.resourceId, revisionRow.revision);
|
|
477
|
+
const payloadHash = this.state.putJson({ decisionId, effectType }, now);
|
|
478
|
+
this.insertDecisionEvent(row.resourceId, revisionRow.revision + 1, "effect.applied", "system", null, payloadHash, now);
|
|
479
|
+
});
|
|
480
|
+
}
|
|
379
481
|
}
|
|
380
482
|
async listRequests() {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
483
|
+
const rows = this.state.connection
|
|
484
|
+
.prepare("SELECT request_hash AS requestHash FROM human_decisions ORDER BY created_at")
|
|
485
|
+
.all();
|
|
486
|
+
return rows.filter(isRequestHashRow).map((row) => this.readRequestFromHash(row.requestHash));
|
|
487
|
+
}
|
|
488
|
+
attemptAcceptance(request, decision, attempt) {
|
|
489
|
+
return this.state.transaction(() => {
|
|
490
|
+
this.requireStoredRequest(request);
|
|
491
|
+
const timing = this.state.connection
|
|
492
|
+
.prepare("SELECT deadline_at AS deadlineAt FROM human_decisions WHERE decision_id = ?")
|
|
493
|
+
.get(request.decisionId);
|
|
494
|
+
if (!isDeadlineRow(timing))
|
|
495
|
+
throw new Error("Human decision request is missing");
|
|
496
|
+
const arbitrationTime = attempt.source === "policy" ? Date.parse(decision.acceptedAt) : Date.now();
|
|
497
|
+
if (attempt.source === "human" &&
|
|
498
|
+
timing.deadlineAt !== null &&
|
|
499
|
+
arbitrationTime > timing.deadlineAt) {
|
|
500
|
+
throw new Error("Human decision request expired before the answer was accepted");
|
|
501
|
+
}
|
|
502
|
+
if (attempt.source === "policy" &&
|
|
503
|
+
(timing.deadlineAt === null || arbitrationTime < timing.deadlineAt)) {
|
|
504
|
+
throw new Error("Human decision timeout default is not eligible yet");
|
|
505
|
+
}
|
|
506
|
+
if (attempt.source === "policy")
|
|
507
|
+
this.assertRunOwner(request.runId, arbitrationTime);
|
|
508
|
+
const candidateHash = this.state.putJson(attempt.candidate);
|
|
509
|
+
const existingSubmission = this.state.connection
|
|
510
|
+
.prepare(`SELECT candidate_hash AS candidateHash
|
|
511
|
+
FROM human_decision_submissions WHERE decision_id = ? AND attempt_id = ?`)
|
|
512
|
+
.get(request.decisionId, attempt.attemptId);
|
|
513
|
+
if (isCandidateHashRow(existingSubmission) &&
|
|
514
|
+
!existingSubmission.candidateHash.equals(candidateHash)) {
|
|
515
|
+
throw new Error("Human decision submission idempotency key conflicts");
|
|
516
|
+
}
|
|
517
|
+
this.state.connection
|
|
518
|
+
.prepare(`INSERT INTO human_decision_submissions(
|
|
519
|
+
decision_id, attempt_id, source, actor_id, channel,
|
|
520
|
+
candidate_hash, outcome, submitted_at
|
|
521
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'rejected', ?)
|
|
522
|
+
ON CONFLICT(decision_id, attempt_id) DO NOTHING`)
|
|
523
|
+
.run(request.decisionId, attempt.attemptId, attempt.source, attempt.actorId ?? null, attempt.channel ?? null, candidateHash, Date.parse(decision.acceptedAt));
|
|
524
|
+
const winner = this.readResolution(request.decisionId);
|
|
525
|
+
if (winner !== null) {
|
|
526
|
+
this.markSubmission(request.decisionId, attempt.attemptId, "already_resolved", null);
|
|
527
|
+
if (winner.outcome === "cancelled") {
|
|
528
|
+
throw new Error(`Human decision request was ${winner.cancellation.reason}`);
|
|
529
|
+
}
|
|
530
|
+
return canonicalJson(winner.decision) === canonicalJson(decision) ||
|
|
531
|
+
sameHumanAnswer(winner.decision, decision)
|
|
532
|
+
? { status: "adopted", decision: winner.decision }
|
|
533
|
+
: { status: "conflict", decision: winner.decision };
|
|
534
|
+
}
|
|
535
|
+
const now = Date.parse(decision.acceptedAt);
|
|
536
|
+
const responseHash = this.state.putJson(decision, now);
|
|
537
|
+
this.state.connection
|
|
538
|
+
.prepare(`INSERT INTO human_decision_resolutions(
|
|
539
|
+
decision_id, outcome, provenance, response_hash, reason, channel,
|
|
540
|
+
actor_id, request_digest, resolved_at
|
|
541
|
+
) VALUES (?, 'accepted', ?, ?, NULL, ?, ?, ?, ?)`)
|
|
542
|
+
.run(request.decisionId, decision.provenance === "timeout" ? "timeout_policy" : "human", responseHash, attempt.channel ?? null, attempt.actorId ?? null, digestBuffer(request.requestDigest), now);
|
|
543
|
+
const row = this.requireDecisionResource(request.decisionId);
|
|
544
|
+
this.bumpDecision(row.resourceId, row.revision, now);
|
|
545
|
+
this.insertDecisionEvent(row.resourceId, row.revision + 1, "decision.accepted", decision.provenance === "timeout" ? "policy" : "human", attempt.actorId ?? null, responseHash, now);
|
|
546
|
+
this.enqueueResolutionEffects(row.resourceId, row.revision + 1, request, "accepted", now);
|
|
547
|
+
this.markSubmission(request.decisionId, attempt.attemptId, "won", responseHash);
|
|
548
|
+
return { status: "accepted", decision };
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
readResolution(decisionId) {
|
|
552
|
+
const row = this.state.connection
|
|
553
|
+
.prepare(`SELECT outcome, provenance, response_hash AS responseHash, reason,
|
|
554
|
+
request_digest AS requestDigest, resolved_at AS resolvedAt
|
|
555
|
+
FROM human_decision_resolutions WHERE decision_id = ?`)
|
|
556
|
+
.get(decisionId);
|
|
557
|
+
if (!isResolutionRow(row))
|
|
558
|
+
return null;
|
|
559
|
+
const request = this.readRequestRow(decisionId);
|
|
560
|
+
if (request === undefined)
|
|
561
|
+
throw new Error("Human decision request is missing");
|
|
562
|
+
const storedRequest = this.readRequestFromHash(request.requestHash);
|
|
563
|
+
if (!row.requestDigest.equals(digestBuffer(storedRequest.requestDigest))) {
|
|
564
|
+
throw new Error("Human decision resolution request digest conflicts");
|
|
384
565
|
}
|
|
385
|
-
|
|
386
|
-
if (
|
|
387
|
-
|
|
388
|
-
|
|
566
|
+
if (row.outcome === "accepted") {
|
|
567
|
+
if (row.responseHash === null)
|
|
568
|
+
throw new Error("Accepted decision response is missing");
|
|
569
|
+
const decision = this.state.readJson(row.responseHash);
|
|
570
|
+
return {
|
|
571
|
+
schema: "pi-workflows.human-decision-resolution.v1",
|
|
572
|
+
outcome: "accepted",
|
|
573
|
+
decision: requireCurrentDecision(decision),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
const cancellation = {
|
|
577
|
+
schema: "pi-workflows.human-decision-cancellation.v1",
|
|
578
|
+
decisionId,
|
|
579
|
+
requestDigest: storedRequest.requestDigest,
|
|
580
|
+
cancelledAt: new Date(row.resolvedAt).toISOString(),
|
|
581
|
+
reason: row.reason === "expired" ? "expired" : "cancelled",
|
|
582
|
+
};
|
|
583
|
+
return {
|
|
584
|
+
schema: "pi-workflows.human-decision-resolution.v1",
|
|
585
|
+
outcome: "cancelled",
|
|
586
|
+
cancellation,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
requireStoredRequest(request) {
|
|
590
|
+
const row = this.readRequestRow(request.decisionId);
|
|
591
|
+
if (row === undefined)
|
|
592
|
+
throw new Error("Human decision request is not durable");
|
|
593
|
+
const stored = this.readRequestFromHash(row.requestHash);
|
|
594
|
+
if (canonicalJson(stored) !== canonicalJson(request)) {
|
|
595
|
+
throw new Error("Human decision request does not match durable state");
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
readRequestRow(decisionId) {
|
|
599
|
+
const row = this.state.connection
|
|
600
|
+
.prepare("SELECT request_hash AS requestHash FROM human_decisions WHERE decision_id = ?")
|
|
601
|
+
.get(decisionId);
|
|
602
|
+
return isRequestHashRow(row) ? row : undefined;
|
|
603
|
+
}
|
|
604
|
+
readRequestFromHash(hash) {
|
|
605
|
+
return validateHumanDecisionRequestIntegrity(this.state.readJson(hash));
|
|
606
|
+
}
|
|
607
|
+
requireDecisionResource(decisionId) {
|
|
608
|
+
const row = this.state.connection
|
|
609
|
+
.prepare(`SELECT d.resource_id AS resourceId, r.revision
|
|
610
|
+
FROM human_decisions d JOIN resources r ON r.resource_id = d.resource_id
|
|
611
|
+
WHERE d.decision_id = ?`)
|
|
612
|
+
.get(decisionId);
|
|
613
|
+
if (!isDecisionResourceRow(row))
|
|
614
|
+
throw new Error(`Human decision is missing: ${decisionId}`);
|
|
615
|
+
return row;
|
|
616
|
+
}
|
|
617
|
+
bumpDecision(resourceId, expectedRevision, now) {
|
|
618
|
+
const result = this.state.connection
|
|
619
|
+
.prepare(`UPDATE resources SET revision = revision + 1, updated_at = ?
|
|
620
|
+
WHERE resource_id = ? AND revision = ?`)
|
|
621
|
+
.run(now, resourceId, expectedRevision);
|
|
622
|
+
if (result.changes !== 1)
|
|
623
|
+
throw new Error("Human decision revision conflict");
|
|
624
|
+
}
|
|
625
|
+
insertDecisionEvent(resourceId, revision, eventType, actorType, actorId, payloadHash, now) {
|
|
626
|
+
this.state.connection
|
|
627
|
+
.prepare(`INSERT INTO events(
|
|
628
|
+
event_id, resource_id, resource_revision, event_type,
|
|
629
|
+
actor_type, actor_id, payload_hash, recorded_at
|
|
630
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
631
|
+
.run(`event-${randomUUID()}`, resourceId, revision, eventType, actorType, actorId, payloadHash, now);
|
|
632
|
+
}
|
|
633
|
+
enqueueResolutionEffects(decisionResourceId, revision, request, outcome, now) {
|
|
634
|
+
const types = outcome === "accepted"
|
|
635
|
+
? ["decision.continue", "decision.settle_presentations"]
|
|
636
|
+
: ["decision.cancel_parent", "decision.settle_presentations"];
|
|
637
|
+
for (const effectType of types) {
|
|
638
|
+
const idempotencyKey = request.decisionId;
|
|
639
|
+
const effectId = `effect-${digestHex({ decisionId: request.decisionId, effectType }).slice(0, 40)}`;
|
|
640
|
+
const effectResourceId = resourceIdFor("effect", effectId);
|
|
641
|
+
const payloadHash = this.state.putJson({ decisionId: request.decisionId, runId: request.runId, outcome }, now);
|
|
642
|
+
this.state.connection
|
|
643
|
+
.prepare(`INSERT INTO resources(
|
|
644
|
+
resource_id, resource_type, aggregate_key, revision, created_at, updated_at
|
|
645
|
+
) VALUES (?, 'effect', ?, 0, ?, ?)`)
|
|
646
|
+
.run(effectResourceId, effectId, now, now);
|
|
647
|
+
this.state.connection
|
|
648
|
+
.prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
|
|
649
|
+
.run(effectResourceId);
|
|
650
|
+
this.state.connection
|
|
651
|
+
.prepare(`INSERT INTO effects(
|
|
652
|
+
effect_id, resource_id, source_resource_id, source_revision,
|
|
653
|
+
effect_type, idempotency_key, payload_hash, owner_scope,
|
|
654
|
+
status, attempt_count, created_at, updated_at
|
|
655
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'run', 'pending', 0, ?, ?)`)
|
|
656
|
+
.run(effectId, effectResourceId, decisionResourceId, revision, effectType, idempotencyKey, payloadHash, now, now);
|
|
389
657
|
}
|
|
390
|
-
const requests = await Promise.all(entries
|
|
391
|
-
.filter((entry) => entry.isDirectory())
|
|
392
|
-
.map(async (entry) => await this.readRequest(entry.name)));
|
|
393
|
-
return requests.filter((request) => request !== null);
|
|
394
658
|
}
|
|
659
|
+
assertRunOwner(runId, now) {
|
|
660
|
+
const authority = this.authorityProvider?.(runId);
|
|
661
|
+
if (authority === undefined) {
|
|
662
|
+
throw new Error("Timeout policy requires the current run owner");
|
|
663
|
+
}
|
|
664
|
+
const row = this.state.connection
|
|
665
|
+
.prepare(`SELECT l.generation, l.owner_type AS ownerType, l.owner_id AS ownerId,
|
|
666
|
+
l.token_hash AS tokenHash, l.expires_at AS expiresAt
|
|
667
|
+
FROM runs r JOIN leases l ON l.resource_id = r.resource_id
|
|
668
|
+
WHERE r.run_id = ?`)
|
|
669
|
+
.get(runId);
|
|
670
|
+
if (!isOwnerRow(row) ||
|
|
671
|
+
row.ownerType !== authority.ownerType ||
|
|
672
|
+
row.ownerId !== authority.ownerId ||
|
|
673
|
+
row.generation !== authority.generation ||
|
|
674
|
+
row.tokenHash === null ||
|
|
675
|
+
!row.tokenHash.equals(tokenHash(authority.token)) ||
|
|
676
|
+
row.expiresAt === null ||
|
|
677
|
+
row.expiresAt <= now) {
|
|
678
|
+
throw new Error("Timeout policy run ownership is stale");
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
recordChannelMessage(decisionId, channel, purpose, value) {
|
|
682
|
+
const safeChannel = requireSimpleId(channel, "Human decision channel");
|
|
683
|
+
assertId(value.attemptId, `${purpose} attempt id`);
|
|
684
|
+
return this.state.transaction(() => {
|
|
685
|
+
if (this.readRequestRow(decisionId) === undefined) {
|
|
686
|
+
throw new Error(`Human decision is missing: ${decisionId}`);
|
|
687
|
+
}
|
|
688
|
+
const channelId = this.ensureChannel(safeChannel, Date.parse(value.createdAt));
|
|
689
|
+
const messageId = `message-${digestHex({
|
|
690
|
+
decisionId,
|
|
691
|
+
channel: safeChannel,
|
|
692
|
+
purpose,
|
|
693
|
+
attemptId: value.attemptId,
|
|
694
|
+
phase: "phase" in value ? value.phase : undefined,
|
|
695
|
+
partIndex: "partIndex" in value ? value.partIndex : undefined,
|
|
696
|
+
}).slice(0, 40)}`;
|
|
697
|
+
const contentHash = this.state.putJson(value, Date.parse(value.createdAt));
|
|
698
|
+
const existing = this.state.connection
|
|
699
|
+
.prepare("SELECT content_hash AS contentHash FROM channel_messages WHERE message_id = ?")
|
|
700
|
+
.get(messageId);
|
|
701
|
+
if (isContentHashRow(existing)) {
|
|
702
|
+
if (!existing.contentHash.equals(contentHash)) {
|
|
703
|
+
throw new Error(`Immutable human decision ${purpose} conflicts`);
|
|
704
|
+
}
|
|
705
|
+
return "adopted";
|
|
706
|
+
}
|
|
707
|
+
const status = channelMessageStatus(value);
|
|
708
|
+
this.state.connection
|
|
709
|
+
.prepare(`INSERT INTO channel_messages(
|
|
710
|
+
message_id, channel_id, decision_id, purpose, content_hash,
|
|
711
|
+
status, created_at, updated_at
|
|
712
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
713
|
+
.run(messageId, channelId, decisionId, purpose, contentHash, status, Date.parse(value.createdAt), "finishedAt" in value && value.finishedAt !== undefined
|
|
714
|
+
? Date.parse(value.finishedAt)
|
|
715
|
+
: Date.parse(value.createdAt));
|
|
716
|
+
return "created";
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
listChannelMessages(decisionId, channel, purpose) {
|
|
720
|
+
const channelId = channelIdFor(requireSimpleId(channel, "Human decision channel"));
|
|
721
|
+
const rows = this.state.connection
|
|
722
|
+
.prepare(`SELECT content_hash AS contentHash
|
|
723
|
+
FROM channel_messages
|
|
724
|
+
WHERE decision_id = ? AND channel_id = ? AND purpose = ?
|
|
725
|
+
ORDER BY created_at, message_id`)
|
|
726
|
+
.all(decisionId, channelId, purpose);
|
|
727
|
+
return rows.filter(isContentHashRow).map((row) => this.state.readJson(row.contentHash));
|
|
728
|
+
}
|
|
729
|
+
ensureChannel(channel, now) {
|
|
730
|
+
const channelId = channelIdFor(channel);
|
|
731
|
+
const resourceId = resourceIdFor("channel", channelId);
|
|
732
|
+
this.state.connection
|
|
733
|
+
.prepare(`INSERT INTO resources(
|
|
734
|
+
resource_id, resource_type, aggregate_key, revision, created_at, updated_at
|
|
735
|
+
) VALUES (?, 'channel', ?, 1, ?, ?)
|
|
736
|
+
ON CONFLICT(resource_type, aggregate_key) DO NOTHING`)
|
|
737
|
+
.run(resourceId, channelId, now, now);
|
|
738
|
+
this.state.connection
|
|
739
|
+
.prepare(`INSERT INTO leases(resource_id, generation)
|
|
740
|
+
VALUES (?, 0) ON CONFLICT(resource_id) DO NOTHING`)
|
|
741
|
+
.run(resourceId);
|
|
742
|
+
this.state.connection
|
|
743
|
+
.prepare(`INSERT INTO channels(channel_id, resource_id, adapter_type, profile_key, created_at)
|
|
744
|
+
VALUES (?, ?, ?, ?, ?)
|
|
745
|
+
ON CONFLICT(channel_id) DO NOTHING`)
|
|
746
|
+
.run(channelId, resourceId, channel.split("-")[0] ?? channel, channel, now);
|
|
747
|
+
return channelId;
|
|
748
|
+
}
|
|
749
|
+
markSubmission(decisionId, attemptId, outcome, resultHash) {
|
|
750
|
+
this.state.connection
|
|
751
|
+
.prepare(`UPDATE human_decision_submissions SET outcome = ?, result_hash = ?
|
|
752
|
+
WHERE decision_id = ? AND attempt_id = ?`)
|
|
753
|
+
.run(outcome, resultHash, decisionId, attemptId);
|
|
754
|
+
}
|
|
755
|
+
readContinuationRow(decisionId) {
|
|
756
|
+
const row = this.state.connection
|
|
757
|
+
.prepare("SELECT response_hash AS recordHash FROM continuations WHERE decision_id = ?")
|
|
758
|
+
.get(decisionId);
|
|
759
|
+
return isContinuationRow(row) ? row : undefined;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function digestBuffer(value) {
|
|
763
|
+
const hex = value.startsWith("sha256:") ? value.slice(7) : value;
|
|
764
|
+
if (!/^[a-f0-9]{64}$/i.test(hex))
|
|
765
|
+
throw new Error("Expected a SHA-256 digest");
|
|
766
|
+
return Buffer.from(hex, "hex");
|
|
767
|
+
}
|
|
768
|
+
function channelIdFor(channel) {
|
|
769
|
+
return `channel-${digestHex(channel).slice(0, 40)}`;
|
|
770
|
+
}
|
|
771
|
+
function sameHumanAnswer(left, right) {
|
|
772
|
+
return (left.provenance === "human" &&
|
|
773
|
+
right.provenance === "human" &&
|
|
774
|
+
left.idempotencyKey === right.idempotencyKey &&
|
|
775
|
+
canonicalJson(left.response) === canonicalJson(right.response) &&
|
|
776
|
+
canonicalJson(left.source) === canonicalJson(right.source));
|
|
777
|
+
}
|
|
778
|
+
function channelMessageStatus(value) {
|
|
779
|
+
if (value.state === "confirmed")
|
|
780
|
+
return "confirmed";
|
|
781
|
+
if (value.state === "failed")
|
|
782
|
+
return "failed";
|
|
783
|
+
if (value.state === "unknown")
|
|
784
|
+
return "ambiguous";
|
|
785
|
+
return "pending";
|
|
786
|
+
}
|
|
787
|
+
function isResourceIdRow(value) {
|
|
788
|
+
return isRecordValue(value);
|
|
789
|
+
}
|
|
790
|
+
function isPresentRow(value) {
|
|
791
|
+
return isRecordValue(value);
|
|
792
|
+
}
|
|
793
|
+
function isRequestHashRow(value) {
|
|
794
|
+
return isRecordValue(value);
|
|
795
|
+
}
|
|
796
|
+
function isDeadlineRow(value) {
|
|
797
|
+
return isRecordValue(value);
|
|
798
|
+
}
|
|
799
|
+
function isCandidateHashRow(value) {
|
|
800
|
+
return isRecordValue(value);
|
|
801
|
+
}
|
|
802
|
+
function isContentHashRow(value) {
|
|
803
|
+
return isRecordValue(value);
|
|
804
|
+
}
|
|
805
|
+
function isContinuationRow(value) {
|
|
806
|
+
return isRecordValue(value);
|
|
807
|
+
}
|
|
808
|
+
function isDecisionResourceRow(value) {
|
|
809
|
+
return isRecordValue(value);
|
|
810
|
+
}
|
|
811
|
+
function isResolutionRow(value) {
|
|
812
|
+
return isRecordValue(value);
|
|
813
|
+
}
|
|
814
|
+
function isEffectIdentityRecord(value) {
|
|
815
|
+
return isRecordValue(value);
|
|
816
|
+
}
|
|
817
|
+
function isRevisionRecord(value) {
|
|
818
|
+
return isRecordValue(value);
|
|
819
|
+
}
|
|
820
|
+
function isOwnerRow(value) {
|
|
821
|
+
return isRecordValue(value);
|
|
822
|
+
}
|
|
823
|
+
function isRecordValue(value) {
|
|
824
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
395
825
|
}
|
|
396
826
|
export function createHumanDecisionAttemptId() {
|
|
397
827
|
return randomUUID();
|
|
@@ -505,43 +935,6 @@ function assertJsonValue(value, label) {
|
|
|
505
935
|
throw new Error(`${label} must be JSON-serializable`);
|
|
506
936
|
}
|
|
507
937
|
}
|
|
508
|
-
async function writeImmutableJson(filePath, value, requireIdentical = true) {
|
|
509
|
-
const bytes = `${canonicalJson(value)}\n`;
|
|
510
|
-
const directory = path.dirname(filePath);
|
|
511
|
-
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
512
|
-
const temporary = path.join(directory, `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
513
|
-
const handle = await fs.open(temporary, "wx", 0o600);
|
|
514
|
-
try {
|
|
515
|
-
await handle.writeFile(bytes, "utf8");
|
|
516
|
-
await handle.sync();
|
|
517
|
-
}
|
|
518
|
-
finally {
|
|
519
|
-
await handle.close();
|
|
520
|
-
}
|
|
521
|
-
try {
|
|
522
|
-
await fs.link(temporary, filePath);
|
|
523
|
-
const directoryHandle = await fs.open(directory, "r");
|
|
524
|
-
try {
|
|
525
|
-
await directoryHandle.sync();
|
|
526
|
-
}
|
|
527
|
-
finally {
|
|
528
|
-
await directoryHandle.close();
|
|
529
|
-
}
|
|
530
|
-
return "created";
|
|
531
|
-
}
|
|
532
|
-
catch (error) {
|
|
533
|
-
if (error.code !== "EEXIST")
|
|
534
|
-
throw error;
|
|
535
|
-
const existing = await fs.readFile(filePath, "utf8");
|
|
536
|
-
if (requireIdentical && existing !== bytes) {
|
|
537
|
-
throw new Error(`Immutable human decision record conflicts: ${filePath}`);
|
|
538
|
-
}
|
|
539
|
-
return "adopted";
|
|
540
|
-
}
|
|
541
|
-
finally {
|
|
542
|
-
await fs.unlink(temporary).catch(() => undefined);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
938
|
function validateExpiry(value, createdAt) {
|
|
546
939
|
if (value === undefined)
|
|
547
940
|
return undefined;
|
|
@@ -554,30 +947,4 @@ function validateExpiry(value, createdAt) {
|
|
|
554
947
|
}
|
|
555
948
|
return new Date(parsed).toISOString();
|
|
556
949
|
}
|
|
557
|
-
async function readJsonDirectory(directory) {
|
|
558
|
-
let names;
|
|
559
|
-
try {
|
|
560
|
-
names = await fs.readdir(directory);
|
|
561
|
-
}
|
|
562
|
-
catch (error) {
|
|
563
|
-
if (error.code === "ENOENT")
|
|
564
|
-
return [];
|
|
565
|
-
throw error;
|
|
566
|
-
}
|
|
567
|
-
const values = await Promise.all(names
|
|
568
|
-
.filter((name) => name.endsWith(".json"))
|
|
569
|
-
.sort()
|
|
570
|
-
.map(async (name) => await readJson(path.join(directory, name))));
|
|
571
|
-
return values.filter((value) => value !== null);
|
|
572
|
-
}
|
|
573
|
-
async function readJson(filePath) {
|
|
574
|
-
try {
|
|
575
|
-
return JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
576
|
-
}
|
|
577
|
-
catch (error) {
|
|
578
|
-
if (error.code === "ENOENT")
|
|
579
|
-
return null;
|
|
580
|
-
throw error;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
950
|
//# sourceMappingURL=human-decision.js.map
|