@vincemakes/kiso-core 0.15.9 → 0.15.11
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/kernel/loop.js +89 -18
- package/package.json +2 -2
package/dist/kernel/loop.js
CHANGED
|
@@ -193,18 +193,37 @@ export async function* loop(config) {
|
|
|
193
193
|
// the launch records it and the loop re-throws it after the settle
|
|
194
194
|
// (same propagation the sequential execute had).
|
|
195
195
|
let launchError = null;
|
|
196
|
+
/** Drain until every launch settles: the receipts land (the write-ahead
|
|
197
|
+
* acks resolve during the pump); the 10ms poll covers mid-handler gaps
|
|
198
|
+
* and the ask pause — never a busy spin, never a deadlock on an ack.
|
|
199
|
+
* One settle semantics, two callers: the turn settle and F4's abandon. */
|
|
200
|
+
const drainSettled = async function* () {
|
|
201
|
+
while (execActive > 0) {
|
|
202
|
+
yield* drainExec();
|
|
203
|
+
await sleep(10);
|
|
204
|
+
}
|
|
205
|
+
yield* drainExec();
|
|
206
|
+
};
|
|
196
207
|
let violated = false;
|
|
197
208
|
// The violated signal: rejects when the turn is voided — the paused
|
|
198
209
|
// ask-branches bail (abort semantics for not-started executions). Typed
|
|
199
210
|
// `never` so the ask race resolves to the human decision alone.
|
|
211
|
+
// F4: PER-ATTEMPT state now — a mid-stream retry voids one attempt and
|
|
212
|
+
// continues, so the signal re-arms after each abandon (pre-F4 a void
|
|
213
|
+
// always ended the run and one signal per run sufficed).
|
|
200
214
|
let violatedReject = () => { };
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
215
|
+
let violatedP;
|
|
216
|
+
const armVoidSignal = () => {
|
|
217
|
+
violated = false;
|
|
218
|
+
violatedP = new Promise((_, reject) => {
|
|
219
|
+
violatedReject = () => reject();
|
|
220
|
+
});
|
|
221
|
+
// The turn's ask-branches race it; a turn with NO ask leaves the
|
|
222
|
+
// rejection un-consumed — the no-op keeps it from surfacing as an
|
|
223
|
+
// unhandled rejection while the race consumers still receive it.
|
|
224
|
+
void violatedP.catch(() => { });
|
|
225
|
+
};
|
|
226
|
+
armVoidSignal();
|
|
208
227
|
// ── EC-1 ① — the DURABLE TURN COMMIT gate ──────────────────────────────
|
|
209
228
|
// Invariant 3 (COMMIT GATING): a commit-required handler never starts
|
|
210
229
|
// before this turn's stop is DURABLE. The gate RESOLVES exactly once per
|
|
@@ -466,8 +485,9 @@ export async function* loop(config) {
|
|
|
466
485
|
// EC-1 ⑤ (the live void): the last durable event BEFORE this turn's
|
|
467
486
|
// model output — the previous turn's stop, the user input, or a
|
|
468
487
|
// microcompact boundary. Everything after it is this turn's draft,
|
|
469
|
-
// which is exactly the range a void must abandon.
|
|
470
|
-
|
|
488
|
+
// which is exactly the range a void must abandon. F4: `let` — a
|
|
489
|
+
// mid-stream retry moves the boundary to its abandonment marker.
|
|
490
|
+
let turnStart = log.lastSeq;
|
|
471
491
|
// EC-1 ①: this turn's commit gate — reset BEFORE any call can launch.
|
|
472
492
|
committed = false;
|
|
473
493
|
turnSettled = new Promise((res) => {
|
|
@@ -563,13 +583,70 @@ export async function* loop(config) {
|
|
|
563
583
|
return;
|
|
564
584
|
}
|
|
565
585
|
const structured = toStructuredError(err);
|
|
566
|
-
// Phase B: never
|
|
567
|
-
//
|
|
586
|
+
// Phase B: never SILENTLY re-stream a turn that already emitted
|
|
587
|
+
// content — duplicates are worse than failures. Nothing streamed
|
|
588
|
+
// yet: the cheap in-place retry (no draft exists to void).
|
|
568
589
|
if (structured.retryable && !streamed && attempts < maxRetries) {
|
|
569
590
|
attempts += 1;
|
|
570
591
|
await sleep(attempts * 250, signal); // abortable backoff
|
|
571
592
|
continue;
|
|
572
593
|
}
|
|
594
|
+
// F4 — ABANDON HYGIENE, every streamed exit: the attempt settles
|
|
595
|
+
// (parked launches bail on the un-committed gate, started ones
|
|
596
|
+
// land their receipts), the draft is durably voided, and only
|
|
597
|
+
// THEN a retry or the terminal. Pre-F4 this path returned with
|
|
598
|
+
// the draft un-voided under the terminal — the next request
|
|
599
|
+
// projected it as committed history (ADR-0047 Gap B, live), and
|
|
600
|
+
// a dangling tool_call_end fed the provider-400 class (EC1-F1).
|
|
601
|
+
settleTurn();
|
|
602
|
+
violated = true;
|
|
603
|
+
violatedReject();
|
|
604
|
+
yield* drainSettled();
|
|
605
|
+
if (launchError !== null)
|
|
606
|
+
throw launchError;
|
|
607
|
+
// Invariant 3 makes a started commit-required execution in the
|
|
608
|
+
// draft structurally impossible (the turn never committed) —
|
|
609
|
+
// CHECKED, not assumed: if one ever exists, fall back to the
|
|
610
|
+
// pre-F4 abandon (no marker over a started fact, no retry).
|
|
611
|
+
const unsafeStarted = log.all.some((e) => e.type === "tool_execution_started" && e.seq > turnStart && registry.get(e.name)?.effects?.precommitSafe !== true);
|
|
612
|
+
if (log.lastSeq > turnStart && !unsafeStarted) {
|
|
613
|
+
// The loop's THIRD producer of `model_output_abandoned` (an
|
|
614
|
+
// existing variant — no new protocol surface): broader than
|
|
615
|
+
// the live void's dangling-call condition, because the SAME
|
|
616
|
+
// process re-requests immediately and an un-voided text
|
|
617
|
+
// draft would glue onto the retried stream's projection.
|
|
618
|
+
const marker = log.append({
|
|
619
|
+
type: "model_output_abandoned",
|
|
620
|
+
voidFromSeq: turnStart,
|
|
621
|
+
reason: "the provider stream failed before this turn committed",
|
|
622
|
+
});
|
|
623
|
+
if (hooks.onEvent)
|
|
624
|
+
await hooks.onEvent(marker, {}).catch(() => { });
|
|
625
|
+
yield marker;
|
|
626
|
+
}
|
|
627
|
+
// F4 — the mid-stream retry: same classification, same per-turn
|
|
628
|
+
// budget (ADR-0005 Amendment 1: frame state, per-process).
|
|
629
|
+
if (structured.retryable && attempts < maxRetries && !unsafeStarted) {
|
|
630
|
+
attempts += 1;
|
|
631
|
+
await sleep(attempts * 250, signal); // the same abortable backoff
|
|
632
|
+
// Fresh per-attempt state — the marker is the boundary now.
|
|
633
|
+
pending.length = 0;
|
|
634
|
+
lastStop = undefined;
|
|
635
|
+
stopCount = 0;
|
|
636
|
+
sawStop = false;
|
|
637
|
+
postStopViolation = false;
|
|
638
|
+
forgedEvent = false;
|
|
639
|
+
duplicateStop = false;
|
|
640
|
+
heldStop = null;
|
|
641
|
+
streamed = false;
|
|
642
|
+
committed = false;
|
|
643
|
+
turnSettled = new Promise((res) => {
|
|
644
|
+
settleTurn = res;
|
|
645
|
+
});
|
|
646
|
+
armVoidSignal();
|
|
647
|
+
turnStart = log.lastSeq;
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
573
650
|
yield await terminal({ kind: "error", error: structured });
|
|
574
651
|
return;
|
|
575
652
|
}
|
|
@@ -699,13 +776,7 @@ export async function* loop(config) {
|
|
|
699
776
|
violated = true;
|
|
700
777
|
violatedReject();
|
|
701
778
|
}
|
|
702
|
-
|
|
703
|
-
for await (const q of drainExec())
|
|
704
|
-
yield q;
|
|
705
|
-
await sleep(10);
|
|
706
|
-
}
|
|
707
|
-
for await (const q of drainExec())
|
|
708
|
-
yield q;
|
|
779
|
+
yield* drainSettled();
|
|
709
780
|
if (launchError !== null)
|
|
710
781
|
throw launchError;
|
|
711
782
|
if (voided !== null) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-core",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.11",
|
|
4
4
|
"description": "kiso (foundation) core \u2014 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.15.
|
|
36
|
+
"@vincemakes/kiso-evals": "0.15.11",
|
|
37
37
|
"@types/node": "^26.1.2",
|
|
38
38
|
"typescript": "^5.7.2",
|
|
39
39
|
"vitest": "^3.0.0"
|