@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 +6 -1
- package/dist/core/providers/claude-code.js +62 -29
- package/dist/db/schema.js +8 -0
- package/dist/electron/index.cjs +8 -28
- package/dist/electron/index.d.ts +4 -4
- package/dist/electron/index.js +8 -28
- package/dist/node/index.cjs +8 -28
- package/dist/scripts/sna.js +9 -7
- package/dist/server/api-types.d.ts +4 -0
- package/dist/server/routes/agent.js +6 -3
- package/dist/server/session-manager.d.ts +17 -1
- package/dist/server/session-manager.js +73 -39
- package/dist/server/standalone.js +198 -84
- package/dist/server/ws.d.ts +1 -0
- package/dist/server/ws.js +49 -13
- package/package.json +4 -2
|
@@ -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
|
-
|
|
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"
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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. */
|