@vincemakes/kiso-core 0.25.0 → 0.26.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.
@@ -959,12 +959,76 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
959
959
  // its verdict. A throwing chain counts as ask. Allow/deny are recorded
960
960
  // durably with decidedBy = the deciding extension, never pausing for a
961
961
  // human.
962
- const originalCall = [...log.all].reverse().find((e) => e.type === "tool_call_end" && e.callId === call.callId);
963
- const durable = originalCall !== undefined && JSON.stringify(originalCall.input) === JSON.stringify(call.input)
964
- ? log.all.find((e) => e.type === "permission_decided" && e.decidedBy !== undefined && e.callId === call.callId)
965
- : undefined;
962
+ // CX-1 F1 (audit F1): a durable decision is consumable ONCE — by the
963
+ // invocation it was recorded for, or by that invocation's RE-ISSUE on
964
+ // resume (same callId, same tool, identical input, and the original
965
+ // never produced a result). A provider callId alone is correlation,
966
+ // never identity: the old lookup found the current call itself
967
+ // (already appended), compared its input to its own, and took the
968
+ // FIRST decision with that callId in the whole log — so a reused
969
+ // callId inherited an old allow for a new call.
970
+ //
971
+ // The decision's invocationSeq names the original call (every
972
+ // producer stamps it, round 5). A legacy decision (no invocationSeq,
973
+ // pre-round-5 logs) binds to the nearest PRECEDING same-callId call
974
+ // before it — call first, decision after — with no run boundary
975
+ // between; one that binds to nothing is ambiguous, and the call goes
976
+ // to ask, never to the chain's allow.
977
+ const all = log.all;
978
+ const legacyBinding = (d) => {
979
+ for (let i = all.length - 1; i >= 0; i -= 1) {
980
+ const e = all[i];
981
+ if (e.seq >= d.seq)
982
+ continue;
983
+ if (e.type === "terminal")
984
+ return undefined;
985
+ if (e.type === "tool_call_end" && e.callId === call.callId)
986
+ return e.seq;
987
+ }
988
+ return undefined;
989
+ };
990
+ /** The re-issue rule: a resume re-issues the pending call under the SAME
991
+ * provider id — same callId, same tool, identical input — and the
992
+ * original never completed: no result and no other call under that id
993
+ * stands between the original and this call. A different callId with
994
+ * the same arguments is a new invocation (the /mode-switch shape: a
995
+ * write denied under plan is asked afresh under default). */
996
+ const reissues = (origSeq) => {
997
+ const orig = all.find((e) => e.seq === origSeq);
998
+ if (orig === undefined || orig.type !== "tool_call_end")
999
+ return false;
1000
+ if (orig.callId !== call.callId || orig.name !== call.name)
1001
+ return false;
1002
+ if (JSON.stringify(orig.input) !== JSON.stringify(call.input))
1003
+ return false;
1004
+ for (const e of all) {
1005
+ if (e.seq <= origSeq || e.seq >= call.seq)
1006
+ continue;
1007
+ if ((e.type === "tool_result" || e.type === "tool_call_end") && e.callId === orig.callId)
1008
+ return false;
1009
+ }
1010
+ return true;
1011
+ };
1012
+ let durable;
1013
+ let legacyAmbiguous = false;
1014
+ for (const e of all) {
1015
+ if (e.type !== "permission_decided" || e.decidedBy === undefined)
1016
+ continue;
1017
+ if (e.invocationSeq === undefined && e.callId !== call.callId)
1018
+ continue;
1019
+ const origSeq = e.invocationSeq ?? legacyBinding(e);
1020
+ if (origSeq === undefined) {
1021
+ legacyAmbiguous = true;
1022
+ continue;
1023
+ }
1024
+ if (origSeq === call.seq || reissues(origSeq))
1025
+ durable = e;
1026
+ }
966
1027
  let chainVerdict;
967
- if (durable === undefined && approvalPolicy !== undefined) {
1028
+ if (durable === undefined && legacyAmbiguous) {
1029
+ chainVerdict = { action: "ask" }; // the conservative reading of an old log
1030
+ }
1031
+ else if (durable === undefined && approvalPolicy !== undefined) {
968
1032
  try {
969
1033
  chainVerdict = await raceAbort(Promise.resolve(approvalPolicy.decide(payload, ctx)), signal);
970
1034
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-core",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
4
4
  "description": "kiso (foundation) core — protocol, event log, loop, hooks, modes, permissions, compaction, delivery truth. The 2,000-line kernel at the bottom of the kiso framework.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "openai"
34
34
  ],
35
35
  "devDependencies": {
36
- "@vincemakes/kiso-evals": "0.25.0",
36
+ "@vincemakes/kiso-evals": "0.26.1",
37
37
  "@types/node": "^26.1.2",
38
38
  "typescript": "^5.7.2",
39
39
  "vitest": "^3.0.0"