@sna-sdk/core 0.3.0 → 0.5.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.
package/dist/server/ws.js CHANGED
@@ -4,6 +4,7 @@ import { getDb } from "../db/schema.js";
4
4
  import { logger } from "../lib/logger.js";
5
5
  import { runOnce } from "./routes/agent.js";
6
6
  import { wsReply } from "./api-types.js";
7
+ import { buildHistoryFromDb } from "./history-builder.js";
7
8
  import { saveImages } from "./image-store.js";
8
9
  function send(ws, data) {
9
10
  if (ws.readyState === ws.OPEN) {
@@ -30,13 +31,16 @@ function attachWebSocket(server, sessionManager) {
30
31
  });
31
32
  wss.on("connection", (ws) => {
32
33
  logger.log("ws", "client connected");
33
- const state = { agentUnsubs: /* @__PURE__ */ new Map(), skillEventUnsub: null, skillPollTimer: null, permissionUnsub: null, lifecycleUnsub: null, configChangedUnsub: null };
34
+ const state = { agentUnsubs: /* @__PURE__ */ new Map(), skillEventUnsub: null, skillPollTimer: null, permissionUnsub: null, lifecycleUnsub: null, configChangedUnsub: null, stateChangedUnsub: null };
34
35
  state.lifecycleUnsub = sessionManager.onSessionLifecycle((event) => {
35
36
  send(ws, { type: "session.lifecycle", ...event });
36
37
  });
37
38
  state.configChangedUnsub = sessionManager.onConfigChanged((event) => {
38
39
  send(ws, { type: "session.config-changed", ...event });
39
40
  });
41
+ state.stateChangedUnsub = sessionManager.onStateChanged((event) => {
42
+ send(ws, { type: "session.state-changed", ...event });
43
+ });
40
44
  ws.on("message", (raw) => {
41
45
  let msg;
42
46
  try {
@@ -67,6 +71,8 @@ function attachWebSocket(server, sessionManager) {
67
71
  state.lifecycleUnsub = null;
68
72
  state.configChangedUnsub?.();
69
73
  state.configChangedUnsub = null;
74
+ state.stateChangedUnsub?.();
75
+ state.stateChangedUnsub = null;
70
76
  });
71
77
  });
72
78
  return wss;
@@ -85,6 +91,8 @@ function handleMessage(ws, msg, sm, state) {
85
91
  return handleAgentStart(ws, msg, sm);
86
92
  case "agent.send":
87
93
  return handleAgentSend(ws, msg, sm);
94
+ case "agent.resume":
95
+ return handleAgentResume(ws, msg, sm);
88
96
  case "agent.restart":
89
97
  return handleAgentRestart(ws, msg, sm);
90
98
  case "agent.interrupt":
@@ -226,7 +234,13 @@ function handleAgentSend(ws, msg, sm) {
226
234
  db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, textContent, Object.keys(meta).length > 0 ? JSON.stringify(meta) : null);
227
235
  } catch {
228
236
  }
229
- session.state = "processing";
237
+ sm.pushEvent(sessionId, {
238
+ type: "user_message",
239
+ message: textContent,
240
+ data: Object.keys(meta).length > 0 ? meta : void 0,
241
+ timestamp: Date.now()
242
+ });
243
+ sm.updateSessionState(sessionId, "processing");
230
244
  sm.touch(sessionId);
231
245
  if (images?.length) {
232
246
  const content = [
@@ -242,6 +256,43 @@ function handleAgentSend(ws, msg, sm) {
242
256
  }
243
257
  wsReply(ws, msg, { status: "sent" });
244
258
  }
259
+ function handleAgentResume(ws, msg, sm) {
260
+ const sessionId = msg.session ?? "default";
261
+ const session = sm.getOrCreateSession(sessionId);
262
+ if (session.process?.alive) {
263
+ return replyError(ws, msg, "Session already running. Use agent.send instead.");
264
+ }
265
+ const history = buildHistoryFromDb(sessionId);
266
+ if (history.length === 0 && !msg.prompt) {
267
+ return replyError(ws, msg, "No history in DB \u2014 nothing to resume.");
268
+ }
269
+ const providerName = msg.provider ?? session.lastStartConfig?.provider ?? "claude-code";
270
+ const model = msg.model ?? session.lastStartConfig?.model ?? "claude-sonnet-4-6";
271
+ const permissionMode = msg.permissionMode ?? session.lastStartConfig?.permissionMode ?? "acceptEdits";
272
+ const extraArgs = msg.extraArgs ?? session.lastStartConfig?.extraArgs;
273
+ const provider = getProvider(providerName);
274
+ try {
275
+ const proc = provider.spawn({
276
+ cwd: session.cwd,
277
+ prompt: msg.prompt,
278
+ model,
279
+ permissionMode,
280
+ env: { SNA_SESSION_ID: sessionId },
281
+ history: history.length > 0 ? history : void 0,
282
+ extraArgs
283
+ });
284
+ sm.setProcess(sessionId, proc, "resumed");
285
+ sm.saveStartConfig(sessionId, { provider: providerName, model, permissionMode, extraArgs });
286
+ wsReply(ws, msg, {
287
+ status: "resumed",
288
+ provider: providerName,
289
+ sessionId: session.id,
290
+ historyCount: history.length
291
+ });
292
+ } catch (e) {
293
+ replyError(ws, msg, e.message);
294
+ }
295
+ }
245
296
  function handleAgentRestart(ws, msg, sm) {
246
297
  const sessionId = msg.session ?? "default";
247
298
  try {
@@ -298,11 +349,25 @@ function handleAgentKill(ws, msg, sm) {
298
349
  function handleAgentStatus(ws, msg, sm) {
299
350
  const sessionId = msg.session ?? "default";
300
351
  const session = sm.getSession(sessionId);
352
+ const alive = session?.process?.alive ?? false;
353
+ let messageCount = 0;
354
+ let lastMessage = null;
355
+ try {
356
+ const db = getDb();
357
+ const count = db.prepare("SELECT COUNT(*) as c FROM chat_messages WHERE session_id = ?").get(sessionId);
358
+ messageCount = count?.c ?? 0;
359
+ const last = db.prepare("SELECT role, content, created_at FROM chat_messages WHERE session_id = ? ORDER BY id DESC LIMIT 1").get(sessionId);
360
+ if (last) lastMessage = { role: last.role, content: last.content, created_at: last.created_at };
361
+ } catch {
362
+ }
301
363
  wsReply(ws, msg, {
302
- alive: session?.process?.alive ?? false,
364
+ alive,
365
+ agentStatus: !alive ? "disconnected" : session?.state === "processing" ? "busy" : "idle",
303
366
  sessionId: session?.process?.sessionId ?? null,
304
367
  ccSessionId: session?.ccSessionId ?? null,
305
368
  eventCount: session?.eventCounter ?? 0,
369
+ messageCount,
370
+ lastMessage,
306
371
  config: session?.lastStartConfig ?? null
307
372
  });
308
373
  }
@@ -319,7 +384,38 @@ function handleAgentSubscribe(ws, msg, sm, state) {
319
384
  const sessionId = msg.session ?? "default";
320
385
  const session = sm.getOrCreateSession(sessionId);
321
386
  state.agentUnsubs.get(sessionId)?.();
322
- let cursor = typeof msg.since === "number" ? msg.since : session.eventCounter;
387
+ const includeHistory = msg.since === 0 || msg.includeHistory === true;
388
+ let cursor = 0;
389
+ if (includeHistory) {
390
+ try {
391
+ const db = getDb();
392
+ const rows = db.prepare(
393
+ `SELECT role, content, meta, created_at FROM chat_messages
394
+ WHERE session_id = ? ORDER BY id ASC`
395
+ ).all(sessionId);
396
+ for (const row of rows) {
397
+ cursor++;
398
+ const eventType = row.role === "user" ? "user_message" : row.role === "assistant" ? "assistant" : row.role === "thinking" ? "thinking" : row.role === "tool" ? "tool_use" : row.role === "tool_result" ? "tool_result" : row.role === "error" ? "error" : null;
399
+ if (!eventType) continue;
400
+ const meta = row.meta ? JSON.parse(row.meta) : void 0;
401
+ send(ws, {
402
+ type: "agent.event",
403
+ session: sessionId,
404
+ cursor,
405
+ isHistory: true,
406
+ event: {
407
+ type: eventType,
408
+ message: row.content,
409
+ data: meta,
410
+ timestamp: new Date(row.created_at).getTime()
411
+ }
412
+ });
413
+ }
414
+ } catch {
415
+ }
416
+ }
417
+ const bufferStart = typeof msg.since === "number" && msg.since > 0 ? msg.since : session.eventCounter;
418
+ if (!includeHistory) cursor = bufferStart;
323
419
  if (cursor < session.eventCounter) {
324
420
  const startIdx = Math.max(0, session.eventBuffer.length - (session.eventCounter - cursor));
325
421
  const events = session.eventBuffer.slice(startIdx);
@@ -327,6 +423,8 @@ function handleAgentSubscribe(ws, msg, sm, state) {
327
423
  cursor++;
328
424
  send(ws, { type: "agent.event", session: sessionId, cursor, event });
329
425
  }
426
+ } else {
427
+ cursor = session.eventCounter;
330
428
  }
331
429
  const unsub = sm.onSessionEvent(sessionId, (eventCursor, event) => {
332
430
  send(ws, { type: "agent.event", session: sessionId, cursor: eventCursor, event });
@@ -439,10 +537,14 @@ function handlePermissionPending(ws, msg, sm) {
439
537
  }
440
538
  function handlePermissionSubscribe(ws, msg, sm, state) {
441
539
  state.permissionUnsub?.();
540
+ const pending = sm.getAllPendingPermissions();
541
+ for (const p of pending) {
542
+ send(ws, { type: "permission.request", session: p.sessionId, request: p.request, createdAt: p.createdAt, isHistory: true });
543
+ }
442
544
  state.permissionUnsub = sm.onPermissionRequest((sessionId, request, createdAt) => {
443
545
  send(ws, { type: "permission.request", session: sessionId, request, createdAt });
444
546
  });
445
- reply(ws, msg, {});
547
+ reply(ws, msg, { pendingCount: pending.length });
446
548
  }
447
549
  function handlePermissionUnsubscribe(ws, msg, state) {
448
550
  state.permissionUnsub?.();
@@ -1,4 +1,6 @@
1
1
  import http from "http";
2
+ import fs from "fs";
3
+ import path from "path";
2
4
  function ts() {
3
5
  return (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
4
6
  }
@@ -35,6 +37,24 @@ async function startMockAnthropicServer() {
35
37
  const realText = textBlocks.find((t) => !t.startsWith("<system-reminder>"));
36
38
  userText = realText ?? textBlocks[textBlocks.length - 1] ?? "(no text)";
37
39
  }
40
+ console.log(`[${ts()}] BODY KEYS: ${Object.keys(body).join(", ")}`);
41
+ try {
42
+ const dumpPath = path.join(process.cwd(), ".sna/mock-api-last-request.json");
43
+ fs.writeFileSync(dumpPath, JSON.stringify(body, null, 2));
44
+ console.log(`[${ts()}] FULL BODY dumped to .sna/mock-api-last-request.json`);
45
+ } catch {
46
+ }
47
+ if (body.system) {
48
+ const sysText = typeof body.system === "string" ? body.system : JSON.stringify(body.system);
49
+ console.log(`[${ts()}] SYSTEM PROMPT (${sysText.length} chars): ${sysText.slice(0, 300)}...`);
50
+ if (sysText.includes("\uC720\uB2C8") || sysText.includes("\uCEE4\uD53C") || sysText.includes("\uAE30\uC5B5")) {
51
+ console.log(`[${ts()}] *** HISTORY FOUND IN SYSTEM PROMPT ***`);
52
+ for (const keyword of ["\uC720\uB2C8", "\uCEE4\uD53C", "\uAE30\uC5B5"]) {
53
+ const idx = sysText.indexOf(keyword);
54
+ if (idx >= 0) console.log(`[${ts()}] "${keyword}" at pos ${idx}: ...${sysText.slice(Math.max(0, idx - 50), idx + 80)}...`);
55
+ }
56
+ }
57
+ }
38
58
  console.log(`[${ts()}] REQ model=${body.model} stream=${body.stream} messages=${body.messages?.length} user="${userText.slice(0, 120)}"`);
39
59
  for (let mi = 0; mi < body.messages.length; mi++) {
40
60
  const m = body.messages[mi];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sna-sdk/core",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Skills-Native Application runtime — server, providers, session management, database, and CLI",
5
5
  "type": "module",
6
6
  "bin": {