@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.
@@ -12,6 +12,7 @@ class SessionManager {
12
12
  this.lifecycleListeners = /* @__PURE__ */ new Set();
13
13
  this.configChangedListeners = /* @__PURE__ */ new Set();
14
14
  this.stateChangedListeners = /* @__PURE__ */ new Set();
15
+ this.metadataChangedListeners = /* @__PURE__ */ new Set();
15
16
  this.maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;
16
17
  this.restoreFromDb();
17
18
  }
@@ -64,26 +65,11 @@ class SessionManager {
64
65
  } catch {
65
66
  }
66
67
  }
67
- /** Create a new session. Throws if max sessions reached. */
68
+ /** Create a new session. Throws if session already exists or max sessions reached. */
68
69
  createSession(opts = {}) {
69
70
  const id = opts.id ?? crypto.randomUUID().slice(0, 8);
70
71
  if (this.sessions.has(id)) {
71
- const existing = this.sessions.get(id);
72
- let changed = false;
73
- if (opts.cwd && opts.cwd !== existing.cwd) {
74
- existing.cwd = opts.cwd;
75
- changed = true;
76
- }
77
- if (opts.label && opts.label !== existing.label) {
78
- existing.label = opts.label;
79
- changed = true;
80
- }
81
- if (opts.meta !== void 0 && opts.meta !== existing.meta) {
82
- existing.meta = opts.meta ?? null;
83
- changed = true;
84
- }
85
- if (changed) this.persistSession(existing);
86
- return existing;
72
+ throw new Error(`Session "${id}" already exists`);
87
73
  }
88
74
  const aliveCount = Array.from(this.sessions.values()).filter((s) => s.process?.alive).length;
89
75
  if (aliveCount >= this.maxSessions) {
@@ -107,6 +93,17 @@ class SessionManager {
107
93
  this.persistSession(session);
108
94
  return session;
109
95
  }
96
+ /** Update an existing session's metadata. Throws if session not found. */
97
+ updateSession(id, opts) {
98
+ const session = this.sessions.get(id);
99
+ if (!session) throw new Error(`Session "${id}" not found`);
100
+ if (opts.label !== void 0) session.label = opts.label;
101
+ if (opts.meta !== void 0) session.meta = opts.meta;
102
+ if (opts.cwd !== void 0) session.cwd = opts.cwd;
103
+ this.persistSession(session);
104
+ this.emitMetadataChanged(id);
105
+ return session;
106
+ }
110
107
  /** Get a session by ID. */
111
108
  getSession(id) {
112
109
  return this.sessions.get(id);
@@ -128,27 +125,45 @@ class SessionManager {
128
125
  const session = this.sessions.get(sessionId);
129
126
  if (!session) throw new Error(`Session "${sessionId}" not found`);
130
127
  session.process = proc;
131
- this.setSessionState(sessionId, session, "processing");
132
128
  session.lastActivityAt = Date.now();
129
+ session.eventBuffer.length = 0;
130
+ try {
131
+ const db = getDb();
132
+ const row = db.prepare(
133
+ `SELECT COUNT(*) as c FROM chat_messages WHERE session_id = ?`
134
+ ).get(sessionId);
135
+ session.eventCounter = row.c;
136
+ } catch {
137
+ }
133
138
  proc.on("event", (e) => {
134
- if (e.type === "init" && e.data?.sessionId && !session.ccSessionId) {
135
- session.ccSessionId = e.data.sessionId;
136
- this.persistSession(session);
139
+ if (e.type === "init") {
140
+ if (e.data?.sessionId && !session.ccSessionId) {
141
+ session.ccSessionId = e.data.sessionId;
142
+ this.persistSession(session);
143
+ }
144
+ this.setSessionState(sessionId, session, "waiting");
145
+ }
146
+ if (e.type === "thinking" || e.type === "tool_use" || e.type === "assistant_delta") {
147
+ this.setSessionState(sessionId, session, "processing");
148
+ } else if (e.type === "complete" || e.type === "error" || e.type === "interrupted") {
149
+ this.setSessionState(sessionId, session, "waiting");
137
150
  }
138
- if (e.type !== "assistant_delta") {
151
+ const persisted = this.persistEvent(sessionId, e);
152
+ if (persisted) {
153
+ session.eventCounter++;
139
154
  session.eventBuffer.push(e);
140
155
  if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
141
156
  session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
142
157
  }
143
- }
144
- session.eventCounter++;
145
- if (e.type === "complete" || e.type === "error" || e.type === "interrupted") {
146
- this.setSessionState(sessionId, session, "waiting");
147
- }
148
- this.persistEvent(sessionId, e);
149
- const listeners = this.eventListeners.get(sessionId);
150
- if (listeners) {
151
- for (const cb of listeners) cb(session.eventCounter, e);
158
+ const listeners = this.eventListeners.get(sessionId);
159
+ if (listeners) {
160
+ for (const cb of listeners) cb(session.eventCounter, e);
161
+ }
162
+ } else if (e.type === "assistant_delta") {
163
+ const listeners = this.eventListeners.get(sessionId);
164
+ if (listeners) {
165
+ for (const cb of listeners) cb(-1, e);
166
+ }
152
167
  }
153
168
  });
154
169
  proc.on("exit", (code) => {
@@ -186,11 +201,17 @@ class SessionManager {
186
201
  for (const cb of this.skillEventListeners) cb(event);
187
202
  }
188
203
  /** Push a synthetic event into a session's event stream (for user message broadcast). */
204
+ /**
205
+ * Push an externally-persisted event into the session.
206
+ * The caller is responsible for DB persistence — this method only updates
207
+ * the in-memory counter/buffer and notifies listeners.
208
+ * eventCounter increments to stay in sync with the DB row count.
209
+ */
189
210
  pushEvent(sessionId, event) {
190
211
  const session = this.sessions.get(sessionId);
191
212
  if (!session) return;
192
- session.eventBuffer.push(event);
193
213
  session.eventCounter++;
214
+ session.eventBuffer.push(event);
194
215
  if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
195
216
  session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
196
217
  }
@@ -223,6 +244,14 @@ class SessionManager {
223
244
  emitConfigChanged(sessionId, config) {
224
245
  for (const cb of this.configChangedListeners) cb({ session: sessionId, config });
225
246
  }
247
+ // ── Session metadata change pub/sub ─────────────────────────────
248
+ onMetadataChanged(cb) {
249
+ this.metadataChangedListeners.add(cb);
250
+ return () => this.metadataChangedListeners.delete(cb);
251
+ }
252
+ emitMetadataChanged(sessionId) {
253
+ for (const cb of this.metadataChangedListeners) cb(sessionId);
254
+ }
226
255
  // ── Agent status change pub/sub ────────────────────────────────
227
256
  onStateChanged(cb) {
228
257
  this.stateChangedListeners.add(cb);
@@ -303,7 +332,6 @@ class SessionManager {
303
332
  extraArgs: overrides.extraArgs ?? base.extraArgs
304
333
  };
305
334
  if (session.process?.alive) session.process.kill();
306
- session.eventBuffer.length = 0;
307
335
  const proc = spawnFn(config);
308
336
  this.setProcess(id, proc);
309
337
  session.lastStartConfig = config;
@@ -408,6 +436,7 @@ class SessionManager {
408
436
  return { messageCount: 0, lastMessage: null };
409
437
  }
410
438
  }
439
+ /** Persist an agent event to chat_messages. Returns true if a row was inserted. */
411
440
  persistEvent(sessionId, e) {
412
441
  try {
413
442
  const db = getDb();
@@ -415,29 +444,34 @@ class SessionManager {
415
444
  case "assistant":
416
445
  if (e.message) {
417
446
  db.prepare(`INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'assistant', ?)`).run(sessionId, e.message);
447
+ return true;
418
448
  }
419
- break;
449
+ return false;
420
450
  case "thinking":
421
451
  if (e.message) {
422
452
  db.prepare(`INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'thinking', ?)`).run(sessionId, e.message);
453
+ return true;
423
454
  }
424
- break;
455
+ return false;
425
456
  case "tool_use": {
426
457
  const toolName = e.data?.toolName ?? e.message ?? "tool";
427
458
  db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'tool', ?, ?)`).run(sessionId, toolName, JSON.stringify(e.data ?? {}));
428
- break;
459
+ return true;
429
460
  }
430
461
  case "tool_result":
431
462
  db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'tool_result', ?, ?)`).run(sessionId, e.message ?? "", JSON.stringify(e.data ?? {}));
432
- break;
463
+ return true;
433
464
  case "complete":
434
465
  db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'status', '', ?)`).run(sessionId, JSON.stringify({ status: "complete", ...e.data }));
435
- break;
466
+ return true;
436
467
  case "error":
437
468
  db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'error', ?, ?)`).run(sessionId, e.message ?? "Error", JSON.stringify({ status: "error" }));
438
- break;
469
+ return true;
470
+ default:
471
+ return false;
439
472
  }
440
473
  } catch {
474
+ return false;
441
475
  }
442
476
  }
443
477
  /** Kill all sessions. Used during shutdown. */