@sna-sdk/core 0.7.1 → 0.8.0

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.
@@ -1008,7 +1008,9 @@ Run "sna help submit" for data submission patterns.`);
1008
1008
  cmdApiUp();
1009
1009
  break;
1010
1010
  case "tu":
1011
- cmdTu(args);
1011
+ console.log(' "sna tu" has moved to @sna-sdk/testing.');
1012
+ console.log(' Install it and use "sna-test" instead.');
1013
+ console.log(' e.g. sna-test api:up, sna-test claude "hello"');
1012
1014
  break;
1013
1015
  case "restart":
1014
1016
  cmdDown();
@@ -49,7 +49,7 @@ interface ApiResponses {
49
49
  };
50
50
  "agent.set-permission-mode": {
51
51
  status: "updated" | "no_session";
52
- permissionMode: string;
52
+ permissionMode?: string;
53
53
  };
54
54
  "agent.kill": {
55
55
  status: "killed" | "no_session";
@@ -69,7 +69,7 @@ interface ApiResponses {
69
69
  config: {
70
70
  provider: string;
71
71
  model: string;
72
- permissionMode: string;
72
+ permissionMode?: string;
73
73
  extraArgs?: string[];
74
74
  } | null;
75
75
  };
@@ -142,7 +142,7 @@ function createAgentRoutes(sessionManager) {
142
142
  }
143
143
  const providerName = body.provider ?? "claude-code";
144
144
  const model = body.model ?? "claude-sonnet-4-6";
145
- const permissionMode = body.permissionMode ?? "acceptEdits";
145
+ const permissionMode = body.permissionMode;
146
146
  const extraArgs = body.extraArgs;
147
147
  try {
148
148
  const proc = provider.spawn({
@@ -253,7 +253,7 @@ function createAgentRoutes(sessionManager) {
253
253
  } else {
254
254
  cursor = session.eventCounter;
255
255
  }
256
- 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();
257
257
  while (!signal.aborted) {
258
258
  if (queue.length === 0) {
259
259
  await Promise.race([
@@ -267,7 +267,11 @@ function createAgentRoutes(sessionManager) {
267
267
  if (queue.length > 0) {
268
268
  while (queue.length > 0) {
269
269
  const item = queue.shift();
270
- 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
+ }
271
275
  }
272
276
  } else {
273
277
  await stream.writeSSE({ data: "" });
@@ -318,7 +322,7 @@ function createAgentRoutes(sessionManager) {
318
322
  }
319
323
  const providerName = body.provider ?? "claude-code";
320
324
  const model = body.model ?? session.lastStartConfig?.model ?? "claude-sonnet-4-6";
321
- const permissionMode = body.permissionMode ?? session.lastStartConfig?.permissionMode ?? "acceptEdits";
325
+ const permissionMode = body.permissionMode ?? session.lastStartConfig?.permissionMode;
322
326
  const extraArgs = body.extraArgs ?? session.lastStartConfig?.extraArgs;
323
327
  const provider = getProvider(providerName);
324
328
  try {
@@ -11,7 +11,7 @@ type SessionState = "idle" | "processing" | "waiting" | "permission";
11
11
  interface StartConfig {
12
12
  provider: string;
13
13
  model: string;
14
- permissionMode: string;
14
+ permissionMode?: string;
15
15
  extraArgs?: string[];
16
16
  }
17
17
  interface Session {
@@ -108,6 +108,12 @@ declare class SessionManager {
108
108
  /** Broadcast a skill event to all subscribers (called after DB insert). */
109
109
  broadcastSkillEvent(event: Record<string, unknown>): void;
110
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
+ */
111
117
  pushEvent(sessionId: string, event: AgentEvent): void;
112
118
  /** Subscribe to permission request notifications. Returns unsubscribe function. */
113
119
  onPermissionRequest(cb: (sessionId: string, request: Record<string, unknown>, createdAt: number) => void): () => void;
@@ -165,6 +171,7 @@ declare class SessionManager {
165
171
  touch(id: string): void;
166
172
  /** Persist an agent event to chat_messages. */
167
173
  private getMessageStats;
174
+ /** Persist an agent event to chat_messages. Returns true if a row was inserted. */
168
175
  private persistEvent;
169
176
  /** Kill all sessions. Used during shutdown. */
170
177
  killAll(): void;
@@ -143,22 +143,27 @@ class SessionManager {
143
143
  }
144
144
  this.setSessionState(sessionId, session, "waiting");
145
145
  }
146
- if (e.type !== "assistant_delta") {
147
- session.eventBuffer.push(e);
148
- if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
149
- session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
150
- }
151
- }
152
- session.eventCounter++;
153
146
  if (e.type === "thinking" || e.type === "tool_use" || e.type === "assistant_delta") {
154
147
  this.setSessionState(sessionId, session, "processing");
155
148
  } else if (e.type === "complete" || e.type === "error" || e.type === "interrupted") {
156
149
  this.setSessionState(sessionId, session, "waiting");
157
150
  }
158
- this.persistEvent(sessionId, e);
159
- const listeners = this.eventListeners.get(sessionId);
160
- if (listeners) {
161
- for (const cb of listeners) cb(session.eventCounter, e);
151
+ const persisted = this.persistEvent(sessionId, e);
152
+ if (persisted) {
153
+ session.eventCounter++;
154
+ session.eventBuffer.push(e);
155
+ if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
156
+ session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
157
+ }
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
+ }
162
167
  }
163
168
  });
164
169
  proc.on("exit", (code) => {
@@ -196,11 +201,17 @@ class SessionManager {
196
201
  for (const cb of this.skillEventListeners) cb(event);
197
202
  }
198
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
+ */
199
210
  pushEvent(sessionId, event) {
200
211
  const session = this.sessions.get(sessionId);
201
212
  if (!session) return;
202
- session.eventBuffer.push(event);
203
213
  session.eventCounter++;
214
+ session.eventBuffer.push(event);
204
215
  if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
205
216
  session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
206
217
  }
@@ -425,6 +436,7 @@ class SessionManager {
425
436
  return { messageCount: 0, lastMessage: null };
426
437
  }
427
438
  }
439
+ /** Persist an agent event to chat_messages. Returns true if a row was inserted. */
428
440
  persistEvent(sessionId, e) {
429
441
  try {
430
442
  const db = getDb();
@@ -432,29 +444,34 @@ class SessionManager {
432
444
  case "assistant":
433
445
  if (e.message) {
434
446
  db.prepare(`INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'assistant', ?)`).run(sessionId, e.message);
447
+ return true;
435
448
  }
436
- break;
449
+ return false;
437
450
  case "thinking":
438
451
  if (e.message) {
439
452
  db.prepare(`INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'thinking', ?)`).run(sessionId, e.message);
453
+ return true;
440
454
  }
441
- break;
455
+ return false;
442
456
  case "tool_use": {
443
457
  const toolName = e.data?.toolName ?? e.message ?? "tool";
444
458
  db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'tool', ?, ?)`).run(sessionId, toolName, JSON.stringify(e.data ?? {}));
