@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
|
@@ -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
|
|
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.
|
|
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.
|
|
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
|
|
460
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
}
|
|
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.
|
|
626
|
+
this.enqueue(e);
|
|
588
627
|
}
|
|
589
628
|
for (const text of textBlocks) {
|
|
590
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
1496
|
-
|
|
1497
|
-
|
|
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
|
|
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
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
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
|
-
|
|
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.
|
|
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 });
|
package/dist/server/ws.d.ts
CHANGED
|
@@ -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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|