@vincemakes/kiso-server 0.53.0 → 0.54.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/http.d.ts CHANGED
@@ -18,6 +18,7 @@ import { type ProjectionOptions } from "./wire.js";
18
18
  * POST {prefix}/:id/run 202 { runId } — or the run's own stream with ?stream=1
19
19
  * POST {prefix}/:id/resume 202 { runId } — or the stream with ?stream=1
20
20
  * POST {prefix}/:id/steer 202 { runId } — 409 idle | closed (ADR-0057)
21
+ * POST {prefix}/:id/retract 200 { runId, count } — the person's steers not admitted yet, taken back (ADR-0057)
21
22
  * POST {prefix}/:id/abort 200 idle | stopped — 409 parked (who is waited for)
22
23
  * POST {prefix}/:id/approve 200 { needsResume }
23
24
  * POST {prefix}/:id/uncertain 200 { remaining }
package/dist/http.js CHANGED
@@ -6,7 +6,7 @@ import { toWireEvent } from "./wire.js";
6
6
  * store never did — a nanoid can start with `_` or `-` (about 1 in 32 do)
7
7
  * and every such session answered 404. */
8
8
  const SESSION_ID = /^[A-Za-z0-9._-]{1,128}$/;
9
- const ACTIONS = new Set(["", "state", "events", "replay", "run", "resume", "steer", "abort", "approve", "uncertain"]);
9
+ const ACTIONS = new Set(["", "state", "events", "replay", "run", "resume", "steer", "retract", "abort", "approve", "uncertain"]);
10
10
  export function createHttpHandler(service, options) {
11
11
  const prefix = (options.prefix ?? "/v1/sessions").replace(/\/+$/, "");
12
12
  const keepaliveMs = options.keepaliveMs ?? 15_000;
@@ -238,6 +238,10 @@ export function createHttpHandler(service, options) {
238
238
  json(res, 202, service.steer(sessionId, input));
239
239
  return true;
240
240
  }
241
+ if (action === "retract") {
242
+ json(res, 200, service.retract(sessionId));
243
+ return true;
244
+ }
241
245
  if (action === "abort") {
242
246
  const outcome = await service.abort(sessionId, body["force"] === true ? { force: true } : {});
243
247
  const reply = outcome.kind === "stopped" ? { kind: "stopped", runId: outcome.runId } : outcome;
package/dist/service.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ContentBlock, Event, MessageSource } from "@vincemakes/kiso-core";
1
+ import type { ContentBlock, Event, MessageSource, UserInputVia } from "@vincemakes/kiso-core";
2
2
  import type { Agent, ApprovalRequest, Session, SessionStore } from "@vincemakes/kiso-runtime";
3
3
  import { type Listener } from "./tail.js";
4
4
  /**
@@ -81,6 +81,8 @@ export interface RunHandle {
81
81
  }
82
82
  export interface RunOptions {
83
83
  readonly source?: MessageSource;
84
+ /** ADR-0058 §8: the receipt a task wake carries (the host's onWake hands it over), recorded on the run's user_input. */
85
+ readonly via?: UserInputVia;
84
86
  /** The log holds an open run from a previous process: resume it to its
85
87
  * terminal first, then start this turn. Without this the call refuses
86
88
  * with `OpenRunError` — the service never resumes silently. */
@@ -127,6 +129,13 @@ export declare class SessionService {
127
129
  steer(sessionId: string, input: string | readonly ContentBlock[]): {
128
130
  readonly runId: string;
129
131
  };
132
+ /** ADR-0057 — take back the person's input the live run has not admitted yet (a task notice stays). The host
133
+ * steers again what it still wants, in order: a page cancelling one waiting line takes them all back and
134
+ * re-steers the rest. Nothing to take back when no run is live. */
135
+ retract(sessionId: string): {
136
+ readonly runId: string | null;
137
+ readonly count: number;
138
+ };
130
139
  /** ADR-0057 — what the run that ended at `terminalSeq` accepted and never
131
140
  * admitted; null when nothing, or when that is not the latest terminal. */
132
141
  unadmittedAt(sessionId: string, terminalSeq: number): {
package/dist/service.js CHANGED
@@ -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, mismatch: null, unadmitted: null };
27
+ const held = { id: sessionId, agent, session, listeners: new Set(), live: null, pumping: null, executing: 0, highWater: -1, published: session.log.lastSeq, mismatch: null, unadmitted: null };
28
28
  this.#held.set(sessionId, held);
29
29
  return held;
30
30
  })();
@@ -48,6 +48,28 @@ export class SessionService {
48
48
  held.agent = agent;
49
49
  held.session = await agent.session({ id: sessionId });
50
50
  }
51
+ /** Fan out every event on the open log after `published`, in seq order.
52
+ * A run's events arrive through `#pump`, but a verdict given while no
53
+ * run is live (`resolveUncertain`, an offline `approve`) is appended
54
+ * straight to the log; reading the log here — not the one event in
55
+ * hand — is what keeps an event appended outside a run from being
56
+ * skipped when a run's next event reaches a subscriber first. */
57
+ #publish(held) {
58
+ const log = held.session.log.all;
59
+ // the log's seq is its index (EventLog refuses a discontinuity)
60
+ for (let i = held.published + 1; i < log.length; i++) {
61
+ const event = log[i];
62
+ held.published = event.seq;
63
+ for (const listener of [...held.listeners]) {
64
+ try {
65
+ listener(event);
66
+ }
67
+ catch {
68
+ held.listeners.delete(listener); // a throwing observer leaves; the run does not
69
+ }
70
+ }
71
+ }
72
+ }
51
73
  // ---- runs -----------------------------------------------------------------