445
- break;
459
+ return true;
446
460
  }
447
461
  case "tool_result":
448
462
  db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'tool_result', ?, ?)`).run(sessionId, e.message ?? "", JSON.stringify(e.data ?? {}));
449
- break;
463
+ return true;
450
464
  case "complete":
451
465
  db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'status', '', ?)`).run(sessionId, JSON.stringify({ status: "complete", ...e.data }));
452
- break;
466
+ return true;
453
467
  case "error":
454
468
  db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'error', ?, ?)`).run(sessionId, e.message ?? "Error", JSON.stringify({ status: "error" }));
455
- break;
469
+ return true;
470
+ default:
471
+ return false;
456
472
  }
457
473
  } catch {
474
+ return false;
458
475
  }
459
476
  }
460
477
  /** Kill all sessions. Used during shutdown. */
@@ -1004,7 +1004,7 @@ function createAgentRoutes(sessionManager2) {
1004
1004
  }
1005
1005
  const providerName = body.provider ?? "claude-code";
1006
1006
  const model = body.model ?? "claude-sonnet-4-6";
1007
- const permissionMode2 = body.permissionMode ?? "acceptEdits";
1007
+ const permissionMode2 = body.permissionMode;
1008
1008
  const extraArgs = body.extraArgs;
1009
1009
  try {
1010
1010
  const proc = provider2.spawn({
@@ -1115,7 +1115,7 @@ function createAgentRoutes(sessionManager2) {
1115
1115
  } else {
1116
1116
  cursor = session.eventCounter;
1117
1117
  }
1118
- 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();
1119
1119
  while (!signal.aborted) {
1120
1120
  if (queue.length === 0) {
1121
1121
  await Promise.race([
@@ -1129,7 +1129,11 @@ function createAgentRoutes(sessionManager2) {
1129
1129
  if (queue.length > 0) {
1130
1130
  while (queue.length > 0) {
1131
1131
  const item = queue.shift();
1132
- 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
+ }
1133
1137
  }
1134
1138
  } else {
1135
1139
  await stream.writeSSE({ data: "" });
@@ -1180,7 +1184,7 @@ function createAgentRoutes(sessionManager2) {
1180
1184
  }
1181
1185
  const providerName = body.provider ?? "claude-code";
1182
1186
  const model = body.model ?? session.lastStartConfig?.model ?? "claude-sonnet-4-6";
1183
- const permissionMode2 = body.permissionMode ?? session.lastStartConfig?.permissionMode ?? "acceptEdits";
1187
+ const permissionMode2 = body.permissionMode ?? session.lastStartConfig?.permissionMode;
1184
1188
  const extraArgs = body.extraArgs ?? session.lastStartConfig?.extraArgs;
1185
1189
  const provider2 = getProvider(providerName);
1186
1190
  try {
@@ -1544,22 +1548,27 @@ var SessionManager = class {
1544
1548
  }
1545
1549
  this.setSessionState(sessionId, session, "waiting");
1546
1550
  }
1547
- if (e.type !== "assistant_delta") {
1548
- session.eventBuffer.push(e);
1549
- if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
1550
- session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
1551
- }
1552
- }
1553
- session.eventCounter++;
1554
1551
  if (e.type === "thinking" || e.type === "tool_use" || e.type === "assistant_delta") {
1555
1552
  this.setSessionState(sessionId, session, "processing");
1556
1553
  } else if (e.type === "complete" || e.type === "error" || e.type === "interrupted") {
1557
1554
  this.setSessionState(sessionId, session, "waiting");
1558
1555
  }
1559
- this.persistEvent(sessionId, e);
1560
- const listeners = this.eventListeners.get(sessionId);
1561
- if (listeners) {
1562
- for (const cb of listeners) cb(session.eventCounter, e);
1556
+ const persisted = this.persistEvent(sessionId, e);
1557
+ if (persisted) {
1558
+ session.eventCounter++;
1559
+ session.eventBuffer.push(e);
1560
+ if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
1561
+ session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
1562
+ }
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
+ }
1563
1572
  }
1564
1573
  });
1565
1574
  proc.on("exit", (code) => {
@@ -1597,11 +1606,17 @@ var SessionManager = class {
1597
1606
  for (const cb of this.skillEventListeners) cb(event);
1598
1607
  }
1599
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
+ */
1600
1615
  pushEvent(sessionId, event) {
1601
1616
  const session = this.sessions.get(sessionId);
1602
1617
  if (!session) return;
1603
- session.eventBuffer.push(event);
1604
1618
  session.eventCounter++;
1619
+ session.eventBuffer.push(event);
1605
1620
  if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
1606
1621
  session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
1607
1622
  }
@@ -1826,6 +1841,7 @@ var SessionManager = class {
1826
1841
  return { messageCount: 0, lastMessage: null };
1827
1842
  }
1828
1843
  }
1844
+ /** Persist an agent event to chat_messages. Returns true if a row was inserted. */
1829
1845
  persistEvent(sessionId, e) {
1830
1846
  try {
1831
1847
  const db = getDb();
@@ -1833,29 +1849,34 @@ var SessionManager = class {
1833
1849
  case "assistant":
1834
1850
  if (e.message) {
1835
1851
  db.prepare(`INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'assistant', ?)`).run(sessionId, e.message);
1852
+ return true;
1836
1853
  }
1837
- break;
1854
+ return false;
1838
1855
  case "thinking":
1839
1856
  if (e.message) {
1840
1857
  db.prepare(`INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'thinking', ?)`).run(sessionId, e.message);
1858
+ return true;
1841
1859
  }
1842
- break;
1860
+ return false;
1843
1861
  case "tool_use": {
1844
1862
  const toolName = e.data?.toolName ?? e.message ?? "tool";
1845
1863
  db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'tool', ?, ?)`).run(sessionId, toolName, JSON.stringify(e.data ?? {}));
