@sna-sdk/core 0.6.1 → 0.7.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.
package/README.md CHANGED
@@ -84,13 +84,18 @@ const db = getDb(); // SQLite instance (data/sna.db)
84
84
  | `@sna-sdk/core/server/routes/agent` | `createAgentRoutes()`, `runOnce()` |
85
85
  | `@sna-sdk/core/db/schema` | `getDb()`, `ChatSession`, `ChatMessage`, `SkillEvent` types |
86
86
  | `@sna-sdk/core/providers` | Agent provider factory, `ClaudeCodeProvider` |
87
- | `@sna-sdk/core/lib/sna-run` | `snaRun()` helper for spawning Claude Code |
87
+ | `@sna-sdk/core/lib/sna-run` | `sna` object with `sna.run()` awaitable skill invocation |
88
88
  | `@sna-sdk/core/testing` | `startMockAnthropicServer()` for testing without real API calls |
89
+ | `@sna-sdk/core/electron` | `startSnaServer()` — launch SNA server from Electron main process (asar-aware) |
90
+ | `@sna-sdk/core/node` | `startSnaServer()` — launch SNA server from Node.js (Next.js, Express, etc.) |
89
91
 
90
92
  **Environment Variables:**
91
93
  - `SNA_DB_PATH` — Override SQLite database location (default: `process.cwd()/data/sna.db`)
92
94
  - `SNA_CLAUDE_COMMAND` — Override claude binary path
93
95
  - `SNA_PORT` — API server port (default: 3099)
96
+ - `SNA_SQLITE_NATIVE_BINDING` — Absolute path to `better_sqlite3.node` native binary; bypasses `bindings` module resolution for Electron packaged apps
97
+ - `SNA_MAX_SESSIONS` — Maximum concurrent agent sessions (default: 5)
98
+ - `SNA_PERMISSION_MODE` — Default permission mode for spawned agents (`acceptEdits` | `bypassPermissions` | `default`)
94
99
 
95
100
  ## Documentation
96
101
 
@@ -35,13 +35,20 @@ function resolveClaudePath(cwd) {
35
35
  return "claude";
36
36
  }
37
37
  }
38
- class ClaudeCodeProcess {
38
+ const _ClaudeCodeProcess = class _ClaudeCodeProcess {
39
39
  constructor(proc, options) {
40
40
  this.emitter = new EventEmitter();
41
41
  this._alive = true;
42
42
  this._sessionId = null;
43
43
  this._initEmitted = false;
44
44
  this.buffer = "";
45
+ /**
46
+ * FIFO event queue — ALL events (deltas, assistant, complete, etc.) go through
47
+ * this queue. A fixed-interval timer drains one item at a time, guaranteeing
48
+ * strict ordering: deltas → assistant → complete, never out of order.
49
+ */
50
+ this.eventQueue = [];
51
+ this.drainTimer = null;
45
52
  this.proc = proc;
46
53
  proc.stdout.on("data", (chunk) => {
47
54
  this.buffer += chunk.toString();
@@ -56,7 +63,7 @@ class ClaudeCodeProcess {
56
63
  this._sessionId = msg.session_id;
57
64
  }
58
65
  const event = this.normalizeEvent(msg);
59
- if (event) this.emitter.emit("event", event);
66
+ if (event) this.enqueue(event);
60
67
  } catch {
61
68
  }
62
69
  }
@@ -69,10 +76,11 @@ class ClaudeCodeProcess {
69
76
  try {
70
77
  const msg = JSON.parse(this.buffer);
71
78
  const event = this.normalizeEvent(msg);
72
- if (event) this.emitter.emit("event", event);
79
+ if (event) this.enqueue(event);
73
80
  } catch {
74
81
  }
75
82
  }
83
+ this.flushQueue();
76
84
  this.emitter.emit("exit", code);
77
85
  logger.log("agent", `process exited (code=${code})`);
78
86
  });
@@ -88,35 +96,58 @@ class ClaudeCodeProcess {
88
96
  this.send(options.prompt);
89
97
  }
90
98
  }
99
+ // ~67 events/sec
91
100
  /**
92
- * Split completed assistant text into chunks and emit assistant_delta events
93
- * at a fixed rate (~270 chars/sec), followed by the final assistant event.
94
- *
95
- * CHUNK_SIZE chars every CHUNK_DELAY_MS → natural TPS feel regardless of length.
101
+ * Enqueue an event for ordered emission.
102
+ * Starts the drain timer if not already running.
96
103
  */
