@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.
@@ -18,6 +18,14 @@ var DB_PATH = process.env.SNA_DB_PATH ?? path.join(process.cwd(), "data/sna.db")
18
18
  var NATIVE_DIR = path.join(process.cwd(), ".sna/native");
19
19
  var _db = null;
20
20
  function loadBetterSqlite3() {
21
+ const modulesPath = process.env.SNA_MODULES_PATH;
22
+ if (modulesPath) {
23
+ const entry = path.join(modulesPath, "better-sqlite3");
24
+ if (fs.existsSync(entry)) {
25
+ const req2 = createRequire(path.join(modulesPath, "noop.js"));
26
+ return req2("better-sqlite3");
27
+ }
28
+ }
21
29
  const nativeEntry = path.join(NATIVE_DIR, "node_modules", "better-sqlite3");
22
30
  if (fs.existsSync(nativeEntry)) {
23
31
  const req2 = createRequire(path.join(NATIVE_DIR, "noop.js"));
@@ -402,13 +410,20 @@ function resolveClaudePath(cwd) {
402
410
  return "claude";
403
411
  }
404
412
  }
405
- var ClaudeCodeProcess = class {
413
+ var _ClaudeCodeProcess = class _ClaudeCodeProcess {
406
414
  constructor(proc, options) {
407
415
  this.emitter = new EventEmitter();
408
416
  this._alive = true;
409
417
  this._sessionId = null;
410
418
  this._initEmitted = false;
411
419
  this.buffer = "";
420
+ /**
421
+ * FIFO event queue — ALL events (deltas, assistant, complete, etc.) go through
422
+ * this queue. A fixed-interval timer drains one item at a time, guaranteeing
423
+ * strict ordering: deltas → assistant → complete, never out of order.
424
+ */
425
+ this.eventQueue = [];
426
+ this.drainTimer = null;
412
427
  this.proc = proc;
413
428
  proc.stdout.on("data", (chunk) => {
414
429
  this.buffer += chunk.toString();
@@ -423,7 +438,7 @@ var ClaudeCodeProcess = class {
423
438
  this._sessionId = msg.session_id;
424
439
  }
425
440
  const event = this.normalizeEvent(msg);
426
- if (event) this.emitter.emit("event", event);
441
+ if (event) this.enqueue(event);
427
442
  } catch {
428
443
  }
429
444
  }
@@ -436,10 +451,11 @@ var ClaudeCodeProcess = class {
436
451
  try {
437
452
  const msg = JSON.parse(this.buffer);
438
453
  const event = this.normalizeEvent(msg);
439
- if (event) this.emitter.emit("event", event);
454
+ if (event) this.enqueue(event);
440
455
  } catch {
441
456
  }
442
457
  }
458
+ this.flushQueue();
443
459
  this.emitter.emit("exit", code);
444
460
  logger.log("agent", `process exited (code=${code})`);
445
461
  });
@@ -455,35 +471,58 @@ var ClaudeCodeProcess = class {
455
471
  this.send(options.prompt);
456
472
  }
457
473
  }
474
+ // ~67 events/sec
475
+ /**
476
+ * Enqueue an event for ordered emission.
477
+ * Starts the drain timer if not already running.
478
+ */
479
+ enqueue(event) {
480
+ this.eventQueue.push(event);
481
+ if (!this.drainTimer) {
482
+ this.drainTimer = setInterval(() => this.drainOne(), _ClaudeCodeProcess.DRAIN_INTERVAL_MS);
483
+ }
484
+ }
485
+ /** Emit one event from the front of the queue. Stop timer when empty. */
486
+ drainOne() {
487
+ const event = this.eventQueue.shift();
488
+ if (event) {
489
+ this.emitter.emit("event", event);
490
+ }
491
+ if (this.eventQueue.length === 0 && this.drainTimer) {
492
+ clearInterval(this.drainTimer);
493
+ this.drainTimer = null;
494
+ }
495
+ }
496
+ /** Flush all remaining queued events immediately (used on process exit). */
497
+ flushQueue() {
498
+ if (this.drainTimer) {
499
+ clearInterval(this.drainTimer);
500
+ this.drainTimer = null;
501
+ }
502
+ while (this.eventQueue.length > 0) {
503
+ this.emitter.emit("event", this.eventQueue.shift());
504
+ }
505
+ }
458
506
  /**
459
- * Split completed assistant text into chunks and emit assistant_delta events
460
- * at a fixed rate (~270 chars/sec), followed by the final assistant event.
461
- *
462
- * CHUNK_SIZE chars every CHUNK_DELAY_MS → natural TPS feel regardless of length.
507
+ * Split completed assistant text into delta chunks and enqueue them,
508
+ * followed by the final assistant event. All go through the FIFO queue
509
+ * so subsequent events (complete, etc.) are guaranteed to come after.
463
510
  */
464
- emitTextAsDeltas(text) {
511
+ enqueueTextAsDeltas(text) {
465
512
  const CHUNK_SIZE = 4;
466
- const CHUNK_DELAY_MS = 15;
467
- let t = 0;
468
513
  for (let i = 0; i < text.length; i += CHUNK_SIZE) {
469
- const chunk = text.slice(i, i + CHUNK_SIZE);
470
- setTimeout(() => {
471
- this.emitter.emit("event", {
472
- type: "assistant_delta",
473
- delta: chunk,
474
- index: 0,
475
- timestamp: Date.now()
476
- });
477
- }, t);
478
- t += CHUNK_DELAY_MS;
479
- }
480
- setTimeout(() => {
481
- this.emitter.emit("event", {
482
- type: "assistant",
483
- message: text,
514
+ this.enqueue({
515
+ type: "assistant_delta",
516
+ delta: text.slice(i, i + CHUNK_SIZE),
517
+ index: 0,
484
518
  timestamp: Date.now()
485
519
  });
486
- }, t);
520
+ }
521
+ this.enqueue({
522
+ type: "assistant",
523
+ message: text,
524
+ timestamp: Date.now()
525
+ });
487
526
  }
488
527
  get alive() {
489
528
  return this._alive;
@@ -584,10 +623,10 @@ var ClaudeCodeProcess = class {
584
623
  }
585
624
  if (events.length > 0 || textBlocks.length > 0) {
586
625
  for (const e of events) {
587
- this.emitter.emit("event", e);
626
+ this.enqueue(e);
588
627
  }
589
628
  for (const text of textBlocks) {
590
- this.emitTextAsDeltas(text);
629
+ this.enqueueTextAsDeltas(text);
591
630
  }
592
631
  }
593
632
  return null;
@@ -656,6 +695,8 @@ var ClaudeCodeProcess = class {
656
695
  }
657
696
  }
658
697
  };
698
+ _ClaudeCodeProcess.DRAIN_INTERVAL_MS = 15;
699
+ var ClaudeCodeProcess = _ClaudeCodeProcess;
659
700
  var ClaudeCodeProvider = class {
660
701
  constructor() {
661
702
  this.name = "claude-code";
@@ -946,7 +987,6 @@ function createAgentRoutes(sessionManager2) {
946
987
  if (session.process?.alive) {
947
988
  session.process.kill();
948
989
  }
949
- session.eventBuffer.length = 0;
950
990
  const provider2 = getProvider(body.provider ?? "claude-code");
951
991
  try {
952
992
  const db = getDb();
@@ -1075,7 +1115,7 @@ function createAgentRoutes(sessionManager2) {
1075
1115
  } else {
1076
1116
  cursor = session.eventCounter;
1077
1117
  }
1078
- while (queue.length > 0 && queue[0].cursor <= cursor) queue.shift();
1118
+ while (queue.length > 0 && queue[0].cursor !== -1 && queue[0].cursor <= cursor) queue.shift();
1079
1119
  while (!signal.aborted) {
1080
1120
  if (queue.length === 0) {
1081
1121
  await Promise.race([
@@ -1089,7 +1129,11 @@ function createAgentRoutes(sessionManager2) {
1089
1129
  if (queue.length > 0) {
1090
1130
  while (queue.length > 0) {
1091
1131
  const item = queue.shift();
1092
- await stream.writeSSE({ id: String(item.cursor), data: JSON.stringify(item.event) });
1132
+ if (item.cursor === -1) {
1133
+ await stream.writeSSE({ data: JSON.stringify(item.event) });
1134
+ } else {
1135
+ await stream.writeSSE({ id: String(item.cursor), data: JSON.stringify(item.event) });
1136
+ }
1093
1137
  }
1094
1138
  } else {
1095
1139
  await stream.writeSSE({ data: "" });
@@ -1373,6 +1417,7 @@ var SessionManager = class {
1373
1417
  this.lifecycleListeners = /* @__PURE__ */ new Set();
1374
1418
  this.configChangedListeners = /* @__PURE__ */ new Set();
1375
1419
  this.stateChangedListeners = /* @__PURE__ */ new Set();
1420
+ this.metadataChangedListeners = /* @__PURE__ */ new Set();
1376
1421
  this.maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;
1377
1422
  this.restoreFromDb();
1378
1423
  }
@@ -1425,26 +1470,11 @@ var SessionManager = class {
1425
1470
  } catch {
1426
1471
  }
1427
1472
  }
1428
- /** Create a new session. Throws if max sessions reached. */
1473
+ /** Create a new session. Throws if session already exists or max sessions reached. */
1429
1474
  createSession(opts = {}) {
1430
1475
  const id = opts.id ?? crypto.randomUUID().slice(0, 8);
1431
1476
  if (this.sessions.has(id)) {
1432
- const existing = this.sessions.get(id);
1433
- let changed = false;
1434
- if (opts.cwd && opts.cwd !== existing.cwd) {
1435
- existing.cwd = opts.cwd;
1436
- changed = true;
1437
- }
1438
- if (opts.label && opts.label !== existing.label) {
1439
- existing.label = opts.label;
1440
- changed = true;
1441
- }
1442
- if (opts.meta !== void 0 && opts.meta !== existing.meta) {
1443
- existing.meta = opts.meta ?? null;
1444
- changed = true;
1445
- }
1446
- if (changed) this.persistSession(existing);
1447
- return existing;
1477
+ throw new Error(`Session "${id}" already exists`);
1448
1478
  }
1449
1479
  const aliveCount = Array.from(this.sessions.values()).filter((s) => s.process?.alive).length;
1450
1480
  if (aliveCount >= this.maxSessions) {
@@ -1468,6 +1498,17 @@ var SessionManager = class {
1468
1498
  this.persistSession(session);
1469
1499
  return session;
1470
1500
  }
1501
+ /** Update an existing session's metadata. Throws if session not found. */
1502
+ updateSession(id, opts) {
1503
+ const session = this.sessions.get(id);
1504
+ if (!session) throw new Error(`Session "${id}" not found`);
1505
+ if (opts.label !== void 0) session.label = opts.label;
1506
+ if (opts.meta !== void 0) session.meta = opts.meta;
1507
+ if (opts.cwd !== void 0) session.cwd = opts.cwd;
1508
+ this.persistSession(session);
1509
+ this.emitMetadataChanged(id);
1510
+ return session;
1511
+ }
1471
1512
  /** Get a session by ID. */
1472
1513
  getSession(id) {
1473
1514
  return this.sessions.get(id);
@@ -1489,27 +1530,45 @@ var SessionManager = class {
1489
1530
  const session = this.sessions.get(sessionId);
1490
1531
  if (!session) throw new Error(`Session "${sessionId}" not found`);
1491
1532
  session.process = proc;
1492
- this.setSessionState(sessionId, session, "processing");
1493
1533
  session.lastActivityAt = Date.now();
1534
+ session.eventBuffer.length = 0;
1535
+ try {
1536
+ const db = getDb();
1537
+ const row = db.prepare(
1538
+ `SELECT COUNT(*) as c FROM chat_messages WHERE session_id = ?`
1539
+ ).get(sessionId);
1540
+ session.eventCounter = row.c;
1541
+ } catch {
1542
+ }
1494
1543
  proc.on("event", (e) => {
1495
- if (e.type === "init" && e.data?.sessionId && !session.ccSessionId) {
1496
- session.ccSessionId = e.data.sessionId;
1497
- this.persistSession(session);
1544
+ if (e.type === "init") {
1545
+ if (e.data?.sessionId && !session.ccSessionId) {
1546
+ session.ccSessionId = e.data.sessionId;
1547
+ this.persistSession(session);
1548
+ }
1549
+ this.setSessionState(sessionId, session, "waiting");
1498
1550
  }
1499
- if (e.type !== "assistant_delta") {
1551
+ if (e.type === "thinking" || e.type === "tool_use" || e.type === "assistant_delta") {
1552
+ this.setSessionState(sessionId, session, "processing");
1553
+ } else if (e.type === "complete" || e.type === "error" || e.type === "interrupted") {
1554
+ this.setSessionState(sessionId, session, "waiting");
1555
+ }
1556
+ const persisted = this.persistEvent(sessionId, e);
1557
+ if (persisted) {
1558
+ session.eventCounter++;
1500
1559
  session.eventBuffer.push(e);
1501
1560
  if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
1502
1561
  session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
1503
1562
  }
1504
- }
1505
- session.eventCounter++;
1506
- if (e.type === "complete" || e.type === "error" || e.type === "interrupted") {
1507
- this.setSessionState(sessionId, session, "waiting");
1508
- }
1509
- this.persistEvent(sessionId, e);
1510
- const listeners = this.eventListeners.get(sessionId);
1511
- if (listeners) {
1512
- for (const cb of listeners) cb(session.eventCounter, e);
1563
+ const listeners = this.eventListeners.get(sessionId);
1564
+ if (listeners) {
1565
+ for (const cb of listeners) cb(session.eventCounter, e);
1566
+ }
1567
+ } else if (e.type === "assistant_delta") {
1568
+ const listeners = this.eventListeners.get(sessionId);
1569
+ if (listeners) {
1570
+ for (const cb of listeners) cb(-1, e);
1571
+ }
1513
1572
  }
1514
1573
  });
1515
1574
  proc.on("exit", (code) => {
@@ -1547,11 +1606,17 @@ var SessionManager = class {
1547
1606
  for (const cb of this.skillEventListeners) cb(event);
1548
1607
  }
1549
1608
  /** Push a synthetic event into a session's event stream (for user message broadcast). */
1609
+ /**
1610
+ * Push an externally-persisted event into the session.
1611
+ * The caller is responsible for DB persistence — this method only updates
1612
+ * the in-memory counter/buffer and notifies listeners.
1613
+ * eventCounter increments to stay in sync with the DB row count.
1614
+ */
1550
1615
  pushEvent(sessionId, event) {
1551
1616
  const session = this.sessions.get(sessionId);
1552
1617
  if (!session) return;
1553
- session.eventBuffer.push(event);
1554
1618
  session.eventCounter++;
1619
+ session.eventBuffer.push(event);
1555
1620
  if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
1556
1621
  session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
1557
1622
  }
@@ -1584,6 +1649,14 @@ var SessionManager = class {
1584
1649
  emitConfigChanged(sessionId, config) {
1585
1650
  for (const cb of this.configChangedListeners) cb({ session: sessionId, config });
1586
1651
  }
1652
+ // ── Session metadata change pub/sub ─────────────────────────────
1653
+ onMetadataChanged(cb) {
1654
+ this.metadataChangedListeners.add(cb);
1655
+ return () => this.metadataChangedListeners.delete(cb);
1656
+ }
1657
+ emitMetadataChanged(sessionId) {
1658
+ for (const cb of this.metadataChangedListeners) cb(sessionId);
1659
+ }
1587
1660
  // ── Agent status change pub/sub ────────────────────────────────
1588
1661
  onStateChanged(cb) {
1589
1662
  this.stateChangedListeners.add(cb);
@@ -1664,7 +1737,6 @@ var SessionManager = class {
1664
1737
  extraArgs: overrides.extraArgs ?? base.extraArgs
1665
1738
  };
1666
1739
  if (session.process?.alive) session.process.kill();
1667
- session.eventBuffer.length = 0;
1668
1740
  const proc = spawnFn(config);
1669
1741
  this.setProcess(id, proc);
1670
1742
  session.lastStartConfig = config;
@@ -1769,6 +1841,7 @@ var SessionManager = class {
1769
1841
  return { messageCount: 0, lastMessage: null };
1770
1842
  }
1771
1843
  }
1844
+ /** Persist an agent event to chat_messages. Returns true if a row was inserted. */
1772
1845
  persistEvent(sessionId, e) {
1773
1846
  try {
1774
1847
  const db = getDb();
@@ -1776,29 +1849,34 @@ var SessionManager = class {
1776
1849
  case "assistant":
1777
1850
  if (e.message) {
1778
1851
  db.prepare(`INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'assistant', ?)`).run(sessionId, e.message);
1852
+ return true;
1779
1853
  }
1780
- break;
1854
+ return false;
1781
1855
  case "thinking":
1782
1856
  if (e.message) {
1783
1857
  db.prepare(`INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'thinking', ?)`).run(sessionId, e.message);
1858
+ return true;
1784
1859
  }
1785
- break;
1860
+ return false;
1786
1861
  case "tool_use": {
1787
1862
  const toolName = e.data?.toolName ?? e.message ?? "tool";
1788
1863
  db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'tool', ?, ?)`).run(sessionId, toolName, JSON.stringify(e.data ?? {}));
1789
- break;
1864
+ return true;
1790
1865
  }
1791
1866
  case "tool_result":
1792
1867
  db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'tool_result', ?, ?)`).run(sessionId, e.message ?? "", JSON.stringify(e.data ?? {}));
1793
- break;
1868
+ return true;
1794
1869
  case "complete":
1795
1870
  db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'status', '', ?)`).run(sessionId, JSON.stringify({ status: "complete", ...e.data }));
1796
- break;
1871
+ return true;
1797
1872
  case "error":
1798
1873
  db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'error', ?, ?)`).run(sessionId, e.message ?? "Error", JSON.stringify({ status: "error" }));
1799
- break;
1874
+ return true;
1875
+ default:
1876
+ return false;
1800
1877
  }
1801
1878
  } catch {
1879
+ return false;
1802
1880
  }
1803
1881
  }
1804
1882
  /** Kill all sessions. Used during shutdown. */
@@ -1841,15 +1919,22 @@ function attachWebSocket(server2, sessionManager2) {
1841
1919
  });
1842
1920
  wss.on("connection", (ws) => {
1843
1921
  logger.log("ws", "client connected");
1844
- const state = { agentUnsubs: /* @__PURE__ */ new Map(), skillEventUnsub: null, skillPollTimer: null, permissionUnsub: null, lifecycleUnsub: null, configChangedUnsub: null, stateChangedUnsub: null };
1922
+ const state = { agentUnsubs: /* @__PURE__ */ new Map(), skillEventUnsub: null, skillPollTimer: null, permissionUnsub: null, lifecycleUnsub: null, configChangedUnsub: null, stateChangedUnsub: null, metadataChangedUnsub: null };
1923
+ const pushSnapshot = () => send(ws, { type: "sessions.snapshot", sessions: sessionManager2.listSessions() });
1924
+ pushSnapshot();
1845
1925
  state.lifecycleUnsub = sessionManager2.onSessionLifecycle((event) => {
1846
1926
  send(ws, { type: "session.lifecycle", ...event });
1927
+ pushSnapshot();
1847
1928
  });
1848
1929
  state.configChangedUnsub = sessionManager2.onConfigChanged((event) => {
1849
1930
  send(ws, { type: "session.config-changed", ...event });
1850
1931
  });
1851
1932
  state.stateChangedUnsub = sessionManager2.onStateChanged((event) => {
1852
1933
  send(ws, { type: "session.state-changed", ...event });
1934
+ pushSnapshot();
1935
+ });
1936
+ state.metadataChangedUnsub = sessionManager2.onMetadataChanged(() => {
1937
+ pushSnapshot();
1853
1938
  });
1854
1939
  ws.on("message", (raw) => {
1855
1940
  let msg;
@@ -1883,6 +1968,8 @@ function attachWebSocket(server2, sessionManager2) {
1883
1968
  state.configChangedUnsub = null;
1884
1969
  state.stateChangedUnsub?.();
1885
1970
  state.stateChangedUnsub = null;
1971
+ state.metadataChangedUnsub?.();
1972
+ state.metadataChangedUnsub = null;
1886
1973
  });
1887
1974
  });
1888
1975
  return wss;
@@ -1894,6 +1981,8 @@ function handleMessage(ws, msg, sm, state) {
1894
1981
  return handleSessionsCreate(ws, msg, sm);
1895
1982
  case "sessions.list":
1896
1983
  return wsReply(ws, msg, { sessions: sm.listSessions() });
1984
+ case "sessions.update":
1985
+ return handleSessionsUpdate(ws, msg, sm);
1897
1986
  case "sessions.remove":
1898
1987
  return handleSessionsRemove(ws, msg, sm);
1899
1988
  // ── Agent lifecycle ───────────────────────────────
@@ -1969,6 +2058,20 @@ function handleSessionsCreate(ws, msg, sm) {
1969
2058
  replyError(ws, msg, e.message);
1970
2059
  }
1971
2060
  }
2061
+ function handleSessionsUpdate(ws, msg, sm) {
2062
+ const id = msg.session;
2063
+ if (!id) return replyError(ws, msg, "session is required");
2064
+ try {
2065
+ sm.updateSession(id, {
2066
+ label: msg.label,
2067
+ meta: msg.meta,
2068
+ cwd: msg.cwd
2069
+ });
2070
+ wsReply(ws, msg, { status: "updated", session: id });
2071
+ } catch (e) {
2072
+ replyError(ws, msg, e.message);
2073
+ }
2074
+ }
1972
2075
  function handleSessionsRemove(ws, msg, sm) {
1973
2076
  const id = msg.session;
1974
2077
  if (!id) return replyError(ws, msg, "session is required");
@@ -1987,7 +2090,6 @@ function handleAgentStart(ws, msg, sm) {
1987
2090
  return;
1988
2091
  }
1989
2092
  if (session.process?.alive) session.process.kill();
1990
- session.eventBuffer.length = 0;
1991
2093
  const provider2 = getProvider(msg.provider ?? "claude-code");
1992
2094
  try {
1993
2095
  const db = getDb();
@@ -2223,21 +2325,33 @@ function handleAgentSubscribe(ws, msg, sm, state) {
2223
2325
  }
2224
2326
  } catch {
2225
2327
  }
2226
- }
2227
- const bufferStart = typeof msg.since === "number" && msg.since > 0 ? msg.since : session.eventCounter;
2228
- if (!includeHistory) cursor = bufferStart;
2229
- if (cursor < session.eventCounter) {
2230
- const startIdx = Math.max(0, session.eventBuffer.length - (session.eventCounter - cursor));
2231
- const events = session.eventBuffer.slice(startIdx);
2232
- for (const event of events) {
2233
- cursor++;
2234
- send(ws, { type: "agent.event", session: sessionId, cursor, event });
2328
+ if (cursor < session.eventCounter) {
2329
+ const unpersisted = session.eventCounter - cursor;
2330
+ const bufferSlice = session.eventBuffer.slice(-unpersisted);
2331
+ for (const event of bufferSlice) {
2332
+ cursor++;
2333
+ send(ws, { type: "agent.event", session: sessionId, cursor, event });
2334
+ }
2235
2335
  }
2236
2336
  } else {
2237
- cursor = session.eventCounter;
2337
+ cursor = typeof msg.since === "number" && msg.since > 0 ? msg.since : session.eventCounter;
2338
+ if (cursor < session.eventCounter) {
2339
+ const startIdx = Math.max(0, session.eventBuffer.length - (session.eventCounter - cursor));
2340
+ const events = session.eventBuffer.slice(startIdx);
2341
+ for (const event of events) {
2342
+ cursor++;
2343
+ send(ws, { type: "agent.event", session: sessionId, cursor, event });
2344
+ }
2345
+ } else {
2346
+ cursor = session.eventCounter;
2347
+ }
2238
2348
  }
2239
2349
  const unsub = sm.onSessionEvent(sessionId, (eventCursor, event) => {
2240
- send(ws, { type: "agent.event", session: sessionId, cursor: eventCursor, event });
2350
+ if (eventCursor === -1) {
2351
+ send(ws, { type: "agent.event", session: sessionId, event });
2352
+ } else {
2353
+ send(ws, { type: "agent.event", session: sessionId, cursor: eventCursor, event });
2354
+ }
2241
2355
  });
2242
2356
  state.agentUnsubs.set(sessionId, unsub);
2243
2357
  reply(ws, msg, { cursor });
@@ -2496,7 +2610,7 @@ root.use("*", async (c, next) => {
2496
2610
  await next();
2497
2611
  });
2498
2612
  var sessionManager = new SessionManager({ maxSessions });
2499
- sessionManager.createSession({ id: "default", cwd: process.cwd() });
2613
+ sessionManager.getOrCreateSession("default", { cwd: process.cwd() });
2500
2614
  var provider = getProvider("claude-code");
2501
2615
  logger.log("sna", "spawning agent...");
2502
2616
  var agentProcess = provider.spawn({ cwd: process.cwd(), permissionMode, model: defaultModel });
@@ -13,6 +13,7 @@ import '../core/providers/types.js';
13
13
  * Server → Client: { type: "sessions.list", rid: "1", sessions: [...] }
14
14
  * Server → Client: { type: "error", rid: "1", message: "..." }
15
15
  * Server → Client: { type: "agent.event", session: "abc", cursor: 42, event: {...} } (push)
16
+ * Server → Client: { type: "sessions.snapshot", sessions: [...] } (auto-push on connect + state change)
16
17
  * Server → Client: { type: "session.lifecycle", session: "abc", state: "killed" } (auto-push)
17
18
  * Server → Client: { type: "skill.event", data: {...} } (push)
18
19
  *
package/dist/server/ws.js CHANGED
@@ -31,15 +31,22 @@ function attachWebSocket(server, sessionManager) {
31
31
  });
32
32
  wss.on("connection", (ws) => {
33
33
  logger.log("ws", "client connected");
34
- const state = { agentUnsubs: /* @__PURE__ */ new Map(), skillEventUnsub: null, skillPollTimer: null, permissionUnsub: null, lifecycleUnsub: null, configChangedUnsub: null, stateChangedUnsub: null };
34
+ const state = { agentUnsubs: /* @__PURE__ */ new Map(), skillEventUnsub: null, skillPollTimer: null, permissionUnsub: null, lifecycleUnsub: null, configChangedUnsub: null, stateChangedUnsub: null, metadataChangedUnsub: null };
35
+ const pushSnapshot = () => send(ws, { type: "sessions.snapshot", sessions: sessionManager.listSessions() });
36
+ pushSnapshot();
35
37
  state.lifecycleUnsub = sessionManager.onSessionLifecycle((event) => {
36
38
  send(ws, { type: "session.lifecycle", ...event });
39
+ pushSnapshot();
37
40
  });
38
41
  state.configChangedUnsub = sessionManager.onConfigChanged((event) => {
39
42
  send(ws, { type: "session.config-changed", ...event });
40
43
  });
41
44
  state.stateChangedUnsub = sessionManager.onStateChanged((event) => {
42
45
  send(ws, { type: "session.state-changed", ...event });
46
+ pushSnapshot();
47
+ });
48
+ state.metadataChangedUnsub = sessionManager.onMetadataChanged(() => {
49
+ pushSnapshot();
43
50
  });
44
51
  ws.on("message", (raw) => {
45
52
  let msg;
@@ -73,6 +80,8 @@ function attachWebSocket(server, sessionManager) {
73
80
  state.configChangedUnsub = null;
74
81
  state.stateChangedUnsub?.();
75
82
  state.stateChangedUnsub = null;
83
+ state.metadataChangedUnsub?.();
84
+ state.metadataChangedUnsub = null;
76
85
  });
77
86
  });
78
87
  return wss;
@@ -84,6 +93,8 @@ function handleMessage(ws, msg, sm, state) {
84
93
  return handleSessionsCreate(ws, msg, sm);
85
94
  case "sessions.list":
86
95
  return wsReply(ws, msg, { sessions: sm.listSessions() });
96
+ case "sessions.update":
97
+ return handleSessionsUpdate(ws, msg, sm);
87
98
  case "sessions.remove":
88
99
  return handleSessionsRemove(ws, msg, sm);
89
100
  // ── Agent lifecycle ───────────────────────────────
@@ -159,6 +170,20 @@ function handleSessionsCreate(ws, msg, sm) {
159
170
  replyError(ws, msg, e.message);
160
171
  }
161
172
  }
173
+ function handleSessionsUpdate(ws, msg, sm) {
174
+ const id = msg.session;
175
+ if (!id) return replyError(ws, msg, "session is required");
176
+ try {
177
+ sm.updateSession(id, {
178
+ label: msg.label,
179
+ meta: msg.meta,
180
+ cwd: msg.cwd
181
+ });
182
+ wsReply(ws, msg, { status: "updated", session: id });
183
+ } catch (e) {
184
+ replyError(ws, msg, e.message);
185
+ }
186
+ }
162
187
  function handleSessionsRemove(ws, msg, sm) {
163
188
  const id = msg.session;
164
189
  if (!id) return replyError(ws, msg, "session is required");
@@ -177,7 +202,6 @@ function handleAgentStart(ws, msg, sm) {
177
202
  return;
178
203
  }
179
204
  if (session.process?.alive) session.process.kill();
180
- session.eventBuffer.length = 0;
181
205
  const provider = getProvider(msg.provider ?? "claude-code");
182
206
  try {
183
207
  const db = getDb();
@@ -413,21 +437,33 @@ function handleAgentSubscribe(ws, msg, sm, state) {
413
437
  }
414
438
  } catch {
415
439
  }
416
- }
417
- const bufferStart = typeof msg.since === "number" && msg.since > 0 ? msg.since : session.eventCounter;
418
- if (!includeHistory) cursor = bufferStart;
419
- if (cursor < session.eventCounter) {
420
- const startIdx = Math.max(0, session.eventBuffer.length - (session.eventCounter - cursor));
421
- const events = session.eventBuffer.slice(startIdx);
422
- for (const event of events) {
423
- cursor++;
424
- send(ws, { type: "agent.event", session: sessionId, cursor, event });
440
+ if (cursor < session.eventCounter) {
441
+ const unpersisted = session.eventCounter - cursor;
442
+ const bufferSlice = session.eventBuffer.slice(-unpersisted);
443
+ for (const event of bufferSlice) {
444
+ cursor++;
445
+ send(ws, { type: "agent.event", session: sessionId, cursor, event });
446
+ }
425
447
  }
426
448
  } else {
427
- cursor = session.eventCounter;
449
+ cursor = typeof msg.since === "number" && msg.since > 0 ? msg.since : session.eventCounter;
450
+ if (cursor < session.eventCounter) {
451
+ const startIdx = Math.max(0, session.eventBuffer.length - (session.eventCounter - cursor));
452
+ const events = session.eventBuffer.slice(startIdx);
453
+ for (const event of events) {
454
+ cursor++;
455
+ send(ws, { type: "agent.event", session: sessionId, cursor, event });
456
+ }
457
+ } else {
458
+ cursor = session.eventCounter;
459
+ }
428
460
  }
429
461
  const unsub = sm.onSessionEvent(sessionId, (eventCursor, event) => {
430
- send(ws, { type: "agent.event", session: sessionId, cursor: eventCursor, event });
462
+ if (eventCursor === -1) {
463
+ send(ws, { type: "agent.event", session: sessionId, event });
464
+ } else {
465
+ send(ws, { type: "agent.event", session: sessionId, cursor: eventCursor, event });
466
+ }
431
467
  });
432
468
  state.agentUnsubs.set(sessionId, unsub);
433
469
  reply(ws, msg, { cursor });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sna-sdk/core",
3
- "version": "0.6.1",
3
+ "version": "0.7.2",
4
4
  "description": "Skills-Native Application runtime — server, providers, session management, database, and CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -102,12 +102,14 @@
102
102
  ],
103
103
  "dependencies": {
104
104
  "@hono/node-server": "^1.19.11",
105
- "better-sqlite3": "^12.6.2",
106
105
  "chalk": "^5.0.0",
107
106
  "hono": "^4.12.7",
108
107
  "js-yaml": "^4.1.0",
109
108
  "ws": "^8.20.0"
110
109
  },
110
+ "peerDependencies": {
111
+ "better-sqlite3": ">=11.0.0"
112
+ },
111
113
  "devDependencies": {
112
114
  "@types/better-sqlite3": "^7.6.13",
113
115
  "@types/js-yaml": "^4.0.9",