@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/README.md +6 -0
- package/dist/core/providers/cc-history-adapter.d.ts +37 -0
- package/dist/core/providers/cc-history-adapter.js +70 -0
- package/dist/core/providers/claude-code.js +50 -27
- package/dist/core/providers/types.d.ts +7 -1
- package/dist/db/schema.js +1 -1
- package/dist/scripts/sna.js +20 -2
- package/dist/scripts/tu-oneshot.d.ts +2 -0
- package/dist/scripts/tu-oneshot.js +66 -0
- package/dist/server/api-types.d.ts +13 -0
- package/dist/server/history-builder.d.ts +16 -0
- package/dist/server/history-builder.js +25 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -0
- package/dist/server/routes/agent.js +105 -17
- package/dist/server/session-manager.d.ts +23 -3
- package/dist/server/session-manager.js +67 -13
- package/dist/server/standalone.js +442 -87
- package/dist/server/ws.js +107 -5
- package/dist/testing/mock-api.js +20 -0
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@ import { streamSSE } from "hono/streaming";
|
|
|
14
14
|
import { createRequire } from "module";
|
|
15
15
|
import fs from "fs";
|
|
16
16
|
import path from "path";
|
|
17
|
-
var DB_PATH = path.join(process.cwd(), "data/sna.db");
|
|
17
|
+
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() {
|
|
@@ -247,16 +247,84 @@ import { streamSSE as streamSSE3 } from "hono/streaming";
|
|
|
247
247
|
// src/core/providers/claude-code.ts
|
|
248
248
|
import { spawn as spawn2, execSync } from "child_process";
|
|
249
249
|
import { EventEmitter } from "events";
|
|
250
|
-
import
|
|
251
|
-
import
|
|
250
|
+
import fs4 from "fs";
|
|
251
|
+
import path4 from "path";
|
|
252
252
|
|
|
253
|
-
// src/
|
|
254
|
-
import chalk from "chalk";
|
|
253
|
+
// src/core/providers/cc-history-adapter.ts
|
|
255
254
|
import fs2 from "fs";
|
|
256
255
|
import path2 from "path";
|
|
257
|
-
|
|
256
|
+
function writeHistoryJsonl(history, opts) {
|
|
257
|
+
for (let i = 1; i < history.length; i++) {
|
|
258
|
+
if (history[i].role === history[i - 1].role) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`History validation failed: consecutive ${history[i].role} at index ${i - 1} and ${i}. Messages must alternate user\u2194assistant. Merge tool results into text before injecting.`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
const dir = path2.join(opts.cwd, ".sna", "history");
|
|
266
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
267
|
+
const sessionId = crypto.randomUUID();
|
|
268
|
+
const filePath = path2.join(dir, `${sessionId}.jsonl`);
|
|
269
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
270
|
+
const lines = [];
|
|
271
|
+
let prevUuid = null;
|
|
272
|
+
for (const msg of history) {
|
|
273
|
+
const uuid = crypto.randomUUID();
|
|
274
|
+
if (msg.role === "user") {
|
|
275
|
+
lines.push(JSON.stringify({
|
|
276
|
+
parentUuid: prevUuid,
|
|
277
|
+
isSidechain: false,
|
|
278
|
+
type: "user",
|
|
279
|
+
uuid,
|
|
280
|
+
timestamp: now,
|
|
281
|
+
cwd: opts.cwd,
|
|
282
|
+
sessionId,
|
|
283
|
+
message: { role: "user", content: msg.content }
|
|
284
|
+
}));
|
|
285
|
+
} else {
|
|
286
|
+
lines.push(JSON.stringify({
|
|
287
|
+
parentUuid: prevUuid,
|
|
288
|
+
isSidechain: false,
|
|
289
|
+
type: "assistant",
|
|
290
|
+
uuid,
|
|
291
|
+
timestamp: now,
|
|
292
|
+
cwd: opts.cwd,
|
|
293
|
+
sessionId,
|
|
294
|
+
message: {
|
|
295
|
+
role: "assistant",
|
|
296
|
+
content: [{ type: "text", text: msg.content }]
|
|
297
|
+
}
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
prevUuid = uuid;
|
|
301
|
+
}
|
|
302
|
+
fs2.writeFileSync(filePath, lines.join("\n") + "\n");
|
|
303
|
+
return { filePath, extraArgs: ["--resume", filePath] };
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function buildRecalledConversation(history) {
|
|
309
|
+
const xml = history.map((msg) => `<${msg.role}>${msg.content}</${msg.role}>`).join("\n");
|
|
310
|
+
return JSON.stringify({
|
|
311
|
+
type: "assistant",
|
|
312
|
+
message: {
|
|
313
|
+
role: "assistant",
|
|
314
|
+
content: [{ type: "text", text: `<recalled-conversation>
|
|
315
|
+
${xml}
|
|
316
|
+
</recalled-conversation>` }]
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/lib/logger.ts
|
|
322
|
+
import chalk from "chalk";
|
|
323
|
+
import fs3 from "fs";
|
|
324
|
+
import path3 from "path";
|
|
325
|
+
var LOG_PATH = path3.join(process.cwd(), ".dev.log");
|
|
258
326
|
try {
|
|
259
|
-
|
|
327
|
+
fs3.writeFileSync(LOG_PATH, "");
|
|
260
328
|
} catch {
|
|
261
329
|
}
|
|
262
330
|
function tsPlain() {
|
|
@@ -288,7 +356,7 @@ var tagPlain = {
|
|
|
288
356
|
function appendFile(tag, args) {
|
|
289
357
|
const line = `${tsPlain()} ${tag} ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
|
|
290
358
|
`;
|
|
291
|
-
|
|
359
|
+
fs3.appendFile(LOG_PATH, line, () => {
|
|
292
360
|
});
|
|
293
361
|
}
|
|
294
362
|
function log(tag, ...args) {
|
|
@@ -305,9 +373,9 @@ var logger = { log, err };
|
|
|
305
373
|
var SHELL = process.env.SHELL || "/bin/zsh";
|
|
306
374
|
function resolveClaudePath(cwd) {
|
|
307
375
|
if (process.env.SNA_CLAUDE_COMMAND) return process.env.SNA_CLAUDE_COMMAND;
|
|
308
|
-
const cached =
|
|
309
|
-
if (
|
|
310
|
-
const p =
|
|
376
|
+
const cached = path4.join(cwd, ".sna/claude-path");
|
|
377
|
+
if (fs4.existsSync(cached)) {
|
|
378
|
+
const p = fs4.readFileSync(cached, "utf8").trim();
|
|
311
379
|
if (p) {
|
|
312
380
|
try {
|
|
313
381
|
execSync(`test -x "${p}"`, { stdio: "pipe" });
|
|
@@ -378,33 +446,44 @@ var ClaudeCodeProcess = class {
|
|
|
378
446
|
this._alive = false;
|
|
379
447
|
this.emitter.emit("error", err2);
|
|
380
448
|
});
|
|
381
|
-
if (options.history?.length) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
for (const msg of options.history) {
|
|
386
|
-
if (msg.role === "user") {
|
|
387
|
-
const line = JSON.stringify({
|
|
388
|
-
type: "user",
|
|
389
|
-
message: { role: "user", content: msg.content }
|
|
390
|
-
});
|
|
391
|
-
this.proc.stdin.write(line + "\n");
|
|
392
|
-
} else if (msg.role === "assistant") {
|
|
393
|
-
const line = JSON.stringify({
|
|
394
|
-
type: "assistant",
|
|
395
|
-
message: {
|
|
396
|
-
role: "assistant",
|
|
397
|
-
content: [{ type: "text", text: msg.content }]
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
this.proc.stdin.write(line + "\n");
|
|
401
|
-
}
|
|
402
|
-
}
|
|
449
|
+
if (options.history?.length && !options._historyViaResume) {
|
|
450
|
+
const line = buildRecalledConversation(options.history);
|
|
451
|
+
this.proc.stdin.write(line + "\n");
|
|
403
452
|
}
|
|
404
453
|
if (options.prompt) {
|
|
405
454
|
this.send(options.prompt);
|
|
406
455
|
}
|
|
407
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Split completed assistant text into chunks and emit assistant_delta events
|
|
459
|
+
* at a fixed rate (~270 chars/sec), followed by the final assistant event.
|
|
460
|
+
*
|
|
461
|
+
* CHUNK_SIZE chars every CHUNK_DELAY_MS → natural TPS feel regardless of length.
|
|
462
|
+
*/
|
|
463
|
+
emitTextAsDeltas(text) {
|
|
464
|
+
const CHUNK_SIZE = 4;
|
|
465
|
+
const CHUNK_DELAY_MS = 15;
|
|
466
|
+
let t = 0;
|
|
467
|
+
for (let i = 0; i < text.length; i += CHUNK_SIZE) {
|
|
468
|
+
const chunk = text.slice(i, i + CHUNK_SIZE);
|
|
469
|
+
setTimeout(() => {
|
|
470
|
+
this.emitter.emit("event", {
|
|
471
|
+
type: "assistant_delta",
|
|
472
|
+
delta: chunk,
|
|
473
|
+
index: 0,
|
|
474
|
+
timestamp: Date.now()
|
|
475
|
+
});
|
|
476
|
+
}, t);
|
|
477
|
+
t += CHUNK_DELAY_MS;
|
|
478
|
+
}
|
|
479
|
+
setTimeout(() => {
|
|
480
|
+
this.emitter.emit("event", {
|
|
481
|
+
type: "assistant",
|
|
482
|
+
message: text,
|
|
483
|
+
timestamp: Date.now()
|
|
484
|
+
});
|
|
485
|
+
}, t);
|
|
486
|
+
}
|
|
408
487
|
get alive() {
|
|
409
488
|
return this._alive;
|
|
410
489
|
}
|
|
@@ -480,6 +559,7 @@ var ClaudeCodeProcess = class {
|
|
|
480
559
|
const content = msg.message?.content;
|
|
481
560
|
if (!Array.isArray(content)) return null;
|
|
482
561
|
const events = [];
|
|
562
|
+
const textBlocks = [];
|
|
483
563
|
for (const block of content) {
|
|
484
564
|
if (block.type === "thinking") {
|
|
485
565
|
events.push({
|
|
@@ -497,15 +577,17 @@ var ClaudeCodeProcess = class {
|
|
|
497
577
|
} else if (block.type === "text") {
|
|
498
578
|
const text = (block.text ?? "").trim();
|
|
499
579
|
if (text) {
|
|
500
|
-
|
|
580
|
+
textBlocks.push(text);
|
|
501
581
|
}
|
|
502
582
|
}
|
|
503
583
|
}
|
|
504
|
-
if (events.length > 0) {
|
|
505
|
-
for (
|
|
506
|
-
this.emitter.emit("event",
|
|
584
|
+
if (events.length > 0 || textBlocks.length > 0) {
|
|
585
|
+
for (const e of events) {
|
|
586
|
+
this.emitter.emit("event", e);
|
|
587
|
+
}
|
|
588
|
+
for (const text of textBlocks) {
|
|
589
|
+
this.emitTextAsDeltas(text);
|
|
507
590
|
}
|
|
508
|
-
return events[0];
|
|
509
591
|
}
|
|
510
592
|
return null;
|
|
511
593
|
}
|
|
@@ -640,6 +722,14 @@ var ClaudeCodeProvider = class {
|
|
|
640
722
|
if (options.permissionMode) {
|
|
641
723
|
args.push("--permission-mode", options.permissionMode);
|
|
642
724
|
}
|
|
725
|
+
if (options.history?.length && options.prompt) {
|
|
726
|
+
const result = writeHistoryJsonl(options.history, { cwd: options.cwd });
|
|
727
|
+
if (result) {
|
|
728
|
+
args.push(...result.extraArgs);
|
|
729
|
+
options._historyViaResume = true;
|
|
730
|
+
logger.log("agent", `history via JSONL resume \u2192 ${result.filePath}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
643
733
|
if (extraArgsClean.length > 0) {
|
|
644
734
|
args.push(...extraArgsClean);
|
|
645
735
|
}
|
|
@@ -682,11 +772,34 @@ function getProvider(name = "claude-code") {
|
|
|
682
772
|
return provider2;
|
|
683
773
|
}
|
|
684
774
|
|
|
775
|
+
// src/server/history-builder.ts
|
|
776
|
+
function buildHistoryFromDb(sessionId) {
|
|
777
|
+
const db = getDb();
|
|
778
|
+
const rows = db.prepare(
|
|
779
|
+
`SELECT role, content FROM chat_messages
|
|
780
|
+
WHERE session_id = ? AND role IN ('user', 'assistant')
|
|
781
|
+
ORDER BY id ASC`
|
|
782
|
+
).all(sessionId);
|
|
783
|
+
if (rows.length === 0) return [];
|
|
784
|
+
const merged = [];
|
|
785
|
+
for (const row of rows) {
|
|
786
|
+
const role = row.role;
|
|
787
|
+
if (!row.content?.trim()) continue;
|
|
788
|
+
const last = merged[merged.length - 1];
|
|
789
|
+
if (last && last.role === role) {
|
|
790
|
+
last.content += "\n\n" + row.content;
|
|
791
|
+
} else {
|
|
792
|
+
merged.push({ role, content: row.content });
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return merged;
|
|
796
|
+
}
|
|
797
|
+
|
|
685
798
|
// src/server/image-store.ts
|
|
686
|
-
import
|
|
687
|
-
import
|
|
799
|
+
import fs5 from "fs";
|
|
800
|
+
import path5 from "path";
|
|
688
801
|
import { createHash } from "crypto";
|
|
689
|
-
var IMAGE_DIR =
|
|
802
|
+
var IMAGE_DIR = path5.join(process.cwd(), "data/images");
|
|
690
803
|
var MIME_TO_EXT = {
|
|
691
804
|
"image/png": "png",
|
|
692
805
|
"image/jpeg": "jpg",
|
|
@@ -695,23 +808,23 @@ var MIME_TO_EXT = {
|
|
|
695
808
|
"image/svg+xml": "svg"
|
|
696
809
|
};
|
|
697
810
|
function saveImages(sessionId, images) {
|
|
698
|
-
const dir =
|
|
699
|
-
|
|
811
|
+
const dir = path5.join(IMAGE_DIR, sessionId);
|
|
812
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
700
813
|
return images.map((img) => {
|
|
701
814
|
const ext = MIME_TO_EXT[img.mimeType] ?? "bin";
|
|
702
815
|
const hash = createHash("sha256").update(img.base64).digest("hex").slice(0, 12);
|
|
703
816
|
const filename = `${hash}.${ext}`;
|
|
704
|
-
const filePath =
|
|
705
|
-
if (!
|
|
706
|
-
|
|
817
|
+
const filePath = path5.join(dir, filename);
|
|
818
|
+
if (!fs5.existsSync(filePath)) {
|
|
819
|
+
fs5.writeFileSync(filePath, Buffer.from(img.base64, "base64"));
|
|
707
820
|
}
|
|
708
821
|
return filename;
|
|
709
822
|
});
|
|
710
823
|
}
|
|
711
824
|
function resolveImagePath(sessionId, filename) {
|
|
712
825
|
if (filename.includes("..") || filename.includes("/")) return null;
|
|
713
|
-
const filePath =
|
|
714
|
-
return
|
|
826
|
+
const filePath = path5.join(IMAGE_DIR, sessionId, filename);
|
|
827
|
+
return fs5.existsSync(filePath) ? filePath : null;
|
|
715
828
|
}
|
|
716
829
|
|
|
717
830
|
// src/server/routes/agent.ts
|
|
@@ -902,7 +1015,13 @@ function createAgentRoutes(sessionManager2) {
|
|
|
902
1015
|
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);
|
|
903
1016
|
} catch {
|
|
904
1017
|
}
|
|
905
|
-
|
|
1018
|
+
sessionManager2.pushEvent(sessionId, {
|
|
1019
|
+
type: "user_message",
|
|
1020
|
+
message: textContent,
|
|
1021
|
+
data: Object.keys(meta).length > 0 ? meta : void 0,
|
|
1022
|
+
timestamp: Date.now()
|
|
1023
|
+
});
|
|
1024
|
+
sessionManager2.updateSessionState(sessionId, "processing");
|
|
906
1025
|
sessionManager2.touch(sessionId);
|
|
907
1026
|
if (body.images?.length) {
|
|
908
1027
|
const content = [
|
|
@@ -924,32 +1043,59 @@ function createAgentRoutes(sessionManager2) {
|
|
|
924
1043
|
const sessionId = getSessionId(c);
|
|
925
1044
|
const session = sessionManager2.getOrCreateSession(sessionId);
|
|
926
1045
|
const sinceParam = c.req.query("since");
|
|
927
|
-
|
|
1046
|
+
const sinceCursor = sinceParam ? parseInt(sinceParam, 10) : session.eventCounter;
|
|
928
1047
|
return streamSSE3(c, async (stream) => {
|
|
929
|
-
const POLL_MS = 300;
|
|
930
1048
|
const KEEPALIVE_MS = 15e3;
|
|
931
|
-
|
|
932
|
-
|
|
1049
|
+
const signal = c.req.raw.signal;
|
|
1050
|
+
const queue = [];
|
|
1051
|
+
let wakeUp = null;
|
|
1052
|
+
const unsub = sessionManager2.onSessionEvent(sessionId, (eventCursor, event) => {
|
|
1053
|
+
queue.push({ cursor: eventCursor, event });
|
|
1054
|
+
const fn = wakeUp;
|
|
1055
|
+
wakeUp = null;
|
|
1056
|
+
fn?.();
|
|
1057
|
+
});
|
|
1058
|
+
signal.addEventListener("abort", () => {
|
|
1059
|
+
const fn = wakeUp;
|
|
1060
|
+
wakeUp = null;
|
|
1061
|
+
fn?.();
|
|
1062
|
+
});
|
|
1063
|
+
try {
|
|
1064
|
+
let cursor = sinceCursor;
|
|
933
1065
|
if (cursor < session.eventCounter) {
|
|
934
1066
|
const startIdx = Math.max(
|
|
935
1067
|
0,
|
|
936
1068
|
session.eventBuffer.length - (session.eventCounter - cursor)
|
|
937
1069
|
);
|
|
938
|
-
const
|
|
939
|
-
for (const event of newEvents) {
|
|
1070
|
+
for (const event of session.eventBuffer.slice(startIdx)) {
|
|
940
1071
|
cursor++;
|
|
941
|
-
await stream.writeSSE({
|
|
942
|
-
id: String(cursor),
|
|
943
|
-
data: JSON.stringify(event)
|
|
944
|
-
});
|
|
945
|
-
lastSend = Date.now();
|
|
1072
|
+
await stream.writeSSE({ id: String(cursor), data: JSON.stringify(event) });
|
|
946
1073
|
}
|
|
1074
|
+
} else {
|
|
1075
|
+
cursor = session.eventCounter;
|
|
947
1076
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1077
|
+
while (queue.length > 0 && queue[0].cursor <= cursor) queue.shift();
|
|
1078
|
+
while (!signal.aborted) {
|
|
1079
|
+
if (queue.length === 0) {
|
|
1080
|
+
await Promise.race([
|
|
1081
|
+
new Promise((r) => {
|
|
1082
|
+
wakeUp = r;
|
|
1083
|
+
}),
|
|
1084
|
+
new Promise((r) => setTimeout(r, KEEPALIVE_MS))
|
|
1085
|
+
]);
|
|
1086
|
+
}
|
|
1087
|
+
if (signal.aborted) break;
|
|
1088
|
+
if (queue.length > 0) {
|
|
1089
|
+
while (queue.length > 0) {
|
|
1090
|
+
const item = queue.shift();
|
|
1091
|
+
await stream.writeSSE({ id: String(item.cursor), data: JSON.stringify(item.event) });
|
|
1092
|
+
}
|
|
1093
|
+
} else {
|
|
1094
|
+
await stream.writeSSE({ data: "" });
|
|
1095
|
+
}
|
|
951
1096
|
}
|
|
952
|
-
|
|
1097
|
+
} finally {
|
|
1098
|
+
unsub();
|
|
953
1099
|
}
|
|
954
1100
|
});
|
|
955
1101
|
});
|
|
@@ -980,6 +1126,46 @@ function createAgentRoutes(sessionManager2) {
|
|
|
980
1126
|
return c.json({ status: "error", message: e.message }, 500);
|
|
981
1127
|
}
|
|
982
1128
|
});
|
|
1129
|
+
app.post("/resume", async (c) => {
|
|
1130
|
+
const sessionId = getSessionId(c);
|
|
1131
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1132
|
+
const session = sessionManager2.getOrCreateSession(sessionId);
|
|
1133
|
+
if (session.process?.alive) {
|
|
1134
|
+
return c.json({ status: "error", message: "Session already running. Use agent.send instead." }, 400);
|
|
1135
|
+
}
|
|
1136
|
+
const history = buildHistoryFromDb(sessionId);
|
|
1137
|
+
if (history.length === 0 && !body.prompt) {
|
|
1138
|
+
return c.json({ status: "error", message: "No history in DB \u2014 nothing to resume." }, 400);
|
|
1139
|
+
}
|
|
1140
|
+
const providerName = body.provider ?? "claude-code";
|
|
1141
|
+
const model = body.model ?? session.lastStartConfig?.model ?? "claude-sonnet-4-6";
|
|
1142
|
+
const permissionMode2 = body.permissionMode ?? session.lastStartConfig?.permissionMode ?? "acceptEdits";
|
|
1143
|
+
const extraArgs = body.extraArgs ?? session.lastStartConfig?.extraArgs;
|
|
1144
|
+
const provider2 = getProvider(providerName);
|
|
1145
|
+
try {
|
|
1146
|
+
const proc = provider2.spawn({
|
|
1147
|
+
cwd: session.cwd,
|
|
1148
|
+
prompt: body.prompt,
|
|
1149
|
+
model,
|
|
1150
|
+
permissionMode: permissionMode2,
|
|
1151
|
+
env: { SNA_SESSION_ID: sessionId },
|
|
1152
|
+
history: history.length > 0 ? history : void 0,
|
|
1153
|
+
extraArgs
|
|
1154
|
+
});
|
|
1155
|
+
sessionManager2.setProcess(sessionId, proc, "resumed");
|
|
1156
|
+
sessionManager2.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, extraArgs });
|
|
1157
|
+
logger.log("route", `POST /resume?session=${sessionId} \u2192 resumed (${history.length} history msgs)`);
|
|
1158
|
+
return httpJson(c, "agent.resume", {
|
|
1159
|
+
status: "resumed",
|
|
1160
|
+
provider: providerName,
|
|
1161
|
+
sessionId: session.id,
|
|
1162
|
+
historyCount: history.length
|
|
1163
|
+
});
|
|
1164
|
+
} catch (e) {
|
|
1165
|
+
logger.err("err", `POST /resume?session=${sessionId} \u2192 ${e.message}`);
|
|
1166
|
+
return c.json({ status: "error", message: e.message }, 500);
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
983
1169
|
app.post("/interrupt", async (c) => {
|
|
984
1170
|
const sessionId = getSessionId(c);
|
|
985
1171
|
const interrupted = sessionManager2.interruptSession(sessionId);
|
|
@@ -1007,11 +1193,25 @@ function createAgentRoutes(sessionManager2) {
|
|
|
1007
1193
|
app.get("/status", (c) => {
|
|
1008
1194
|
const sessionId = getSessionId(c);
|
|
1009
1195
|
const session = sessionManager2.getSession(sessionId);
|
|
1196
|
+
const alive = session?.process?.alive ?? false;
|
|
1197
|
+
let messageCount = 0;
|
|
1198
|
+
let lastMessage = null;
|
|
1199
|
+
try {
|
|
1200
|
+
const db = getDb();
|
|
1201
|
+
const count = db.prepare("SELECT COUNT(*) as c FROM chat_messages WHERE session_id = ?").get(sessionId);
|
|
1202
|
+
messageCount = count?.c ?? 0;
|
|
1203
|
+
const last = db.prepare("SELECT role, content, created_at FROM chat_messages WHERE session_id = ? ORDER BY id DESC LIMIT 1").get(sessionId);
|
|
1204
|
+
if (last) lastMessage = { role: last.role, content: last.content, created_at: last.created_at };
|
|
1205
|
+
} catch {
|
|
1206
|
+
}
|
|
1010
1207
|
return httpJson(c, "agent.status", {
|
|
1011
|
-
alive
|
|
1208
|
+
alive,
|
|
1209
|
+
agentStatus: !alive ? "disconnected" : session?.state === "processing" ? "busy" : "idle",
|
|
1012
1210
|
sessionId: session?.process?.sessionId ?? null,
|
|
1013
1211
|
ccSessionId: session?.ccSessionId ?? null,
|
|
1014
1212
|
eventCount: session?.eventCounter ?? 0,
|
|
1213
|
+
messageCount,
|
|
1214
|
+
lastMessage,
|
|
1015
1215
|
config: session?.lastStartConfig ?? null
|
|
1016
1216
|
});
|
|
1017
1217
|
});
|
|
@@ -1046,7 +1246,7 @@ function createAgentRoutes(sessionManager2) {
|
|
|
1046
1246
|
|
|
1047
1247
|
// src/server/routes/chat.ts
|
|
1048
1248
|
import { Hono as Hono2 } from "hono";
|
|
1049
|
-
import
|
|
1249
|
+
import fs6 from "fs";
|
|
1050
1250
|
function createChatRoutes() {
|
|
1051
1251
|
const app = new Hono2();
|
|
1052
1252
|
app.get("/sessions", (c) => {
|
|
@@ -1152,7 +1352,7 @@ function createChatRoutes() {
|
|
|
1152
1352
|
svg: "image/svg+xml"
|
|
1153
1353
|
};
|
|
1154
1354
|
const contentType = mimeMap[ext ?? ""] ?? "application/octet-stream";
|
|
1155
|
-
const data =
|
|
1355
|
+
const data = fs6.readFileSync(filePath);
|
|
1156
1356
|
return new Response(data, { headers: { "Content-Type": contentType, "Cache-Control": "public, max-age=31536000, immutable" } });
|
|
1157
1357
|
});
|
|
1158
1358
|
return app;
|
|
@@ -1171,6 +1371,7 @@ var SessionManager = class {
|
|
|
1171
1371
|
this.permissionRequestListeners = /* @__PURE__ */ new Set();
|
|
1172
1372
|
this.lifecycleListeners = /* @__PURE__ */ new Set();
|
|
1173
1373
|
this.configChangedListeners = /* @__PURE__ */ new Set();
|
|
1374
|
+
this.stateChangedListeners = /* @__PURE__ */ new Set();
|
|
1174
1375
|
this.maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
1175
1376
|
this.restoreFromDb();
|
|
1176
1377
|
}
|
|
@@ -1283,24 +1484,26 @@ var SessionManager = class {
|
|
|
1283
1484
|
return this.createSession({ id, ...opts });
|
|
1284
1485
|
}
|
|
1285
1486
|
/** Set the agent process for a session. Subscribes to events. */
|
|
1286
|
-
setProcess(sessionId, proc) {
|
|
1487
|
+
setProcess(sessionId, proc, lifecycleState) {
|
|
1287
1488
|
const session = this.sessions.get(sessionId);
|
|
1288
1489
|
if (!session) throw new Error(`Session "${sessionId}" not found`);
|
|
1289
1490
|
session.process = proc;
|
|
1290
|
-
|
|
1491
|
+
this.setSessionState(sessionId, session, "processing");
|
|
1291
1492
|
session.lastActivityAt = Date.now();
|
|
1292
1493
|
proc.on("event", (e) => {
|
|
1293
1494
|
if (e.type === "init" && e.data?.sessionId && !session.ccSessionId) {
|
|
1294
1495
|
session.ccSessionId = e.data.sessionId;
|
|
1295
1496
|
this.persistSession(session);
|
|
1296
1497
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1498
|
+
if (e.type !== "assistant_delta") {
|
|
1499
|
+
session.eventBuffer.push(e);
|
|
1500
|
+
if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
|
|
1501
|
+
session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
|
|
1502
|
+
}
|
|
1301
1503
|
}
|
|
1504
|
+
session.eventCounter++;
|
|
1302
1505
|
if (e.type === "complete" || e.type === "error" || e.type === "interrupted") {
|
|
1303
|
-
|
|
1506
|
+
this.setSessionState(sessionId, session, "waiting");
|
|
1304
1507
|
}
|
|
1305
1508
|
this.persistEvent(sessionId, e);
|
|
1306
1509
|
const listeners = this.eventListeners.get(sessionId);
|
|
@@ -1309,14 +1512,14 @@ var SessionManager = class {
|
|
|
1309
1512
|
}
|
|
1310
1513
|
});
|
|
1311
1514
|
proc.on("exit", (code) => {
|
|
1312
|
-
|
|
1515
|
+
this.setSessionState(sessionId, session, "idle");
|
|
1313
1516
|
this.emitLifecycle({ session: sessionId, state: code != null ? "exited" : "crashed", code });
|
|
1314
1517
|
});
|
|
1315
1518
|
proc.on("error", () => {
|
|
1316
|
-
|
|
1519
|
+
this.setSessionState(sessionId, session, "idle");
|
|
1317
1520
|
this.emitLifecycle({ session: sessionId, state: "crashed" });
|
|
1318
1521
|
});
|
|
1319
|
-
this.emitLifecycle({ session: sessionId, state: "started" });
|
|
1522
|
+
this.emitLifecycle({ session: sessionId, state: lifecycleState ?? "started" });
|
|
1320
1523
|
}
|
|
1321
1524
|
// ── Event pub/sub (for WebSocket) ─────────────────────────────
|
|
1322
1525
|
/** Subscribe to real-time events for a session. Returns unsubscribe function. */
|
|
@@ -1342,6 +1545,20 @@ var SessionManager = class {
|
|
|
1342
1545
|
broadcastSkillEvent(event) {
|
|
1343
1546
|
for (const cb of this.skillEventListeners) cb(event);
|
|
1344
1547
|
}
|
|
1548
|
+
/** Push a synthetic event into a session's event stream (for user message broadcast). */
|
|
1549
|
+
pushEvent(sessionId, event) {
|
|
1550
|
+
const session = this.sessions.get(sessionId);
|
|
1551
|
+
if (!session) return;
|
|
1552
|
+
session.eventBuffer.push(event);
|
|
1553
|
+
session.eventCounter++;
|
|
1554
|
+
if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
|
|
1555
|
+
session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
|
|
1556
|
+
}
|
|
1557
|
+
const listeners = this.eventListeners.get(sessionId);
|
|
1558
|
+
if (listeners) {
|
|
1559
|
+
for (const cb of listeners) cb(session.eventCounter, event);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1345
1562
|
// ── Permission pub/sub ────────────────────────────────────────
|
|
1346
1563
|
/** Subscribe to permission request notifications. Returns unsubscribe function. */
|
|
1347
1564
|
onPermissionRequest(cb) {
|
|
@@ -1366,11 +1583,29 @@ var SessionManager = class {
|
|
|
1366
1583
|
emitConfigChanged(sessionId, config) {
|
|
1367
1584
|
for (const cb of this.configChangedListeners) cb({ session: sessionId, config });
|
|
1368
1585
|
}
|
|
1586
|
+
// ── Agent status change pub/sub ────────────────────────────────
|
|
1587
|
+
onStateChanged(cb) {
|
|
1588
|
+
this.stateChangedListeners.add(cb);
|
|
1589
|
+
return () => this.stateChangedListeners.delete(cb);
|
|
1590
|
+
}
|
|
1591
|
+
/** Update session state and push agentStatus change to subscribers. */
|
|
1592
|
+
updateSessionState(sessionId, newState) {
|
|
1593
|
+
const session = this.sessions.get(sessionId);
|
|
1594
|
+
if (session) this.setSessionState(sessionId, session, newState);
|
|
1595
|
+
}
|
|
1596
|
+
setSessionState(sessionId, session, newState) {
|
|
1597
|
+
const oldState = session.state;
|
|
1598
|
+
session.state = newState;
|
|
1599
|
+
const newStatus = !session.process?.alive ? "disconnected" : newState === "processing" ? "busy" : "idle";
|
|
1600
|
+
if (oldState !== newState) {
|
|
1601
|
+
for (const cb of this.stateChangedListeners) cb({ session: sessionId, agentStatus: newStatus, state: newState });
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1369
1604
|
// ── Permission management ─────────────────────────────────────
|
|
1370
1605
|
/** Create a pending permission request. Returns a promise that resolves when approved/denied. */
|
|
1371
1606
|
createPendingPermission(sessionId, request) {
|
|
1372
1607
|
const session = this.sessions.get(sessionId);
|
|
1373
|
-
if (session)
|
|
1608
|
+
if (session) this.setSessionState(sessionId, session, "permission");
|
|
1374
1609
|
return new Promise((resolve) => {
|
|
1375
1610
|
const createdAt = Date.now();
|
|
1376
1611
|
this.pendingPermissions.set(sessionId, { resolve, request, createdAt });
|
|
@@ -1390,7 +1625,7 @@ var SessionManager = class {
|
|
|
1390
1625
|
pending.resolve(approved);
|
|
1391
1626
|
this.pendingPermissions.delete(sessionId);
|
|
1392
1627
|
const session = this.sessions.get(sessionId);
|
|
1393
|
-
if (session)
|
|
1628
|
+
if (session) this.setSessionState(sessionId, session, "processing");
|
|
1394
1629
|
return true;
|
|
1395
1630
|
}
|
|
1396
1631
|
/** Get a pending permission for a specific session. */
|
|
@@ -1442,7 +1677,7 @@ var SessionManager = class {
|
|
|
1442
1677
|
const session = this.sessions.get(id);
|
|
1443
1678
|
if (!session?.process?.alive) return false;
|
|
1444
1679
|
session.process.interrupt();
|
|
1445
|
-
|
|
1680
|
+
this.setSessionState(id, session, "waiting");
|
|
1446
1681
|
return true;
|
|
1447
1682
|
}
|
|
1448
1683
|
/** Change model. Sends control message if alive, always persists to config. */
|
|
@@ -1499,11 +1734,13 @@ var SessionManager = class {
|
|
|
1499
1734
|
label: s.label,
|
|
1500
1735
|
alive: s.process?.alive ?? false,
|
|
1501
1736
|
state: s.state,
|
|
1737
|
+
agentStatus: !s.process?.alive ? "disconnected" : s.state === "processing" ? "busy" : "idle",
|
|
1502
1738
|
cwd: s.cwd,
|
|
1503
1739
|
meta: s.meta,
|
|
1504
1740
|
config: s.lastStartConfig,
|
|
1505
1741
|
ccSessionId: s.ccSessionId,
|
|
1506
1742
|
eventCount: s.eventCounter,
|
|
1743
|
+
...this.getMessageStats(s.id),
|
|
1507
1744
|
createdAt: s.createdAt,
|
|
1508
1745
|
lastActivityAt: s.lastActivityAt
|
|
1509
1746
|
}));
|
|
@@ -1514,6 +1751,23 @@ var SessionManager = class {
|
|
|
1514
1751
|
if (session) session.lastActivityAt = Date.now();
|
|
1515
1752
|
}
|
|
1516
1753
|
/** Persist an agent event to chat_messages. */
|
|
1754
|
+
getMessageStats(sessionId) {
|
|
1755
|
+
try {
|
|
1756
|
+
const db = getDb();
|
|
1757
|
+
const count = db.prepare(
|
|
1758
|
+
`SELECT COUNT(*) as c FROM chat_messages WHERE session_id = ?`
|
|
1759
|
+
).get(sessionId);
|
|
1760
|
+
const last = db.prepare(
|
|
1761
|
+
`SELECT role, content, created_at FROM chat_messages WHERE session_id = ? ORDER BY id DESC LIMIT 1`
|
|
1762
|
+
).get(sessionId);
|
|
1763
|
+
return {
|
|
1764
|
+
messageCount: count.c,
|
|
1765
|
+
lastMessage: last ? { role: last.role, content: last.content, created_at: last.created_at } : null
|
|
1766
|
+
};
|
|
1767
|
+
} catch {
|
|
1768
|
+
return { messageCount: 0, lastMessage: null };
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1517
1771
|
persistEvent(sessionId, e) {
|
|
1518
1772
|
try {
|
|
1519
1773
|
const db = getDb();
|
|
@@ -1586,13 +1840,16 @@ function attachWebSocket(server2, sessionManager2) {
|
|
|
1586
1840
|
});
|
|
1587
1841
|
wss.on("connection", (ws) => {
|
|
1588
1842
|
logger.log("ws", "client connected");
|
|
1589
|
-
const state = { agentUnsubs: /* @__PURE__ */ new Map(), skillEventUnsub: null, skillPollTimer: null, permissionUnsub: null, lifecycleUnsub: null, configChangedUnsub: null };
|
|
1843
|
+
const state = { agentUnsubs: /* @__PURE__ */ new Map(), skillEventUnsub: null, skillPollTimer: null, permissionUnsub: null, lifecycleUnsub: null, configChangedUnsub: null, stateChangedUnsub: null };
|
|
1590
1844
|
state.lifecycleUnsub = sessionManager2.onSessionLifecycle((event) => {
|
|
1591
1845
|
send(ws, { type: "session.lifecycle", ...event });
|
|
1592
1846
|
});
|
|
1593
1847
|
state.configChangedUnsub = sessionManager2.onConfigChanged((event) => {
|
|
1594
1848
|
send(ws, { type: "session.config-changed", ...event });
|
|
1595
1849
|
});
|
|
1850
|
+
state.stateChangedUnsub = sessionManager2.onStateChanged((event) => {
|
|
1851
|
+
send(ws, { type: "session.state-changed", ...event });
|
|
1852
|
+
});
|
|
1596
1853
|
ws.on("message", (raw) => {
|
|
1597
1854
|
let msg;
|
|
1598
1855
|
try {
|
|
@@ -1623,6 +1880,8 @@ function attachWebSocket(server2, sessionManager2) {
|
|
|
1623
1880
|
state.lifecycleUnsub = null;
|
|
1624
1881
|
state.configChangedUnsub?.();
|
|
1625
1882
|
state.configChangedUnsub = null;
|
|
1883
|
+
state.stateChangedUnsub?.();
|
|
1884
|
+
state.stateChangedUnsub = null;
|
|
1626
1885
|
});
|
|
1627
1886
|
});
|
|
1628
1887
|
return wss;
|
|
@@ -1641,6 +1900,8 @@ function handleMessage(ws, msg, sm, state) {
|
|
|
1641
1900
|
return handleAgentStart(ws, msg, sm);
|
|
1642
1901
|
case "agent.send":
|
|
1643
1902
|
return handleAgentSend(ws, msg, sm);
|
|
1903
|
+
case "agent.resume":
|
|
1904
|
+
return handleAgentResume(ws, msg, sm);
|
|
1644
1905
|
case "agent.restart":
|
|
1645
1906
|
return handleAgentRestart(ws, msg, sm);
|
|
1646
1907
|
case "agent.interrupt":
|
|
@@ -1782,7 +2043,13 @@ function handleAgentSend(ws, msg, sm) {
|
|
|
1782
2043
|
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);
|
|
1783
2044
|
} catch {
|
|
1784
2045
|
}
|
|
1785
|
-
|
|
2046
|
+
sm.pushEvent(sessionId, {
|
|
2047
|
+
type: "user_message",
|
|
2048
|
+
message: textContent,
|
|
2049
|
+
data: Object.keys(meta).length > 0 ? meta : void 0,
|
|
2050
|
+
timestamp: Date.now()
|
|
2051
|
+
});
|
|
2052
|
+
sm.updateSessionState(sessionId, "processing");
|
|
1786
2053
|
sm.touch(sessionId);
|
|
1787
2054
|
if (images?.length) {
|
|
1788
2055
|
const content = [
|
|
@@ -1798,6 +2065,43 @@ function handleAgentSend(ws, msg, sm) {
|
|
|
1798
2065
|
}
|
|
1799
2066
|
wsReply(ws, msg, { status: "sent" });
|
|
1800
2067
|
}
|
|
2068
|
+
function handleAgentResume(ws, msg, sm) {
|
|
2069
|
+
const sessionId = msg.session ?? "default";
|
|
2070
|
+
const session = sm.getOrCreateSession(sessionId);
|
|
2071
|
+
if (session.process?.alive) {
|
|
2072
|
+
return replyError(ws, msg, "Session already running. Use agent.send instead.");
|
|
2073
|
+
}
|
|
2074
|
+
const history = buildHistoryFromDb(sessionId);
|
|
2075
|
+
if (history.length === 0 && !msg.prompt) {
|
|
2076
|
+
return replyError(ws, msg, "No history in DB \u2014 nothing to resume.");
|
|
2077
|
+
}
|
|
2078
|
+
const providerName = msg.provider ?? session.lastStartConfig?.provider ?? "claude-code";
|
|
2079
|
+
const model = msg.model ?? session.lastStartConfig?.model ?? "claude-sonnet-4-6";
|
|
2080
|
+
const permissionMode2 = msg.permissionMode ?? session.lastStartConfig?.permissionMode ?? "acceptEdits";
|
|
2081
|
+
const extraArgs = msg.extraArgs ?? session.lastStartConfig?.extraArgs;
|
|
2082
|
+
const provider2 = getProvider(providerName);
|
|
2083
|
+
try {
|
|
2084
|
+
const proc = provider2.spawn({
|
|
2085
|
+
cwd: session.cwd,
|
|
2086
|
+
prompt: msg.prompt,
|
|
2087
|
+
model,
|
|
2088
|
+
permissionMode: permissionMode2,
|
|
2089
|
+
env: { SNA_SESSION_ID: sessionId },
|
|
2090
|
+
history: history.length > 0 ? history : void 0,
|
|
2091
|
+
extraArgs
|
|
2092
|
+
});
|
|
2093
|
+
sm.setProcess(sessionId, proc, "resumed");
|
|
2094
|
+
sm.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, extraArgs });
|
|
2095
|
+
wsReply(ws, msg, {
|
|
2096
|
+
status: "resumed",
|
|
2097
|
+
provider: providerName,
|
|
2098
|
+
sessionId: session.id,
|
|
2099
|
+
historyCount: history.length
|
|
2100
|
+
});
|
|
2101
|
+
} catch (e) {
|
|
2102
|
+
replyError(ws, msg, e.message);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
1801
2105
|
function handleAgentRestart(ws, msg, sm) {
|
|
1802
2106
|
const sessionId = msg.session ?? "default";
|
|
1803
2107
|
try {
|
|
@@ -1854,11 +2158,25 @@ function handleAgentKill(ws, msg, sm) {
|
|
|
1854
2158
|
function handleAgentStatus(ws, msg, sm) {
|
|
1855
2159
|
const sessionId = msg.session ?? "default";
|
|
1856
2160
|
const session = sm.getSession(sessionId);
|
|
2161
|
+
const alive = session?.process?.alive ?? false;
|
|
2162
|
+
let messageCount = 0;
|
|
2163
|
+
let lastMessage = null;
|
|
2164
|
+
try {
|
|
2165
|
+
const db = getDb();
|
|
2166
|
+
const count = db.prepare("SELECT COUNT(*) as c FROM chat_messages WHERE session_id = ?").get(sessionId);
|
|
2167
|
+
messageCount = count?.c ?? 0;
|
|
2168
|
+
const last = db.prepare("SELECT role, content, created_at FROM chat_messages WHERE session_id = ? ORDER BY id DESC LIMIT 1").get(sessionId);
|
|
2169
|
+
if (last) lastMessage = { role: last.role, content: last.content, created_at: last.created_at };
|
|
2170
|
+
} catch {
|
|
2171
|
+
}
|
|
1857
2172
|
wsReply(ws, msg, {
|
|
1858
|
-
alive
|
|
2173
|
+
alive,
|
|
2174
|
+
agentStatus: !alive ? "disconnected" : session?.state === "processing" ? "busy" : "idle",
|
|
1859
2175
|
sessionId: session?.process?.sessionId ?? null,
|
|
1860
2176
|
ccSessionId: session?.ccSessionId ?? null,
|
|
1861
2177
|
eventCount: session?.eventCounter ?? 0,
|
|
2178
|
+
messageCount,
|
|
2179
|
+
lastMessage,
|
|
1862
2180
|
config: session?.lastStartConfig ?? null
|
|
1863
2181
|
});
|
|
1864
2182
|
}
|
|
@@ -1875,7 +2193,38 @@ function handleAgentSubscribe(ws, msg, sm, state) {
|
|
|
1875
2193
|
const sessionId = msg.session ?? "default";
|
|
1876
2194
|
const session = sm.getOrCreateSession(sessionId);
|
|
1877
2195
|
state.agentUnsubs.get(sessionId)?.();
|
|
1878
|
-
|
|
2196
|
+
const includeHistory = msg.since === 0 || msg.includeHistory === true;
|
|
2197
|
+
let cursor = 0;
|
|
2198
|
+
if (includeHistory) {
|
|
2199
|
+
try {
|
|
2200
|
+
const db = getDb();
|
|
2201
|
+
const rows = db.prepare(
|
|
2202
|
+
`SELECT role, content, meta, created_at FROM chat_messages
|
|
2203
|
+
WHERE session_id = ? ORDER BY id ASC`
|
|
2204
|
+
).all(sessionId);
|
|
2205
|
+
for (const row of rows) {
|
|
2206
|
+
cursor++;
|
|
2207
|
+
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;
|
|
2208
|
+
if (!eventType) continue;
|
|
2209
|
+
const meta = row.meta ? JSON.parse(row.meta) : void 0;
|
|
2210
|
+
send(ws, {
|
|
2211
|
+
type: "agent.event",
|
|
2212
|
+
session: sessionId,
|
|
2213
|
+
cursor,
|
|
2214
|
+
isHistory: true,
|
|
2215
|
+
event: {
|
|
2216
|
+
type: eventType,
|
|
2217
|
+
message: row.content,
|
|
2218
|
+
data: meta,
|
|
2219
|
+
timestamp: new Date(row.created_at).getTime()
|
|
2220
|
+
}
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
} catch {
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
const bufferStart = typeof msg.since === "number" && msg.since > 0 ? msg.since : session.eventCounter;
|
|
2227
|
+
if (!includeHistory) cursor = bufferStart;
|
|
1879
2228
|
if (cursor < session.eventCounter) {
|
|
1880
2229
|
const startIdx = Math.max(0, session.eventBuffer.length - (session.eventCounter - cursor));
|
|
1881
2230
|
const events = session.eventBuffer.slice(startIdx);
|
|
@@ -1883,6 +2232,8 @@ function handleAgentSubscribe(ws, msg, sm, state) {
|
|
|
1883
2232
|
cursor++;
|
|
1884
2233
|
send(ws, { type: "agent.event", session: sessionId, cursor, event });
|
|
1885
2234
|
}
|
|
2235
|
+
} else {
|
|
2236
|
+
cursor = session.eventCounter;
|
|
1886
2237
|
}
|
|
1887
2238
|
const unsub = sm.onSessionEvent(sessionId, (eventCursor, event) => {
|
|
1888
2239
|
send(ws, { type: "agent.event", session: sessionId, cursor: eventCursor, event });
|
|
@@ -1995,10 +2346,14 @@ function handlePermissionPending(ws, msg, sm) {
|
|
|
1995
2346
|
}
|
|
1996
2347
|
function handlePermissionSubscribe(ws, msg, sm, state) {
|
|
1997
2348
|
state.permissionUnsub?.();
|
|
2349
|
+
const pending = sm.getAllPendingPermissions();
|
|
2350
|
+
for (const p of pending) {
|
|
2351
|
+
send(ws, { type: "permission.request", session: p.sessionId, request: p.request, createdAt: p.createdAt, isHistory: true });
|
|
2352
|
+
}
|
|
1998
2353
|
state.permissionUnsub = sm.onPermissionRequest((sessionId, request, createdAt) => {
|
|
1999
2354
|
send(ws, { type: "permission.request", session: sessionId, request, createdAt });
|
|
2000
2355
|
});
|
|
2001
|
-
reply(ws, msg, {});
|
|
2356
|
+
reply(ws, msg, { pendingCount: pending.length });
|
|
2002
2357
|
}
|
|
2003
2358
|
function handlePermissionUnsubscribe(ws, msg, state) {
|
|
2004
2359
|
state.permissionUnsub?.();
|
|
@@ -2135,8 +2490,8 @@ var methodColor = {
|
|
|
2135
2490
|
root.use("*", async (c, next) => {
|
|
2136
2491
|
const m = c.req.method;
|
|
2137
2492
|
const colorFn = methodColor[m] ?? chalk2.white;
|
|
2138
|
-
const
|
|
2139
|
-
logger.log("req", `${colorFn(m.padEnd(6))} ${
|
|
2493
|
+
const path6 = new URL(c.req.url).pathname;
|
|
2494
|
+
logger.log("req", `${colorFn(m.padEnd(6))} ${path6}`);
|
|
2140
2495
|
await next();
|
|
2141
2496
|
});
|
|
2142
2497
|
var sessionManager = new SessionManager({ maxSessions });
|