@decidio/sdk 0.4.1 → 0.4.3
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 +158 -0
- package/README.md +18 -6
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +597 -148
- package/dist/resume.d.ts +3 -3
- package/dist/resume.js +56 -23
- package/package.json +3 -2
package/dist/resume.d.ts
CHANGED
|
@@ -455,9 +455,9 @@ export declare class DecidioSuspendedError extends Error {
|
|
|
455
455
|
* to read `unknown`, the word for "not ours" (prodsec V033-06). An `invoking` row is NOT final and answers
|
|
456
456
|
* `indeterminate` (prodsec V035-02). Both members are additive: an exhaustive switch over this union
|
|
457
457
|
* gains a case per release, which the changelog announces. */
|
|
458
|
-
/** `lost_race` (0.4.1): this signal lost
|
|
459
|
-
*
|
|
460
|
-
*
|
|
458
|
+
/** `lost_race` (0.4.1): this signal lost the pre-invocation claim to ANOTHER ATTEMPT that is still in flight, or
|
|
459
|
+
* the invocation boundary could not be recorded at all (a store fault) - and it did NOTHING. (A boundary lost to
|
|
460
|
+
* an attempt already in flight reads `indeterminate`: that attempt may be running.) Nothing ran on this signal; the winner reports, or the entry stays
|
|
461
461
|
* parked for a later signal. It used to read `unknown` (the word for "not ours") for the claim loser and
|
|
462
462
|
* `no_handler` for the boundary loser although a handler was registered (prodsec V040-01). A loss to a FINAL
|
|
463
463
|
* answer (a denial, an execution) stays `already_resolved`. Additive, like the two above. */
|
package/dist/resume.js
CHANGED
|
@@ -335,8 +335,10 @@ export class FilePendingStore {
|
|
|
335
335
|
try {
|
|
336
336
|
names = readdirSync(this.dir);
|
|
337
337
|
}
|
|
338
|
-
catch {
|
|
339
|
-
|
|
338
|
+
catch (e) {
|
|
339
|
+
if (e?.code === "ENOENT")
|
|
340
|
+
return out;
|
|
341
|
+
throw new DecidioStateCorruptError(key, [`${this.dir}: ${e?.message ?? e}`]);
|
|
340
342
|
}
|
|
341
343
|
}
|
|
342
344
|
for (const name of names) {
|
|
@@ -753,6 +755,20 @@ export class DecidioSuspendedError extends Error {
|
|
|
753
755
|
this.name = "DecidioSuspendedError";
|
|
754
756
|
}
|
|
755
757
|
}
|
|
758
|
+
function outcomeFromHead(head, decisionId, action) {
|
|
759
|
+
if (!head)
|
|
760
|
+
return undefined;
|
|
761
|
+
const base = action ? { decisionId, action } : { decisionId };
|
|
762
|
+
switch (head.state) {
|
|
763
|
+
case "discarded": return { status: "already_resolved", ...base, reason: `this decision was DENIED (${head.verdict ?? "rejected"}); that answer is final and this signal did nothing` };
|
|
764
|
+
case "executed_unreported":
|
|
765
|
+
case "reported": return { status: "already_resolved", ...base, reason: `this decision already ran (durable state ${head.state}); this signal arrived after it and did nothing` };
|
|
766
|
+
case "invoking":
|
|
767
|
+
case "indeterminate": return { status: "indeterminate", ...base, reason: `this decision is in durable state ${head.state}: its action may still be running or may have finished without reporting - it may or may not have run. Nothing will retry it; check the worker and the downstream system, then settle it with reconcile("${decisionId}", …).` };
|
|
768
|
+
case "claimed_preinvoke": return { status: "lost_race", ...base, reason: `another attempt holds the pre-invocation claim on this decision; this signal did nothing. If that attempt is alive it will report; if it died, recover() lists the decision and reconcile() can settle it` };
|
|
769
|
+
default: return undefined;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
756
772
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
757
773
|
function sign(secret, body) {
|
|
758
774
|
return "sha256=" + createHmac("sha256", secret).update(body).digest("hex");
|
|
@@ -992,15 +1008,9 @@ export function createDecidioResume(opts) {
|
|
|
992
1008
|
return { status: "unknown", decisionId: String(decisionId ?? ""), reason: `not a decision id this SDK will act on (${idProblem})` };
|
|
993
1009
|
const pending = await ctrl.store.get(decisionId);
|
|
994
1010
|
if (!pending) {
|
|
995
|
-
const
|
|
996
|
-
if (
|
|
997
|
-
return
|
|
998
|
-
if (head?.state === "executed_unreported" || head?.state === "reported")
|
|
999
|
-
return { status: "already_resolved", decisionId, reason: `this decision already ran (durable state ${head.state}); this signal arrived after it and did nothing` };
|
|
1000
|
-
if (head?.state === "invoking" || head?.state === "indeterminate")
|
|
1001
|
-
return { status: "indeterminate", decisionId, reason: `this decision is in durable state ${head.state}: its action may still be running or may have finished without reporting - it may or may not have run. Nothing will retry it; check the worker and the downstream system, then settle it with reconcile("${decisionId}", …).` };
|
|
1002
|
-
if (head?.state === "claimed_preinvoke")
|
|
1003
|
-
return { status: "lost_race", decisionId, reason: `another attempt holds the pre-invocation claim on this decision; this signal did nothing. If that attempt is alive it will report; if it died, recover() lists the decision and reconcile() can settle it` };
|
|
1011
|
+
const mapped = outcomeFromHead(await ctrl.store.getExecution(decisionId), decisionId);
|
|
1012
|
+
if (mapped)
|
|
1013
|
+
return finish(mapped);
|
|
1004
1014
|
return { status: "unknown", decisionId };
|
|
1005
1015
|
}
|
|
1006
1016
|
if (verdict === "rejected" || verdict === "blocked") {
|
|
@@ -1054,7 +1064,10 @@ export function createDecidioResume(opts) {
|
|
|
1054
1064
|
}
|
|
1055
1065
|
return finish({ status: "already_resolved", decisionId, action: pending.action, reason: claim.state === "discarded" ? `this decision was already DENIED (${(await ctrl.store.getExecution(decisionId))?.verdict ?? "rejected"}); that answer is final and this signal lost the race to it` : `this decision already ran (durable state ${claim.state}); this signal lost the race to it` });
|
|
1056
1066
|
}
|
|
1057
|
-
|
|
1067
|
+
const mapped = outcomeFromHead(await ctrl.store.getExecution(decisionId), decisionId, pending.action);
|
|
1068
|
+
if (mapped)
|
|
1069
|
+
return finish(mapped);
|
|
1070
|
+
return finish({ status: "lost_race", decisionId, action: pending.action, reason: `another attempt holds the pre-invocation claim on this decision${claim.state ? ` (durable state ${claim.state})` : ""}; this signal lost the race and did nothing. If that attempt is alive it will report; if it died, recover() lists the decision and reconcile() can settle it` });
|
|
1058
1071
|
}
|
|
1059
1072
|
try {
|
|
1060
1073
|
await ctrl.store.markInvoking(decisionId, attemptId);
|
|
@@ -1062,13 +1075,9 @@ export function createDecidioResume(opts) {
|
|
|
1062
1075
|
catch (e) {
|
|
1063
1076
|
await ctrl.store.releasePreinvoke(decisionId, attemptId).catch(() => { });
|
|
1064
1077
|
console.error(`[decidio] ${decisionId}: could not record the invocation boundary (${e?.message ?? e}) — refusing to run the action rather than run it unrecorded.`);
|
|
1065
|
-
const
|
|
1066
|
-
if (
|
|
1067
|
-
return finish(
|
|
1068
|
-
if (head?.state === "executed_unreported" || head?.state === "reported")
|
|
1069
|
-
return finish({ status: "already_resolved", decisionId, action: pending.action, reason: `this decision already ran (durable state ${head.state}); this signal lost the race to it and did nothing` });
|
|
1070
|
-
if (head?.state === "invoking" || head?.state === "indeterminate")
|
|
1071
|
-
return finish({ status: "lost_race", decisionId, action: pending.action, reason: `another attempt crossed the invocation boundary first (durable state ${head.state}); this signal did nothing - the winner reports, or reconcile() settles it` });
|
|
1078
|
+
const mapped = outcomeFromHead(await ctrl.store.getExecution(decisionId), decisionId, pending.action);
|
|
1079
|
+
if (mapped)
|
|
1080
|
+
return finish(mapped);
|
|
1072
1081
|
return finish({ status: "lost_race", decisionId, action: pending.action, reason: `could not durably record the invocation boundary (${e?.message ?? e}); nothing ran, the entry stays parked, and a later signal may retry` });
|
|
1073
1082
|
}
|
|
1074
1083
|
let result;
|
|
@@ -1115,7 +1124,7 @@ export function createDecidioResume(opts) {
|
|
|
1115
1124
|
}
|
|
1116
1125
|
catch (e) {
|
|
1117
1126
|
res.statusCode = 500;
|
|
1118
|
-
res.end(JSON.stringify({ error: e
|
|
1127
|
+
res.end(JSON.stringify({ error: e instanceof DecidioStateCorruptError ? "DecidioStateCorruptError" : "resume failed", message: e?.message ?? String(e) }));
|
|
1119
1128
|
}
|
|
1120
1129
|
});
|
|
1121
1130
|
};
|
|
@@ -1138,7 +1147,7 @@ export function createDecidioResume(opts) {
|
|
|
1138
1147
|
return new Response(JSON.stringify(webhookBody(outcome)), { status: 200, headers: { "content-type": "application/json" } });
|
|
1139
1148
|
}
|
|
1140
1149
|
catch (e) {
|
|
1141
|
-
return new Response(JSON.stringify({ error: e
|
|
1150
|
+
return new Response(JSON.stringify({ error: e instanceof DecidioStateCorruptError ? "DecidioStateCorruptError" : "resume failed", message: e?.message ?? String(e) }), { status: 500 });
|
|
1142
1151
|
}
|
|
1143
1152
|
};
|
|
1144
1153
|
},
|
|
@@ -1311,12 +1320,23 @@ export function createDecidioResume(opts) {
|
|
|
1311
1320
|
return items;
|
|
1312
1321
|
},
|
|
1313
1322
|
worker(workerOpts) {
|
|
1323
|
+
const corruptWarned = new Set();
|
|
1324
|
+
let listWarned = false;
|
|
1314
1325
|
const interval = workerOpts?.intervalMs ?? 2_500;
|
|
1315
1326
|
const deadline = workerOpts?.timeoutMs ? Date.now() + workerOpts.timeoutMs : Infinity;
|
|
1316
1327
|
let stopped = false;
|
|
1317
1328
|
(async () => {
|
|
1318
1329
|
while (!stopped) {
|
|
1319
|
-
|
|
1330
|
+
let pendings = [];
|
|
1331
|
+
try {
|
|
1332
|
+
pendings = await ctrl.store.list();
|
|
1333
|
+
}
|
|
1334
|
+
catch (e) {
|
|
1335
|
+
if (!listWarned) {
|
|
1336
|
+
listWarned = true;
|
|
1337
|
+
console.error(`[decidio] the durable store could not be listed (${e?.message ?? e}) — this worker resumes nothing until it can be read again.`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1320
1340
|
for (const p of pendings) {
|
|
1321
1341
|
let st;
|
|
1322
1342
|
try {
|
|
@@ -1331,7 +1351,20 @@ export function createDecidioResume(opts) {
|
|
|
1331
1351
|
if (!verdict)
|
|
1332
1352
|
continue;
|
|
1333
1353
|
try {
|
|
1334
|
-
|
|
1354
|
+
let outcome;
|
|
1355
|
+
try {
|
|
1356
|
+
outcome = await ctrl.resolve({ decisionId: p.decisionId, verdict, reason: st.reason });
|
|
1357
|
+
}
|
|
1358
|
+
catch (e) {
|
|
1359
|
+
if (e instanceof DecidioStateCorruptError) {
|
|
1360
|
+
if (!corruptWarned.has(p.decisionId)) {
|
|
1361
|
+
corruptWarned.add(p.decisionId);
|
|
1362
|
+
console.error(`[decidio] LEDGER CORRUPT for ${p.decisionId}: ${e.message} — this worker will not touch it until the ledger is repaired; the other parked actions still resume.`);
|
|
1363
|
+
}
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1366
|
+
throw e;
|
|
1367
|
+
}
|
|
1335
1368
|
if (outcome.status === "executed" && outcome.reportRecorded)
|
|
1336
1369
|
console.error(`[decidio] resumed ${p.decisionId}: approved → "${p.action}" executed; evidence recorded as ${outcome.evidenceTier}${outcome.executionConfirmed ? "" : " (execution NOT confirmed — see the warning above)"}.`);
|
|
1337
1370
|
else if (outcome.status === "executed")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decidio/sdk",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "One-line approval gate for AI-agent actions. Wrap a risky tool call with guard.protect(); Decidio's policy engine decides proceed | route | block, a human approves routed actions, and every outcome is sealed as a verifiable Authority Receipt.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"@langchain/langgraph": "^1.4.13",
|
|
76
76
|
"@temporalio/workflow": "^1.23.0",
|
|
77
77
|
"@types/node": "^22.20.1",
|
|
78
|
+
"tsx": "^4.23.13",
|
|
78
79
|
"typescript": "^5.9.3"
|
|
79
80
|
},
|
|
80
81
|
"scripts": {
|
|
@@ -83,7 +84,7 @@
|
|
|
83
84
|
"prepublishOnly": "npm run typecheck && npm run build && node ../publish-boundary/check.mjs --only @decidio/sdk"
|
|
84
85
|
},
|
|
85
86
|
"dependencies": {
|
|
86
|
-
"@decidio/verify": "0.4.
|
|
87
|
+
"@decidio/verify": "0.4.3"
|
|
87
88
|
},
|
|
88
89
|
"author": "OmniTwin Technologies Inc.",
|
|
89
90
|
"bugs": {
|