@vincemakes/kiso-runtime 0.31.0 → 0.32.0

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/agent.d.ts CHANGED
@@ -65,6 +65,10 @@ export interface AgentDefinition {
65
65
  /** E6: the session context policy (run-start actions, injection-side only). */
66
66
  readonly contextPolicy?: import("./session.js").ContextPolicy;
67
67
  readonly maxRetries?: number;
68
+ /** LT-1: the stream watchdog — milliseconds between adapter events
69
+ * before the request is aborted and retried (default 120 s; 0 off).
70
+ * Type-only additive; read by the runtime's idle guard. */
71
+ readonly streamIdleMs?: number;
68
72
  /** E1: loaded extensions — their tools merge into the registry (a name
69
73
  * collision with a built-in is a loud startup error), their hooks
70
74
  * compose after the agent's own (the existing come first), their approvals join the
package/dist/agent.js CHANGED
@@ -145,6 +145,7 @@ export class AgentRuntime {
145
145
  ...(this.#definition.microcompact !== undefined ? { microcompact: this.#definition.microcompact } : {}),
146
146
  ...(this.#definition.contextPolicy !== undefined ? { contextPolicy: this.#definition.contextPolicy } : {}),
147
147
  ...(this.#definition.maxRetries !== undefined ? { maxRetries: this.#definition.maxRetries } : {}),
148
+ ...(this.#definition.streamIdleMs !== undefined ? { streamIdleMs: this.#definition.streamIdleMs } : {}),
148
149
  ...(this.#definition.extensions !== undefined ? { extensions: this.#definition.extensions } : {}),
149
150
  };
150
151
  return new AgentSession(options.id, log, store, adapter, config);
@@ -28,6 +28,27 @@ export async function loadProjectExtensions(dir, existing = []) {
28
28
  }
29
29
  return projectExts;
30
30
  }
31
+ /**
32
+ * §2.5 — the load counter behind the import URL's `?load=` query.
33
+ *
34
+ * Node caches ESM modules by URL forever. Without a distinguishing query
35
+ * a second load of the same directory returns the FIRST load's modules,
36
+ * so `/reload` would report success and change nothing — and a
37
+ * module-level Set held across the boundary would be mutated by one side
38
+ * and read by the other.
39
+ *
40
+ * It is unconditional on purpose. A parameter would put a stale mode
41
+ * back within reach and would move `loadExtensions` in the public
42
+ * surface file for no gain: at startup this is one load and invisible,
43
+ * and on a reload it is the entire point.
44
+ *
45
+ * The cost, stated rather than hidden: Node has no module unloader, so
46
+ * each superseded module object stays allocated and unreachable for the
47
+ * life of the process — one per extension file per reload. The only
48
+ * real resources an extension holds are its MCP servers, and those are
49
+ * torn down by `disposeExtensions`, which whoever loaded them owes.
50
+ */
51
+ let loadSerial = 0;
31
52
  export async function loadExtensions(dir) {
32
53
  let files;
33
54
  try {
@@ -39,10 +60,11 @@ export async function loadExtensions(dir) {
39
60
  throw err;
40
61
  }
41
62
  const out = [];
63
+ const serial = ++loadSerial; // one nonce per CALL — a directory's files load together
42
64
  for (const file of files) {
43
65
  let ext;
44
66
  try {
45
- const mod = (await import(pathToFileURL(join(dir, file)).href));
67
+ const mod = (await import(`${pathToFileURL(join(dir, file)).href}?load=${serial}`));
46
68
  ext = mod.default;
47
69
  if (typeof ext === "function")
48
70
  ext = await ext(); // a factory
@@ -0,0 +1,38 @@
1
+ /**
2
+ * LT-1 — the stream watchdog (kiso-doc/kiso-lt1-mini-spec-2026-09-09.md).
3
+ *
4
+ * The third guard beside `truncationGuard` and `traceGuard`: it wraps the
5
+ * adapter's `stream()` so that a request whose events STOP ARRIVING is
6
+ * ended rather than waited on forever. Before this, a model stream that
7
+ * went silent — a hung socket, an upstream that stopped writing without
8
+ * closing — held the run open until a human pressed esc; the 0.31.0
9
+ * review filed it as the one long-task hazard nothing handled.
10
+ *
11
+ * The rule is per EVENT, not per request: a long think that keeps
12
+ * streaming is never tripped, a socket that stops is tripped `idleMs`
13
+ * after its last byte. On a trip the guard aborts the UNDERLYING request
14
+ * through a controller it owns (combined with the caller's signal, so
15
+ * esc still wins) and throws a retryable `network` StructuredError. From
16
+ * there the kernel's existing mid-stream retry does the rest: the
17
+ * attempt is voided (`model_output_abandoned` with this reason), the
18
+ * request is retried with backoff, and when the budget is spent the run
19
+ * ends in the error terminal the CLI prints (OR-5). Nothing new is
20
+ * written to the log; the kernel is untouched.
21
+ *
22
+ * `idleMs` ≤ 0 (or absent) returns the adapter itself: the guard is
23
+ * declared off, not silently ignored.
24
+ */
25
+ import type { Adapter, StructuredError } from "@vincemakes/kiso-core";
26
+ /** The default: 120 s between events. The ChatGPT backend at a high effort
27
+ * can think without writing (no reasoning summaries are requested), and
28
+ * no measured bound exists for an `ultra` turn — 120 s is the order of
29
+ * magnitude the reference implementation allows a whole request, applied
30
+ * here per event: stricter where it matters, looser where it should be.
31
+ * A knob (`streamIdleMs` on the profile) because the first dogfood may
32
+ * move it. */
33
+ export declare const DEFAULT_STREAM_IDLE_MS = 120000;
34
+ export declare function idleGuard(adapter: Adapter, idleMs: number | undefined): Adapter;
35
+ /** The error the kernel receives on a trip — the same shape the adapters
36
+ * produce for a connection failure, so the retry authority (ADR-0005)
37
+ * treats a stall exactly like a dropped socket. */
38
+ export declare function stalledError(idleMs: number, elapsedMs: number): StructuredError;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * LT-1 — the stream watchdog (kiso-doc/kiso-lt1-mini-spec-2026-09-09.md).
3
+ *
4
+ * The third guard beside `truncationGuard` and `traceGuard`: it wraps the
5
+ * adapter's `stream()` so that a request whose events STOP ARRIVING is
6
+ * ended rather than waited on forever. Before this, a model stream that
7
+ * went silent — a hung socket, an upstream that stopped writing without
8
+ * closing — held the run open until a human pressed esc; the 0.31.0
9
+ * review filed it as the one long-task hazard nothing handled.
10
+ *
11
+ * The rule is per EVENT, not per request: a long think that keeps
12
+ * streaming is never tripped, a socket that stops is tripped `idleMs`
13
+ * after its last byte. On a trip the guard aborts the UNDERLYING request
14
+ * through a controller it owns (combined with the caller's signal, so
15
+ * esc still wins) and throws a retryable `network` StructuredError. From
16
+ * there the kernel's existing mid-stream retry does the rest: the
17
+ * attempt is voided (`model_output_abandoned` with this reason), the
18
+ * request is retried with backoff, and when the budget is spent the run
19
+ * ends in the error terminal the CLI prints (OR-5). Nothing new is
20
+ * written to the log; the kernel is untouched.
21
+ *
22
+ * `idleMs` ≤ 0 (or absent) returns the adapter itself: the guard is
23
+ * declared off, not silently ignored.
24
+ */
25
+ /** The default: 120 s between events. The ChatGPT backend at a high effort
26
+ * can think without writing (no reasoning summaries are requested), and
27
+ * no measured bound exists for an `ultra` turn — 120 s is the order of
28
+ * magnitude the reference implementation allows a whole request, applied
29
+ * here per event: stricter where it matters, looser where it should be.
30
+ * A knob (`streamIdleMs` on the profile) because the first dogfood may
31
+ * move it. */
32
+ export const DEFAULT_STREAM_IDLE_MS = 120_000;
33
+ export function idleGuard(adapter, idleMs) {
34
+ if (idleMs === undefined || !(idleMs > 0))
35
+ return adapter;
36
+ return {
37
+ stream: (options) => guardStream(adapter, options, idleMs),
38
+ };
39
+ }
40
+ /** The error the kernel receives on a trip — the same shape the adapters
41
+ * produce for a connection failure, so the retry authority (ADR-0005)
42
+ * treats a stall exactly like a dropped socket. */
43
+ export function stalledError(idleMs, elapsedMs) {
44
+ return {
45
+ code: "network",
46
+ retryable: true,
47
+ message: `stream stalled: no event for ${Math.round(idleMs / 1000)}s (${Math.round(elapsedMs / 1000)}s into the request)`,
48
+ };
49
+ }
50
+ /** The caller's abort, as the kernel's catch reads it: an error NAMED
51
+ * AbortError with the signal set — `aborted by user`, never a stall. */
52
+ function abortError() {
53
+ const err = new Error("stream aborted by the caller");
54
+ err.name = "AbortError";
55
+ return err;
56
+ }
57
+ async function* guardStream(adapter, options, idleMs) {
58
+ const own = new AbortController();
59
+ const caller = options.signal;
60
+ // the caller's abort (esc) reaches the adapter through OUR controller —
61
+ // one signal goes down — and it also settles OUR wait at once: an
62
+ // adapter that ignores its signal (a stub, a transport that only
63
+ // notices on the next write) must not turn the caller's abort into a
64
+ // stall by keeping the guard waiting until the idle timer fires.
65
+ let rejectOnCallerAbort = () => undefined;
66
+ const callerAborted = new Promise((_, reject) => {
67
+ rejectOnCallerAbort = reject;
68
+ });
69
+ callerAborted.catch(() => undefined); // settled only when raced; never an unhandled rejection
70
+ const onCallerAbort = () => {
71
+ own.abort();
72
+ rejectOnCallerAbort(abortError());
73
+ };
74
+ if (caller?.aborted === true)
75
+ onCallerAbort();
76
+ else
77
+ caller?.addEventListener("abort", onCallerAbort, { once: true });
78
+ const iterator = adapter.stream({ ...options, signal: own.signal })[Symbol.asyncIterator]();
79
+ const started = Date.now();
80
+ let tripped = false;
81
+ try {
82
+ for (;;) {
83
+ let timer;
84
+ const stall = new Promise((_, reject) => {
85
+ timer = setTimeout(() => {
86
+ if (caller?.aborted === true) {
87
+ // the caller gave up first; the silence is theirs, not a stall
88
+ reject(abortError());
89
+ return;
90
+ }
91
+ tripped = true;
92
+ own.abort();
93
+ reject(stalledError(idleMs, Date.now() - started));
94
+ }, idleMs);
95
+ });
96
+ let next;
97
+ try {
98
+ next = await Promise.race([iterator.next(), stall, callerAborted]);
99
+ }
100
+ catch (err) {
101
+ // an abort the CALLER asked for is theirs (the kernel reads its
102
+ // own signal and records `aborted by user`); an abort WE caused
103
+ // is the stall, whatever shape the adapter turned it into
104
+ if (tripped && caller?.aborted !== true)
105
+ throw stalledError(idleMs, Date.now() - started);
106
+ throw err;
107
+ }
108
+ finally {
109
+ clearTimeout(timer);
110
+ }
111
+ if (next.done)
112
+ return;
113
+ yield next.value;
114
+ }
115
+ }
116
+ finally {
117
+ caller?.removeEventListener("abort", onCallerAbort);
118
+ // release the underlying generator whichever way this one ends
119
+ // (a trip, the caller's abort, or the consumer walking away)
120
+ await iterator.return?.().catch(() => undefined);
121
+ }
122
+ }
@@ -24,8 +24,15 @@ export interface ModelCapabilities {
24
24
  readonly maxOutputTokens: number | null;
25
25
  /** "automatic" — the provider caches without request markup (DeepSeek,
26
26
  * OpenAI); "explicit" — the request must place cache_control
27
- * breakpoints (Anthropic); "none" — no caching; null = unknown. */
28
- readonly promptCaching: "none" | "automatic" | "explicit" | null;
27
+ * breakpoints (Anthropic); "none" — no caching; "unobservable" — the
28
+ * endpoint answers a cache figure that carries no information (OR-10,
29
+ * owner 2026-09-09: the ChatGPT backend reports `cached_tokens` 0 to
30
+ * a third-party client on every request — three identical 1,922-token
31
+ * requests, one prompt_cache_key, 0 at every attribution level — so
32
+ * its 0 and "not reported" are one value; a display shows no cache
33
+ * meter for such a row, while the record keeps what the backend
34
+ * said); null = unknown. */
35
+ readonly promptCaching: "none" | "automatic" | "explicit" | "unobservable" | null;
29
36
  /** XP-1: the reasoning capability matrix — supersedes the pre-XP-1
30
37
  * boolean IN PLACE (zero consumers existed, verified). null = unknown:
31
38
  * no mode list, no effort levels, nothing downstream may guess. */
@@ -43,8 +50,10 @@ export interface ModelCapabilities {
43
50
  * some model/effort combinations forbid thinking-disabled. */
44
51
  export type ThinkingMode = "default" | "adaptive" | "enabled" | "disabled";
45
52
  /** `none` is the OpenAI Responses dialect's zero-reasoning level — a NATIVE
46
- * value a request carries, distinct from "default" (no field sent). */
47
- export type ReasoningEffort = "default" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
53
+ * value a request carries, distinct from "default" (no field sent).
54
+ * `ultra` (OR-6) is the vendor CLI presets' top rung for the ChatGPT
55
+ * backend — native THERE only; the first-party model pages stop at `max`. */
56
+ export type ReasoningEffort = "default" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | "ultra";
48
57
  export interface ReasoningSetting {
49
58
  readonly thinking: ThinkingMode;
50
59
  readonly effort: ReasoningEffort;
@@ -102,42 +102,45 @@ const OPENAI_MODELS_ASOF = "2026-09-08";
102
102
  * subscription; a first-party model page says nothing about that backend). */
103
103
  const CHATGPT_PRESETS_SOURCE = "https://github.com/openai/codex/blob/37f4bb94c9f4e180535a12e3f2c2f93f4a773df0/codex-rs/models-manager/models.json";
104
104
  const RESPONSES_WIRE = "reasoning.effort";
105
- /** A first-party Responses row: the model page is the source for the
106
- * levels, the window, the output cap and the price alike (read the same
107
- * day). The adapter asks for no reasoning summaries (encrypted items
108
- * only), so no thinking stream is emitted. */
109
- const openaiRow = (model, effortDefault, rate, page) => ({
105
+ const openaiRow = (model, effortDefault, rate, page, opts = {}) => ({
110
106
  model,
111
107
  providerId: "openai",
112
108
  endpoint: "https://api.openai.com",
113
109
  capabilities: { contextWindow: 1_050_000, maxOutputTokens: 128_000, promptCaching: "automatic", reasoning: {
114
110
  emitsThinkingStream: false,
115
111
  thinking: null,
116
- effort: { levels: ["none", "low", "medium", "high", "xhigh"], default: effortDefault, wire: RESPONSES_WIRE },
117
- asOf: OPENAI_MODELS_ASOF,
112
+ effort: { levels: opts.levels ?? ["none", "low", "medium", "high", "xhigh"], default: effortDefault, wire: RESPONSES_WIRE },
113
+ asOf: opts.asOf ?? OPENAI_MODELS_ASOF,
118
114
  source: page,
119
115
  }, inputModalities: null },
120
- capabilitiesAsOf: OPENAI_MODELS_ASOF,
116
+ capabilitiesAsOf: opts.asOf ?? OPENAI_MODELS_ASOF,
121
117
  capabilitiesSource: page,
122
- pricing: { ...rate, cacheWritePerM: 0, asOf: OPENAI_MODELS_ASOF, source: page },
118
+ pricing: { ...rate, cacheWritePerM: 0, asOf: opts.asOf ?? OPENAI_MODELS_ASOF, source: page },
123
119
  });
124
120
  /** The subscription backend's row for the same id: the presets' four
125
- * levels (no `none`), the presets' context window, no price. */
126
- const chatgptRow = (model) => ({
121
+ * levels (no `none`), the presets' context window, no price, and a cache
122
+ * figure no client can observe (OR-10 — see ModelCapabilities). */
123
+ const chatgptRow = (model, opts = {}) => ({
127
124
  model,
128
125
  providerId: "chatgpt",
129
126
  endpoint: "https://chatgpt.com",
130
- capabilities: { contextWindow: 272_000, maxOutputTokens: null, promptCaching: null, reasoning: {
127
+ capabilities: { contextWindow: 272_000, maxOutputTokens: null, promptCaching: "unobservable", reasoning: {
131
128
  emitsThinkingStream: false,
132
129
  thinking: null,
133
- effort: { levels: ["low", "medium", "high", "xhigh"], default: "medium", wire: RESPONSES_WIRE },
134
- asOf: OPENAI_MODELS_ASOF,
135
- source: CHATGPT_PRESETS_SOURCE,
130
+ effort: { levels: opts.levels ?? ["low", "medium", "high", "xhigh"], default: opts.default ?? "medium", wire: RESPONSES_WIRE },
131
+ asOf: opts.asOf ?? OPENAI_MODELS_ASOF,
132
+ source: opts.source ?? CHATGPT_PRESETS_SOURCE,
136
133
  }, inputModalities: null },
137
- capabilitiesAsOf: OPENAI_MODELS_ASOF,
138
- capabilitiesSource: CHATGPT_PRESETS_SOURCE,
134
+ capabilitiesAsOf: opts.asOf ?? OPENAI_MODELS_ASOF,
135
+ capabilitiesSource: opts.source ?? CHATGPT_PRESETS_SOURCE,
139
136
  pricing: null,
140
137
  });
138
+ /** OR-6 (2026-09-09): the newer line, read the same day — the model pages for
139
+ * the first-party rows, the presets pinned to a later commit for the
140
+ * subscription rows (both ids answered on the backend that day). */
141
+ const OPENAI_MODELS_ASOF_2 = "2026-09-09";
142
+ const CHATGPT_PRESETS_SOURCE_2 = "https://github.com/openai/codex/blob/634ebc1865c6ac840ed3ba118f040d527bf4b55d/codex-rs/models-manager/models.json";
143
+ const ULTRA_LADDER = ["low", "medium", "high", "xhigh", "max", "ultra"];
141
144
  /** The v1 table. Nulls outnumber numbers ON PURPOSE: only values with a
142
145
  * named source enter; everything else waits for one. */
143
146
  const ENTRIES = [
@@ -232,6 +235,47 @@ const ENTRIES = [
232
235
  openaiRow("gpt-5.4", "none", { inputPerM: 2.5, outputPerM: 15, cacheReadPerM: 0.25 }, "https://developers.openai.com/api/docs/models/gpt-5.4"),
233
236
  chatgptRow("gpt-5.5"),
234
237
  chatgptRow("gpt-5.4"),
238
+ // OR-6 (2026-09-09): gpt-6-astra and gpt-5.6-sol. The astra page names
239
+ // no default, so the first-party default stays null (unknown is null);
240
+ // the presets say `low` at the subscription. `ultra` exists only there.
241
+ // The ordering keeps the first-party row first for endpoint-less
242
+ // callers; the run-side resolver passes the endpoint (45a9df5), so the
243
+ // two rows need not be nested — astra's subscription row has a level
244
+ // (ultra) its first-party row lacks.
245
+ openaiRow("gpt-6-astra", null, { inputPerM: 10, outputPerM: 50, cacheReadPerM: 1 }, "https://developers.openai.com/api/docs/models/gpt-6-astra", { levels: ["low", "medium", "high", "xhigh", "max"], asOf: OPENAI_MODELS_ASOF_2 }),
246
+ openaiRow("gpt-5.6-sol", "medium", { inputPerM: 4, outputPerM: 20, cacheReadPerM: 0.4 }, "https://developers.openai.com/api/docs/models/gpt-5.6-sol", { levels: ["none", "low", "medium", "high", "xhigh", "max"], asOf: OPENAI_MODELS_ASOF_2 }),
247
+ chatgptRow("gpt-6-astra", { levels: ULTRA_LADDER, default: "low", source: CHATGPT_PRESETS_SOURCE_2, asOf: OPENAI_MODELS_ASOF_2 }),
248
+ chatgptRow("gpt-5.6-sol", { levels: ULTRA_LADDER, default: "low", source: CHATGPT_PRESETS_SOURCE_2, asOf: OPENAI_MODELS_ASOF_2 }),
249
+ // The compat table's SECOND row (roadmap R3.2 §1a step 1): GLM 5.3 Flash
250
+ // through OpenRouter, read from OpenRouter's models API on 2026-09-09. The
251
+ // id is OpenRouter's, so the row is keyed to that origin; the
252
+ // continuation scope stamps such a profile `custom` at openrouter.ai
253
+ // (manifest.ts's KNOWN_ORIGINS is deliberately not widened — the scope
254
+ // identity is a durable fact and this row needs only the lookup). The
255
+ // window is the TOP provider's (1,048,576), not the listing's 1,310,720:
256
+ // what a request actually gets. No default effort — the page states
257
+ // none. `reasoning_effort` is in the model's supported parameters; the
258
+ // stream's think arrives as `reasoning` (GLM-F1, the compat adapter).
259
+ {
260
+ model: "z-ai/glm-5.3-flash",
261
+ endpoint: "https://openrouter.ai",
262
+ capabilities: {
263
+ contextWindow: 1_048_576,
264
+ maxOutputTokens: 131_072,
265
+ promptCaching: "automatic",
266
+ reasoning: {
267
+ emitsThinkingStream: true,
268
+ thinking: null,
269
+ effort: { levels: ["low", "medium", "high"], default: null, wire: "reasoning_effort" },
270
+ asOf: "2026-09-09",
271
+ source: "https://openrouter.ai/docs/use-cases/reasoning-tokens",
272
+ },
273
+ inputModalities: null,
274
+ },
275
+ capabilitiesAsOf: "2026-09-09",
276
+ capabilitiesSource: "https://openrouter.ai/api/v1/models",
277
+ pricing: { inputPerM: 0.075, outputPerM: 0.25, cacheReadPerM: 0.015, cacheWritePerM: 0, asOf: "2026-09-09", source: "https://openrouter.ai/api/v1/models" },
278
+ },
235
279
  ];
236
280
  /**
237
281
  * Look a model up. `endpoint` narrows: an entry WITH an endpoint only
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, composeToolTable, microcompactFor } from "./compose.js";
11
11
  import { truncationGuard } from "./truncation-guard.js";
12
+ import { DEFAULT_STREAM_IDLE_MS, idleGuard } from "./idle-guard.js";
12
13
  import { RequestTracer, traceGuard } from "./trace/guard.js";
13
14
  import { runtimeVersion } from "./trace/writer.js";
14
15
  import { ResumeBlockedError } from "./session.js";
@@ -121,7 +122,10 @@ export class Run {
121
122
  const loopConfig = () => ({
122
123
  // 0.1.40 (R-C item 3): the truncation guard gates the model
123
124
  // stream — a truncated turn's tool batch never executes.
124
- adapter: traceGuard(tracer, truncationGuard(this.#adapter)), // tracer assigned above, before loopConfig
125
+ // LT-1: the idle guard sits closest to the adapter — a stall is
126
+ // aborted and thrown there, the truncation guard and the tracer see
127
+ // the same events and the same error every other transport failure shows them.
128
+ adapter: traceGuard(tracer, truncationGuard(idleGuard(this.#adapter, this.#config.streamIdleMs ?? DEFAULT_STREAM_IDLE_MS))), // tracer assigned above, before loopConfig
125
129
  model: this.#config.model,
126
130
  sessionId: this.#session.id, // P3: tools see their session (ToolContext.sessionId)
127
131
  ...(systemPrompt !== undefined ? { systemPrompt } : {}),
package/dist/session.d.ts CHANGED
@@ -414,6 +414,8 @@ export interface SessionConfig {
414
414
  /** E6: the session context policy (run-start actions, injection-side only). */
415
415
  readonly contextPolicy?: ContextPolicy;
416
416
  readonly maxRetries?: number;
417
+ /** LT-1: the stream watchdog's idle bound (ms); default 120 s, 0 off. */
418
+ readonly streamIdleMs?: number;
417
419
  /**
418
420
  * E1: loaded extensions — their tools join the registry (idempotently;
419
421
  * a collision with a built-in name was already rejected at agent
package/dist/session.js CHANGED
@@ -47,6 +47,7 @@ import { Run } from "./run.js";
47
47
  // TUI2-R3v2 ③ — the side query rides the SAME tracer the runs ride; that
48
48
  // sameness is the whole point (one ledger, one shape, no second path).
49
49
  import { RequestTracer, traceGuard } from "./trace/guard.js";
50
+ import { DEFAULT_STREAM_IDLE_MS, idleGuard } from "./idle-guard.js";
50
51
  import { runtimeVersion } from "./trace/writer.js";
51
52
  /** A session whose disk write was rejected (stale handle) is PERMANENTLY
52
53
  * poisoned: its in-memory log no longer matches the disk, so no further
@@ -338,7 +339,8 @@ export class AgentSession {
338
339
  rentParts: { base: options.systemPrompt, appends: [] },
339
340
  });
340
341
  tracer.init();
341
- const guarded = traceGuard(tracer, this.#adapter);
342
+ // LT-1: the summary request is a model stream too — the same watchdog
343
+ const guarded = traceGuard(tracer, idleGuard(this.#adapter, this.#config.streamIdleMs ?? DEFAULT_STREAM_IDLE_MS));
342
344
  let text = "";
343
345
  try {
344
346
  for await (const ev of guarded.stream({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-runtime",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "kiso runtime — 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.31.0"
28
+ "@vincemakes/kiso-core": "0.32.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@vincemakes/kiso-provider-anthropic": "0.31.0",
32
- "@vincemakes/kiso-provider-openai": "0.31.0",
33
- "@vincemakes/kiso-provider-openai-responses": "0.31.0"
31
+ "@vincemakes/kiso-provider-anthropic": "0.32.0",
32
+ "@vincemakes/kiso-provider-openai": "0.32.0",
33
+ "@vincemakes/kiso-provider-openai-responses": "0.32.0"
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.31.0",
47
+ "@vincemakes/kiso-evals": "0.32.0",
48
48
  "@types/node": "^26.1.2",
49
49
  "typescript": "^5.7.2",
50
50
  "vitest": "^3.0.0"