@decidio/sdk 0.4.0 → 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/inngest.d.ts CHANGED
@@ -31,13 +31,21 @@ export interface InngestClientLike {
31
31
  * `confirmation` is CARRIED rather than discarded. It used to be thrown away inside the confirm
32
32
  * step, which made "it ran and Decidio recorded it" and "it ran and Decidio does not know"
33
33
  * indistinguishable to the caller. */
34
- export type GateStepOutcome<R> = {
35
- status: "executed" | "rejected" | "blocked";
34
+ export type GateStepOutcome<R> =
35
+ /** The action ran: the value AND what Decidio recorded (the step confirms before it returns). Encoded in the
36
+ * type, not only in prose (prodsec V040-02): narrow on `status` and `confirmation` is there. */
37
+ {
38
+ status: "executed";
36
39
  decisionId: string;
37
- value?: R;
38
- /** Always present when `status` is "executed" (the step confirms before it returns); optional in the type
39
- * because the other statuses carry none. */
40
- confirmation?: EvidenceOutcome;
40
+ value: R;
41
+ confirmation: EvidenceOutcome;
42
+ }
43
+ /** The action never ran — a human said no, or a rule blocked it — so there is nothing to confirm. */
44
+ | {
45
+ status: "rejected" | "blocked";
46
+ decisionId: string;
47
+ value?: undefined;
48
+ confirmation?: undefined;
41
49
  };
42
50
  /**
43
51
  * Gate an action INSIDE an Inngest function, with durable suspend on routing.
package/dist/resume.d.ts CHANGED
@@ -455,7 +455,13 @@ 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
- export type ResumeStatus = "executed" | "rejected" | "blocked" | "no_handler" | "unknown" | "indeterminate" | "already_resolved";
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
+ * parked for a later signal. It used to read `unknown` (the word for "not ours") for the claim loser and
462
+ * `no_handler` for the boundary loser although a handler was registered (prodsec V040-01). A loss to a FINAL
463
+ * answer (a denial, an execution) stays `already_resolved`. Additive, like the two above. */
464
+ export type ResumeStatus = "executed" | "rejected" | "blocked" | "no_handler" | "unknown" | "indeterminate" | "already_resolved" | "lost_race";
459
465
  /** The states a durable action can actually be in — every one of them nameable, including the
460
466
  * one nobody likes. */
461
467
  export type RecoveryState =
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");
@@ -951,7 +967,7 @@ export function createDecidioResume(opts) {
951
967
  if (refusalWarned.has(decisionId))
952
968
  return;
953
969
  refusalWarned.add(decisionId);
954
- console.error(msg + (tail ?? " (This prints once — the entry stays parked until you act.)"));
970
+ console.error(msg + (tail ?? " (This prints once per worker process — the entry stays parked until you act.)"));
955
971
  };
956
972
  const ctrl = {
957
973
  ...opts,
@@ -991,8 +1007,12 @@ export function createDecidioResume(opts) {
991
1007
  if (idProblem)
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
- if (!pending)
1010
+ if (!pending) {
1011
+ const mapped = outcomeFromHead(await ctrl.store.getExecution(decisionId), decisionId);
1012
+ if (mapped)
1013
+ return finish(mapped);
995
1014
  return { status: "unknown", decisionId };
1015
+ }
996
1016
  if (verdict === "rejected" || verdict === "blocked") {
997
1017
  await ctrl.store.markDiscarded(decisionId, verdict, reason);
998
1018
  await ctrl.store.delete(decisionId);
@@ -1036,7 +1056,7 @@ export function createDecidioResume(opts) {
1036
1056
  if (claim.state && NON_REPLAYABLE.includes(claim.state)) {
1037
1057
  refuseOnce(decisionId, claim.state === "discarded"
1038
1058
  ? `[decidio] REFUSING to execute ${decisionId}: it was DENIED (${(await ctrl.store.getExecution(decisionId))?.verdict ?? "rejected"}) and that answer is final. Nothing ran. A denial cannot be reconciled or retried — if this action should now proceed, it needs a NEW request and a new approval.`
1039
- : `[decidio] REFUSING to execute ${decisionId}: its durable state is ${claim.state} — invocation already began, so re-running it could duplicate a committed effect. Inspect it with guard.resume.recover(); settle it with guard.resume.reconcile("${decisionId}", …).`, claim.state === "discarded" ? " (This prints once. The decision is closed — no action is needed.)" : undefined);
1059
+ : `[decidio] REFUSING to execute ${decisionId}: its durable state is ${claim.state} — invocation already began, so re-running it could duplicate a committed effect. Inspect it with guard.resume.recover(); settle it with guard.resume.reconcile("${decisionId}", …).`, claim.state === "discarded" ? " (This prints once per worker process. The decision is closed — no action is needed.)" : undefined);
1040
1060
  if (claim.state === "invoking" || claim.state === "indeterminate") {
1041
1061
  return finish({ status: "indeterminate", decisionId, action: pending.action, reason: claim.state === "invoking"
1042
1062
  ? `this decision is in durable state invoking: its action may still be running, may have crashed before any effect, 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}", …).`
@@ -1044,7 +1064,10 @@ export function createDecidioResume(opts) {
1044
1064
  }
1045
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` });
1046
1066
  }
1047
- return { status: "unknown", decisionId };
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` });
1048
1071
  }
1049
1072
  try {
1050
1073
  await ctrl.store.markInvoking(decisionId, attemptId);
@@ -1052,7 +1075,10 @@ export function createDecidioResume(opts) {
1052
1075
  catch (e) {
1053
1076
  await ctrl.store.releasePreinvoke(decisionId, attemptId).catch(() => { });
1054
1077
  console.error(`[decidio] ${decisionId}: could not record the invocation boundary (${e?.message ?? e}) — refusing to run the action rather than run it unrecorded.`);
1055
- return finish({ status: "no_handler", decisionId, action: pending.action, reason: "could not durably record the invocation boundary" });
1078
+ const mapped = outcomeFromHead(await ctrl.store.getExecution(decisionId), decisionId, pending.action);
1079
+ if (mapped)
1080
+ return finish(mapped);
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` });
1056
1082
  }
1057
1083
  let result;
1058
1084
  try {
@@ -1098,7 +1124,7 @@ export function createDecidioResume(opts) {
1098
1124
  }
1099
1125
  catch (e) {
1100
1126
  res.statusCode = 500;
1101
- 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) }));
1102
1128
  }
1103
1129
  });
1104
1130
  };
@@ -1121,7 +1147,7 @@ export function createDecidioResume(opts) {
1121
1147
  return new Response(JSON.stringify(webhookBody(outcome)), { status: 200, headers: { "content-type": "application/json" } });
1122
1148
  }
1123
1149
  catch (e) {
1124
- 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 });
1125
1151
  }
1126
1152
  };
1127
1153
  },
@@ -1294,12 +1320,23 @@ export function createDecidioResume(opts) {
1294
1320
  return items;
1295
1321
  },
1296
1322
  worker(workerOpts) {
1323
+ const corruptWarned = new Set();
1324
+ let listWarned = false;
1297
1325
  const interval = workerOpts?.intervalMs ?? 2_500;
1298
1326
  const deadline = workerOpts?.timeoutMs ? Date.now() + workerOpts.timeoutMs : Infinity;
1299
1327
  let stopped = false;
1300
1328
  (async () => {
1301
1329
  while (!stopped) {
1302
- 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
+ }
1303
1340
  for (const p of pendings) {
1304
1341
  let st;
1305
1342
  try {
@@ -1314,7 +1351,20 @@ export function createDecidioResume(opts) {
1314
1351
  if (!verdict)
1315
1352
  continue;
1316
1353
  try {
1317
- 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
+ }
1318
1368
  if (outcome.status === "executed" && outcome.reportRecorded)
1319
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)"}.`);
1320
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.0",
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.0"
87
+ "@decidio/verify": "0.4.3"
87
88
  },
88
89
  "author": "OmniTwin Technologies Inc.",
89
90
  "bugs": {