@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/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 a durable transition race to ANOTHER ATTEMPT that is still in flight
459
- * (it holds the pre-invocation claim, or crossed the invocation boundary first), or the boundary could not be
460
- * recorded at all - and it did NOTHING. Nothing ran on this signal; the winner reports, or the entry stays
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
- return out;
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 head = await ctrl.store.getExecution(decisionId).catch(() => undefined);
996
- if (head?.state === "discarded")
997
- return { status: "already_resolved", decisionId, reason: `this decision was DENIED (${head.verdict ?? "rejected"}); that answer is final and this signal did nothing` };
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
- return finish({ status: "lost_race", decisionId, action: pending.action, reason: `another attempt holds the pre-invocation claim on this decision (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` });
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 head = await ctrl.store.getExecution(decisionId).catch(() => undefined);
1066
- if (head?.state === "discarded")
1067
- return finish({ status: "already_resolved", decisionId, action: pending.action, reason: `this decision was DENIED (${head.verdict ?? "rejected"}) while this signal was about to invoke; that answer is final and nothing ran` });
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?.message ?? "resume failed" }));
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?.message ?? "resume failed" }), { status: 500 });
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
- const pendings = await ctrl.store.list().catch(() => []);
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
- const outcome = await ctrl.resolve({ decisionId: p.decisionId, verdict, reason: st.reason });
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.1",
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.1"
87
+ "@decidio/verify": "0.4.3"
87
88
  },
88
89
  "author": "OmniTwin Technologies Inc.",
89
90
  "bugs": {