1846
- break;
1864
+ return true;
1847
1865
  }
1848
1866
  case "tool_result":
1849
1867
  db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'tool_result', ?, ?)`).run(sessionId, e.message ?? "", JSON.stringify(e.data ?? {}));
1850
- break;
1868
+ return true;
1851
1869
  case "complete":
1852
1870
  db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'status', '', ?)`).run(sessionId, JSON.stringify({ status: "complete", ...e.data }));
1853
- break;
1871
+ return true;
1854
1872
  case "error":
1855
1873
  db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'error', ?, ?)`).run(sessionId, e.message ?? "Error", JSON.stringify({ status: "error" }));
1856
- break;
1874
+ return true;
1875
+ default:
1876
+ return false;
1857
1877
  }
1858
1878
  } catch {
1879
+ return false;
1859
1880
  }
1860
1881
  }
1861
1882
  /** Kill all sessions. Used during shutdown. */
@@ -2084,7 +2105,7 @@ function handleAgentStart(ws, msg, sm) {
2084
2105
  }
2085
2106
  const providerName = msg.provider ?? "claude-code";
2086
2107
  const model = msg.model ?? "claude-sonnet-4-6";
2087
- const permissionMode2 = msg.permissionMode ?? "acceptEdits";
2108
+ const permissionMode2 = msg.permissionMode;
2088
2109
  const extraArgs = msg.extraArgs;
2089
2110
  try {
2090
2111
  const proc = provider2.spawn({
@@ -2159,7 +2180,7 @@ function handleAgentResume(ws, msg, sm) {
2159
2180
  }
2160
2181
  const providerName = msg.provider ?? session.lastStartConfig?.provider ?? "claude-code";
2161
2182
  const model = msg.model ?? session.lastStartConfig?.model ?? "claude-sonnet-4-6";
2162
- const permissionMode2 = msg.permissionMode ?? session.lastStartConfig?.permissionMode ?? "acceptEdits";
2183
+ const permissionMode2 = msg.permissionMode ?? session.lastStartConfig?.permissionMode;
2163
2184
  const extraArgs = msg.extraArgs ?? session.lastStartConfig?.extraArgs;
2164
2185
  const provider2 = getProvider(providerName);
2165
2186
  try {
@@ -2326,7 +2347,11 @@ function handleAgentSubscribe(ws, msg, sm, state) {
2326
2347
  }
2327
2348
  }
2328
2349
  const unsub = sm.onSessionEvent(sessionId, (eventCursor, event) => {
2329
- 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
+ }
2330
2355
  });
2331
2356
  state.agentUnsubs.set(sessionId, unsub);
2332
2357
  reply(ws, msg, { cursor });
@@ -2562,7 +2587,7 @@ try {
2562
2587
  process.exit(1);
2563
2588
  }
2564
2589
  var port = parseInt(process.env.SNA_PORT ?? "3099", 10);
2565
- var permissionMode = process.env.SNA_PERMISSION_MODE ?? "acceptEdits";
2590
+ var permissionMode = process.env.SNA_PERMISSION_MODE;
2566
2591
  var defaultModel = process.env.SNA_MODEL ?? "claude-sonnet-4-6";
2567
2592
  var maxSessions = parseInt(process.env.SNA_MAX_SESSIONS ?? "5", 10);
2568
2593
  var root = new Hono4();
package/dist/server/ws.js CHANGED
@@ -217,7 +217,7 @@ function handleAgentStart(ws, msg, sm) {
217
217
  }
218
218
  const providerName = msg.provider ?? "claude-code";
219
219
  const model = msg.model ?? "claude-sonnet-4-6";
220
- const permissionMode = msg.permissionMode ?? "acceptEdits";
220
+ const permissionMode = msg.permissionMode;
221
221
  const extraArgs = msg.extraArgs;
222
222
  try {
223
223
  const proc = provider.spawn({
@@ -292,7 +292,7 @@ function handleAgentResume(ws, msg, sm) {
292
292
  }
293
293
  const providerName = msg.provider ?? session.lastStartConfig?.provider ?? "claude-code";
294
294
  const model = msg.model ?? session.lastStartConfig?.model ?? "claude-sonnet-4-6";
295
- const permissionMode = msg.permissionMode ?? session.lastStartConfig?.permissionMode ?? "acceptEdits";
295
+ const permissionMode = msg.permissionMode ?? session.lastStartConfig?.permissionMode;
296
296
  const extraArgs = msg.extraArgs ?? session.lastStartConfig?.extraArgs;
297
297
  const provider = getProvider(providerName);
298
298
  try {
@@ -459,7 +459,11 @@ function handleAgentSubscribe(ws, msg, sm, state) {
459
459
  }
460
460
  }
461
461
  const unsub = sm.onSessionEvent(sessionId, (eventCursor, event) => {
462
- 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
+ }
463
467
  });
464
468
  state.agentUnsubs.set(sessionId, unsub);
465
469
  reply(ws, msg, { cursor });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sna-sdk/core",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Skills-Native Application runtime — server, providers, session management, database, and CLI",
5
5
  "type": "module",
6
6
  "bin": {