@vincemakes/kiso-runtime 0.40.1 → 0.40.2

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.
@@ -0,0 +1,18 @@
1
+ import type { StoreRecord } from "./store.js";
2
+ export interface UnansweredCall {
3
+ /** The run that owns the call — the repair rides it, so no new open run appears. */
4
+ readonly runId: string;
5
+ readonly callId: string;
6
+ /** The call's own seq (its invocation). */
7
+ readonly invocationSeq: number;
8
+ }
9
+ /** The result a repaired call receives — the kernel's own words for an
10
+ * abort that lands after the started event. */
11
+ export declare const ABORTED_BEFORE_EXECUTION = "aborted before execution";
12
+ /**
13
+ * Every committed `tool_call_end` with neither a started event nor a
14
+ * result, in a run whose terminal is `aborted`. Committed: a `stop` follows
15
+ * the call inside its turn. Voided model output (a `model_output_abandoned`
16
+ * range) is not a call at all.
17
+ */
18
+ export declare function unansweredAbortedCalls(records: readonly StoreRecord[]): UnansweredCall[];
@@ -0,0 +1,57 @@
1
+ /** The result a repaired call receives — the kernel's own words for an
2
+ * abort that lands after the started event. */
3
+ export const ABORTED_BEFORE_EXECUTION = "aborted before execution";
4
+ /**
5
+ * Every committed `tool_call_end` with neither a started event nor a
6
+ * result, in a run whose terminal is `aborted`. Committed: a `stop` follows
7
+ * the call inside its turn. Voided model output (a `model_output_abandoned`
8
+ * range) is not a call at all.
9
+ */
10
+ export function unansweredAbortedCalls(records) {
11
+ const byRun = new Map();
12
+ for (const r of records) {
13
+ const events = byRun.get(r.runId) ?? [];
14
+ events.push(r.event);
15
+ byRun.set(r.runId, events);
16
+ }
17
+ // Answered = a started event or a result for THIS invocation. Both carry
18
+ // invocationSeq today; a legacy result without it answers the latest
19
+ // earlier call with its callId (providers may reuse a callId across turns).
20
+ const answeredSeqs = new Set();
21
+ const legacy = [];
22
+ for (const r of records) {
23
+ const e = r.event;
24
+ if (e.type !== "tool_result" && e.type !== "tool_execution_started")
25
+ continue;
26
+ const invocationSeq = e.invocationSeq;
27
+ if (invocationSeq !== undefined)
28
+ answeredSeqs.add(invocationSeq);
29
+ else
30
+ legacy.push({ callId: e.callId, seq: e.seq });
31
+ }
32
+ const answered = (call) => answeredSeqs.has(call.seq) || legacy.some((l) => l.callId === call.callId && l.seq > call.seq);
33
+ const out = [];
34
+ for (const [runId, events] of byRun) {
35
+ const terminal = events.find((e) => e.type === "terminal");
36
+ if (terminal === undefined || terminal.type !== "terminal" || terminal.outcome.kind !== "aborted")
37
+ continue;
38
+ const voided = events.flatMap((e) => (e.type === "model_output_abandoned" ? [{ from: e.voidFromSeq, to: e.seq }] : []));
39
+ let turn = [];
40
+ for (const e of events) {
41
+ // an abandoned draft's calls and stop are model output that never was
42
+ if ((e.type === "tool_call_end" || e.type === "stop") && voided.some((v) => e.seq > v.from && e.seq <= v.to))
43
+ continue;
44
+ if (e.type === "tool_call_end")
45
+ turn.push(e);
46
+ else if (e.type === "stop") {
47
+ for (const call of turn)
48
+ if (!answered(call))
49
+ out.push({ runId, callId: call.callId, invocationSeq: call.seq });
50
+ turn = [];
51
+ }
52
+ else if (e.type === "user_input" || e.type === "terminal")
53
+ turn = [];
54
+ }
55
+ }
56
+ return out;
57
+ }
@@ -23,6 +23,17 @@ export interface Tiers {
23
23
  readonly emergency: number;
24
24
  readonly tail: number;
25
25
  }
26
+ /** ADR-0055 Amendment 2 (decision 4): the output an endpoint may grant when
27
+ * neither kiso nor the registry states one. kiso sends no `max_tokens`
28
+ * where it sends none today, so the endpoint's own default applies — and
29
+ * the one default observed is 131,072 (the op gateway's refusal on
30
+ * 2026-09-22: "you requested 131072 output tokens" with no max_tokens
31
+ * sent). Reserving 32K there put the emergency tier 99K too high. */
32
+ export declare const UNKNOWN_MAX_OUTPUT = 131072;
33
+ /** The emergency reserve: what the request asks for when kiso sends a
34
+ * `max_tokens`, else the registry's max output for the served endpoint,
35
+ * else UNKNOWN_MAX_OUTPUT — never below the summary budget. */
36
+ export declare function outputReserve(maxTokens: number | undefined, registryMaxOutput: number | null | undefined, floor: number): number;
26
37
  export declare function tiersFor(windowTokens: number, reserve: number): Tiers;