52
74
  #pump(held, run) {
53
75
  held.live = run;
@@ -60,14 +82,7 @@ export class SessionService {
60
82
  held.unadmitted = { runId: run.runId, terminalSeq: event.seq, items: run.unadmitted() };
61
83
  held.executing = Math.max(0, held.executing + executionDelta(event));
62
84
  held.highWater = Math.max(held.highWater, event.seq);
63
- for (const listener of [...held.listeners]) {
64
- try {
65
- listener(event);
66
- }
67
- catch {
68
- held.listeners.delete(listener); // a throwing observer leaves; the run does not
69
- }
70
- }
85
+ this.#publish(held);
71
86
  }
72
87
  }
73
88
  finally {
@@ -113,6 +128,15 @@ export class SessionService {
113
128
  live.steer(input);
114
129
  return { runId: live.runId };
115
130
  }
131
+ /** ADR-0057 — take back the person's input the live run has not admitted yet (a task notice stays). The host
132
+ * steers again what it still wants, in order: a page cancelling one waiting line takes them all back and
133
+ * re-steers the rest. Nothing to take back when no run is live. */
134
+ retract(sessionId) {
135
+ const live = this.#held.get(sessionId)?.live ?? null;
136
+ if (live === null)
137
+ return { runId: null, count: 0 };
138
+ return { runId: live.runId, count: live.retract().length };
139
+ }
116
140
  /** ADR-0057 — what the run that ended at `terminalSeq` accepted and never
117
141
  * admitted; null when nothing, or when that is not the latest terminal. */
118
142
  unadmittedAt(sessionId, terminalSeq) {
@@ -136,7 +160,7 @@ export class SessionService {
136
160
  throw new OpenRunError(sessionId, open);
137
161
  await this.#pump(held, held.session.resume()).done;
138
162
  }
139
- const run = held.session.run(input, options.source !== undefined ? { source: options.source } : {});
163
+ const run = held.session.run(input, { ...(options.source !== undefined ? { source: options.source } : {}), ...(options.via !== undefined ? { via: options.via } : {}) });
140
164
  return this.#pump(held, run);
141
165
  }
142
166
  /** Drive the open run (one a previous process left, or one parked at an
@@ -182,6 +206,10 @@ export class SessionService {
182
206
  const held = await this.#hold(sessionId);
183
207
  const wasLive = held.live !== null;
184
208
  await held.session.approve(decisionId, allow, reason);
209
+ // a live run appends the verdict itself and `#pump` fans it out once it
210
+ // is on disk; only a verdict appended with no run live is published here
211
+ if (held.live === null)
212
+ this.#publish(held);
185
213
  return { needsResume: !wasLive };
186
214
  }
187
215
  async pendingApprovals(sessionId) {
@@ -196,6 +224,8 @@ export class SessionService {
196
224
  async resolveUncertain(sessionId, executionId, resolution) {
197
225
  const held = await this.#hold(sessionId);
198
226
  await held.session.resolveUncertain(executionId, resolution);
227
+ if (held.live === null)
228
+ this.#publish(held);
199
229
  return { remaining: held.session.uncertainExecutions().length };
200
230
  }
201
231
  // ---- observation ----------------------------------------------------------
@@ -212,7 +242,9 @@ export class SessionService {
212
242
  events(sessionId, after = -1) {
213
243
  const held = this.#held.get(sessionId);
214
244
  const events = held !== undefined ? held.session.log.all : this.#store.load(sessionId).map((r) => r.event);
215
- return events.filter((e) => e.seq > after);
245
+ // the log's seq is its index (EventLog refuses a discontinuity): no scan, and with no `after` no copy either — a
246
+ // host that read every event's frames through a filtered copy spent seconds per replay on a long Work. Read-only.
247
+ return (after < 0 ? events : events.slice(after + 1));
216
248
  }
217
249
  isRunning(sessionId) {
218
250
  return this.#held.get(sessionId)?.live !== null && this.#held.get(sessionId)?.live !== undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-server",
3
- "version": "0.53.0",
3
+ "version": "0.54.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.53.0",
29
- "@vincemakes/kiso-protocol": "0.53.0",
30
- "@vincemakes/kiso-runtime": "0.53.0"
28
+ "@vincemakes/kiso-core": "0.54.1",
29
+ "@vincemakes/kiso-protocol": "0.54.1",
30
+ "@vincemakes/kiso-runtime": "0.54.1"
31
31
  },
32
32
  "devDependencies": {
33
- "@vincemakes/kiso-evals": "0.53.0",
33
+ "@vincemakes/kiso-evals": "0.54.1",
34
34
  "@types/node": "^26.1.2",
35
35
  "typescript": "^5.7.2",
36
36
  "vitest": "^3.0.0"