nx_toolcall_idem_candidate_t190.nx
buildroot/runtime/nx_toolcall_idem_candidate_t190.nx
about
nx_toolcall_idem_lib.nx -- IDEMPOTENCY KEYS ON tools/call (/compare/dataio DI4 `ta_idempotency_key`, 2026-09-05).
WHY: every seat's write to the estate crosses ONE transport, and that transport answers "Outcome Unknown" whenever
the backend accepted a request and produced no reply inside the edge window. MEASURED the day this landed: nine
such replies in one seat session, each adjudicated by hand by re-reading the artifact, and two verb shapes with NO
safe retry at all (a self-anchored insert applies twice; an append has no anchor to lose). The field's answer is the
IETF Idempotency-Key: the caller names the request, the server records the FIRST outcome under that name, and a
retry carrying the same name is REPLAYED, never re-executed. Done sovereign, on the lane every seat already uses:
params._idem = "<token>" (a sibling of _cap and _async; absent = the call behaves exactly as before)
THE OUTCOME IS RESERVED BEFORE IT IS EXECUTED. A key is claimed with O_EXCL on <dir>/idem_<digest>.claim -- the
same primitive the job lane reserves ids with -- so two racing calls under one key cannot both execute: the loser
reads IN-FLIGHT. The first APPLIED outcome is then recorded in the estate's append-only ledger
(nx_apistack_idempotency: first-write-wins, read whole) as ONE line:
lane=async|promote tool=<t> job=<id> the job id is the outcome; the caller polls it like any JOB-STARTED
lane=sync|http tool=<t> exit=<ec> bytes=<n> out=<dir>/idem_<digest>.out the bytes are kept so the replay is the same text
HONESTY LIMITS, stated: a claim whose holder died before recording (the daemon killed between reserve and record)
reads IN-FLIGHT for as long as the claim exists; the reply names the claim's age so the caller can adjudicate the
artifact -- today's situation, but confined to that one window instead of every call. A refused or harness-failed
run RELEASES its claim: nothing was applied, so a retry must be judged afresh (the same rule nx_fsops_write keeps).
Every path is parameterised by (ledger, dir) so the gate drives it on /tmp fixtures; the API passes the consts.
license_tier: ORIGINAL No hw writes (Rule 26).
dependencies 3 imports · 1 importers
imports: nx_syscalls.nxnx_apistack_idempotency_candidate_t190.nxnx_sha256.nx
imported by: nx_tools_api_idem_candidate_t190.nx
structs
| none |
consts
| 26 | const TI_LEDGER: *u8 = "knowledge/status/toolcall_idem.jrnl" as *u8 |
| 27 | const TI_DIR: *u8 = "_jobs" as *u8 |
| 28 | const TI_KEY_MIN_CH: i64 = 33 // printable ASCII, no whitespace: a TAB or newline would corrupt the ledger row |
| 29 | const TI_KEY_MAX_CH: i64 = 126 |
| 30 | const TI_KEY_MAX_LEN: i64 = 200 // a key is a token, not a payload; the ledger row must stay one line |
| 31 | const TI_HEX_CH: i64 = 16 // 64 bits of the key's SHA-256 name the claim and the replay artifact |
| 32 | const TI_HEX_BYTES: i64 = 8 |
| 33 | const TI_ROW_CAP: i64 = 1024 |
| 34 | const TI_PATH_CAP: i64 = 512 |
| 35 | const TI_DIGEST_B: i64 = 32 |
| 36 | const TI_SYS_OPENAT: i64 = 257 |
| 37 | const TI_SYS_UNLINKAT: i64 = 263 |
| 38 | const TI_AT_FDCWD: i64 = 0 - 100 |
| 39 | const TI_OEXCL: i64 = 193 // O_CREAT|O_EXCL|O_WRONLY |
| 40 | const TI_CLAIMMODE: i64 = 420 |
| 43 | const TI_NOKEY: i64 = 0 // no _idem present: the call behaves exactly as before |
| 44 | const TI_BADKEY: i64 = 1 // present but not a token: REFUSE before executing |
| 45 | const TI_NEW: i64 = 2 // reserved by this call: execute, then record |
| 46 | const TI_REPLAY: i64 = 3 // recorded: emit the first outcome, do NOT execute |
| 47 | const TI_EVIDENCE_ERROR:i64=5 // retained ledger is unreadable, malformed, missing or result exceeds caller extent |
| 48 | const TI_INFLIGHT: i64 = 4 // reserved by another call that has not recorded yet: do NOT execute, say so |
functions
| 50 | func ti_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } |
| 51 | func ti_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; var oo: i64 = o; while s[i] != (0 as u8) { d[oo] = s[i]; oo = oo + 1; i = i + 1 } d[oo] = 0 as u8; return oo } |
| 52 | func ti_catb(d: *u8, o: i64, s: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { d[o + i] = s[i]; i = i + 1 } d[o + n] = 0 as u8; return o + n } |
| 53 | func ti_catn(d: *u8, o: i64, v: i64) -> i64 |
| 65 | func ti_hexd(v: i64) -> i64 { if v < 10 { return 48 + v } return 87 + v } called by 1: ti_key_hex |
| 66 | func ti_is_digit(c: i64) -> i64 { if c >= 48 { if c <= 57 { return 1 } } return 0 } called by 1: ti_row_int |
| 67 | func ti_state_name(st: i64) -> *u8 |
| 77 | func ti_key_ok(key: *u8, n: i64) -> i64 called by 1: ti_classify |
| 90 | func ti_key_hex(key: *u8, n: i64, out: *u8) -> i64 |
| 103 | func ti_claim_path(dir: *u8, hex: *u8, out: *u8) -> i64 { var o: i64 = ti_cat(out, 0, dir); o = ti_cat(out, o, "/idem_" as *u8); o = ti_cat(out, o, hex); o = ti_cat(out, o, ".claim" as *u8); return o } |
| 104 | func ti_out_path(dir: *u8, hex: *u8, out: *u8) -> i64 { var o: i64 = ti_cat(out, 0, dir); o = ti_cat(out, o, "/idem_" as *u8); o = ti_cat(out, o, hex); o = ti_cat(out, o, ".out" as *u8); return o } |
| 108 | func ti_reserve(dir: *u8, hex: *u8, now: i64) -> i64 |
| 123 | func ti_release(dir: *u8, hex: *u8) -> i64 |
| 131 | func ti_row_int(row: *u8, needle: *u8) -> i64 |
| 158 | func ti_row_has(row: *u8, needle: *u8) -> i64 calls 1: ti_slen |
| 173 | func ti_claim_age(dir: *u8, hex: *u8, now: i64) -> i64 |
| 188 | func ti_classify(ledger: *u8, dir: *u8, key: *u8, n: i64, hex_out: *u8, row_out: *u8, rowcap: i64, now: i64) -> i64 |
| 206 | func ti_row_job(row: *u8, cap: i64, lane: *u8, tool: *u8, tl: i64, jid: i64) -> i64 |
| 215 | func ti_row_sync(row: *u8, cap: i64, lane: *u8, tool: *u8, tl: i64, ec: i64, bytes: i64, dir: *u8, hex: *u8) -> i64 |
| 231 | func ti_record(ledger: *u8, key: *u8, n: i64, row: *u8, rl: i64) -> i64 { return id_record(ledger, key, n, row, rl) } |