@vincemakes/kiso-server 0.41.0 → 0.41.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/README.md CHANGED
@@ -50,6 +50,22 @@ await service.close(5_000); // abort what is live,
50
50
  - **Product frames on the stream** (billing, estimates, couriers) are the
51
51
  transport's business; the service delivers the durable events only.
52
52
 
53
+ ## What the host owns
54
+
55
+ - **The store.** `createSessionService({ store })` takes the store the
56
+ factory's agents write to; the service never closes it — `close()`
57
+ aborts runs and forgets sessions, and the host calls `store.closeAll()`
58
+ when its process ends. Everything about an open session is read from
59
+ that session's own log (the object the run writes), so replay and run
60
+ share one truth by construction; a factory that binds its agents to a
61
+ DIFFERENT store is detected — the first run to settle on such a session
62
+ ends with `StoreMismatchError` and the session is refused from then on.
63
+ - **The sanitizer's guarantee.** The default `sanitizeToolArgs` strips
64
+ prose keys and truncates long strings at every depth. It is a UI-noise
65
+ and casual-leak filter for a tool card, not a privacy boundary: a
66
+ product that must guarantee no prose reaches a client supplies its own
67
+ `sanitize` in the projection options.
68
+
53
69
  ## What a host still writes
54
70
 
55
71
  The transport (routes, SSE framing, `Last-Event-ID`, keepalive, status
package/dist/errors.d.ts CHANGED
@@ -19,6 +19,15 @@ export declare class OpenRunError extends Error {
19
19
  readonly runId: string;
20
20
  constructor(sessionId: string, runId: string);
21
21
  }
22
+ /** The factory's agent writes to a different store than the service was
23
+ * given: a run settled and its terminal is not on the service's store.
24
+ * Two stores are two truths, which kiso does not allow — the session is
25
+ * refused from here on (the host's wiring is wrong, not the run). */
26
+ export declare class StoreMismatchError extends Error {
27
+ readonly sessionId: string;
28
+ readonly runId: string;
29
+ constructor(sessionId: string, runId: string, expectedSeq: number);
30
+ }
22
31
  /** drain() was called: no new run or resume starts in this process. */
23
32
  export declare class DrainingError extends Error {
24
33
  constructor();
package/dist/errors.js CHANGED
@@ -29,6 +29,20 @@ export class OpenRunError extends Error {
29
29
  this.runId = runId;
30
30
  }
31
31
  }
32
+ /** The factory's agent writes to a different store than the service was
33
+ * given: a run settled and its terminal is not on the service's store.
34
+ * Two stores are two truths, which kiso does not allow — the session is
35
+ * refused from here on (the host's wiring is wrong, not the run). */
36
+ export class StoreMismatchError extends Error {
37
+ sessionId;
38
+ runId;
39
+ constructor(sessionId, runId, expectedSeq) {
40
+ super(`session ${sessionId}: run ${runId} settled at seq ${expectedSeq} but the service's store does not hold it — the agent factory's store is not the service's store`);
41
+ this.name = "StoreMismatchError";
42
+ this.sessionId = sessionId;
43
+ this.runId = runId;
44
+ }
45
+ }
32
46
  /** drain() was called: no new run or resume starts in this process. */
33
47
  export class DrainingError extends Error {
34
48
  constructor() {
package/dist/index.d.ts CHANGED
@@ -8,9 +8,9 @@
8
8
  */
9
9
  export { createSessionService, SessionService } from "./service.js";
10
10
  export type { AbortOutcome, DrainReport, OpenRun, RunHandle, RunOptions, SessionServiceOptions } from "./service.js";
11
- export { DrainingError, InFlightError, OpenRunError } from "./errors.js";
11
+ export { DrainingError, InFlightError, OpenRunError, StoreMismatchError } from "./errors.js";
12
12
  export { EXECUTION_ENDED, executionDelta } from "./execution.js";
13
13
  export { tail } from "./tail.js";
14
- export { MAX_ARG_VALUE_CHARS, STRIPPED_ARG_KEYS, sanitizeToolArgs, toWireEvent } from "./wire.js";
14
+ export { MAX_ARG_DEPTH, MAX_ARG_VALUE_CHARS, STRIPPED_ARG_KEYS, sanitizeToolArgs, toWireEvent } from "./wire.js";
15
15
  export type { ProjectionOptions } from "./wire.js";
16
16
  export type { Listener } from "./tail.js";
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * the agent factory, and every run is the runtime's own `Session.run()`.
8
8
  */
9
9
  export { createSessionService, SessionService } from "./service.js";
10
- export { DrainingError, InFlightError, OpenRunError } from "./errors.js";
10
+ export { DrainingError, InFlightError, OpenRunError, StoreMismatchError } from "./errors.js";
11
11
  export { EXECUTION_ENDED, executionDelta } from "./execution.js";
12
12
  export { tail } from "./tail.js";
13
- export { MAX_ARG_VALUE_CHARS, STRIPPED_ARG_KEYS, sanitizeToolArgs, toWireEvent } from "./wire.js";
13
+ export { MAX_ARG_DEPTH, MAX_ARG_VALUE_CHARS, STRIPPED_ARG_KEYS, sanitizeToolArgs, toWireEvent } from "./wire.js";
package/dist/service.d.ts CHANGED
@@ -20,9 +20,19 @@ import { type Listener } from "./tail.js";
20
20
  * loop of its own.
21
21
  */
22
22
  export interface SessionServiceOptions {
23
- /** The store every session in this process lives on — the same one the
24
- * factory's agents write to. The service reads it for replay and for
25
- * the open-run check. */
23
+ /** The store every session in this process lives on — the SAME one the
24
+ * factory's agents write to. The service reads it for boot recovery
25
+ * (`openRuns()`) and for cold reads of sessions it has not opened;
26
+ * everything about an open session is read from that session's own
27
+ * log, so the run that writes and the replay that reads share one
28
+ * object by construction. The remaining way to get two stores — a
29
+ * factory that binds its agents to another store — is DETECTED: the
30
+ * first run to settle on such a session ends with StoreMismatchError
31
+ * and the session is refused from then on.
32
+ *
33
+ * Ownership: the host owns the store. `close()` aborts runs and
34
+ * forgets sessions; it never closes the store — the host calls
35
+ * `store.closeAll()` when its process ends. */
26
36
  readonly store: SessionStore;
27
37
  /** The product's agent factory, called once per session id (and again by
28
38
  * `reopen`). Everything product-specific — prompt, tools, extensions,
@@ -121,9 +131,12 @@ export declare class SessionService {
121
131
  readonly remaining: number;
122
132
  }>;
123
133
  /** Every event after `after` — the ones on disk now, then the live ones
124
- * as they land — exactly once each, in order. Returns the unsubscribe. */
134
+ * as they land — exactly once each, in order. Returns the unsubscribe.
135
+ * The replay reads the OPEN session's own log, never a second copy. */
125
136
  subscribe(sessionId: string, after: number, listener: Listener): Promise<() => void>;
126
- /** The durable log after `after` — a plain read, no session opened. */
137
+ /** The durable log after `after`. An open session answers from its own
138
+ * log; a session this process has not opened is read from the store
139
+ * (a cold read, nothing opened). */
127
140
  events(sessionId: string, after?: number): Event[];
128
141
  isRunning(sessionId: string): boolean;
129
142
  /** The highest seq this process has delivered for the session, −1 before any. */
@@ -133,6 +146,8 @@ export declare class SessionService {
133
146
  /** The run a previous process left without a terminal on this session,
134
147
  * or null. The plain read the transport's snapshot needs. */
135
148
  openRun(sessionId: string): string | null;
149
+ /** True once a settled run proved the factory's store is not this one. */
150
+ mismatched(sessionId: string): boolean;
136
151
  /** Boot recovery: every session on the store whose last run has no
137
152
  * terminal. The host decides what to do with them (resume, or leave
138
153
  * for the next request to find). */
@@ -144,7 +159,9 @@ export declare class SessionService {
144
159
  /** Abort every live run, wait up to `graceMs` for each to settle (a
145
160
  * tool that ignores the abort keeps its run open until it returns),
146
161
  * then forget every session. Whatever did not settle is resumable by
147
- * the next process — persist-first means nothing delivered is lost. */
162
+ * the next process — persist-first means nothing delivered is lost.
163
+ * The STORE is not closed here: the host owns it and calls
164
+ * `store.closeAll()` when its process ends. */
148
165
  close(graceMs?: number): Promise<void>;
149
166
  }
150
167
  export declare function createSessionService(options: SessionServiceOptions): SessionService;
package/dist/service.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { openRunId } from "@vincemakes/kiso-runtime/internal";
2
- import { DrainingError, InFlightError, OpenRunError } from "./errors.js";
2
+ import { DrainingError, InFlightError, OpenRunError, StoreMismatchError } from "./errors.js";
3
3
  import { executionDelta } from "./execution.js";
4
4
  import { tail } from "./tail.js";
5
5
  export class SessionService {
@@ -24,7 +24,7 @@ export class SessionService {
24
24
  const opening = (async () => {
25
25
  const agent = await this.#open(sessionId);
26
26
  const session = await agent.session({ id: sessionId });
27
- const held = { id: sessionId, agent, session, listeners: new Set(), live: null, pumping: null, executing: 0, highWater: -1 };
27
+ const held = { id: sessionId, agent, session, listeners: new Set(), live: null, pumping: null, executing: 0, highWater: -1, mismatch: null };
28
28
  this.#held.set(sessionId, held);
29
29
  return held;
30
30
  })();
@@ -73,6 +73,14 @@ export class SessionService {
73
73
  held.executing = 0;
74
74
  }
75
75
  }
76
+ // the one-truth check: what this run wrote must be on the store the
77
+ // service was given. Persist-first means the terminal is on disk
78
+ // before it was yielded above; a store that does not hold it is a
79
+ // different store.
80
+ if (held.highWater >= 0 && !this.#store.load(held.id).some((r) => r.event.seq === held.highWater)) {
81
+ held.mismatch = new StoreMismatchError(held.id, run.runId, held.highWater);
82
+ throw held.mismatch;
83
+ }
76
84
  })();
77
85
  held.pumping = pumping;
78
86
  void pumping.catch(() => { }); // a host that does not await `done` gets no unhandled rejection
@@ -85,6 +93,8 @@ export class SessionService {
85
93
  if (this.#draining)
86
94
  throw new DrainingError();
87
95
  const held = await this.#hold(sessionId);
96
+ if (held.mismatch !== null)
97
+ throw held.mismatch;
88
98
  if (held.live !== null)
89
99
  throw new InFlightError(sessionId, held.live.runId);
90
100
  const open = openRunId(this.#store.load(sessionId));
@@ -102,6 +112,8 @@ export class SessionService {
102
112
  if (this.#draining)
103
113
  throw new DrainingError();
104
114
  const held = await this.#hold(sessionId);
115
+ if (held.mismatch !== null)
116
+ throw held.mismatch;
105
117
  if (held.live !== null)
106
118
  throw new InFlightError(sessionId, held.live.runId);
107
119
  return this.#pump(held, held.session.resume());
@@ -155,17 +167,19 @@ export class SessionService {
155
167
  }
156
168
  // ---- observation ----------------------------------------------------------
157
169
  /** Every event after `after` — the ones on disk now, then the live ones
158
- * as they land — exactly once each, in order. Returns the unsubscribe. */
170
+ * as they land — exactly once each, in order. Returns the unsubscribe.
171
+ * The replay reads the OPEN session's own log, never a second copy. */
159
172
  async subscribe(sessionId, after, listener) {
160
173
  const held = await this.#hold(sessionId);
161
- return tail(() => this.events(sessionId, after), held.listeners, after, listener);
174
+ return tail(() => held.session.log.all.filter((e) => e.seq > after), held.listeners, after, listener);
162
175
  }
163
- /** The durable log after `after` — a plain read, no session opened. */
176
+ /** The durable log after `after`. An open session answers from its own
177
+ * log; a session this process has not opened is read from the store
178
+ * (a cold read, nothing opened). */
164
179
  events(sessionId, after = -1) {
165
- return this.#store
166
- .load(sessionId)
167
- .map((r) => r.event)
168
- .filter((e) => e.seq > after);
180
+ const held = this.#held.get(sessionId);
181
+ const events = held !== undefined ? held.session.log.all : this.#store.load(sessionId).map((r) => r.event);
182
+ return events.filter((e) => e.seq > after);
169
183
  }
170
184
  isRunning(sessionId) {
171
185
  return this.#held.get(sessionId)?.live !== null && this.#held.get(sessionId)?.live !== undefined;
@@ -188,6 +202,10 @@ export class SessionService {
188
202
  openRun(sessionId) {
189
203
  return openRunId(this.#store.load(sessionId)) ?? null;
190
204
  }
205
+ /** True once a settled run proved the factory's store is not this one. */
206
+ mismatched(sessionId) {
207
+ return this.#held.get(sessionId)?.mismatch !== null && this.#held.get(sessionId)?.mismatch !== undefined;
208
+ }
191
209
  /** Boot recovery: every session on the store whose last run has no
192
210
  * terminal. The host decides what to do with them (resume, or leave
193
211
  * for the next request to find). */
@@ -237,7 +255,9 @@ export class SessionService {
237
255
  /** Abort every live run, wait up to `graceMs` for each to settle (a
238
256
  * tool that ignores the abort keeps its run open until it returns),
239
257
  * then forget every session. Whatever did not settle is resumable by
240
- * the next process — persist-first means nothing delivered is lost. */
258
+ * the next process — persist-first means nothing delivered is lost.
259
+ * The STORE is not closed here: the host owns it and calls
260
+ * `store.closeAll()` when its process ends. */
241
261
  async close(graceMs = 0) {
242
262
  this.#draining = true;
243
263
  const settling = [];
package/dist/wire.d.ts CHANGED
@@ -22,8 +22,13 @@ export interface ProjectionOptions {
22
22
  export declare const STRIPPED_ARG_KEYS: ReadonlySet<string>;
23
23
  /** Longer string values are cut here; the card shows a prefix. */
24
24
  export declare const MAX_ARG_VALUE_CHARS = 200;
25
- /** The default sanitizer: drops the prose keys, truncates long strings,
26
- * leaves non-objects alone. Shallow — an argument's nested object is
27
- * passed through as is. */
28
- export declare function sanitizeToolArgs(input: unknown): unknown;
25
+ /** Nesting beyond this is replaced by a marker rather than walked. */
26
+ export declare const MAX_ARG_DEPTH = 8;
27
+ /** The default sanitizer: drops the prose keys and truncates long strings
28
+ * AT EVERY DEPTH — objects and arrays are walked (0.41.1; the 0.41.0
29
+ * version was shallow, so `{ options: { prompt } }` passed the prompt
30
+ * through). What it is: a UI-noise and casual-leak filter for a tool
31
+ * card. What it is not: a privacy boundary — a product that must
32
+ * guarantee no prose reaches a client supplies its own `sanitize`. */
33
+ export declare function sanitizeToolArgs(input: unknown, depth?: number): unknown;
29
34
  export declare function toWireEvent(event: Event, options?: ProjectionOptions): WireEvent | null;
package/dist/wire.js CHANGED
@@ -5,17 +5,28 @@ import { DURABLE_TO_WIRE, SANITIZED_FIELDS, WIRE_FIELDS } from "@vincemakes/kiso
5
5
  export const STRIPPED_ARG_KEYS = new Set(["prompt", "description", "text", "body", "content", "instructions", "system_prompt", "input_text"]);
6
6
  /** Longer string values are cut here; the card shows a prefix. */
7
7
  export const MAX_ARG_VALUE_CHARS = 200;
8
- /** The default sanitizer: drops the prose keys, truncates long strings,
9
- * leaves non-objects alone. Shallow — an argument's nested object is
10
- * passed through as is. */
11
- export function sanitizeToolArgs(input) {
12
- if (input === null || typeof input !== "object" || Array.isArray(input))
8
+ /** Nesting beyond this is replaced by a marker rather than walked. */
9
+ export const MAX_ARG_DEPTH = 8;
10
+ /** The default sanitizer: drops the prose keys and truncates long strings
11
+ * AT EVERY DEPTH — objects and arrays are walked (0.41.1; the 0.41.0
12
+ * version was shallow, so `{ options: { prompt } }` passed the prompt
13
+ * through). What it is: a UI-noise and casual-leak filter for a tool
14
+ * card. What it is not: a privacy boundary — a product that must
15
+ * guarantee no prose reaches a client supplies its own `sanitize`. */
16
+ export function sanitizeToolArgs(input, depth = 0) {
17
+ if (typeof input === "string")
18
+ return input.length > MAX_ARG_VALUE_CHARS ? `${input.slice(0, MAX_ARG_VALUE_CHARS)}…` : input;
19
+ if (input === null || typeof input !== "object")
13
20
  return input;
21
+ if (depth >= MAX_ARG_DEPTH)
22
+ return Array.isArray(input) ? "[…]" : "{…}";
23
+ if (Array.isArray(input))
24
+ return input.map((v) => sanitizeToolArgs(v, depth + 1));
14
25
  const out = {};
15
26
  for (const [key, value] of Object.entries(input)) {
16
27
  if (STRIPPED_ARG_KEYS.has(key))
17
28
  continue;
18
- out[key] = typeof value === "string" && value.length > MAX_ARG_VALUE_CHARS ? `${value.slice(0, MAX_ARG_VALUE_CHARS)}…` : value;
29
+ out[key] = sanitizeToolArgs(value, depth + 1);
19
30
  }
20
31
  return out;
21
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-server",
3
- "version": "0.41.0",
3
+ "version": "0.41.1",
4
4
  "description": "kiso server — the hosted-session service: many durable sessions in one process, observers with exact replay from a sequence number, approve / abort / resume routed to the right run, drain and close. No transport; a product supplies the agent factory.",
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.41.0",
29
- "@vincemakes/kiso-protocol": "0.41.0",
30
- "@vincemakes/kiso-runtime": "0.41.0"
28
+ "@vincemakes/kiso-core": "0.41.1",
29
+ "@vincemakes/kiso-protocol": "0.41.1",
30
+ "@vincemakes/kiso-runtime": "0.41.1"
31
31
  },
32
32
  "devDependencies": {
33
- "@vincemakes/kiso-evals": "0.41.0",
33
+ "@vincemakes/kiso-evals": "0.41.1",
34
34
  "@types/node": "^26.1.2",
35
35
  "typescript": "^5.7.2",
36
36
  "vitest": "^3.0.0"