@decidio/sdk 0.1.12 → 0.2.1

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
@@ -1,5 +1,10 @@
1
1
  import type { RequestListener } from "node:http";
2
2
  import { type EvidenceOutcome, type EvidenceTier } from "./evidence.js";
3
+ export declare function signEvidence(decisionId: string, attemptId: string | null, evidence: unknown, agentKey?: string, webhookSecret?: string): {
4
+ kind: "ed25519" | "hmac";
5
+ sig: string;
6
+ } | undefined;
7
+ export declare function verifyEvidence(rec: ExecutionRecord, agentKey?: string, webhookSecret?: string): "ok" | "mismatch" | "unbound" | "unverifiable";
3
8
  /** A routed action parked on the agent side, awaiting human approval. The `args`
4
9
  * are the original call arguments, replayed verbatim when the action resumes. */
5
10
  export interface PendingAction {
@@ -29,59 +34,278 @@ export interface PendingAction {
29
34
  /** Where suspended actions live between request and approval. The default is a
30
35
  * file store; swap in Redis/Postgres/your queue for production. AGENT-SIDE only —
31
36
  * Decidio never sees this. */
37
+ /** The durable execution state machine (v2).
38
+ *
39
+ * parked → claimed_preinvoke → invoking → (executed_unreported | indeterminate) → reported
40
+ *
41
+ * THE ONE RULE: nothing transitions BACK to parked once `invoking` has been written. Deletion
42
+ * is cleanup, never the single-use guarantee — that was the 0.1.x model, and a store whose
43
+ * delete() failed (or silently kept the row) let a fresh process execute an already-committed
44
+ * action a second time. The state IS the guarantee now; a failed delete is a tidiness problem.
45
+ *
46
+ * `indeterminate` is the honest answer to the case an SDK cannot decide: the handler was
47
+ * invoked and then threw. Whether the downstream write committed is not knowable from here, so
48
+ * the action is never automatically retried — it waits for `reconcile()`. */
49
+ export type ExecutionState = "parked" | "claimed_preinvoke" | "invoking" | "executed_unreported" | "indeterminate" | "reported";
50
+ /** Ordered weakest→strongest. Resolution ALWAYS takes the strongest state present for a
51
+ * decision, which is what makes a partially-failed transition safe: if a crash leaves both the
52
+ * old and new markers on disk, the stronger one wins and an executed action can never read
53
+ * back as parked. */
54
+ export declare const EXECUTION_STATES: readonly ExecutionState[];
55
+ /** States from which the handler must NEVER be invoked again automatically. */
56
+ export declare const NON_REPLAYABLE: readonly ExecutionState[];
57
+ export interface ExecutionRecord {
58
+ decisionId: string;
59
+ state: ExecutionState;
60
+ /** The attempt that owns (or last owned) this decision. A fencing token: a write from a
61
+ * stale attempt is refused. */
62
+ attemptId: string | null;
63
+ action?: string;
64
+ /** The canonical reported value — present from `executed_unreported` onward. This is what
65
+ * `reconfirm()` re-sends; it carries no handler binding, so replaying it can only ever
66
+ * re-send a report, never re-run the action. */
67
+ evidence?: unknown;
68
+ /** Whether `evidence` was actually recorded. Separate from `evidence` because `null` is a
69
+ * legitimate reported value (an action whose return could not be mapped still reports a null
70
+ * captured response), so presence cannot be inferred from the value. Python has carried this
71
+ * flag from the start; TypeScript inferring presence from `!== undefined` was a twin
72
+ * divergence waiting to disagree about a null-evidence decision. */
73
+ hasEvidence?: boolean;
74
+ /** Set when the evidence MAPPER failed, so `evidence` is null because nothing could be captured
75
+ * — not because the action returned null. The ledger could not previously tell those apart, and
76
+ * the difference decides whether an operator may fill the value: a null the SDK OBSERVED is a
77
+ * measurement, a null it failed to produce is a gap. */
78
+ evidenceError?: string;
79
+ /** Integrity binding over {decisionId, attemptId, evidence}, written when the evidence is
80
+ * recorded. `reconfirm()` verifies it before re-signing, so a record altered in a shared store
81
+ * is refused rather than attested. Absent when the agent has no key material — the same
82
+ * "unbound" state the parked args can be in, reported the same way. */
83
+ evidenceSig?: {
84
+ kind: "ed25519" | "hmac";
85
+ sig: string;
86
+ };
87
+ /** Set when this row was reconstructed by an upgrade rather than written by a live execution —
88
+ * currently only `"0.1.x report journal"`. Such a row predates evidence binding and can never
89
+ * carry a valid signature, so the strict check exempts it EXPLICITLY. The exemption must be a
90
+ * positive flag: exempting "anything with no signature" would hand an attacker the bypass. */
91
+ migratedFrom?: string;
92
+ /** Why the outcome is unknown — present on `indeterminate`. */
93
+ error?: string;
94
+ /** The tier Decidio recorded — present on `reported`. */
95
+ confirmation?: string;
96
+ /** Set when an OPERATOR settled this decision rather than the SDK observing it. Kept so an
97
+ * audit of the agent-side ledger can tell an observed execution from an asserted one. */
98
+ reconciled?: {
99
+ outcome: SettleOutcome;
100
+ reason?: string;
101
+ at: string;
102
+ };
103
+ at: string;
104
+ }
105
+ export type SettleOutcome = "committed" | "not_committed";
106
+ /** Refuse a reconciliation that would be a fabrication. Shared by every store so the rule cannot
107
+ * drift between implementations — a store that allowed `committed` from `parked` would let an
108
+ * operator mint an execution report for an action nothing ever invoked. */
109
+ /** A decision an operator SETTLED as committed is finished: its report is recorded and sent. A
110
+ * late write from the attempt that was still running would produce a second authority report for
111
+ * one decision, so it is refused here rather than in each store. `not_committed` settlements are
112
+ * deliberately not covered — that outcome exists so a new attempt can run and record. */
113
+ /** The ledger row is owned by a NEWER attempt. Typed for the same reason `DecidioAlreadySettled`
114
+ * is: `reportAfterExecution` must keep going when the ledger fails, so a bare Error here read as
115
+ * "the disk write failed" and the stale attempt went on to POST — a second authority report for
116
+ * one decision. Reachable through the SDK's own documented flow: `reconcile(id, not_committed)`
117
+ * releases a decision whose original attempt is still alive (recover() invites exactly that, since
118
+ * it says the process "died" when it may not have), the release is legitimate and the new attempt
119
+ * SHOULD run — but the old attempt's report must not be sent. */
120
+ export declare class DecidioStaleAttemptError extends Error {
121
+ decisionId: string;
122
+ owner: string | null;
123
+ state: ExecutionState;
124
+ constructor(decisionId: string, owner: string | null, state: ExecutionState);
125
+ }
126
+ export declare class DecidioAlreadySettledError extends Error {
127
+ decisionId: string;
128
+ constructor(decisionId: string);
129
+ }
130
+ export declare const SETTLE_OUTCOMES: readonly SettleOutcome[];
131
+ export declare function assertSettlable(id: string, state: ExecutionState, outcome: SettleOutcome, hasObservedEvidence?: boolean): void;
132
+ export interface ClaimResult {
133
+ /** True only when THIS attempt may proceed to invoke. */
134
+ ok: boolean;
135
+ /** The state that refused the claim, when ok is false — so a caller can tell "another worker
136
+ * has it" from "this already executed and must be reconciled". */
137
+ state?: ExecutionState;
138
+ reason?: string;
139
+ }
140
+ /** Agent-side durable state. The shipped stores are file- and memory-backed; swap in
141
+ * Redis/Postgres/your queue for production. AGENT-SIDE only — Decidio never sees this.
142
+ *
143
+ * v2 (0.2.0) makes the state machine MANDATORY. The 0.1.x contract left `claim()` optional and
144
+ * fell back to a process-local set, which cannot survive a restart and therefore could not
145
+ * carry a single-execution guarantee across processes at all. Every method below must be
146
+ * atomic against concurrent callers — compare-and-swap, a transaction, or an equivalent
147
+ * fencing token. */
32
148
  export interface PendingStore {
33
149
  put(p: PendingAction): Promise<void>;
34
150
  get(decisionId: string): Promise<PendingAction | null>;
151
+ /** CLEANUP ONLY. Removing a row must never be what prevents a second execution. */
35
152
  delete(decisionId: string): Promise<void>;
153
+ /** Parked entries only — anything claimed or beyond is excluded. */
36
154
  list(): Promise<PendingAction[]>;
37
- /** Atomically take ownership of a parked action for execution. Returns true if THIS
38
- * caller claimed it, false if it's already claimed/gone — the cross-process single-use
39
- * guarantee (a production store backs this with rename/SETNX/SELECT-FOR-UPDATE). A
40
- * claimed entry is excluded from list(). Optional: if absent, the controller falls
41
- * back to an in-process guard (same-process only). */
42
- claim?(decisionId: string): Promise<boolean>;
43
- /** Undo a claim so a failed execution is retryable. */
44
- release?(decisionId: string): Promise<void>;
45
- /** Entries currently CLAIMED (excluded from list()). Needed for honest recovery reporting —
46
- * a claimed entry with no journalled result is the indeterminate case. */
47
- listClaimed?(): Promise<PendingAction[]>;
155
+ /** Atomically take ownership for THIS attempt. Must refuse when the decision is already
156
+ * claimed by a live attempt, or is in any NON_REPLAYABLE state. */
157
+ claim(decisionId: string, attemptId: string): Promise<ClaimResult>;
158
+ /** The handler is about to be called. After this point the store must never report the
159
+ * decision as executable again, whatever else fails. */
160
+ markInvoking(decisionId: string, attemptId: string): Promise<void>;
161
+ /** The handler returned. `evidence` is the canonical reported value. */
162
+ recordExecuted(decisionId: string, attemptId: string, evidence: unknown, evidenceSig?: ExecutionRecord["evidenceSig"], evidenceError?: string): Promise<void>;
163
+ /** The handler was invoked and threw: the downstream outcome is unknown. */
164
+ markIndeterminate(decisionId: string, attemptId: string, error: string): Promise<void>;
165
+ /** Decidio accepted the execution report. */
166
+ markReported(decisionId: string, attemptId: string, confirmation: string): Promise<void>;
167
+ /** The authoritative state for one decision, or null if this store has never seen it. */
168
+ getExecution(decisionId: string): Promise<ExecutionRecord | null>;
169
+ /** Every execution record the store holds — the basis for honest recovery reporting. */
170
+ listExecutions(): Promise<ExecutionRecord[]>;
171
+ /** Operator reconciliation — the only legal backwards transition, and the only exit from
172
+ * `indeterminate`. Never called by any automatic path.
173
+ *
174
+ * A CUSTOM STORE MUST RAISE THE TYPED ERRORS at its own fences: `DecidioStaleAttemptError` when
175
+ * a superseded attempt writes, and `DecidioAlreadySettledError` when a second execution report
176
+ * lands on a decision an operator settled as committed. The reporting stage deliberately keeps
177
+ * going when the ledger fails — that is what stops an evidence gap from costing an execution —
178
+ * so a store that raises a generic error for either case sends a duplicate authority report
179
+ * instead of stopping. */
180
+ settle(decisionId: string, outcome: SettleOutcome, opts?: {
181
+ evidence?: unknown;
182
+ reason?: string;
183
+ evidenceSig?: ExecutionRecord["evidenceSig"];
184
+ }): Promise<void>;
185
+ /** Release a claim taken BEFORE invocation, so a pre-invoke failure stays retryable. It is a
186
+ * contract error to call this once `markInvoking` has been written, and a conforming store
187
+ * must refuse it. */
188
+ releasePreinvoke(decisionId: string, attemptId: string): Promise<void>;
48
189
  }
190
+ /** In-memory store. Single-process only BY CONSTRUCTION — it is the right choice for tests and
191
+ * for a worker that owns its queue, and the wrong one for two processes sharing work, which is
192
+ * exactly what `FilePendingStore` (atomic renames on one volume) or a real database is for. */
49
193
  export declare class MemoryPendingStore implements PendingStore {
50
194
  private m;
51
- private claimed;
195
+ private x;
196
+ private rec;
197
+ /** Advance only. A write from a stale attempt, or one that would move the state BACKWARDS,
198
+ * is refused — the in-memory expression of the same rule the file store gets from
199
+ * strongest-wins resolution. */
200
+ private advance;
52
201
  put(p: PendingAction): Promise<void>;
53
202
  get(id: string): Promise<PendingAction | null>;
54
203
  delete(id: string): Promise<void>;
55
204
  list(): Promise<PendingAction[]>;
56
- claim(id: string): Promise<boolean>;
57
- release(id: string): Promise<void>;
58
- listClaimed(): Promise<PendingAction[]>;
205
+ claim(id: string, attemptId: string): Promise<ClaimResult>;
206
+ markInvoking(id: string, attemptId: string): Promise<void>;
207
+ recordExecuted(id: string, attemptId: string, evidence: unknown, evidenceSig?: ExecutionRecord["evidenceSig"], evidenceError?: string): Promise<void>;
208
+ markIndeterminate(id: string, attemptId: string, error: string): Promise<void>;
209
+ markReported(id: string, attemptId: string, confirmation: string): Promise<void>;
210
+ getExecution(id: string): Promise<ExecutionRecord | null>;
211
+ listExecutions(): Promise<ExecutionRecord[]>;
212
+ /** OPERATOR RECONCILIATION — the ONE legal backwards transition, and the only exit from
213
+ * `indeterminate` or a stalled claim. It is legal only because a human asserted the downstream
214
+ * truth this machine cannot observe, so no automatic path may call it.
215
+ *
216
+ * `committed` → `executed_unreported` carrying the operator's evidence, so the ordinary
217
+ * reporting path can send it. The handler is NEVER invoked.
218
+ * `not_committed` → back to `parked`, permitting exactly one more ordinary attempt. */
219
+ settle(id: string, outcome: SettleOutcome, opts?: {
220
+ evidence?: unknown;
221
+ reason?: string;
222
+ evidenceSig?: ExecutionRecord["evidenceSig"];
223
+ }): Promise<void>;
224
+ releasePreinvoke(id: string, attemptId: string): Promise<void>;
59
225
  }
60
226
  /** Restrict a directory to its owner. On Windows POSIX modes are a no-op, so strip inheritance
61
227
  * and grant only the effective user. A FAILURE IS SAID, never swallowed: these files hold the
62
228
  * arguments of actions awaiting approval, and "silently kept the directory's inherited
63
229
  * permissions" is exactly the outcome an operator must not discover from an auditor. */
64
230
  export declare function hardenDir(dir: string): void;
65
- /** File-backed store so a COLD process (the original requester long gone) can still
66
- * resume — the proof that nothing is "kept alive". One JSON file per decision. A claim
67
- * renames `<id>.json` → `<id>.claimed` atomically (same-volume rename), so two processes
68
- * sharing the dir can't both execute. (A stale `.claimed` from a crash mid-execute needs
69
- * TTL recovery — tracked in the backlog.) */
231
+ /** File-backed store so a COLD process (the original requester long gone) can still resume — the
232
+ * proof that nothing is "kept alive". One JSON file per decision holds the parked payload, and
233
+ * one marker file per execution state holds the ledger.
234
+ *
235
+ * A claim is `link()`, not `rename()`: link fails when the target exists, so two processes racing
236
+ * for the same decision produce one winner and one clean refusal, while rename would silently
237
+ * overwrite and let both proceed. (0.1.x renamed `<id>.json` → `<id>.claimed`, and a stale
238
+ * `.claimed` from a crash then needed a TTL nobody had written — that gap is closed by the
239
+ * `claimed_stalled` state and `reconcile()`, which release it explicitly rather than on a timer.
240
+ * A timer would have to guess whether the handler ran.) */
70
241
  export declare class FilePendingStore implements PendingStore {
71
242
  private dir;
72
243
  constructor(dir: string);
73
244
  private key;
74
245
  private path;
75
- private claimedPath;
246
+ /** One marker file per state. The FILENAME carries the state so a scan never has to parse,
247
+ * and the content carries the record. */
248
+ private statePath;
249
+ /** Durable write: temp file in the SAME directory → fsync → atomic rename. A crash can then
250
+ * only leave the old complete file or the new complete file, never a truncated one. The
251
+ * 0.1.x journal wrote straight to its final path with O_TRUNC, so a crash mid-write
252
+ * destroyed the record that proved an action had already run. */
253
+ private writeAtomic;
254
+ /** The authoritative record: the STRONGEST state present on disk. A crash between "write the
255
+ * new marker" and "remove the old one" leaves two markers; taking the strongest means an
256
+ * executed action can never read back as parked, so a partial transition is safe rather than
257
+ * merely unlikely. Corruption is reported, never silently treated as absent. */
258
+ private resolve;
259
+ private setState;
76
260
  put(p: PendingAction): Promise<void>;
77
261
  get(id: string): Promise<PendingAction | null>;
262
+ /** CLEANUP ONLY — the payload goes, the execution record STAYS. That is the whole point of
263
+ * the v2 model: a failed or skipped delete can no longer make an executed action runnable. */
78
264
  delete(id: string): Promise<void>;
79
265
  list(): Promise<PendingAction[]>;
80
- claim(id: string): Promise<boolean>;
81
- release(id: string): Promise<void>;
82
- /** Claimed entries (`.claimed` files) — invisible to list() by design, but a stranded one is
83
- * exactly what recovery must surface. */
84
- listClaimed(): Promise<PendingAction[]>;
266
+ claim(id: string, attemptId: string): Promise<ClaimResult>;
267
+ markInvoking(id: string, attemptId: string): Promise<void>;
268
+ recordExecuted(id: string, attemptId: string, evidence: unknown, evidenceSig?: ExecutionRecord["evidenceSig"], evidenceError?: string): Promise<void>;
269
+ markIndeterminate(id: string, attemptId: string, error: string): Promise<void>;
270
+ markReported(id: string, attemptId: string, confirmation: string): Promise<void>;
271
+ getExecution(id: string): Promise<ExecutionRecord | null>;
272
+ listExecutions(): Promise<ExecutionRecord[]>;
273
+ /** OPERATOR RECONCILIATION — see MemoryPendingStore.settle. Writes the new marker first and
274
+ * then clears every other state file for this decision, including the STRONGER ones: this is
275
+ * the single place where a state legitimately moves backwards, so leaving a stronger marker
276
+ * behind would let resolve()'s strongest-wins rule undo the operator's decision. */
277
+ settle(id: string, outcome: SettleOutcome, opts?: {
278
+ evidence?: unknown;
279
+ reason?: string;
280
+ evidenceSig?: ExecutionRecord["evidenceSig"];
281
+ }): Promise<void>;
282
+ releasePreinvoke(id: string, attemptId: string): Promise<void>;
283
+ /** ONE-WAY MIGRATION from the 0.1.x layout, run once at construction. `<id>.json` → parked;
284
+ * `<id>.claimed` → INDETERMINATE, never parked: a claimed 0.1.x entry may have been mid-flight
285
+ * when the process died, and assuming it did not run is the assumption that duplicates a
286
+ * payment. Nothing ambiguous is made executable by an upgrade. */
287
+ migrateLegacy(): {
288
+ migrated: number;
289
+ indeterminate: number;
290
+ reports: number;
291
+ };
292
+ /** 0.1.x wrote execution reports to `<pendingDir>/reports/<id>.json` — evidence that an action
293
+ * ALREADY RAN and still owes Decidio a report. 0.2.0 keeps that in the ledger, so without this
294
+ * those files become invisible: `recover()` returns nothing and `reconfirm()` answers "nothing
295
+ * is recorded for X … or this process never executed it" about an action that demonstrably
296
+ * did. That is the SDK losing a durable authority fact on upgrade, so the reports are pulled
297
+ * into `executed_unreported` rows where reconfirm() can actually re-send them.
298
+ *
299
+ * The file is CONSUMED (renamed to `.migrated`) rather than deleted: if anything here is
300
+ * wrong, the original bytes are still on disk to inspect. */
301
+ private migrateLegacyReports;
302
+ }
303
+ /** The state on disk could not be read. Never silently treated as "nothing here": an
304
+ * unreadable record is the one that proves an action already ran. */
305
+ export declare class DecidioStateCorruptError extends Error {
306
+ decisionId: string;
307
+ details: string[];
308
+ constructor(decisionId: string, details: string[]);
85
309
  }
86
310
  /** Thrown by a `withApproval`-wrapped call when the action is routed and configured
87
311
  * for async resume: the action is now SUSPENDED (parked in the store), not failed.
@@ -92,29 +316,21 @@ export declare class DecidioSuspendedError extends Error {
92
316
  action: string;
93
317
  constructor(decisionId: string, action: string);
94
318
  }
95
- export type ResumeStatus = "executed" | "rejected" | "blocked" | "no_handler" | "unknown";
96
- /** A durable note that an action ALREADY RAN and what must still be reported. It lives OUTSIDE
97
- * the pending store on purpose: a journal entry carries no handler binding, so replaying it can
98
- * only ever re-send a report — it can never re-run the business action. That is what makes
99
- * recovery safe on stores with and without claim(). */
100
- export interface ExecutionJournalEntry {
101
- decisionId: string;
102
- action: string;
103
- /** The canonical value that was reported — the durable FACT, journalled before the first
104
- * attempt. Deliberately NOT the signed request: both proofs on a confirm are short-lived (the
105
- * identity proof lives 120s, the execution report 5 minutes), so a replayed envelope is
106
- * refused by the server for any outage longer than that — which is every outage worth
107
- * recovering from. `reconfirm` re-signs at SEND time over these same bytes, so the report
108
- * stays replay-safe (it still cannot re-run anything: no handler is journalled) while staying
109
- * usable a week later. */
110
- reported: unknown;
111
- at: string;
112
- }
319
+ /** `indeterminate` (0.2.0): the handler was INVOKED and then threw. Whether the downstream
320
+ * effect committed is not knowable from here, so the action is never retried automatically —
321
+ * it waits for reconcile(). Callers that switch exhaustively on this union must handle it. */
322
+ export type ResumeStatus = "executed" | "rejected" | "blocked" | "no_handler" | "unknown" | "indeterminate";
113
323
  /** The states a durable action can actually be in — every one of them nameable, including the
114
324
  * one nobody likes. */
115
325
  export type RecoveryState =
116
326
  /** Parked, waiting on a human. Nothing has executed. */
117
327
  "parked"
328
+ /** Claimed by an attempt that never reached the invocation boundary, so the handler PROVABLY
329
+ * did not run — but the row is not parked either: `list()` excludes it and every ordinary
330
+ * signal answers `unknown`, so nothing will pick it up on its own. It is stalled until an
331
+ * operator releases it. Reporting this as "parked" (as 0.2.0's first draft did) told a UI an
332
+ * approved action was still queued to resume when in fact it had been silently dropped. */
333
+ | "claimed_stalled"
118
334
  /** Claimed for execution and NOT journalled: the process died somewhere between claiming and
119
335
  * recording the result. The action MAY OR MAY NOT have run — this cannot be resolved locally
120
336
  * and must never be retried automatically. */
@@ -128,6 +344,28 @@ export interface RecoveryItem {
128
344
  /** What a human should do about it, in words. */
129
345
  guidance: string;
130
346
  }
347
+ /** What the SDK knows about THIS execution, handed to a handler that opts in with
348
+ * `withContext: true`.
349
+ *
350
+ * The point is `idempotencyKey`. This SDK can guarantee it will not invoke your handler twice;
351
+ * it cannot guarantee your downstream API only commits once, because that is the vendor's
352
+ * property, not ours. Passing this key to Stripe/Salesforce/your own service is what closes the
353
+ * remaining window — and until now the handler had no way to reach the decision id at all, so
354
+ * the one thing that could close it was the one thing a caller could not get.
355
+ *
356
+ * Opt-in, never inferred from arity. A signature check would have to guess whether a bound
357
+ * method, a *args function or a decorated tool wanted the context, and guessing wrong means
358
+ * passing a context object as someone's first business argument — in a library whose whole job
359
+ * is not doing surprising things to a payment call. */
360
+ export interface ExecutionContext {
361
+ decisionId: string;
362
+ /** The attempt that owns this execution. Null on the inline path, where there is no durable
363
+ * attempt — the value is honest about which door you came through. */
364
+ attemptId: string | null;
365
+ /** Defaults to `decisionId`. Stable across every attempt at the same decision, which is exactly
366
+ * what a downstream idempotency key must be. */
367
+ idempotencyKey: string;
368
+ }
131
369
  export interface ResumeOutcome {
132
370
  status: ResumeStatus;
133
371
  decisionId: string;
@@ -147,6 +385,14 @@ export interface ResumeOutcome {
147
385
  executionReportVerified?: boolean;
148
386
  /** executed only: why the evidence landed where it did, when that is worth explaining. */
149
387
  diagnostic?: string;
388
+ /** The four fields the INLINE door returns. The durable door computed the same outcome and kept
389
+ * three of them for its own prose, so the same question got a field on one path and a sentence
390
+ * on the other — and `reportJournalled` was worst, because the durable path is the one where
391
+ * `reconfirm()` genuinely CAN help and it never said so. */
392
+ executionReportReason?: string;
393
+ confirmationRetryable?: boolean;
394
+ reportJournalled?: boolean;
395
+ evidenceError?: string;
150
396
  }
151
397
  /** Minimal config the controller needs to confirm outcomes + poll status. Shared
152
398
  * with GuardConfig (apiUrl/apiToken). */
@@ -165,8 +411,12 @@ export interface ResumeConfig {
165
411
  /** Shared secret Decidio signs the resume webhook with (HMAC-SHA256). Defaults to
166
412
  * the API token if unset. Verified before any re-execution. */
167
413
  webhookSecret?: string;
168
- /** Where the execution journal lives (default `<pending dir>/reports`). Separate from the
169
- * pending store by design: journal entries can only re-send a report, never re-run an action. */
414
+ /** @deprecated Ignored since 0.2.0 and accepted only so existing callers keep compiling.
415
+ * Execution evidence now lives in the PendingStore's own execution record, beside the state
416
+ * that makes a decision non-replayable. 0.1.x kept it in a separate local directory, so two
417
+ * workers sharing one Redis/Postgres store still held two disjoint journals and neither could
418
+ * reconfirm the other's work — and a store-recorded execution could read back as "nothing has
419
+ * executed" if the local file was lost. One store, one truth. */
170
420
  reportDir?: string;
171
421
  /** Map the action's natural return value to the evidence Decidio records — same option, same
172
422
  * meaning as `protect`'s, so a durable resume reports exactly what the inline path would. */
@@ -182,8 +432,11 @@ export interface ResumeConfig {
182
432
  /** The callback URL Decidio should POST the verdict to (webhook transport). Sent
183
433
  * on /agent/authorize so the decision carries it; omit when using the poll worker. */
184
434
  resumeUrl?: string;
185
- /** Fired after every terminal resolution (executed / rejected / blocked / no_handler),
186
- * via either transport. Use it to log, close a listener, or exit a CLI. */
435
+ /** Fired after every terminal resolution (executed / indeterminate / rejected / blocked /
436
+ * no_handler), via either transport. Use it to log, close a listener, or exit a CLI.
437
+ * `indeterminate` is new in 0.2.0 and DOES fire here: a handler that ran and then threw is a
438
+ * terminal outcome for automation — nothing will retry it — so a listener that only handled
439
+ * the old four would silently miss the one case that needs a human. */
187
440
  onResolved?: (o: ResumeOutcome) => void;
188
441
  fetchImpl?: typeof fetch;
189
442
  }
@@ -194,7 +447,7 @@ export interface ResumeController extends ResumeConfig {
194
447
  /** Register a RAW fn under its action name so a (possibly cold) resume can replay
195
448
  * it. `withApproval` auto-registers on the hot path; a cold worker/service should
196
449
  * register at startup so it can resume actions it never originated. */
197
- register(action: string, fn: (...args: any[]) => Promise<any>, evidence?: (result: any) => unknown): void;
450
+ register(action: string, fn: (...args: any[]) => Promise<any>, evidence?: (result: any) => unknown, withContext?: boolean): void;
198
451
  /** Park a routed action WITH its integrity binding (prefer this over store.put — the
199
452
  * binding is what lets resolve() refuse a tampered payload). */
200
453
  park(p: Omit<PendingAction, "createdAt" | "argsSig">): Promise<void>;
@@ -219,11 +472,27 @@ export interface ResumeController extends ResumeConfig {
219
472
  }): {
220
473
  stop: () => void;
221
474
  };
222
- /** Re-send a journalled execution report, IDEMPOTENTLY. Replays the exact request built before
223
- * the first attempt — it can never re-run the business action, because the journal holds no
224
- * handler binding. Returns the evidence outcome; a decision with nothing journalled returns
475
+ /** Re-send a recorded execution report, IDEMPOTENTLY. Re-signs at send time over the value in
476
+ * the store's execution ledger — it can never re-run the business action, because the ledger
477
+ * holds no handler binding. Returns the evidence outcome; a decision with nothing recorded returns
225
478
  * `reportRecorded: false` with guidance rather than pretending. */
226
- reconfirm(decisionId: string): Promise<EvidenceOutcome>;
479
+ /** `acceptUnbound` is an OPERATOR override for a record whose integrity binding is missing or
480
+ * unverifiable — the case a 0.1.x upgrade legitimately produces. It is a call-site argument on
481
+ * purpose: any flag stored in the record itself would be writable by whoever planted the
482
+ * record. */
483
+ reconfirm(decisionId: string, opts?: {
484
+ acceptUnbound?: boolean;
485
+ }): Promise<EvidenceOutcome>;
486
+ /** Settle a decision an operator has checked downstream — the ONLY exit from `indeterminate`
487
+ * and from a claim that stalled before invocation, and the only backwards transition in the
488
+ * machine. `committed` records and reports the evidence you supply WITHOUT invoking the
489
+ * handler; `not_committed` parks the decision so exactly one more ordinary attempt can run.
490
+ * Never called automatically: the whole point is that a human looked. */
491
+ reconcile(decisionId: string, opts: {
492
+ outcome: SettleOutcome;
493
+ evidence?: unknown;
494
+ reason?: string;
495
+ }): Promise<EvidenceOutcome>;
227
496
  /** Every durable item that needs a human's attention, with its state named — including the one
228
497
  * state that CANNOT be resolved automatically: `indeterminate`, where a process died between
229
498
  * claiming an action and journalling its result, so whether the action ran is unknown locally.
@@ -234,10 +503,15 @@ declare function sign(secret: string, body: string): string;
234
503
  /** Verify Decidio's `x-decidio-signature` over the raw webhook body (HMAC-SHA256).
235
504
  * Exported so engine adapters (e.g. the Inngest bridge) reuse the exact check. */
236
505
  export declare function verifyResumeSignature(secret: string, body: string, header: string | undefined): boolean;
237
- /** The ONE confirm-body rule, shared by every confirm door (resume, inline withApproval,
238
- * adapter gate). Transport success is not confirmation: a 2xx whose body carries no recorded
239
- * tier is a FAILED confirm; a SIGNED confirm coming back below application_confirmed is
240
- * disclosed, never silent. Returns the recorded tier or throws. */
506
+ /** Throwing wrapper over the ONE confirm-body rule, for a caller building their own confirm door.
507
+ * Returns the recorded tier or throws.
508
+ *
509
+ * It now DELEGATES to `readConfirmResponse` rather than re-implementing the check, because the
510
+ * two had drifted into opposite answers: this function accepted `confirmation: "anything"` and
511
+ * handed it back as a recorded tier, while the parser every real door uses treats an
512
+ * unrecognised tier as NOT evidence (closed enum, fail-closed — an answer we do not understand
513
+ * is never evidence). Its own comment claimed it was "shared by every confirm door", and no door
514
+ * called it: the claim was what kept anyone from noticing the disagreement. */
241
515
  export declare function assertConfirmRecorded(body: any, decisionId: string, signed: boolean): string;
242
516
  export declare function createDecidioResume(opts: ResumeConfig & {
243
517
  store: PendingStore;