27
38
  /**
28
39
  * A1 — which tier a measured context is past, the most urgent first; the
@@ -11,6 +11,19 @@
11
11
  */
12
12
  import { DO_NOT_COMPACT, MICROCOMPACTABLE } from "@vincemakes/kiso-core";
13
13
  import { estimateEventTokens } from "./summarize.js";
14
+ /** ADR-0055 Amendment 2 (decision 4): the output an endpoint may grant when
15
+ * neither kiso nor the registry states one. kiso sends no `max_tokens`
16
+ * where it sends none today, so the endpoint's own default applies — and
17
+ * the one default observed is 131,072 (the op gateway's refusal on
18
+ * 2026-09-22: "you requested 131072 output tokens" with no max_tokens
19
+ * sent). Reserving 32K there put the emergency tier 99K too high. */
20
+ export const UNKNOWN_MAX_OUTPUT = 131_072;
21
+ /** The emergency reserve: what the request asks for when kiso sends a
22
+ * `max_tokens`, else the registry's max output for the served endpoint,
23
+ * else UNKNOWN_MAX_OUTPUT — never below the summary budget. */
24
+ export function outputReserve(maxTokens, registryMaxOutput, floor) {
25
+ return Math.max(maxTokens ?? registryMaxOutput ?? UNKNOWN_MAX_OUTPUT, floor);
26
+ }
14
27
  export function tiersFor(windowTokens, reserve) {
15
28
  return {
16
29
  soft: Math.min(0.5 * windowTokens, 400_000),
@@ -0,0 +1,34 @@
1
+ /**
2
+ * ADR-0055 Amendment 2 (decision 3) — the overflow belt: a runtime adapter
3
+ * wrapper, beside the idle and truncation guards.
4
+ *
5
+ * WHY: d7aa's last request carried an anchored 1,047,981 tokens to a model
6
+ * whose window is 1,048,576. The gateway answered `500 Internal server
7
+ * error` — no text a classifier could read — and the kernel, told a 5xx is
8
+ * retryable, re-sent a request that could not fit ten times: the twelve-
9
+ * minute freeze. kiso already KNEW the request could not fit. This belt
10
+ * says so: when a request is sent with the anchored context at or above
11
+ * the stated window minus the output reserve, any provider error it meets
12
+ * is `context_overflow`, not retryable, and the kernel's one compaction and
13
+ * one retry take over.
14
+ *
15
+ * It is a belt, not the cure — the cure is that the context never gets
16
+ * there (decisions 1, 2 and 4). Below the limit nothing changes: a bare
17
+ * 5xx keeps its retries. The limit is measured ONCE, at send time, from a
18
+ * STATED window only; the caller's `measure` returns null when no window is
19
+ * stated (the 200K policy fallback never arms it — keyed on the fallback, a
20
+ * session at 170K on a real 1M model would lose its retries to every
21
+ * transient 5xx) or when no bill anchors the context.
22
+ *
23
+ * A caller's abort passes through untouched: it is not the provider's error.
24
+ */
25
+ import type { Adapter } from "@vincemakes/kiso-core";
26
+ export interface OverflowMeasure {
27
+ /** The anchored context the request carries (the last bill + what was appended since). */
28
+ readonly used: number;
29
+ /** The stated window. */
30
+ readonly window: number;
31
+ /** The output the endpoint may grant (compaction-policy.ts outputReserve). */
32
+ readonly reserve: number;
33
+ }
34
+ export declare function overflowBelt(adapter: Adapter, measure: () => OverflowMeasure | null): Adapter;
@@ -0,0 +1,28 @@
1
+ export function overflowBelt(adapter, measure) {
2
+ return {
3
+ stream: (options) => {
4
+ const m = measure();
5
+ if (m === null || m.used < m.window - m.reserve)
6
+ return adapter.stream(options);
7
+ return belted(adapter, options, m);
8
+ },
9
+ };
10
+ }
11
+ async function* belted(adapter, options, m) {
12
+ try {
13
+ yield* adapter.stream(options);
14
+ }
15
+ catch (err) {
16
+ if (options.signal?.aborted === true || !isStructured(err))
17
+ throw err;
18
+ throw {
19
+ ...err,
20
+ code: "context_overflow",
21
+ retryable: false,
22
+ message: `${err.message} — the request carried ~${m.used} tokens against a stated ${m.window}-token window less a ${m.reserve}-token output reserve; it could not fit, so it is not retried`,
23
+ };
24
+ }
25
+ }
26
+ function isStructured(err) {
27
+ return err !== null && typeof err === "object" && typeof err.code === "string" && typeof err.message === "string";
28
+ }
package/dist/run.js CHANGED
@@ -9,6 +9,7 @@ import { resolveReasoning } from "./provider/metadata.js";
9
9
  import { deriveRecoveryPlan, invocationSeqOf } from "./recovery-plan.js";
10
10
  import { composeApprovalChain, composeSystemPrompt, runBasePrompt } from "./compose.js";
11
11
  import { truncationGuard } from "./truncation-guard.js";
12
+ import { overflowBelt } from "./overflow-belt.js";
12
13
  import { DEFAULT_STREAM_IDLE_MS, idleGuard } from "./idle-guard.js";
13
14
  import { RequestTracer, traceGuard } from "./trace/guard.js";
14
15
  import { runtimeVersion } from "./trace/writer.js";
@@ -145,7 +146,10 @@ export class Run {
145
146
  // LT-1: the idle guard sits closest to the adapter — a stall is
146
147
  // aborted and thrown there, the truncation guard and the tracer see
147
148
  // the same events and the same error every other transport failure shows them.
148
- adapter: traceGuard(tracer, truncationGuard(idleGuard(this.#adapter, this.#config.streamIdleMs ?? DEFAULT_STREAM_IDLE_MS))), // tracer assigned above, before loopConfig
149
+ // ADR-0055 Amendment 2 (decision 3): the overflow belt sits outside
150
+ // the guards, so a stall or a cut on a request known not to fit is
151
+ // classified as the overflow it is, and the tracer records that.
152
+ adapter: traceGuard(tracer, overflowBelt(truncationGuard(idleGuard(this.#adapter, this.#config.streamIdleMs ?? DEFAULT_STREAM_IDLE_MS)), () => this.#session.overflowMeasure())), // tracer assigned above, before loopConfig
149
153
  model: this.#config.model,
150
154
  sessionId: this.#session.id, // P3: tools see their session (ToolContext.sessionId)
151
155
  ...(systemPrompt !== undefined ? { systemPrompt } : {}),
@@ -195,6 +199,9 @@ export class Run {
195
199
  // dangling approvals closed (permission_expired) — a dead
196
200
  // run's approval is never re-presented or resurrected.
197
201
  const records = this.#store.load(this.#session.id);
202
+ // 0.40.2: a dead run's committed calls that never ran are answered
203
+ // before anything below projects the session into a request.
204
+ await this.#session.repairAbortedCalls(records);
198
205
  const runs = new Map();
199
206
  const order = [];
200
207
  for (const r of records) {
@@ -267,10 +274,14 @@ export class Run {
267
274
  // persistence layer — a second open run would be permanently
268
275
  // orphaned (recovery only ever recovers the last one). The
269
276
  // open run is continued via resume(), never by starting another.
270
- const openRun = openRunId(this.#store.load(this.#session.id));
277
+ const records = this.#store.load(this.#session.id);
278
+ const openRun = openRunId(records);
271
279
  if (openRun !== undefined) {
272
280
  throw new Error(`session ${this.#session.id} still has an open run (${openRun}) — resume() it instead of starting a new run`);
273
281
  }
282
+ // 0.40.2: an aborted run's committed calls that never ran are
283
+ // answered before this run's first request projects them.
284
+ await this.#session.repairAbortedCalls(records);
274
285
  // E6 — the run-start context policy: BEFORE this run's
275
286
  // user_input lands, the policy may persist one `summarized`
276
287
  // fact through the existing summarize() path (the manual
package/dist/session.d.ts CHANGED
@@ -28,10 +28,11 @@
28
28
  * same package, same exports (index.ts re-exports all four).
29
29
  */
30
30
  import { EventLog, type AbortSignalLike, type Adapter, type Event, type KisoExtension, type Message, type PermissionDecision, type Tool, type ToolSpec, type EventInput } from "@vincemakes/kiso-core";
31
+ import { type OverflowMeasure } from "./overflow-belt.js";
31
32
  import { type TaskAssessment } from "./task-assessment.js";
32
33
  import { type ContinuationScope } from "@vincemakes/kiso-core";
33
34
  import { type ReasoningSetting } from "./provider/metadata.js";
34
- import { type SessionStore } from "./store.js";
35
+ import { type SessionStore, type StoreRecord } from "./store.js";
35
36
  import { Run } from "./run.js";
36
37
  /** TUI2-R3v2 ③ — one off-trajectory model request (session.sideQuery).
37
38
  * Deliberately tiny: a purpose to mark it in the trace, its own short
@@ -109,6 +110,16 @@ export declare class AgentSession {
109
110
  * the cause (stale handle, corruption, a live external writer, an I/O
110
111
  * fault) — so no further run, resume, or log mutation may proceed.
111
112
  * The health check runs BEFORE every write, on every path. */
113
+ /**
114
+ * 0.40.2 — a committed call an aborted run left with no started and no
115
+ * result gets ONE durable result, "aborted before execution", riding the
116
+ * run that owns it (aborted-calls.ts). Every path that projects the
117
+ * session into a request calls this first — a run's start (fresh or
118
+ * resumed) and /compact — so no dangling tool_use reaches a provider,
119
+ * and the already-poisoned logs heal with no migration. Idempotent per
120
+ * invocation: a crash between two repairs leaves the rest for next time.
121
+ */
122
+ repairAbortedCalls(records: readonly StoreRecord[]): Promise<void>;
112
123
  persist(runId: string, event: Event): Promise<void>;
113
124
  beginRun(run: Run): void;
114
125
  endRun(run: Run): void;
@@ -208,6 +219,9 @@ export declare class AgentSession {
208
219
  * every open.
209
220
  */
210
221
  setMicrocompactThreshold(thresholdTokens: number): void;
222
+ /** ADR-0055 Amendment 2 (decision 3): the overflow belt's measure at send
223
+ * time — null unless a window is STATED and a bill anchors the context. */
224
+ overflowMeasure(): OverflowMeasure | null;
211
225
  /** A1b: the window the in-run tiers are drawn from, for a restored or
212
226
  * re-bound session (the CLI's one binding step calls both). */
213
227
  setContextWindow(windowTokens: number): void;
@@ -474,6 +488,19 @@ export interface ContextPolicy {
474
488
  readonly windowTokens: number;
475
489
  readonly isCheck?: (command: string) => boolean;
476
490
  readonly maxFailures?: number;
491
+ /** ADR-0055 Amendment 2: told when a checkpoint is discarded because it
492
+ * did not shrink the context — chars/4 estimates only, never text. */
493
+ readonly onDiscard?: (info: {
494
+ readonly reason: string;
495
+ readonly pre: number;
496
+ readonly post: number;
497
+ readonly summary: number;
498
+ }) => void;
499
+ /** ADR-0055 Amendment 2 (decision 3): the window someone STATED for the
500
+ * live binding, or null when the tiers run on a fallback. Absent, the
501
+ * caller's own windowTokens counts as stated. Only a stated window
502
+ * arms the overflow belt. */
503
+ readonly statedWindow?: () => number | null;
477
504
  };
478
505
  }
479
506
  export interface SessionConfig {
package/dist/session.js CHANGED
@@ -27,8 +27,9 @@
27
27
  * support in recovery.ts, the E1/E2 composition helpers in compose.ts —
28
28
  * same package, same exports (index.ts re-exports all four).
29
29
  */
30
- import { EventLog, projectMessages, } from "@vincemakes/kiso-core";
30
+ import { EventLog, projectMessages, SUMMARY_FRAMING, } from "@vincemakes/kiso-core";
31
31
  import { executionLedger } from "./ledger.js";
32
+ import { overflowBelt } from "./overflow-belt.js";
32
33
  import { assessTasks } from "./task-assessment.js";
33
34
  /** TV-1A — the session-level evidence policy: the PURE projection defaults
34
35
  * to ∅ (never inventing evidence); the session names the one built-in
@@ -47,7 +48,8 @@ import { estimateTokens } from "@vincemakes/kiso-core";
47
48
  import { StaleWriterError } from "./store.js";
48
49
  import { composeHooks, composeSystemPrompt, microcompactFor, runBasePrompt } from "./compose.js";
49
50
  import { checkpointBoundarySeq } from "./checkpoint.js";
50
- import { breakEvenFactor, guardedPruneSeq, KEEP_COMPACTABLE_RESULTS, microcompactBoundarySeq, phaseEnd, runsACheck, tierReason, tiersFor } from "./compaction-policy.js";
51
+ import { ABORTED_BEFORE_EXECUTION, unansweredAbortedCalls } from "./aborted-calls.js";
52
+ import { breakEvenFactor, guardedPruneSeq, KEEP_COMPACTABLE_RESULTS, microcompactBoundarySeq, outputReserve, phaseEnd, runsACheck, tierReason, tiersFor } from "./compaction-policy.js";
51
53
  import { Run } from "./run.js";
52
54
  // TUI2-R3v2 ③ — the side query rides the SAME tracer the runs ride; that
53
55
  // sameness is the whole point (one ledger, one shape, no second path).
@@ -221,6 +223,21 @@ export class AgentSession {
221
223
  * the cause (stale handle, corruption, a live external writer, an I/O
222
224
  * fault) — so no further run, resume, or log mutation may proceed.
223
225
  * The health check runs BEFORE every write, on every path. */
226
+ /**
227
+ * 0.40.2 — a committed call an aborted run left with no started and no
228
+ * result gets ONE durable result, "aborted before execution", riding the
229
+ * run that owns it (aborted-calls.ts). Every path that projects the
230
+ * session into a request calls this first — a run's start (fresh or
231
+ * resumed) and /compact — so no dangling tool_use reaches a provider,
232
+ * and the already-poisoned logs heal with no migration. Idempotent per
233
+ * invocation: a crash between two repairs leaves the rest for next time.
234
+ */
235
+ async repairAbortedCalls(records) {
236
+ for (const c of unansweredAbortedCalls(records)) {
237
+ const ev = this.log.append({ type: "tool_result", callId: c.callId, invocationSeq: c.invocationSeq, content: ABORTED_BEFORE_EXECUTION, isError: true, errorKind: "precondition" });
238
+ await this.persist(c.runId, ev);
239
+ }
240
+ }
224
241
  async persist(runId, event) {
225
242
  this.ensureHealthy();
226
243
  try {
@@ -293,16 +310,20 @@ export class AgentSession {
293
310
  return beforeSeq === undefined ? [] : [{ type: "microcompacted", beforeSeq }];
294
311
  }
295
312
  const window = this.#tiersWindow ?? tiersPolicy.windowTokens;
296
- const maxOutput = this.#config.maxTokens ?? lookupModelMetadata(this.#model, this.#baseUrl)?.capabilities.maxOutputTokens ?? 0;
297
- const t = tiersFor(window, Math.max(maxOutput, MANUAL_SUMMARY_BUDGET));
313
+ const t = tiersFor(window, this.#outputReserve());
298
314
  const isCheck = tiersPolicy.isCheck ?? ((command) => runsACheck(command));
299
315
  const reason = tierReason(used, t, why, () => phaseEnd(events, lastSummaryPoint(events), isCheck));
300
316
  if (reason === null)
301
317
  return [];
302
318
  const urgent = reason === "overflow" || reason === "emergency";
303
- if (!urgent && this.#summaryFailures >= (tiersPolicy.maxFailures ?? MAX_SUMMARY_FAILURES))
319
+ // ADR-0055 Amendment 2: the breaker stands at every tier but
320
+ // overflow (once per request by construction). An urgent tier that
321
+ // meets it skips the summary call and keeps the prune fallback — a
322
+ // session whose checkpoints cannot shrink it stops paying for them.
323
+ const breakerOpen = reason !== "overflow" && this.#summaryFailures >= (tiersPolicy.maxFailures ?? MAX_SUMMARY_FAILURES);
324
+ if (breakerOpen && !urgent)
304
325
  return [];
305
- const summarized = await this.#summarizeInRun(events, messages, t.tail, reason, run);
326
+ const summarized = breakerOpen ? null : await this.#summarizeInRun(events, messages, t.tail, reason, run, tiersPolicy.onDiscard);
306
327
  if (summarized !== null)
307
328
  return [summarized];
308
329
  if (!urgent)
@@ -333,7 +354,11 @@ export class AgentSession {
333
354
  ...(args.onProgress !== undefined ? { onProgress: args.onProgress } : {}),
334
355
  };
335
356
  try {
336
- const result = await summarizeConversation({ ...common, messages: args.messages, inBand: args.inBand, ...(args.reasoning !== undefined ? { reasoning: args.reasoning } : {}) });
357
+ // The in-band call carries the run's whole context, so the overflow
358
+ // belt applies to it; the serialised fallback below is small, and
359
+ // its failures keep their own classification.
360
+ const inBandAdapter = overflowBelt(this.#adapter, () => this.overflowMeasure());
361
+ const result = await summarizeConversation({ ...common, adapter: inBandAdapter, messages: args.messages, inBand: args.inBand, ...(args.reasoning !== undefined ? { reasoning: args.reasoning } : {}) });
337
362
  return { result, path: "in-band" };
338
363
  }
339
364
  catch (err) {
@@ -353,7 +378,7 @@ export class AgentSession {
353
378
  /** A2 — one in-run summary: in-band first, the serialised form once on
354
379
  * a rejection; null when neither produced a valid checkpoint. The
355
380
  * summary call's usage and path ride the trace ledger, as /compact's do. */
356
- async #summarizeInRun(events, messages, tail, reason, run) {
381
+ async #summarizeInRun(events, messages, tail, reason, run, onDiscard) {
357
382
  const boundary = checkpointBoundarySeq(events, { keepTokens: tail });
358
383
  if (boundary === undefined)
359
384
  return null;
@@ -375,20 +400,43 @@ export class AgentSession {
375
400
  this.#summaryFailures += 1;
376
401
  return null;
377
402
  }
378
- this.#summaryFailures = 0;
403
+ // ADR-0055 Amendment 2 (the shrink invariant): a fire is kept only if
404
+ // it removes at least what it writes — post ≤ pre − summary, all by
405
+ // the chars/4 estimate over the SAME projection (never the bill
406
+ // against an estimate). A checkpoint that fails it is discarded:
407
+ // nothing is appended, the run goes on as before, and the failure
408
+ // counts toward the breaker.
409
+ const fire = { type: "summarized", coversToSeq: boundary, summary: result.text };
410
+ const pre = estimateTokens(projectMessages(events));
411
+ const post = estimateTokens(projectMessages([...events, { ...fire, seq: (events.at(-1)?.seq ?? -1) + 1 }]));
412
+ const written = estimateTokens([{ role: "user", content: `${SUMMARY_FRAMING}\n\n${result.text}` }]);
413
+ const shrinks = pre - post >= written;
379
414
  // Every in-run fire is recorded — its reason and path are what the
380
- // measurement counts — and its usage when the provider reported one.
415
+ // measurement counts — and its usage when the provider reported one;
416
+ // a discarded one too (it was paid for), marked so.
381
417
  {
382
418
  try {
383
419
  const canonical = result.usage === null ? null : canonicalizeUsageForModel(this.#model, this.#baseUrl, this.#provider ?? "adapter", result.usage);
384
420
  mkdirSync(join(this.#store.root, "traces"), { recursive: true, mode: 0o700 });
385
- appendFileSync(join(this.#store.root, "traces", `${this.id}.jsonl`), `${JSON.stringify({ kind: "summary", canonical, reason, path })}\n`, { mode: 0o600 });
421
+ appendFileSync(join(this.#store.root, "traces", `${this.id}.jsonl`), `${JSON.stringify({ kind: "summary", canonical, reason, path, ...(shrinks ? {} : { discarded: "no-shrink" }) })}\n`, { mode: 0o600 });
386
422
  }
387
423
  catch (err) {
388
424
  console.error(`[kiso] summary usage ledger degraded (${err instanceof Error ? err.message : String(err)}); the summary call's cost is not recorded`);
389
425
  }
390
426
  }
391
- return { type: "summarized", coversToSeq: boundary, summary: result.text };
427
+ if (!shrinks) {
428
+ this.#summaryFailures += 1;
429
+ // sizes only — the checkpoint's text is the owner's work
430
+ try {
431
+ onDiscard?.({ reason, pre, post, summary: written });
432
+ }
433
+ catch {
434
+ // observation only — a notice that cannot be shown changes nothing
435
+ }
436
+ return null;
437
+ }
438
+ this.#summaryFailures = 0;
439
+ return fire;
392
440
  }
393
441
  /**
394
442
  * merge round B (/model): replace the adapter for SUBSEQUENT runs. The
@@ -451,6 +499,21 @@ export class AgentSession {
451
499
  setMicrocompactThreshold(thresholdTokens) {
452
500
  this.#microcompact = { thresholdTokens };
453
501
  }
502
+ /** ADR-0055 Amendment 2 (decision 3): the overflow belt's measure at send
503
+ * time — null unless a window is STATED and a bill anchors the context. */
504
+ overflowMeasure() {
505
+ const tiers = this.#config.contextPolicy?.tiers;
506
+ if (tiers === undefined)
507
+ return null;
508
+ const window = tiers.statedWindow !== undefined ? tiers.statedWindow() : (this.#tiersWindow ?? tiers.windowTokens);
509
+ const used = this.contextAnchor();
510
+ return window === null || used === undefined ? null : { used, window, reserve: this.#outputReserve() };
511
+ }
512
+ /** ADR-0055 Amendment 2 (decision 4): the emergency reserve — what the
513
+ * endpoint may grant for this binding (compaction-policy.ts). */
514
+ #outputReserve() {
515
+ return outputReserve(this.#config.maxTokens, lookupModelMetadata(this.#model, this.#baseUrl)?.capabilities.maxOutputTokens, MANUAL_SUMMARY_BUDGET);
516
+ }
454
517
  /** The raw tail a manual cut at a settled round keeps: the tiers' tail
455
518
  * when a window is known, else the policy's keep floor. */
456
519
  #manualTailTokens() {
@@ -675,6 +738,7 @@ export class AgentSession {
675
738
  const cancelled = () => new Error("the compaction was cancelled");
676
739
  if (options.signal !== undefined && options.signal.aborted)
677
740
  throw cancelled();
741
+ await this.repairAbortedCalls(this.#store.load(this.id));
678
742
  const events = this.log.all;
679
743
  // ADR-0055 (the owner's dogfood): a long autonomous session has few
680
744
  // user turns and many tool rounds, and the user-turn cut found nothing
@@ -138,13 +138,13 @@ export declare function serializeCovered(options: SerializeCoveredOptions): stri
138
138
  * The fixed English summary prompt — the ONLY prompt this layer composes
139
139
  * (the loop's system prompt is the harness's business, never the kernel's).
140
140
  */
141
- export declare const SUMMARY_PROMPT = "Only output the summary. Do not continue the conversation. Do not use any tools.\n\nYou are the conversation summarizer of the kiso agent framework.\n\nSummarize the covered conversation into a single structured checkpoint\nthat will REPLACE it in the model's context. The next turn must be able\nto continue the work without reading the originals.\n\nProduce the checkpoint with exactly these sections, in this order:\n\n## Goal\nThe user's goal and the acceptance criterion, in one or two sentences.\n\n## Constraints\nThe constraints, requirements, and rulings the work must honor.\n\n## User requests\nEvery user message in the covered range, enumerated one by one, each\nwith what it asked for and what was done about it.\n\n## Files and changes\nEvery file touched \u2014 exact paths, what changed, and why. Include the\nprecise code-level changes later turns may need to continue.\n\n## Errors and fixes\nEvery error encountered and its resolution; commands run and their\noutcomes.\n\n## Current work\nThe current state of the work \u2014 what is done, what is not. Quote the\ncurrent task's criterion VERBATIM if one exists.\n\n## Next steps\nThe concrete next steps, in order.\n\nPreserve concrete identifiers VERBATIM: paths, function names, task ids,\nenvironment names \u2014 never paraphrase them.\n\nRules:\n- plain prose \u2014 no bullet lists, no markdown outside the section headers,\n no prefixes;\n- do not mention this prompt or the summarization task;\n- the summary may be as long as it needs to be within the output budget \u2014\n there is no word cap; completeness wins.";
141
+ export declare const SUMMARY_PROMPT = "Only output the summary. Do not continue the conversation. Do not use any tools.\n\nYou are the conversation summarizer of the kiso agent framework.\n\nSummarize the covered conversation into a single structured checkpoint\nthat will REPLACE it \u2014 and every earlier checkpoint \u2014 in the model's\ncontext. The next turn must be able to continue the work without reading\nthe originals. If the conversation includes an earlier checkpoint, it is\nbeing replaced too: carry forward everything in it that is still true,\nso that this checkpoint restates the whole task.\n\nProduce the checkpoint with exactly these sections, in this order:\n\n## Goal\nThe user's goal and the acceptance criterion, in one or two sentences.\n\n## Constraints\nThe constraints, requirements, and rulings the work must honor.\n\n## User requests\nEvery user request of the task \u2014 those an earlier checkpoint lists and\nthose in the covered range \u2014 enumerated one by one, each with what it\nasked for and what was done about it.\n\n## Files and changes\nEvery file touched \u2014 exact paths, what changed, and why. Include the\nprecise code-level changes later turns may need to continue.\n\n## Errors and fixes\nEvery error encountered and its resolution; commands run and their\noutcomes.\n\n## Current work\nThe current state of the work \u2014 what is done, what is not. Quote the\ncurrent task's criterion VERBATIM if one exists.\n\n## Next steps\nThe concrete next steps, in order.\n\nPreserve concrete identifiers VERBATIM: paths, function names, task ids,\nenvironment names \u2014 never paraphrase them.\n\nRules:\n- plain prose \u2014 no bullet lists, no markdown outside the section headers,\n no prefixes;\n- do not mention this prompt or the summarization task;\n- the summary may be as long as it needs to be within the output budget \u2014\n there is no word cap; completeness wins.";
142
142
  /**
143
143
  * ADR-0055 A2 — the IN-BAND form: the run's own request with this appended
144
144
  * as the last user message, so everything before it reads at the cache-hit
145
145
  * price. The same sections, the same validation; the guard sentence first.
146
146
  */
147
- export declare const SUMMARY_IN_BAND = "Stop the task for this one reply and write a checkpoint of the conversation above instead.\n\nOnly output the summary. Do not continue the conversation. Do not use any tools.\n\nYou are the conversation summarizer of the kiso agent framework.\n\nSummarize the covered conversation into a single structured checkpoint\nthat will REPLACE it in the model's context. The next turn must be able\nto continue the work without reading the originals.\n\nProduce the checkpoint with exactly these sections, in this order:\n\n## Goal\nThe user's goal and the acceptance criterion, in one or two sentences.\n\n## Constraints\nThe constraints, requirements, and rulings the work must honor.\n\n## User requests\nEvery user message in the covered range, enumerated one by one, each\nwith what it asked for and what was done about it.\n\n## Files and changes\nEvery file touched \u2014 exact paths, what changed, and why. Include the\nprecise code-level changes later turns may need to continue.\n\n## Errors and fixes\nEvery error encountered and its resolution; commands run and their\noutcomes.\n\n## Current work\nThe current state of the work \u2014 what is done, what is not. Quote the\ncurrent task's criterion VERBATIM if one exists.\n\n## Next steps\nThe concrete next steps, in order.\n\nPreserve concrete identifiers VERBATIM: paths, function names, task ids,\nenvironment names \u2014 never paraphrase them.\n\nRules:\n- plain prose \u2014 no bullet lists, no markdown outside the section headers,\n no prefixes;\n- do not mention this prompt or the summarization task;\n- the summary may be as long as it needs to be within the output budget \u2014\n there is no word cap; completeness wins.";
147
+ export declare const SUMMARY_IN_BAND = "Stop the task for this one reply and write a checkpoint of the conversation above instead.\n\nOnly output the summary. Do not continue the conversation. Do not use any tools.\n\nYou are the conversation summarizer of the kiso agent framework.\n\nSummarize the covered conversation into a single structured checkpoint\nthat will REPLACE it \u2014 and every earlier checkpoint \u2014 in the model's\ncontext. The next turn must be able to continue the work without reading\nthe originals. If the conversation includes an earlier checkpoint, it is\nbeing replaced too: carry forward everything in it that is still true,\nso that this checkpoint restates the whole task.\n\nProduce the checkpoint with exactly these sections, in this order:\n\n## Goal\nThe user's goal and the acceptance criterion, in one or two sentences.\n\n## Constraints\nThe constraints, requirements, and rulings the work must honor.\n\n## User requests\nEvery user request of the task \u2014 those an earlier checkpoint lists and\nthose in the covered range \u2014 enumerated one by one, each with what it\nasked for and what was done about it.\n\n## Files and changes\nEvery file touched \u2014 exact paths, what changed, and why. Include the\nprecise code-level changes later turns may need to continue.\n\n## Errors and fixes\nEvery error encountered and its resolution; commands run and their\noutcomes.\n\n## Current work\nThe current state of the work \u2014 what is done, what is not. Quote the\ncurrent task's criterion VERBATIM if one exists.\n\n## Next steps\nThe concrete next steps, in order.\n\nPreserve concrete identifiers VERBATIM: paths, function names, task ids,\nenvironment names \u2014 never paraphrase them.\n\nRules:\n- plain prose \u2014 no bullet lists, no markdown outside the section headers,\n no prefixes;\n- do not mention this prompt or the summarization task;\n- the summary may be as long as it needs to be within the output budget \u2014\n there is no word cap; completeness wins.";
148
148
  /**
149
149
  * E6 (b) — the output-side validation (the finding E6-F4/F5 follow-up):
150
150
  * a summary must be a complete checkpoint or NOTHING. The marker family
package/dist/summarize.js CHANGED
@@ -122,16 +122,20 @@ export const MAX_SUMMARY_FAILURES = 3;
122
122
  export function serializeCovered(options) {
123
123
  const { events, prevPoint, boundary } = options;
124
124
  const lines = ["<conversation>"];
125
- // E6 (d) (the order's R4): the old summary texts are RETAINED CONTEXT —
126
- // the durable record of the earlier ranges. They render first, labeled
127
- // do-not-re-summarize: the summarizer must know what the earlier
128
- // summaries covered, but never fold them into the new checkpoint.
129
- const retained = events.filter((e) => e.type === "summarized" && e.coversToSeq <= prevPoint);
130
- if (retained.length > 0) {
131
- lines.push("[retained context — do not re-summarize]");
132
- for (const r of retained)
133
- lines.push(`[summary covers to seq ${r.coversToSeq}] ${r.summary}`);
134
- lines.push("[end retained context]");
125
+ // ADR-0055 Amendment 2 (E6 (d)'s "do not re-summarize" retired): the
126
+ // new checkpoint REPLACES the previous one, so the summariser is given
127
+ // exactly the checkpoint the projection shows — the latest at or before
128
+ // prevPoint — as material to fold in. The superseded ones are not shown:
129
+ // the latest already restates them.
130
+ let previous;
131
+ for (const e of events) {
132
+ if (e.type === "summarized" && e.coversToSeq <= prevPoint && (previous === undefined || e.coversToSeq >= previous.coversToSeq))
133
+ previous = e;
134
+ }
135
+ if (previous !== undefined) {
136
+ lines.push("[previous checkpoint — the new checkpoint replaces it: carry forward everything in it that is still true]");
137
+ lines.push(previous.summary);
138
+ lines.push("[end previous checkpoint]");
135
139
  }
136
140
  for (const ev of events) {
137
141
  if (ev.seq <= prevPoint || ev.seq > boundary || ev.type === "summarized")
@@ -173,8 +177,11 @@ export const SUMMARY_PROMPT = `${SUMMARY_GUARD}
173
177
  You are the conversation summarizer of the kiso agent framework.
174
178
 
175
179
  Summarize the covered conversation into a single structured checkpoint
176
- that will REPLACE it in the model's context. The next turn must be able
177
- to continue the work without reading the originals.
180
+ that will REPLACE it — and every earlier checkpoint — in the model's
181
+ context. The next turn must be able to continue the work without reading
182
+ the originals. If the conversation includes an earlier checkpoint, it is
183
+ being replaced too: carry forward everything in it that is still true,
184
+ so that this checkpoint restates the whole task.
178
185
 
179
186
  Produce the checkpoint with exactly these sections, in this order:
180
187
 
@@ -185,8 +192,9 @@ The user's goal and the acceptance criterion, in one or two sentences.
185
192
  The constraints, requirements, and rulings the work must honor.
186
193
 
187
194
  ## User requests
188
- Every user message in the covered range, enumerated one by one, each
189
- with what it asked for and what was done about it.
195
+ Every user request of the task — those an earlier checkpoint lists and
196
+ those in the covered range — enumerated one by one, each with what it
197
+ asked for and what was done about it.
190
198
 
191
199
  ## Files and changes
192
200
  Every file touched — exact paths, what changed, and why. Include the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-runtime",
3
- "version": "0.40.1",
3
+ "version": "0.40.2",
4
4
  "description": "kiso runtime \u2014 durable multi-turn agent sessions: AgentDefinition, AgentRuntime, AgentSession, Run, append-only JSONL store.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,12 +25,12 @@
25
25
  "test": "vitest run"
26
26
  },
27
27
  "dependencies": {
28
- "@vincemakes/kiso-core": "0.40.1"
28
+ "@vincemakes/kiso-core": "0.40.2"
29
29
  },
30
30
  "peerDependencies": {
31
- "@vincemakes/kiso-provider-anthropic": "0.40.1",
32
- "@vincemakes/kiso-provider-openai": "0.40.1",
33
- "@vincemakes/kiso-provider-openai-responses": "0.40.1"
31
+ "@vincemakes/kiso-provider-anthropic": "0.40.2",
32
+ "@vincemakes/kiso-provider-openai": "0.40.2",
33
+ "@vincemakes/kiso-provider-openai-responses": "0.40.2"
34
34
  },
35
35
  "peerDependenciesMeta": {
36
36
  "@vincemakes/kiso-provider-anthropic": {
@@ -44,7 +44,7 @@
44
44
  }
45
45
  },
46
46
  "devDependencies": {
47
- "@vincemakes/kiso-evals": "0.40.1",
47
+ "@vincemakes/kiso-evals": "0.40.2",
48
48
  "@types/node": "^26.1.2",
49
49
  "typescript": "^5.7.2",
50
50
  "vitest": "^3.0.0"