97
- emitTextAsDeltas(text) {
104
+ enqueue(event) {
105
+ this.eventQueue.push(event);
106
+ if (!this.drainTimer) {
107
+ this.drainTimer = setInterval(() => this.drainOne(), _ClaudeCodeProcess.DRAIN_INTERVAL_MS);
108
+ }
109
+ }
110
+ /** Emit one event from the front of the queue. Stop timer when empty. */
111
+ drainOne() {
112
+ const event = this.eventQueue.shift();
113
+ if (event) {
114
+ this.emitter.emit("event", event);
115
+ }
116
+ if (this.eventQueue.length === 0 && this.drainTimer) {
117
+ clearInterval(this.drainTimer);
118
+ this.drainTimer = null;
119
+ }
120
+ }
121
+ /** Flush all remaining queued events immediately (used on process exit). */
122
+ flushQueue() {
123
+ if (this.drainTimer) {
124
+ clearInterval(this.drainTimer);
125
+ this.drainTimer = null;
126
+ }
127
+ while (this.eventQueue.length > 0) {
128
+ this.emitter.emit("event", this.eventQueue.shift());
129
+ }
130
+ }
131
+ /**
132
+ * Split completed assistant text into delta chunks and enqueue them,
133
+ * followed by the final assistant event. All go through the FIFO queue
134
+ * so subsequent events (complete, etc.) are guaranteed to come after.
135
+ */
136
+ enqueueTextAsDeltas(text) {
98
137
  const CHUNK_SIZE = 4;
99
- const CHUNK_DELAY_MS = 15;
100
- let t = 0;
101
138
  for (let i = 0; i < text.length; i += CHUNK_SIZE) {
102
- const chunk = text.slice(i, i + CHUNK_SIZE);
103
- setTimeout(() => {
104
- this.emitter.emit("event", {
105
- type: "assistant_delta",
106
- delta: chunk,
107
- index: 0,
108
- timestamp: Date.now()
109
- });
110
- }, t);
111
- t += CHUNK_DELAY_MS;
112
- }
113
- setTimeout(() => {
114
- this.emitter.emit("event", {
115
- type: "assistant",
116
- message: text,
139
+ this.enqueue({
140
+ type: "assistant_delta",
141
+ delta: text.slice(i, i + CHUNK_SIZE),
142
+ index: 0,
117
143
  timestamp: Date.now()
118
144
  });
119
- }, t);
145
+ }
146
+ this.enqueue({
147
+ type: "assistant",
148
+ message: text,
149
+ timestamp: Date.now()
150
+ });
120
151
  }
121
152
  get alive() {
122
153
  return this._alive;
@@ -217,10 +248,10 @@ class ClaudeCodeProcess {
217
248
  }
218
249
  if (events.length > 0 || textBlocks.length > 0) {
219
250
  for (const e of events) {
220
- this.emitter.emit("event", e);
251
+ this.enqueue(e);
221
252
  }
222
253
  for (const text of textBlocks) {
223
- this.emitTextAsDeltas(text);
254
+ this.enqueueTextAsDeltas(text);
224
255
  }
225
256
  }
226
257
  return null;
@@ -288,7 +319,9 @@ class ClaudeCodeProcess {
288
319
  return null;
289
320
  }
290
321
  }
291
- }
322
+ };
323
+ _ClaudeCodeProcess.DRAIN_INTERVAL_MS = 15;
324
+ let ClaudeCodeProcess = _ClaudeCodeProcess;
292
325
  class ClaudeCodeProvider {
293
326
  constructor() {
294
327
  this.name = "claude-code";
package/dist/db/schema.js CHANGED
@@ -5,6 +5,14 @@ const DB_PATH = process.env.SNA_DB_PATH ?? path.join(process.cwd(), "data/sna.db
5
5
  const NATIVE_DIR = path.join(process.cwd(), ".sna/native");
6
6
  let _db = null;
7
7
  function loadBetterSqlite3() {
8
+ const modulesPath = process.env.SNA_MODULES_PATH;
9
+ if (modulesPath) {
10
+ const entry = path.join(modulesPath, "better-sqlite3");
11
+ if (fs.existsSync(entry)) {
12
+ const req2 = createRequire(path.join(modulesPath, "noop.js"));
13
+ return req2("better-sqlite3");
14
+ }
15
+ }
8
16
  const nativeEntry = path.join(NATIVE_DIR, "node_modules", "better-sqlite3");
9
17
  if (fs.existsSync(nativeEntry)) {
10
18
  const req2 = createRequire(path.join(NATIVE_DIR, "noop.js"));
@@ -57,32 +57,6 @@ Ensure "@sna-sdk/core" is listed in asarUnpack in your electron-builder config.`
57
57
  }
58
58
  return script;
59
59
  }
60
- function resolveNativeBinding(override) {
61
- if (override) {
62
- if (!import_fs.default.existsSync(override)) {
63
- console.warn(`[sna] SNA nativeBinding override not found: ${override}`);
64
- return void 0;
65
- }
66
- return override;
67
- }
68
- const BINDING_REL = import_path.default.join("better-sqlite3", "build", "Release", "better_sqlite3.node");
69
- const resourcesPath = process.resourcesPath;
70
- if (resourcesPath) {
71
- const unpackedBase = import_path.default.join(resourcesPath, "app.asar.unpacked", "node_modules");
72
- const candidates = [
73
- import_path.default.join(unpackedBase, BINDING_REL),
74
- // nested under @sna-sdk/core if hoisting differs
75
- import_path.default.join(unpackedBase, "@sna-sdk", "core", "node_modules", BINDING_REL)
76
- ];
77
- for (const c of candidates) {
78
- if (import_fs.default.existsSync(c)) return c;
79
- }
80
- }
81
- const selfPath = (0, import_url.fileURLToPath)(importMetaUrl);
82
- const local = import_path.default.resolve(import_path.default.dirname(selfPath), "../../node_modules", BINDING_REL);
83
- if (import_fs.default.existsSync(local)) return local;
84
- return void 0;
85
- }
86
60
  function buildNodePath() {
87
61
  const resourcesPath = process.resourcesPath;
88
62
  if (!resourcesPath) return void 0;
@@ -97,8 +71,13 @@ async function startSnaServer(options) {
97
71
  const readyTimeout = options.readyTimeout ?? 15e3;
98
72
  const { onLog } = options;
99
73
  const standaloneScript = resolveStandaloneScript();
100
- const nativeBinding = resolveNativeBinding(options.nativeBinding);
101
74
  const nodePath = buildNodePath();
75
+ let consumerModules;
76
+ try {
77
+ const bsPkg = require.resolve("better-sqlite3/package.json", { paths: [process.cwd()] });
78
+ consumerModules = import_path.default.resolve(bsPkg, "../..");
79
+ } catch {
80
+ }
102
81
  const env = {
103
82
  ...process.env,
104
83
  SNA_PORT: String(port),
@@ -106,7 +85,8 @@ async function startSnaServer(options) {
106
85
  ...options.maxSessions != null ? { SNA_MAX_SESSIONS: String(options.maxSessions) } : {},
107
86
  ...options.permissionMode ? { SNA_PERMISSION_MODE: options.permissionMode } : {},
108
87
  ...options.model ? { SNA_MODEL: options.model } : {},
109
- ...nativeBinding ? { SNA_SQLITE_NATIVE_BINDING: nativeBinding } : {},
88
+ ...options.nativeBinding ? { SNA_SQLITE_NATIVE_BINDING: options.nativeBinding } : {},
89
+ ...consumerModules ? { SNA_MODULES_PATH: consumerModules } : {},
110
90
  ...nodePath ? { NODE_PATH: nodePath } : {},
111
91
  // Consumer overrides last so they can always win
112
92
  ...options.env ?? {}
@@ -29,10 +29,10 @@ import { ChildProcess } from 'child_process';
29
29
  *
30
30
  * asarUnpack: ["node_modules/@sna-sdk/core/**"]
31
31
  *
32
- * The better-sqlite3 native binding used by the forked process must be
33
- * compiled for the system Node.js (not Electron). If your app uses
34
- * electron-rebuild, set options.nativeBinding to a Node.js-compiled
35
- * .node file, or let SNA manage its own native install via `sna api:up`.
32
+ * The forked server process runs on Electron's Node.js. The launcher
33
+ * automatically detects the consumer app's electron-rebuilt native modules
34
+ * and passes their path to the server process, so better-sqlite3 just works
35
+ * without any manual configuration.
36
36
  */
37
37
 
38
38
  interface SnaServerOptions {
@@ -16,32 +16,6 @@ Ensure "@sna-sdk/core" is listed in asarUnpack in your electron-builder config.`
16
16
  }
17
17
  return script;
18
18
  }
19
- function resolveNativeBinding(override) {
20
- if (override) {
21
- if (!fs.existsSync(override)) {
22
- console.warn(`[sna] SNA nativeBinding override not found: ${override}`);
23
- return void 0;
24
- }
25
- return override;
26
- }
27
- const BINDING_REL = path.join("better-sqlite3", "build", "Release", "better_sqlite3.node");
28
- const resourcesPath = process.resourcesPath;
29
- if (resourcesPath) {
30
- const unpackedBase = path.join(resourcesPath, "app.asar.unpacked", "node_modules");
31
- const candidates = [
32
- path.join(unpackedBase, BINDING_REL),
33
- // nested under @sna-sdk/core if hoisting differs
34
- path.join(unpackedBase, "@sna-sdk", "core", "node_modules", BINDING_REL)
35
- ];
36
- for (const c of candidates) {
37
- if (fs.existsSync(c)) return c;
38
- }
39
- }
40
- const selfPath = fileURLToPath(import.meta.url);
41
- const local = path.resolve(path.dirname(selfPath), "../../node_modules", BINDING_REL);
42
- if (fs.existsSync(local)) return local;
43
- return void 0;
44
- }
45
19
  function buildNodePath() {
46
20
  const resourcesPath = process.resourcesPath;
47
21
  if (!resourcesPath) return void 0;
@@ -56,8 +30,13 @@ async function startSnaServer(options) {
56
30
  const readyTimeout = options.readyTimeout ?? 15e3;
57
31
  const { onLog } = options;
58
32
  const standaloneScript = resolveStandaloneScript();
59
- const nativeBinding = resolveNativeBinding(options.nativeBinding);
60
33
  const nodePath = buildNodePath();
34
+ let consumerModules;
35
+ try {
36
+ const bsPkg = require.resolve("better-sqlite3/package.json", { paths: [process.cwd()] });
37
+ consumerModules = path.resolve(bsPkg, "../..");
38
+ } catch {
39
+ }
61
40
  const env = {
62
41
  ...process.env,
63
42
  SNA_PORT: String(port),
@@ -65,7 +44,8 @@ async function startSnaServer(options) {
65
44
  ...options.maxSessions != null ? { SNA_MAX_SESSIONS: String(options.maxSessions) } : {},
66
45
  ...options.permissionMode ? { SNA_PERMISSION_MODE: options.permissionMode } : {},
67
46
  ...options.model ? { SNA_MODEL: options.model } : {},
68
- ...nativeBinding ? { SNA_SQLITE_NATIVE_BINDING: nativeBinding } : {},
47
+ ...options.nativeBinding ? { SNA_SQLITE_NATIVE_BINDING: options.nativeBinding } : {},
48
+ ...consumerModules ? { SNA_MODULES_PATH: consumerModules } : {},
69
49
  ...nodePath ? { NODE_PATH: nodePath } : {},
70
50
  // Consumer overrides last so they can always win
71
51
  ...options.env ?? {}
@@ -57,32 +57,6 @@ Ensure "@sna-sdk/core" is listed in asarUnpack in your electron-builder config.`
57
57
  }
58
58
  return script;
59
59
  }
60
- function resolveNativeBinding(override) {
61
- if (override) {
62
- if (!import_fs.default.existsSync(override)) {
63
- console.warn(`[sna] SNA nativeBinding override not found: ${override}`);
64
- return void 0;
65
- }
66
- return override;
67
- }
68
- const BINDING_REL = import_path.default.join("better-sqlite3", "build", "Release", "better_sqlite3.node");
69
- const resourcesPath = process.resourcesPath;
70
- if (resourcesPath) {
71
- const unpackedBase = import_path.default.join(resourcesPath, "app.asar.unpacked", "node_modules");
72
- const candidates = [
73
- import_path.default.join(unpackedBase, BINDING_REL),
74
- // nested under @sna-sdk/core if hoisting differs
75
- import_path.default.join(unpackedBase, "@sna-sdk", "core", "node_modules", BINDING_REL)
76
- ];
77
- for (const c of candidates) {
78
- if (import_fs.default.existsSync(c)) return c;
79
- }
80
- }
81
- const selfPath = (0, import_url.fileURLToPath)(importMetaUrl);
82
- const local = import_path.default.resolve(import_path.default.dirname(selfPath), "../../node_modules", BINDING_REL);
83
- if (import_fs.default.existsSync(local)) return local;
84
- return void 0;
85
- }
86
60
  function buildNodePath() {
87
61
  const resourcesPath = process.resourcesPath;
88
62
  if (!resourcesPath) return void 0;
@@ -97,8 +71,13 @@ async function startSnaServer(options) {
97
71
  const readyTimeout = options.readyTimeout ?? 15e3;
98
72
  const { onLog } = options;
99
73
  const standaloneScript = resolveStandaloneScript();
100
- const nativeBinding = resolveNativeBinding(options.nativeBinding);
101
74
  const nodePath = buildNodePath();
75
+ let consumerModules;
76
+ try {
77
+ const bsPkg = require.resolve("better-sqlite3/package.json", { paths: [process.cwd()] });
78
+ consumerModules = import_path.default.resolve(bsPkg, "../..");
79
+ } catch {
80
+ }
102
81
  const env = {
103
82
  ...process.env,
104
83
  SNA_PORT: String(port),
@@ -106,7 +85,8 @@ async function startSnaServer(options) {
106
85
  ...options.maxSessions != null ? { SNA_MAX_SESSIONS: String(options.maxSessions) } : {},
107
86
  ...options.permissionMode ? { SNA_PERMISSION_MODE: options.permissionMode } : {},
108
87
  ...options.model ? { SNA_MODEL: options.model } : {},
109
- ...nativeBinding ? { SNA_SQLITE_NATIVE_BINDING: nativeBinding } : {},
88
+ ...options.nativeBinding ? { SNA_SQLITE_NATIVE_BINDING: options.nativeBinding } : {},
89
+ ...consumerModules ? { SNA_MODULES_PATH: consumerModules } : {},
110
90
  ...nodePath ? { NODE_PATH: nodePath } : {},
111
91
  // Consumer overrides last so they can always win
112
92
  ...options.env ?? {}
@@ -379,6 +379,14 @@ function isPortInUse(port) {
379
379
  }
380
380
  function resolveAndCacheClaudePath() {
381
381
  const SHELL = process.env.SHELL || "/bin/zsh";
382
+ try {
383
+ const resolved = execSync(`${SHELL} -l -c "which claude"`, { encoding: "utf8" }).trim();
384
+ if (resolved) {
385
+ fs.writeFileSync(CLAUDE_PATH_FILE, resolved);
386
+ return resolved;
387
+ }
388
+ } catch {
389
+ }
382
390
  const candidates = [
383
391
  "/opt/homebrew/bin/claude",
384
392
  "/usr/local/bin/claude",
@@ -392,13 +400,7 @@ function resolveAndCacheClaudePath() {
392
400
  } catch {
393
401
  }
394
402
  }
395
- try {
396
- const resolved = execSync(`${SHELL} -l -c "which claude"`, { encoding: "utf8" }).trim();
397
- fs.writeFileSync(CLAUDE_PATH_FILE, resolved);
398
- return resolved;
399
- } catch {
400
- return "claude";
401
- }
403
+ return "claude";
402
404
  }
403
405
  function openBrowser(url) {
404
406
  try {
@@ -14,6 +14,10 @@ interface ApiResponses {
14
14
  "sessions.list": {
15
15
  sessions: SessionInfo[];
16
16
  };
17
+ "sessions.update": {
18
+ status: "updated";
19
+ session: string;
20
+ };
17
21
  "sessions.remove": {
18
22
  status: "removed";
19
23
  };
@@ -125,7 +125,6 @@ function createAgentRoutes(sessionManager) {
125
125
  if (session.process?.alive) {
126
126
  session.process.kill();
127
127
  }
128
- session.eventBuffer.length = 0;
129
128
  const provider = getProvider(body.provider ?? "claude-code");
130
129
  try {
131
130
  const db = getDb();
@@ -254,7 +253,7 @@ function createAgentRoutes(sessionManager) {
254
253
  } else {
255
254
  cursor = session.eventCounter;
256
255
  }
257
- while (queue.length > 0 && queue[0].cursor <= cursor) queue.shift();
256
+ while (queue.length > 0 && queue[0].cursor !== -1 && queue[0].cursor <= cursor) queue.shift();
258
257
  while (!signal.aborted) {
259
258
  if (queue.length === 0) {
260
259
  await Promise.race([
@@ -268,7 +267,11 @@ function createAgentRoutes(sessionManager) {
268
267
  if (queue.length > 0) {
269
268
  while (queue.length > 0) {
270
269
  const item = queue.shift();
271
- await stream.writeSSE({ id: String(item.cursor), data: JSON.stringify(item.event) });
270
+ if (item.cursor === -1) {
271
+ await stream.writeSSE({ data: JSON.stringify(item.event) });
272
+ } else {
273
+ await stream.writeSSE({ id: String(item.cursor), data: JSON.stringify(item.event) });
274
+ }
272
275
  }
273
276
  } else {
274
277
  await stream.writeSSE({ data: "" });
@@ -73,18 +73,25 @@ declare class SessionManager {
73
73
  private lifecycleListeners;
74
74
  private configChangedListeners;
75
75
  private stateChangedListeners;
76
+ private metadataChangedListeners;
76
77
  constructor(options?: SessionManagerOptions);
77
78
  /** Restore session metadata from DB (cwd, label, meta). Process state is not restored. */
78
79
  private restoreFromDb;
79
80
  /** Persist session metadata to DB. */
80
81
  private persistSession;
81
- /** Create a new session. Throws if max sessions reached. */
82
+ /** Create a new session. Throws if session already exists or max sessions reached. */
82
83
  createSession(opts?: {
83
84
  id?: string;
84
85
  label?: string;
85
86
  cwd?: string;
86
87
  meta?: Record<string, unknown> | null;
87
88
  }): Session;
89
+ /** Update an existing session's metadata. Throws if session not found. */
90
+ updateSession(id: string, opts: {
91
+ label?: string;
92
+ meta?: Record<string, unknown> | null;
93
+ cwd?: string;
94
+ }): Session;
88
95
  /** Get a session by ID. */
89
96
  getSession(id: string): Session | undefined;
90
97
  /** Get or create a session (used for "default" backward compat). */
@@ -101,6 +108,12 @@ declare class SessionManager {
101
108
  /** Broadcast a skill event to all subscribers (called after DB insert). */
102
109
  broadcastSkillEvent(event: Record<string, unknown>): void;
103
110
  /** Push a synthetic event into a session's event stream (for user message broadcast). */
111
+ /**
112
+ * Push an externally-persisted event into the session.
113
+ * The caller is responsible for DB persistence — this method only updates
114
+ * the in-memory counter/buffer and notifies listeners.
115
+ * eventCounter increments to stay in sync with the DB row count.
116
+ */
104
117
  pushEvent(sessionId: string, event: AgentEvent): void;
105
118
  /** Subscribe to permission request notifications. Returns unsubscribe function. */
106
119
  onPermissionRequest(cb: (sessionId: string, request: Record<string, unknown>, createdAt: number) => void): () => void;
@@ -110,6 +123,8 @@ declare class SessionManager {
110
123
  /** Subscribe to session config changes. Returns unsubscribe function. */
111
124
  onConfigChanged(cb: (event: SessionConfigChangedEvent) => void): () => void;
112
125
  private emitConfigChanged;
126
+ onMetadataChanged(cb: (sessionId: string) => void): () => void;
127
+ private emitMetadataChanged;
113
128
  onStateChanged(cb: (event: {
114
129
  session: string;
115
130
  agentStatus: AgentStatus;
@@ -156,6 +171,7 @@ declare class SessionManager {
156
171
  touch(id: string): void;
157
172
  /** Persist an agent event to chat_messages. */
158
173
  private getMessageStats;
174
+ /** Persist an agent event to chat_messages. Returns true if a row was inserted. */
159
175
  private persistEvent;
160
176
  /** Kill all sessions. Used during shutdown. */
161
177
  killAll(): void;