@vincemakes/kiso-runtime 0.31.1 → 0.32.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.
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
+ }
@@ -246,6 +246,36 @@ const ENTRIES = [
246
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
247
  chatgptRow("gpt-6-astra", { levels: ULTRA_LADDER, default: "low", source: CHATGPT_PRESETS_SOURCE_2, asOf: OPENAI_MODELS_ASOF_2 }),
248
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
+ },
249
279
  ];
250
280
  /**
251
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/dist/store.js CHANGED
@@ -108,7 +108,14 @@ export function sessionTitle(records) {
108
108
  if (asked.length === 0)
109
109
  return "(no prompt)";
110
110
  const substantive = asked.find((t) => !isOpener(t));
111
- return (substantive ?? asked[0]).slice(0, 60);
111
+ // HF-2 (0.32.1): a title is a LABEL and goes on one row — the resume
112
+ // picker's card row, `kiso sessions`, the session card. A pasted
113
+ // heredoc as the first turn kept its newlines here and reached the
114
+ // compositor's one-physical-row check through the picker (the HF-1
115
+ // family). Projected at the source, where the value is produced, so
116
+ // the three consumers cannot differ: a break or a tab is one space,
117
+ // runs collapse, and the cut applies to the projected line.
118
+ return (substantive ?? asked[0]).replace(/[\r\n\t ]+/g, " ").trim().slice(0, 60);
112
119
  }
113
120
  export class SessionStore {
114
121
  root;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-runtime",
3
- "version": "0.31.1",
3
+ "version": "0.32.1",
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.1"
28
+ "@vincemakes/kiso-core": "0.32.1"
29
29
  },
30
30
  "peerDependencies": {
31
- "@vincemakes/kiso-provider-anthropic": "0.31.1",
32
- "@vincemakes/kiso-provider-openai": "0.31.1",
33
- "@vincemakes/kiso-provider-openai-responses": "0.31.1"
31
+ "@vincemakes/kiso-provider-anthropic": "0.32.1",
32
+ "@vincemakes/kiso-provider-openai": "0.32.1",
33
+ "@vincemakes/kiso-provider-openai-responses": "0.32.1"
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.1",
47
+ "@vincemakes/kiso-evals": "0.32.1",
48
48
  "@types/node": "^26.1.2",
49
49
  "typescript": "^5.7.2",
50
50
  "vitest": "^3.0.0"