@vincemakes/kiso-core 0.26.2 → 0.26.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.
@@ -641,15 +641,13 @@ export async function* loop(config) {
641
641
  return;
642
642
  }
643
643
  let structured = toStructuredError(err);
644
- // CX-1 F8 (ruled: the kernel owns retries): the wait is
645
- // max(attempts × 250 ms, the provider's Retry-After), abortable.
646
- // A wait above the cap is never shortened — the run stops with
647
- // an explicit error instead of retrying early.
648
- const retryWait = () => {
649
- const ms = Math.max((attempts + 1) * 250, structured.retryAfterMs ?? 0);
650
- return ms > RETRY_AFTER_MAX_MS ? null : ms;
651
- };
652
- if (structured.retryable && attempts < maxRetries && retryWait() === null) {
644
+ // CX-1 F8 (ruled: the kernel owns retries): the n-th retry waits
645
+ // max(n × 250 ms, the provider's Retry-After), abortable — computed
646
+ // BEFORE `attempts` advances (the 2026-09-07 review's P3: computed
647
+ // after it, the first wait was 500 ms). A wait above the cap is
648
+ // never shortened — the run stops with an explicit error instead.
649
+ const retryWait = () => Math.max((attempts + 1) * 250, structured.retryAfterMs ?? 0);
650
+ if (structured.retryable && attempts < maxRetries && retryWait() > RETRY_AFTER_MAX_MS) {
653
651
  structured = {
654
652
  ...structured,
655
653
  retryable: false,
@@ -660,8 +658,8 @@ export async function* loop(config) {
660
658
  // content — duplicates are worse than failures. Nothing streamed
661
659
  // yet: the cheap in-place retry (no draft exists to void).
662
660
  if (structured.retryable && !streamed && attempts < maxRetries) {
661
+ await sleep(retryWait(), signal); // abortable backoff, Retry-After honored
663
662
  attempts += 1;
664
- await sleep(retryWait() ?? attempts * 250, signal); // abortable backoff, Retry-After honored
665
663
  continue;
666
664
  }
667
665
  // F4 — ABANDON HYGIENE, every streamed exit: settle, drain,
@@ -675,8 +673,8 @@ export async function* loop(config) {
675
673
  // F4 — the mid-stream retry: same classification, same per-turn
676
674
  // budget (ADR-0005 Amendment 1: frame state, per-process).
677
675
  if (structured.retryable && attempts < maxRetries && !unsafeStartedInDraft()) {
676
+ await sleep(retryWait(), signal); // the same abortable backoff, Retry-After honored
678
677
  attempts += 1;
679
- await sleep(retryWait() ?? attempts * 250, signal); // the same abortable backoff, Retry-After honored
680
678
  // Fresh per-attempt state — the marker is the boundary now.
681
679
  pending.length = 0;
682
680
  lastStop = undefined;
@@ -984,14 +982,18 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
984
982
  // its verdict. A throwing chain counts as ask. Allow/deny are recorded
985
983
  // durably with decidedBy = the deciding extension, never pausing for a
986
984
  // human.
987
- // CX-1 F1 (audit F1): a durable decision is consumable ONCE — by the
988
- // invocation it was recorded for, or by that invocation's RE-ISSUE on
989
- // resume (same callId, same tool, identical input, and the original
990
- // never produced a result). A provider callId alone is correlation,
991
- // never identity: the old lookup found the current call itself
992
- // (already appended), compared its input to its own, and took the
993
- // FIRST decision with that callId in the whole log — so a reused
994
- // callId inherited an old allow for a new call.
985
+ // CX-1 F1 (audit F1): a durable decision binds to EXACTLY the
986
+ // invocation it was recorded for — `invocationSeq === call.seq`. A
987
+ // provider callId alone is correlation, never identity: the old lookup
988
+ // took the FIRST decision with that callId in the whole log, so a
989
+ // reused callId inherited an old allow for a new call. The 0.26.1
990
+ // "re-issue" exception (same callId, same input, no result yet) is
991
+ // RETRACTED (the 2026-09-07 review's P1): it let a same-turn duplicate
992
+ // and a voided draft's retry inherit a verdict, and nothing needed it —
993
+ // a committed turn's pending call is executed by the recovery driver
994
+ // under its ORIGINAL seq, and a re-streamed call after a void is a new
995
+ // invocation the chain decides again (a policy is a function; running
996
+ // it twice costs nothing and gives a stateful one its say).
995
997
  //
996
998
  // The decision's invocationSeq names the original call (every
997
999
  // producer stamps it, round 5). A legacy decision (no invocationSeq,
@@ -1012,28 +1014,6 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
1012
1014
  }
1013
1015
  return undefined;
1014
1016
  };
1015
- /** The re-issue rule: a resume re-issues the pending call under the SAME
1016
- * provider id — same callId, same tool, identical input — and the
1017
- * original never completed: no result and no other call under that id
1018
- * stands between the original and this call. A different callId with
1019
- * the same arguments is a new invocation (the /mode-switch shape: a
1020
- * write denied under plan is asked afresh under default). */
1021
- const reissues = (origSeq) => {
1022
- const orig = all.find((e) => e.seq === origSeq);
1023
- if (orig === undefined || orig.type !== "tool_call_end")
1024
- return false;
1025
- if (orig.callId !== call.callId || orig.name !== call.name)
1026
- return false;
1027
- if (JSON.stringify(orig.input) !== JSON.stringify(call.input))
1028
- return false;
1029
- for (const e of all) {
1030
- if (e.seq <= origSeq || e.seq >= call.seq)
1031
- continue;
1032
- if ((e.type === "tool_result" || e.type === "tool_call_end") && e.callId === orig.callId)
1033
- return false;
1034
- }
1035
- return true;
1036
- };
1037
1017
  let durable;
1038
1018
  let legacyAmbiguous = false;
1039
1019
  for (const e of all) {
@@ -1046,7 +1026,7 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
1046
1026
  legacyAmbiguous = true;
1047
1027
  continue;
1048
1028
  }
1049
- if (origSeq === call.seq || reissues(origSeq))
1029
+ if (origSeq === call.seq)
1050
1030
  durable = e;
1051
1031
  }
1052
1032
  let chainVerdict;
@@ -40,9 +40,11 @@ export declare class ToolRegistry {
40
40
  * snapshot at request assembly and dispatches from it: the definition
41
41
  * sent to the model, the schema validated against, the `execute` that
42
42
  * runs and the `effects` the scheduler reads are the SAME captured
43
- * values (a shallow copy of each Tool — extensions replace their Tool
44
- * objects rather than mutating them, and the copy makes that a
45
- * contract). Two sources publishing one name is an error the moment
43
+ * values: the declaration data (schema, effects) is COPIED into the
44
+ * table, the handler stays by reference — an in-place edit to the
45
+ * source after the snapshot cannot reach a request already under way
46
+ * (the 2026-09-07 review's P2: a shallow copy shared the nested
47
+ * objects). Two sources publishing one name is an error the moment
46
48
  * it is observed, naming both owners — never a traversal-order pick;
47
49
  * the one exception is the SAME Tool object reached twice (the 0.1.27
48
50
  * dedup), which is one tool. */
@@ -40,9 +40,11 @@ export class ToolRegistry {
40
40
  * snapshot at request assembly and dispatches from it: the definition
41
41
  * sent to the model, the schema validated against, the `execute` that
42
42
  * runs and the `effects` the scheduler reads are the SAME captured
43
- * values (a shallow copy of each Tool — extensions replace their Tool
44
- * objects rather than mutating them, and the copy makes that a
45
- * contract). Two sources publishing one name is an error the moment
43
+ * values: the declaration data (schema, effects) is COPIED into the
44
+ * table, the handler stays by reference — an in-place edit to the
45
+ * source after the snapshot cannot reach a request already under way
46
+ * (the 2026-09-07 review's P2: a shallow copy shared the nested
47
+ * objects). Two sources publishing one name is an error the moment
46
48
  * it is observed, naming both owners — never a traversal-order pick;
47
49
  * the one exception is the SAME Tool object reached twice (the 0.1.27
48
50
  * dedup), which is one tool. */
@@ -55,7 +57,8 @@ export class ToolRegistry {
55
57
  return; // the same object reached twice — one tool
56
58
  throw new Error(`tool table: '${t.name}' is published by two sources (${prior.owner}, ${owner})`);
57
59
  }
58
- captured.set(t.name, { tool: Object.freeze({ ...t }), original: t, owner });
60
+ const copy = { ...t, parameters: structuredClone(t.parameters), ...(t.effects === undefined ? {} : { effects: Object.freeze(structuredClone(t.effects)) }) };
61
+ captured.set(t.name, { tool: Object.freeze(copy), original: t, owner });
59
62
  };
60
63
  for (const t of this.#tools.values())
61
64
  take(t, "registered");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-core",
3
- "version": "0.26.2",
3
+ "version": "0.26.3",
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.26.2",
36
+ "@vincemakes/kiso-evals": "0.26.3",
37
37
  "@types/node": "^26.1.2",
38
38
  "typescript": "^5.7.2",
39
39
  "vitest": "^3.0.0"