@foothill/agent-move 1.0.11 → 1.0.12

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.
Files changed (58) hide show
  1. package/package.json +1 -1
  2. package/packages/client/dist/assets/{BufferResource-Dl7AyA-b.js → BufferResource-Dhljy8H8.js} +1 -1
  3. package/packages/client/dist/assets/{CanvasRenderer-CoGUh0dQ.js → CanvasRenderer-Bpr11iOT.js} +1 -1
  4. package/packages/client/dist/assets/{Filter-DNvhyFhm.js → Filter-DL2yN3-o.js} +1 -1
  5. package/packages/client/dist/assets/{RenderTargetSystem-C8Ap8tmK.js → RenderTargetSystem-BTwylEdr.js} +1 -1
  6. package/packages/client/dist/assets/{WebGLRenderer-DGZDe07g.js → WebGLRenderer-wH1P7d1x.js} +1 -1
  7. package/packages/client/dist/assets/{WebGPURenderer-D3zU-ngm.js → WebGPURenderer-C7n8jUXC.js} +1 -1
  8. package/packages/client/dist/assets/{browserAll-Dw8CUBpx.js → browserAll-CgAMpWnT.js} +1 -1
  9. package/packages/client/dist/assets/index-DG7HqEmM.js +1338 -0
  10. package/packages/client/dist/assets/{index-edh-N9Dm.css → index-Nz5TZeB1.css} +1 -1
  11. package/packages/client/dist/assets/{webworkerAll-XKfx082n.js → webworkerAll-wrP2P1GC.js} +1 -1
  12. package/packages/client/dist/index.html +7 -2
  13. package/packages/server/dist/index.d.ts.map +1 -1
  14. package/packages/server/dist/index.js +954 -31
  15. package/packages/server/dist/index.js.map +1 -1
  16. package/packages/server/dist/routes/sessions-api.d.ts +5 -0
  17. package/packages/server/dist/routes/sessions-api.d.ts.map +1 -0
  18. package/packages/server/dist/routes/sessions-api.js +88 -0
  19. package/packages/server/dist/routes/sessions-api.js.map +1 -0
  20. package/packages/server/dist/state/activity-processor.d.ts.map +1 -1
  21. package/packages/server/dist/state/activity-processor.js +0 -2
  22. package/packages/server/dist/state/activity-processor.js.map +1 -1
  23. package/packages/server/dist/state/agent-state-manager.d.ts.map +1 -1
  24. package/packages/server/dist/state/agent-state-manager.js +3 -5
  25. package/packages/server/dist/state/agent-state-manager.js.map +1 -1
  26. package/packages/server/dist/state/identity-manager.d.ts.map +1 -1
  27. package/packages/server/dist/state/identity-manager.js +0 -3
  28. package/packages/server/dist/state/identity-manager.js.map +1 -1
  29. package/packages/server/dist/storage/session-recorder.d.ts +38 -0
  30. package/packages/server/dist/storage/session-recorder.d.ts.map +1 -0
  31. package/packages/server/dist/storage/session-recorder.js +941 -0
  32. package/packages/server/dist/storage/session-recorder.js.map +1 -0
  33. package/packages/server/dist/storage/session-store.d.ts +60 -0
  34. package/packages/server/dist/storage/session-store.d.ts.map +1 -0
  35. package/packages/server/dist/storage/session-store.js +330 -0
  36. package/packages/server/dist/storage/session-store.js.map +1 -0
  37. package/packages/server/dist/watcher/opencode/opencode-watcher.d.ts +15 -0
  38. package/packages/server/dist/watcher/opencode/opencode-watcher.d.ts.map +1 -1
  39. package/packages/server/dist/watcher/opencode/opencode-watcher.js +61 -4
  40. package/packages/server/dist/watcher/opencode/opencode-watcher.js.map +1 -1
  41. package/packages/server/dist/ws/broadcaster.d.ts.map +1 -1
  42. package/packages/server/dist/ws/broadcaster.js +3 -18
  43. package/packages/server/dist/ws/broadcaster.js.map +1 -1
  44. package/packages/shared/dist/constants/tools.d.ts +4 -0
  45. package/packages/shared/dist/constants/tools.d.ts.map +1 -1
  46. package/packages/shared/dist/constants/tools.js +4 -0
  47. package/packages/shared/dist/constants/tools.js.map +1 -1
  48. package/packages/shared/dist/index.d.ts +2 -1
  49. package/packages/shared/dist/index.d.ts.map +1 -1
  50. package/packages/shared/dist/index.js +1 -1
  51. package/packages/shared/dist/index.js.map +1 -1
  52. package/packages/shared/dist/types/session-record.d.ts +87 -0
  53. package/packages/shared/dist/types/session-record.d.ts.map +1 -0
  54. package/packages/shared/dist/types/session-record.js +2 -0
  55. package/packages/shared/dist/types/session-record.js.map +1 -0
  56. package/packages/shared/dist/types/websocket.d.ts +3 -0
  57. package/packages/shared/dist/types/websocket.d.ts.map +1 -1
  58. package/packages/client/dist/assets/index-F7EORPP_.js +0 -850
@@ -1,6 +1,6 @@
1
1
  // dist/index.js
2
2
  import { fileURLToPath } from "url";
3
- import { dirname as dirname2, join as join10 } from "path";
3
+ import { dirname as dirname2, join as join11 } from "path";
4
4
  import Fastify from "fastify";
5
5
  import cors from "@fastify/cors";
6
6
  import websocket from "@fastify/websocket";
@@ -34,8 +34,13 @@ import { existsSync as existsSync4 } from "fs";
34
34
  import { EventEmitter as EventEmitter2 } from "events";
35
35
  import { EventEmitter } from "events";
36
36
  import { execSync } from "child_process";
37
- import { EventEmitter as EventEmitter3 } from "events";
38
37
  import { randomUUID } from "crypto";
38
+ import { mkdirSync } from "fs";
39
+ import { join as join10 } from "path";
40
+ import { homedir as homedir5 } from "os";
41
+ import Database2 from "better-sqlite3";
42
+ import { EventEmitter as EventEmitter3 } from "events";
43
+ import { randomUUID as randomUUID2 } from "crypto";
39
44
  var config = {
40
45
  port: parseInt(process.env.AGENT_MOVE_PORT || "3333", 10),
41
46
  claudeHome: join(homedir(), ".claude"),
@@ -675,6 +680,86 @@ var AGENT_PALETTES = [
675
680
  { name: "lime", body: 10275941, outline: 5606191, highlight: 12968357, eye: 3355443, skin: 16767916 },
676
681
  { name: "brown", body: 9268835, outline: 5125166, highlight: 12364452, eye: 16777215, skin: 16767916 }
677
682
  ];
683
+ var MODEL_PRICING = {
684
+ // ── Anthropic Claude ────────────────────────────────────────────────────────
685
+ "claude-opus-4-6": { input: 15, output: 75 },
686
+ "claude-opus-4-5-20250620": { input: 15, output: 75 },
687
+ "claude-sonnet-4-6": { input: 3, output: 15 },
688
+ "claude-sonnet-4-5-20250514": { input: 3, output: 15 },
689
+ "claude-sonnet-4-0-20250514": { input: 3, output: 15 },
690
+ "claude-haiku-4-5-20251001": { input: 1, output: 5 },
691
+ "claude-3-7-sonnet-20250219": { input: 3, output: 15 },
692
+ "claude-3-5-sonnet-20241022": { input: 3, output: 15 },
693
+ "claude-3-5-haiku-20241022": { input: 1, output: 5 },
694
+ "claude-3-opus-20240229": { input: 15, output: 75 },
695
+ // ── OpenAI ──────────────────────────────────────────────────────────────────
696
+ "gpt-4o": { input: 2.5, output: 10 },
697
+ "gpt-4o-mini": { input: 0.15, output: 0.6 },
698
+ "gpt-4-turbo": { input: 10, output: 30 },
699
+ "gpt-4": { input: 30, output: 60 },
700
+ "o1": { input: 15, output: 60 },
701
+ "o1-mini": { input: 1.1, output: 4.4 },
702
+ "o3": { input: 10, output: 40 },
703
+ "o3-mini": { input: 1.1, output: 4.4 },
704
+ "o4-mini": { input: 1.1, output: 4.4 },
705
+ // ── Google Gemini ───────────────────────────────────────────────────────────
706
+ "gemini-2.5-pro": { input: 1.25, output: 10 },
707
+ "gemini-2.5-flash": { input: 0.15, output: 0.6 },
708
+ "gemini-2.0-flash": { input: 0.1, output: 0.4 },
709
+ "gemini-2.0-flash-lite": { input: 0.075, output: 0.3 },
710
+ "gemini-1.5-pro": { input: 1.25, output: 5 },
711
+ "gemini-1.5-flash": { input: 0.075, output: 0.3 },
712
+ // ── DeepSeek ────────────────────────────────────────────────────────────────
713
+ "deepseek-chat": { input: 0.27, output: 1.1 },
714
+ // DeepSeek V3
715
+ "deepseek-reasoner": { input: 0.55, output: 2.19 },
716
+ // DeepSeek R1
717
+ // ── xAI Grok ────────────────────────────────────────────────────────────────
718
+ "grok-3": { input: 3, output: 15 },
719
+ "grok-3-mini": { input: 0.3, output: 0.5 },
720
+ "grok-2": { input: 2, output: 10 },
721
+ // ── Mistral ─────────────────────────────────────────────────────────────────
722
+ "mistral-large": { input: 2, output: 6 },
723
+ "mistral-small": { input: 0.1, output: 0.3 },
724
+ "codestral": { input: 0.1, output: 0.3 }
725
+ };
726
+ var DEFAULT_PRICING = { input: 3, output: 15 };
727
+ function getModelPricing(model) {
728
+ if (!model)
729
+ return DEFAULT_PRICING;
730
+ const m = model.toLowerCase();
731
+ if (MODEL_PRICING[m])
732
+ return MODEL_PRICING[m];
733
+ for (const [key, pricing] of Object.entries(MODEL_PRICING)) {
734
+ if (m.startsWith(key) || m.includes(key))
735
+ return pricing;
736
+ }
737
+ if (m.includes("opus"))
738
+ return MODEL_PRICING["claude-opus-4-6"];
739
+ if (m.includes("haiku"))
740
+ return MODEL_PRICING["claude-haiku-4-5-20251001"];
741
+ if (m.includes("sonnet"))
742
+ return MODEL_PRICING["claude-sonnet-4-6"];
743
+ if (m.includes("o3-mini") || m.includes("o4-mini"))
744
+ return MODEL_PRICING["o3-mini"];
745
+ if (m.includes("o1-mini"))
746
+ return MODEL_PRICING["o1-mini"];
747
+ if (m.includes("gemini-2.5"))
748
+ return MODEL_PRICING["gemini-2.5-pro"];
749
+ if (m.includes("gemini-2"))
750
+ return MODEL_PRICING["gemini-2.0-flash"];
751
+ if (m.includes("gemini"))
752
+ return MODEL_PRICING["gemini-1.5-pro"];
753
+ if (m.includes("gpt-4o"))
754
+ return MODEL_PRICING["gpt-4o"];
755
+ if (m.includes("deepseek"))
756
+ return MODEL_PRICING["deepseek-chat"];
757
+ if (m.includes("grok"))
758
+ return MODEL_PRICING["grok-3"];
759
+ if (m.includes("mistral"))
760
+ return MODEL_PRICING["mistral-large"];
761
+ return DEFAULT_PRICING;
762
+ }
678
763
  function getProjectColorIndex(projectPath) {
679
764
  let hash = 0;
680
765
  for (let i = 0; i < projectPath.length; i++) {
@@ -682,6 +767,10 @@ function getProjectColorIndex(projectPath) {
682
767
  }
683
768
  return Math.abs(hash) % AGENT_PALETTES.length;
684
769
  }
770
+ function computeAgentCost(tokens) {
771
+ const pricing = getModelPricing(tokens.model);
772
+ return tokens.totalInputTokens / 1e6 * pricing.input + tokens.totalOutputTokens / 1e6 * pricing.output + tokens.cacheReadTokens / 1e6 * pricing.input * 0.1 + tokens.cacheCreationTokens / 1e6 * pricing.input * 1.25;
773
+ }
685
774
  var OpenCodeParser = class {
686
775
  /**
687
776
  * Convert an OpenCode part JSON object into a ParsedActivity.
@@ -753,7 +842,7 @@ var OpenCodeParser = class {
753
842
  };
754
843
  }
755
844
  };
756
- var OpenCodeWatcher = class {
845
+ var OpenCodeWatcher = class _OpenCodeWatcher {
757
846
  stateManager;
758
847
  watcher = null;
759
848
  db = null;
@@ -770,6 +859,19 @@ var OpenCodeWatcher = class {
770
859
  seenCallIds = /* @__PURE__ */ new Set();
771
860
  /** row id → true — deduplicates text/token events */
772
861
  seenIds = /* @__PURE__ */ new Set();
862
+ /**
863
+ * Per-session idle timers: after step-finish, if no new step-start
864
+ * arrives within this window, call hookStop to idle the agent.
865
+ */
866
+ stepFinishTimers = /* @__PURE__ */ new Map();
867
+ static STEP_FINISH_IDLE_MS = 4e3;
868
+ /**
869
+ * Per-session shutdown timers: after step-finish, if no new activity
870
+ * arrives within this longer window, call hookSessionEnd to fully shut
871
+ * down the agent and finalize the session. This handles /exit and closed terminals.
872
+ */
873
+ sessionEndTimers = /* @__PURE__ */ new Map();
874
+ static SESSION_END_MS = 18e4;
773
875
  // Prepared statements (initialised after DB opens)
774
876
  stmtAllSessions;
775
877
  stmtRecentSessions;
@@ -820,6 +922,12 @@ var OpenCodeWatcher = class {
820
922
  this.messages.clear();
821
923
  this.seenCallIds.clear();
822
924
  this.seenIds.clear();
925
+ for (const t of this.stepFinishTimers.values())
926
+ clearTimeout(t);
927
+ this.stepFinishTimers.clear();
928
+ for (const t of this.sessionEndTimers.values())
929
+ clearTimeout(t);
930
+ this.sessionEndTimers.clear();
823
931
  }
824
932
  // ── Prepared statements ────────────────────────────────────────────────────
825
933
  prepareStatements() {
@@ -908,8 +1016,11 @@ var OpenCodeWatcher = class {
908
1016
  const activity = this.parser.parseTokenUsage(data);
909
1017
  if (!activity)
910
1018
  return;
1019
+ const prefixedId = this.prefixed(row.session_id);
1020
+ this.cancelStepFinishTimer(prefixedId);
1021
+ this.cancelSessionEndTimer(prefixedId);
911
1022
  const sessionInfo = this.getSessionInfo(row.session_id);
912
- this.stateManager.processMessage(this.prefixed(row.session_id), activity, sessionInfo);
1023
+ this.stateManager.processMessage(prefixedId, activity, sessionInfo);
913
1024
  }
914
1025
  processPartRow(row) {
915
1026
  let data;
@@ -923,7 +1034,25 @@ var OpenCodeWatcher = class {
923
1034
  if (this.seenIds.has(row.id))
924
1035
  return;
925
1036
  this.seenIds.add(row.id);
926
- this.stateManager.heartbeat(this.prefixed(row.session_id));
1037
+ const prefixedId2 = this.prefixed(row.session_id);
1038
+ if (data.type === "step-start") {
1039
+ this.cancelStepFinishTimer(prefixedId2);
1040
+ this.cancelSessionEndTimer(prefixedId2);
1041
+ this.stateManager.heartbeat(prefixedId2);
1042
+ } else {
1043
+ this.cancelStepFinishTimer(prefixedId2);
1044
+ this.cancelSessionEndTimer(prefixedId2);
1045
+ const idleTimer = setTimeout(() => {
1046
+ this.stepFinishTimers.delete(prefixedId2);
1047
+ this.stateManager.hookStop(prefixedId2);
1048
+ }, _OpenCodeWatcher.STEP_FINISH_IDLE_MS);
1049
+ this.stepFinishTimers.set(prefixedId2, idleTimer);
1050
+ const endTimer = setTimeout(() => {
1051
+ this.sessionEndTimers.delete(prefixedId2);
1052
+ this.stateManager.hookSessionEnd(prefixedId2);
1053
+ }, _OpenCodeWatcher.SESSION_END_MS);
1054
+ this.sessionEndTimers.set(prefixedId2, endTimer);
1055
+ }
927
1056
  return;
928
1057
  }
929
1058
  if (data.type === "tool" && data.callID) {
@@ -938,10 +1067,27 @@ var OpenCodeWatcher = class {
938
1067
  const activity = this.parser.parsePart(data, messageData);
939
1068
  if (!activity)
940
1069
  return;
1070
+ const prefixedId = this.prefixed(row.session_id);
1071
+ this.cancelStepFinishTimer(prefixedId);
1072
+ this.cancelSessionEndTimer(prefixedId);
941
1073
  const sessionInfo = this.getSessionInfo(row.session_id);
942
- this.stateManager.processMessage(this.prefixed(row.session_id), activity, sessionInfo);
1074
+ this.stateManager.processMessage(prefixedId, activity, sessionInfo);
943
1075
  }
944
1076
  // ── Helpers ────────────────────────────────────────────────────────────────
1077
+ cancelStepFinishTimer(prefixedId) {
1078
+ const timer = this.stepFinishTimers.get(prefixedId);
1079
+ if (timer) {
1080
+ clearTimeout(timer);
1081
+ this.stepFinishTimers.delete(prefixedId);
1082
+ }
1083
+ }
1084
+ cancelSessionEndTimer(prefixedId) {
1085
+ const timer = this.sessionEndTimers.get(prefixedId);
1086
+ if (timer) {
1087
+ clearTimeout(timer);
1088
+ this.sessionEndTimers.delete(prefixedId);
1089
+ }
1090
+ }
945
1091
  getSessionInfo(sessionId) {
946
1092
  const cached = this.sessions.get(sessionId);
947
1093
  if (cached)
@@ -2095,9 +2241,6 @@ function promoteAgent(deps, agentId) {
2095
2241
  const spawnEvent = { type: "agent:spawn", agent: { ...agent }, timestamp: now };
2096
2242
  recordTimeline(spawnEvent);
2097
2243
  emit("agent:spawn", spawnEvent);
2098
- const updateEvent = { type: "agent:update", agent: { ...agent }, timestamp: now };
2099
- recordTimeline(updateEvent);
2100
- emit("agent:update", updateEvent);
2101
2244
  }
2102
2245
  function determineRole(_activity, sessionInfo) {
2103
2246
  if (sessionInfo.isSubagent)
@@ -2172,8 +2315,6 @@ var LONG_RUNNING_TOOLS = /* @__PURE__ */ new Set([
2172
2315
  "WebSearch",
2173
2316
  // Tools that block waiting for user input
2174
2317
  "AskUserQuestion",
2175
- // Reasoning / extended thinking pseudo-tool (emitted for OpenCode reasoning parts)
2176
- "thinking",
2177
2318
  // Browser/playwright tools that wait for navigation or network
2178
2319
  "mcp__playwright__browser_navigate",
2179
2320
  "mcp__playwright__browser_wait_for",
@@ -2898,7 +3039,10 @@ var AgentStateManager = class extends EventEmitter2 {
2898
3039
  hookSessionEnd(sessionId) {
2899
3040
  const canonicalId = this.resolveAgentId(sessionId);
2900
3041
  if (this.agents.has(canonicalId)) {
3042
+ console.log(`[hookSessionEnd] Shutting down agent: session=${sessionId.slice(0, 12)}\u2026 canonical=${canonicalId.slice(0, 12)}\u2026`);
2901
3043
  this.shutdown(canonicalId);
3044
+ } else {
3045
+ console.log(`[hookSessionEnd] Agent not found: session=${sessionId.slice(0, 12)}\u2026 canonical=${canonicalId.slice(0, 12)}\u2026 | known agents: [${[...this.agents.keys()].map((k) => k.slice(0, 12)).join(", ")}]`);
2902
3046
  }
2903
3047
  }
2904
3048
  /** Hook: user submitted a prompt — agent is now running */
@@ -3104,27 +3248,12 @@ var Broadcaster = class {
3104
3248
  const fullState = {
3105
3249
  type: "full_state",
3106
3250
  agents: this.stateManager.getAll(),
3251
+ timeline: this.stateManager.getTimeline(),
3252
+ toolchain: this.stateManager.getToolChainSnapshot(),
3253
+ taskgraph: this.stateManager.getTaskGraphSnapshot(),
3107
3254
  timestamp: Date.now()
3108
3255
  };
3109
3256
  ws.send(JSON.stringify(fullState));
3110
- const timeline = {
3111
- type: "timeline:snapshot",
3112
- events: this.stateManager.getTimeline(),
3113
- timestamp: Date.now()
3114
- };
3115
- ws.send(JSON.stringify(timeline));
3116
- const toolchain = {
3117
- type: "toolchain:snapshot",
3118
- data: this.stateManager.getToolChainSnapshot(),
3119
- timestamp: Date.now()
3120
- };
3121
- ws.send(JSON.stringify(toolchain));
3122
- const taskgraph = {
3123
- type: "taskgraph:snapshot",
3124
- data: this.stateManager.getTaskGraphSnapshot(),
3125
- timestamp: Date.now()
3126
- };
3127
- ws.send(JSON.stringify(taskgraph));
3128
3257
  } catch {
3129
3258
  this.clients.delete(ws);
3130
3259
  }
@@ -3238,6 +3367,797 @@ function registerApiRoutes(app, stateManager) {
3238
3367
  return { removed: req.params.id };
3239
3368
  });
3240
3369
  }
3370
+ function registerSessionRoutes(app, recorder, stateManager) {
3371
+ const store = recorder.getStore();
3372
+ app.get("/api/sessions/live", async () => {
3373
+ return { sessions: store.listLiveSessions() };
3374
+ });
3375
+ app.get("/api/sessions", async (req) => {
3376
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : 50;
3377
+ const offset = req.query.offset ? parseInt(req.query.offset, 10) : 0;
3378
+ const sessions = store.listSessions({
3379
+ project: req.query.project,
3380
+ limit,
3381
+ offset
3382
+ });
3383
+ const total = store.getSessionCount(req.query.project);
3384
+ return { sessions, total, limit, offset };
3385
+ });
3386
+ app.get("/api/sessions/compare", async (req, reply) => {
3387
+ const { a, b } = req.query;
3388
+ if (!a || !b)
3389
+ return reply.status(400).send({ error: "Both ?a and ?b session IDs required" });
3390
+ const sessionA = store.getSession(a);
3391
+ const sessionB = store.getSession(b);
3392
+ if (!sessionA)
3393
+ return reply.status(404).send({ error: `Session ${a} not found` });
3394
+ if (!sessionB)
3395
+ return reply.status(404).send({ error: `Session ${b} not found` });
3396
+ const timelineA = store.getTimeline(a);
3397
+ const timelineB = store.getTimeline(b);
3398
+ return {
3399
+ sessionA: { ...sessionA, timeline: timelineA },
3400
+ sessionB: { ...sessionB, timeline: timelineB }
3401
+ };
3402
+ });
3403
+ app.get("/api/sessions/:id", async (req, reply) => {
3404
+ const session = store.getSession(req.params.id);
3405
+ if (!session)
3406
+ return reply.status(404).send({ error: "Session not found" });
3407
+ return session;
3408
+ });
3409
+ app.get("/api/sessions/:id/timeline", async (req, reply) => {
3410
+ const session = store.getSession(req.params.id);
3411
+ if (!session)
3412
+ return reply.status(404).send({ error: "Session not found" });
3413
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : 1e4;
3414
+ const offset = req.query.offset ? parseInt(req.query.offset, 10) : 0;
3415
+ const timeline = store.getTimeline(req.params.id, { limit, offset });
3416
+ return { timeline };
3417
+ });
3418
+ app.patch("/api/sessions/:id", async (req, reply) => {
3419
+ const body = req.body;
3420
+ let updated = false;
3421
+ if (body.label !== void 0) {
3422
+ updated = store.updateLabel(req.params.id, body.label ?? null) || updated;
3423
+ }
3424
+ if (body.tags) {
3425
+ updated = store.updateTags(req.params.id, body.tags) || updated;
3426
+ }
3427
+ if (!updated)
3428
+ return reply.status(404).send({ error: "Session not found" });
3429
+ return { ok: true };
3430
+ });
3431
+ app.delete("/api/sessions/:id", async (req, reply) => {
3432
+ const deleted = store.deleteSession(req.params.id);
3433
+ if (!deleted)
3434
+ return reply.status(404).send({ error: "Session not found" });
3435
+ return { ok: true };
3436
+ });
3437
+ app.post("/api/sessions/record-current", async (req, reply) => {
3438
+ const body = req.body;
3439
+ let rootId = body?.rootSessionId;
3440
+ if (!rootId) {
3441
+ const agents = stateManager.getAll();
3442
+ if (agents.length === 0) {
3443
+ return reply.status(400).send({ error: "No active agents" });
3444
+ }
3445
+ rootId = agents[0].rootSessionId;
3446
+ }
3447
+ const sessionId = recorder.recordCurrentSession(rootId);
3448
+ if (!sessionId) {
3449
+ return reply.status(400).send({ error: "No session data to record" });
3450
+ }
3451
+ return { sessionId };
3452
+ });
3453
+ }
3454
+ var DB_DIR = join10(homedir5(), ".agent-move");
3455
+ var DB_PATH = join10(DB_DIR, "sessions.db");
3456
+ var SCHEMA_VERSION = 2;
3457
+ var SessionStore = class {
3458
+ db;
3459
+ constructor(dbPath) {
3460
+ mkdirSync(DB_DIR, { recursive: true });
3461
+ this.db = new Database2(dbPath ?? DB_PATH);
3462
+ this.db.pragma("journal_mode = WAL");
3463
+ this.db.pragma("busy_timeout = 5000");
3464
+ this.initSchema();
3465
+ }
3466
+ initSchema() {
3467
+ this.db.exec(`CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)`);
3468
+ const row = this.db.prepare("SELECT version FROM schema_version").get();
3469
+ const currentVersion = row?.version ?? 0;
3470
+ if (currentVersion < SCHEMA_VERSION) {
3471
+ this.db.exec(`
3472
+ CREATE TABLE IF NOT EXISTS sessions (
3473
+ id TEXT PRIMARY KEY,
3474
+ source TEXT NOT NULL,
3475
+ root_session_id TEXT NOT NULL,
3476
+ project_name TEXT NOT NULL,
3477
+ project_path TEXT NOT NULL,
3478
+ started_at INTEGER NOT NULL,
3479
+ ended_at INTEGER NOT NULL,
3480
+ duration_ms INTEGER NOT NULL,
3481
+ total_cost REAL NOT NULL DEFAULT 0,
3482
+ total_input_tokens INTEGER NOT NULL DEFAULT 0,
3483
+ total_output_tokens INTEGER NOT NULL DEFAULT 0,
3484
+ total_cache_read_tokens INTEGER NOT NULL DEFAULT 0,
3485
+ total_cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
3486
+ total_tool_uses INTEGER NOT NULL DEFAULT 0,
3487
+ agent_count INTEGER NOT NULL DEFAULT 0,
3488
+ model TEXT,
3489
+ label TEXT,
3490
+ tags TEXT NOT NULL DEFAULT '[]',
3491
+ agents_json TEXT NOT NULL DEFAULT '[]',
3492
+ tool_chain_json TEXT NOT NULL DEFAULT '{}'
3493
+ );
3494
+
3495
+ CREATE TABLE IF NOT EXISTS timeline_events (
3496
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3497
+ session_id TEXT NOT NULL,
3498
+ timestamp INTEGER NOT NULL,
3499
+ agent_id TEXT NOT NULL,
3500
+ kind TEXT NOT NULL,
3501
+ zone TEXT,
3502
+ tool TEXT,
3503
+ tool_args TEXT,
3504
+ text_content TEXT,
3505
+ input_tokens INTEGER,
3506
+ output_tokens INTEGER
3507
+ );
3508
+
3509
+ -- Live (in-progress) sessions: written incrementally so data survives crashes
3510
+ CREATE TABLE IF NOT EXISTS live_sessions (
3511
+ root_session_id TEXT PRIMARY KEY,
3512
+ session_id TEXT NOT NULL,
3513
+ source TEXT NOT NULL DEFAULT 'claude',
3514
+ project_name TEXT NOT NULL,
3515
+ project_path TEXT NOT NULL,
3516
+ started_at INTEGER NOT NULL,
3517
+ last_activity_at INTEGER NOT NULL,
3518
+ agents_json TEXT NOT NULL DEFAULT '[]'
3519
+ );
3520
+
3521
+ CREATE TABLE IF NOT EXISTS live_timeline_events (
3522
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3523
+ root_session_id TEXT NOT NULL,
3524
+ timestamp INTEGER NOT NULL,
3525
+ agent_id TEXT NOT NULL,
3526
+ kind TEXT NOT NULL,
3527
+ zone TEXT,
3528
+ tool TEXT,
3529
+ tool_args TEXT,
3530
+ text_content TEXT,
3531
+ input_tokens INTEGER,
3532
+ output_tokens INTEGER
3533
+ );
3534
+
3535
+ CREATE INDEX IF NOT EXISTS idx_timeline_session ON timeline_events(session_id);
3536
+ CREATE INDEX IF NOT EXISTS idx_timeline_timestamp ON timeline_events(session_id, timestamp);
3537
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_name);
3538
+ CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
3539
+ CREATE INDEX IF NOT EXISTS idx_live_timeline_root ON live_timeline_events(root_session_id);
3540
+ `);
3541
+ if (currentVersion === 0) {
3542
+ this.db.prepare("INSERT INTO schema_version (version) VALUES (?)").run(SCHEMA_VERSION);
3543
+ } else {
3544
+ this.db.prepare("UPDATE schema_version SET version = ?").run(SCHEMA_VERSION);
3545
+ }
3546
+ }
3547
+ }
3548
+ /** Save a complete recorded session with its timeline */
3549
+ saveSession(session, timeline) {
3550
+ const insertSession = this.db.prepare(`
3551
+ INSERT OR REPLACE INTO sessions (
3552
+ id, source, root_session_id, project_name, project_path,
3553
+ started_at, ended_at, duration_ms,
3554
+ total_cost, total_input_tokens, total_output_tokens,
3555
+ total_cache_read_tokens, total_cache_creation_tokens,
3556
+ total_tool_uses, agent_count, model, label, tags,
3557
+ agents_json, tool_chain_json
3558
+ ) VALUES (
3559
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
3560
+ )
3561
+ `);
3562
+ const insertEvent = this.db.prepare(`
3563
+ INSERT INTO timeline_events (
3564
+ session_id, timestamp, agent_id, kind, zone, tool, tool_args,
3565
+ text_content, input_tokens, output_tokens
3566
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3567
+ `);
3568
+ const txn = this.db.transaction(() => {
3569
+ this.db.prepare("DELETE FROM timeline_events WHERE session_id = ?").run(session.id);
3570
+ insertSession.run(session.id, session.source, session.rootSessionId, session.projectName, session.projectPath, session.startedAt, session.endedAt, session.durationMs, session.totalCost, session.totalInputTokens, session.totalOutputTokens, session.totalCacheReadTokens, session.totalCacheCreationTokens, session.totalToolUses, session.agentCount, session.model, session.label, JSON.stringify(session.tags), JSON.stringify(session.agents), JSON.stringify(session.toolChain));
3571
+ for (const evt of timeline) {
3572
+ insertEvent.run(session.id, evt.timestamp, evt.agentId, evt.kind, evt.zone ?? null, evt.tool ?? null, evt.toolArgs ?? null, evt.text ?? null, evt.inputTokens ?? null, evt.outputTokens ?? null);
3573
+ }
3574
+ });
3575
+ txn();
3576
+ }
3577
+ /** List sessions with optional filtering */
3578
+ listSessions(opts) {
3579
+ const limit = opts?.limit ?? 50;
3580
+ const offset = opts?.offset ?? 0;
3581
+ let sql = `SELECT id, source, project_name, started_at, ended_at, duration_ms,
3582
+ total_cost, total_tool_uses, agent_count, model, label, tags
3583
+ FROM sessions`;
3584
+ const params = [];
3585
+ if (opts?.project) {
3586
+ sql += " WHERE project_name = ?";
3587
+ params.push(opts.project);
3588
+ }
3589
+ sql += " ORDER BY started_at DESC LIMIT ? OFFSET ?";
3590
+ params.push(limit, offset);
3591
+ const rows = this.db.prepare(sql).all(...params);
3592
+ return rows.map((r) => ({
3593
+ id: r.id,
3594
+ source: r.source,
3595
+ projectName: r.project_name,
3596
+ startedAt: r.started_at,
3597
+ endedAt: r.ended_at,
3598
+ durationMs: r.duration_ms,
3599
+ totalCost: r.total_cost,
3600
+ totalToolUses: r.total_tool_uses,
3601
+ agentCount: r.agent_count,
3602
+ model: r.model,
3603
+ label: r.label,
3604
+ tags: JSON.parse(r.tags)
3605
+ }));
3606
+ }
3607
+ /** Get a full session by ID (without timeline) */
3608
+ getSession(id) {
3609
+ const row = this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
3610
+ if (!row)
3611
+ return null;
3612
+ return {
3613
+ id: row.id,
3614
+ source: row.source,
3615
+ rootSessionId: row.root_session_id,
3616
+ projectName: row.project_name,
3617
+ projectPath: row.project_path,
3618
+ startedAt: row.started_at,
3619
+ endedAt: row.ended_at,
3620
+ durationMs: row.duration_ms,
3621
+ totalCost: row.total_cost,
3622
+ totalInputTokens: row.total_input_tokens,
3623
+ totalOutputTokens: row.total_output_tokens,
3624
+ totalCacheReadTokens: row.total_cache_read_tokens,
3625
+ totalCacheCreationTokens: row.total_cache_creation_tokens,
3626
+ totalToolUses: row.total_tool_uses,
3627
+ agentCount: row.agent_count,
3628
+ model: row.model,
3629
+ label: row.label,
3630
+ tags: JSON.parse(row.tags),
3631
+ agents: JSON.parse(row.agents_json),
3632
+ toolChain: JSON.parse(row.tool_chain_json)
3633
+ };
3634
+ }
3635
+ /** Get timeline events for a session */
3636
+ getTimeline(sessionId, opts) {
3637
+ const limit = opts?.limit ?? 1e4;
3638
+ const offset = opts?.offset ?? 0;
3639
+ const rows = this.db.prepare(`
3640
+ SELECT timestamp, agent_id, kind, zone, tool, tool_args, text_content,
3641
+ input_tokens, output_tokens
3642
+ FROM timeline_events
3643
+ WHERE session_id = ?
3644
+ ORDER BY timestamp ASC
3645
+ LIMIT ? OFFSET ?
3646
+ `).all(sessionId, limit, offset);
3647
+ return rows.map((r) => ({
3648
+ timestamp: r.timestamp,
3649
+ agentId: r.agent_id,
3650
+ kind: r.kind,
3651
+ ...r.zone && { zone: r.zone },
3652
+ ...r.tool && { tool: r.tool },
3653
+ ...r.tool_args && { toolArgs: r.tool_args },
3654
+ ...r.text_content && { text: r.text_content },
3655
+ ...r.input_tokens != null && { inputTokens: r.input_tokens },
3656
+ ...r.output_tokens != null && { outputTokens: r.output_tokens }
3657
+ }));
3658
+ }
3659
+ /** Delete a session and its timeline */
3660
+ deleteSession(id) {
3661
+ let changed = false;
3662
+ this.db.transaction(() => {
3663
+ this.db.prepare("DELETE FROM timeline_events WHERE session_id = ?").run(id);
3664
+ const result = this.db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
3665
+ changed = result.changes > 0;
3666
+ })();
3667
+ return changed;
3668
+ }
3669
+ /** Update session label */
3670
+ updateLabel(id, label) {
3671
+ const result = this.db.prepare("UPDATE sessions SET label = ? WHERE id = ?").run(label, id);
3672
+ return result.changes > 0;
3673
+ }
3674
+ /** Update session tags */
3675
+ updateTags(id, tags) {
3676
+ const result = this.db.prepare("UPDATE sessions SET tags = ? WHERE id = ?").run(JSON.stringify(tags), id);
3677
+ return result.changes > 0;
3678
+ }
3679
+ /** Get total session count (for pagination) */
3680
+ getSessionCount(project) {
3681
+ if (project) {
3682
+ const row2 = this.db.prepare("SELECT COUNT(*) as count FROM sessions WHERE project_name = ?").get(project);
3683
+ return row2.count;
3684
+ }
3685
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM sessions").get();
3686
+ return row.count;
3687
+ }
3688
+ // ── Live session methods (incremental writes for crash safety) ──
3689
+ /** Create or update a live session entry */
3690
+ upsertLiveSession(rootSessionId, data) {
3691
+ this.db.prepare(`
3692
+ INSERT INTO live_sessions (root_session_id, session_id, source, project_name, project_path, started_at, last_activity_at)
3693
+ VALUES (?, ?, ?, ?, ?, ?, ?)
3694
+ ON CONFLICT(root_session_id) DO UPDATE SET
3695
+ session_id = excluded.session_id,
3696
+ source = excluded.source,
3697
+ project_name = excluded.project_name,
3698
+ project_path = excluded.project_path,
3699
+ last_activity_at = excluded.last_activity_at
3700
+ `).run(rootSessionId, data.sessionId, data.source, data.projectName, data.projectPath, data.startedAt, Date.now());
3701
+ }
3702
+ /** Update the agent snapshot for a live session */
3703
+ updateLiveAgents(rootSessionId, agentsJson) {
3704
+ this.db.prepare(`
3705
+ UPDATE live_sessions SET agents_json = ?, last_activity_at = ? WHERE root_session_id = ?
3706
+ `).run(agentsJson, Date.now(), rootSessionId);
3707
+ }
3708
+ /** Append a timeline event to the live buffer */
3709
+ appendLiveTimelineEvent(rootSessionId, evt) {
3710
+ this.db.prepare(`
3711
+ INSERT INTO live_timeline_events (root_session_id, timestamp, agent_id, kind, zone, tool, tool_args, text_content, input_tokens, output_tokens)
3712
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3713
+ `).run(rootSessionId, evt.timestamp, evt.agentId, evt.kind, evt.zone ?? null, evt.tool ?? null, evt.toolArgs ?? null, evt.text ?? null, evt.inputTokens ?? null, evt.outputTokens ?? null);
3714
+ }
3715
+ /** Get all live timeline events for a root session */
3716
+ getLiveTimeline(rootSessionId) {
3717
+ const rows = this.db.prepare(`
3718
+ SELECT timestamp, agent_id, kind, zone, tool, tool_args, text_content, input_tokens, output_tokens
3719
+ FROM live_timeline_events WHERE root_session_id = ? ORDER BY timestamp ASC
3720
+ `).all(rootSessionId);
3721
+ return rows.map((r) => ({
3722
+ timestamp: r.timestamp,
3723
+ agentId: r.agent_id,
3724
+ kind: r.kind,
3725
+ ...r.zone && { zone: r.zone },
3726
+ ...r.tool && { tool: r.tool },
3727
+ ...r.tool_args && { toolArgs: r.tool_args },
3728
+ ...r.text_content && { text: r.text_content },
3729
+ ...r.input_tokens != null && { inputTokens: r.input_tokens },
3730
+ ...r.output_tokens != null && { outputTokens: r.output_tokens }
3731
+ }));
3732
+ }
3733
+ /** Remove live session data after finalization (atomic to prevent orphans on crash) */
3734
+ removeLiveSession(rootSessionId) {
3735
+ this.db.transaction(() => {
3736
+ this.db.prepare("DELETE FROM live_timeline_events WHERE root_session_id = ?").run(rootSessionId);
3737
+ this.db.prepare("DELETE FROM live_sessions WHERE root_session_id = ?").run(rootSessionId);
3738
+ })();
3739
+ }
3740
+ /** List currently active (in-progress) live sessions */
3741
+ listLiveSessions() {
3742
+ const rows = this.db.prepare("SELECT root_session_id, source, project_name, started_at, last_activity_at, agents_json FROM live_sessions ORDER BY started_at DESC").all();
3743
+ return rows.map((r) => {
3744
+ let agentCount = 0;
3745
+ try {
3746
+ agentCount = JSON.parse(r.agents_json).length;
3747
+ } catch {
3748
+ }
3749
+ return {
3750
+ rootSessionId: r.root_session_id,
3751
+ source: r.source,
3752
+ projectName: r.project_name,
3753
+ startedAt: r.started_at,
3754
+ lastActivityAt: r.last_activity_at,
3755
+ agentCount
3756
+ };
3757
+ });
3758
+ }
3759
+ /** Get all orphaned live sessions (for crash recovery on startup) */
3760
+ getOrphanedLiveSessions() {
3761
+ return this.db.prepare("SELECT * FROM live_sessions").all().map((r) => ({
3762
+ rootSessionId: r.root_session_id,
3763
+ sessionId: r.session_id,
3764
+ source: r.source,
3765
+ projectName: r.project_name,
3766
+ projectPath: r.project_path,
3767
+ startedAt: r.started_at,
3768
+ lastActivityAt: r.last_activity_at,
3769
+ agentsJson: r.agents_json
3770
+ }));
3771
+ }
3772
+ close() {
3773
+ this.db.close();
3774
+ }
3775
+ };
3776
+ var FINALIZE_DELAY_MS = 3e3;
3777
+ var AGENT_FLUSH_INTERVAL_MS = 3e4;
3778
+ var SessionRecorder = class {
3779
+ store;
3780
+ staging = /* @__PURE__ */ new Map();
3781
+ stateManager;
3782
+ flushTimer;
3783
+ constructor(stateManager) {
3784
+ this.stateManager = stateManager;
3785
+ this.store = new SessionStore();
3786
+ this.recoverOrphans();
3787
+ stateManager.on("agent:spawn", (event) => this.onSpawn(event));
3788
+ stateManager.on("agent:update", (event) => this.onUpdate(event));
3789
+ stateManager.on("agent:idle", (event) => this.onIdle(event));
3790
+ stateManager.on("agent:shutdown", (event) => this.onShutdown(event));
3791
+ this.flushTimer = setInterval(() => this.flushAllAgentStates(), AGENT_FLUSH_INTERVAL_MS);
3792
+ }
3793
+ getStore() {
3794
+ return this.store;
3795
+ }
3796
+ /** Record the currently active session (manual trigger) */
3797
+ recordCurrentSession(rootSessionId) {
3798
+ const staging = this.staging.get(rootSessionId);
3799
+ if (!staging)
3800
+ return null;
3801
+ for (const agent of this.stateManager.getAll()) {
3802
+ if (agent.rootSessionId === rootSessionId) {
3803
+ const history = this.stateManager.getHistory(agent.id);
3804
+ staging.agents.set(agent.id, {
3805
+ state: { ...agent },
3806
+ history: [...history],
3807
+ endedAt: Date.now()
3808
+ });
3809
+ }
3810
+ }
3811
+ staging.toolChain = this.stateManager.getToolChainSnapshot();
3812
+ return this.finalize(rootSessionId);
3813
+ }
3814
+ /** Recover orphaned live sessions from DB (previous crash/restart) */
3815
+ recoverOrphans() {
3816
+ const orphans = this.store.getOrphanedLiveSessions();
3817
+ for (const orphan of orphans) {
3818
+ console.log(`Recovering orphaned session: ${orphan.projectName} (root: ${orphan.rootSessionId.slice(0, 12)}...)`);
3819
+ let agents = [];
3820
+ try {
3821
+ agents = JSON.parse(orphan.agentsJson);
3822
+ } catch {
3823
+ }
3824
+ const timeline = this.store.getLiveTimeline(orphan.rootSessionId);
3825
+ const endedAt = orphan.lastActivityAt;
3826
+ let totalInputTokens = 0;
3827
+ let totalOutputTokens = 0;
3828
+ let totalCacheRead = 0;
3829
+ let totalCacheCreation = 0;
3830
+ let totalToolUses = 0;
3831
+ let primaryModel = null;
3832
+ for (const ag of agents) {
3833
+ totalInputTokens += ag.totalInputTokens;
3834
+ totalOutputTokens += ag.totalOutputTokens;
3835
+ totalCacheRead += ag.cacheReadTokens;
3836
+ totalCacheCreation += ag.cacheCreationTokens;
3837
+ totalToolUses += ag.toolUseCount;
3838
+ if (!primaryModel && ag.model)
3839
+ primaryModel = ag.model;
3840
+ }
3841
+ if (agents.length === 0) {
3842
+ this.store.removeLiveSession(orphan.rootSessionId);
3843
+ console.log(`Skipped empty orphaned session: ${orphan.rootSessionId.slice(0, 12)}...`);
3844
+ continue;
3845
+ }
3846
+ const sessionId = randomUUID();
3847
+ const totalCost = agents.reduce((sum, ag) => sum + ag.cost, 0);
3848
+ const recorded = {
3849
+ id: sessionId,
3850
+ source: orphan.source,
3851
+ rootSessionId: orphan.rootSessionId,
3852
+ projectName: orphan.projectName,
3853
+ projectPath: orphan.projectPath,
3854
+ startedAt: orphan.startedAt,
3855
+ endedAt,
3856
+ durationMs: endedAt - orphan.startedAt,
3857
+ totalCost,
3858
+ totalInputTokens,
3859
+ totalOutputTokens,
3860
+ totalCacheReadTokens: totalCacheRead,
3861
+ totalCacheCreationTokens: totalCacheCreation,
3862
+ totalToolUses,
3863
+ agentCount: agents.length,
3864
+ model: primaryModel,
3865
+ agents,
3866
+ toolChain: { transitions: [], tools: [], toolCounts: {}, toolSuccesses: {}, toolFailures: {}, toolAvgDuration: {} },
3867
+ label: "(recovered)",
3868
+ tags: []
3869
+ };
3870
+ try {
3871
+ this.store.saveSession(recorded, timeline);
3872
+ console.log(`Recovered session: ${sessionId} (${orphan.projectName}, ${timeline.length} events)`);
3873
+ } catch (err) {
3874
+ console.error("Failed to recover session:", err);
3875
+ }
3876
+ this.store.removeLiveSession(orphan.rootSessionId);
3877
+ }
3878
+ }
3879
+ getOrCreateStaging(agent) {
3880
+ const rootId = agent.rootSessionId;
3881
+ let session = this.staging.get(rootId);
3882
+ if (!session) {
3883
+ const sessionId = randomUUID();
3884
+ const source = rootId.startsWith("oc:") ? "opencode" : "claude";
3885
+ session = {
3886
+ rootSessionId: rootId,
3887
+ sessionId,
3888
+ projectName: agent.projectName,
3889
+ projectPath: agent.projectPath,
3890
+ source,
3891
+ startedAt: agent.spawnedAt,
3892
+ agents: /* @__PURE__ */ new Map(),
3893
+ toolChain: null,
3894
+ finalizeTimer: null
3895
+ };
3896
+ this.staging.set(rootId, session);
3897
+ this.store.upsertLiveSession(rootId, {
3898
+ sessionId,
3899
+ source,
3900
+ projectName: agent.projectName,
3901
+ projectPath: agent.projectPath,
3902
+ startedAt: agent.spawnedAt
3903
+ });
3904
+ }
3905
+ return session;
3906
+ }
3907
+ onSpawn(event) {
3908
+ const agent = event.agent;
3909
+ const session = this.getOrCreateStaging(agent);
3910
+ if (session.finalizeTimer) {
3911
+ clearTimeout(session.finalizeTimer);
3912
+ session.finalizeTimer = null;
3913
+ }
3914
+ const timelineEvent = {
3915
+ timestamp: event.timestamp,
3916
+ agentId: agent.id,
3917
+ kind: "spawn",
3918
+ zone: agent.currentZone
3919
+ };
3920
+ this.store.appendLiveTimelineEvent(session.rootSessionId, timelineEvent);
3921
+ }
3922
+ onUpdate(event) {
3923
+ const agent = event.agent;
3924
+ const session = this.staging.get(agent.rootSessionId);
3925
+ if (!session)
3926
+ return;
3927
+ if (agent.currentTool) {
3928
+ const timelineEvent = {
3929
+ timestamp: event.timestamp,
3930
+ agentId: agent.id,
3931
+ kind: "tool",
3932
+ zone: agent.currentZone,
3933
+ tool: agent.currentTool,
3934
+ toolArgs: agent.currentActivity ?? void 0
3935
+ };
3936
+ this.store.appendLiveTimelineEvent(session.rootSessionId, timelineEvent);
3937
+ }
3938
+ }
3939
+ onIdle(event) {
3940
+ const agent = event.agent;
3941
+ const session = this.staging.get(agent.rootSessionId);
3942
+ if (!session)
3943
+ return;
3944
+ this.store.appendLiveTimelineEvent(session.rootSessionId, {
3945
+ timestamp: event.timestamp,
3946
+ agentId: agent.id,
3947
+ kind: "idle",
3948
+ zone: "idle"
3949
+ });
3950
+ }
3951
+ onShutdown(event) {
3952
+ const agent = event.agent;
3953
+ const rootId = agent.rootSessionId;
3954
+ const session = this.staging.get(rootId);
3955
+ if (!session)
3956
+ return;
3957
+ const history = this.stateManager.getHistory(agent.id);
3958
+ session.agents.set(agent.id, {
3959
+ state: { ...agent },
3960
+ history: [...history],
3961
+ endedAt: event.timestamp
3962
+ });
3963
+ session.toolChain = this.stateManager.getToolChainSnapshot();
3964
+ this.store.appendLiveTimelineEvent(rootId, {
3965
+ timestamp: event.timestamp,
3966
+ agentId: agent.id,
3967
+ kind: "shutdown"
3968
+ });
3969
+ this.flushAgentState(session);
3970
+ const activeAgents = this.stateManager.getAll().filter((a) => a.rootSessionId === rootId && a.id !== agent.id);
3971
+ if (activeAgents.length === 0) {
3972
+ if (session.finalizeTimer)
3973
+ clearTimeout(session.finalizeTimer);
3974
+ session.finalizeTimer = setTimeout(() => {
3975
+ this.finalize(rootId);
3976
+ }, FINALIZE_DELAY_MS);
3977
+ }
3978
+ }
3979
+ /** Flush agent state snapshots to DB for crash safety */
3980
+ flushAgentState(session) {
3981
+ const agents = [];
3982
+ for (const [, staging] of session.agents) {
3983
+ const s = staging.state;
3984
+ agents.push({
3985
+ agentId: s.id,
3986
+ agentName: s.agentName,
3987
+ role: s.role,
3988
+ model: s.model,
3989
+ spawnedAt: s.spawnedAt,
3990
+ endedAt: staging.endedAt,
3991
+ totalInputTokens: s.totalInputTokens,
3992
+ totalOutputTokens: s.totalOutputTokens,
3993
+ cacheReadTokens: s.cacheReadTokens,
3994
+ cacheCreationTokens: s.cacheCreationTokens,
3995
+ toolUseCount: s.toolUseCount,
3996
+ cost: computeAgentCost(s)
3997
+ });
3998
+ }
3999
+ for (const agent of this.stateManager.getAll()) {
4000
+ if (agent.rootSessionId === session.rootSessionId && !session.agents.has(agent.id)) {
4001
+ agents.push({
4002
+ agentId: agent.id,
4003
+ agentName: agent.agentName,
4004
+ role: agent.role,
4005
+ model: agent.model,
4006
+ spawnedAt: agent.spawnedAt,
4007
+ endedAt: Date.now(),
4008
+ totalInputTokens: agent.totalInputTokens,
4009
+ totalOutputTokens: agent.totalOutputTokens,
4010
+ cacheReadTokens: agent.cacheReadTokens,
4011
+ cacheCreationTokens: agent.cacheCreationTokens,
4012
+ toolUseCount: agent.toolUseCount,
4013
+ cost: computeAgentCost(agent)
4014
+ });
4015
+ }
4016
+ }
4017
+ this.store.updateLiveAgents(session.rootSessionId, JSON.stringify(agents));
4018
+ }
4019
+ /** Periodically flush all active sessions' agent states */
4020
+ flushAllAgentStates() {
4021
+ for (const session of this.staging.values()) {
4022
+ this.flushAgentState(session);
4023
+ }
4024
+ }
4025
+ /** Finalize and persist a staged session. Returns the session ID. */
4026
+ finalize(rootSessionId) {
4027
+ const session = this.staging.get(rootSessionId);
4028
+ if (!session) {
4029
+ this.staging.delete(rootSessionId);
4030
+ return null;
4031
+ }
4032
+ if (session.finalizeTimer) {
4033
+ clearTimeout(session.finalizeTimer);
4034
+ session.finalizeTimer = null;
4035
+ }
4036
+ const sessionId = session.sessionId;
4037
+ const now = Date.now();
4038
+ const agents = [];
4039
+ let totalInputTokens = 0;
4040
+ let totalOutputTokens = 0;
4041
+ let totalCacheRead = 0;
4042
+ let totalCacheCreation = 0;
4043
+ let totalToolUses = 0;
4044
+ let primaryModel = null;
4045
+ let earliestSpawn = Infinity;
4046
+ let latestEnd = 0;
4047
+ for (const [, staging] of session.agents) {
4048
+ const s = staging.state;
4049
+ const cost = computeAgentCost(s);
4050
+ agents.push({
4051
+ agentId: s.id,
4052
+ agentName: s.agentName,
4053
+ role: s.role,
4054
+ model: s.model,
4055
+ spawnedAt: s.spawnedAt,
4056
+ endedAt: staging.endedAt,
4057
+ totalInputTokens: s.totalInputTokens,
4058
+ totalOutputTokens: s.totalOutputTokens,
4059
+ cacheReadTokens: s.cacheReadTokens,
4060
+ cacheCreationTokens: s.cacheCreationTokens,
4061
+ toolUseCount: s.toolUseCount,
4062
+ cost
4063
+ });
4064
+ totalInputTokens += s.totalInputTokens;
4065
+ totalOutputTokens += s.totalOutputTokens;
4066
+ totalCacheRead += s.cacheReadTokens;
4067
+ totalCacheCreation += s.cacheCreationTokens;
4068
+ totalToolUses += s.toolUseCount;
4069
+ if (s.spawnedAt < earliestSpawn)
4070
+ earliestSpawn = s.spawnedAt;
4071
+ if (staging.endedAt > latestEnd)
4072
+ latestEnd = staging.endedAt;
4073
+ if (s.role === "main" && s.model)
4074
+ primaryModel = s.model;
4075
+ if (!primaryModel && s.model)
4076
+ primaryModel = s.model;
4077
+ }
4078
+ if (agents.length === 0) {
4079
+ this.store.removeLiveSession(rootSessionId);
4080
+ this.staging.delete(rootSessionId);
4081
+ return null;
4082
+ }
4083
+ const endedAt = latestEnd || now;
4084
+ const startedAt = earliestSpawn === Infinity ? session.startedAt : earliestSpawn;
4085
+ const recorded = {
4086
+ id: sessionId,
4087
+ source: session.source,
4088
+ rootSessionId: session.rootSessionId,
4089
+ projectName: session.projectName,
4090
+ projectPath: session.projectPath,
4091
+ startedAt,
4092
+ endedAt,
4093
+ durationMs: endedAt - startedAt,
4094
+ // Sum per-agent costs (each agent already computed with its own model's pricing)
4095
+ totalCost: agents.reduce((sum, ag) => sum + ag.cost, 0),
4096
+ totalInputTokens,
4097
+ totalOutputTokens,
4098
+ totalCacheReadTokens: totalCacheRead,
4099
+ totalCacheCreationTokens: totalCacheCreation,
4100
+ totalToolUses,
4101
+ agentCount: agents.length,
4102
+ model: primaryModel,
4103
+ agents,
4104
+ toolChain: session.toolChain ?? {
4105
+ transitions: [],
4106
+ tools: [],
4107
+ toolCounts: {},
4108
+ toolSuccesses: {},
4109
+ toolFailures: {},
4110
+ toolAvgDuration: {}
4111
+ },
4112
+ label: null,
4113
+ tags: []
4114
+ };
4115
+ const liveTimeline = this.store.getLiveTimeline(rootSessionId);
4116
+ const historyTimeline = this.buildTimelineFromHistory(session);
4117
+ const timeline = historyTimeline.length > 0 ? historyTimeline : liveTimeline;
4118
+ try {
4119
+ this.store.saveSession(recorded, timeline);
4120
+ this.store.removeLiveSession(rootSessionId);
4121
+ console.log(`Session recorded: ${sessionId} (${session.projectName}, ${agents.length} agents, ${totalToolUses} tools, $${recorded.totalCost.toFixed(4)})`);
4122
+ } catch (err) {
4123
+ console.error("Failed to save session:", err);
4124
+ return null;
4125
+ }
4126
+ this.staging.delete(rootSessionId);
4127
+ return sessionId;
4128
+ }
4129
+ /** Build a merged timeline from all agents' activity histories */
4130
+ buildTimelineFromHistory(session) {
4131
+ const events = [];
4132
+ for (const [agentId, staging] of session.agents) {
4133
+ for (const entry of staging.history) {
4134
+ events.push({
4135
+ timestamp: entry.timestamp,
4136
+ agentId,
4137
+ kind: entry.kind,
4138
+ ...entry.zone && { zone: entry.zone },
4139
+ ...entry.tool && { tool: entry.tool },
4140
+ ...entry.toolArgs && { toolArgs: entry.toolArgs },
4141
+ ...entry.text && { text: entry.text },
4142
+ ...entry.inputTokens != null && { inputTokens: entry.inputTokens },
4143
+ ...entry.outputTokens != null && { outputTokens: entry.outputTokens }
4144
+ });
4145
+ }
4146
+ }
4147
+ events.sort((a, b) => a.timestamp - b.timestamp);
4148
+ return events;
4149
+ }
4150
+ dispose() {
4151
+ clearInterval(this.flushTimer);
4152
+ for (const session of this.staging.values()) {
4153
+ if (session.finalizeTimer)
4154
+ clearTimeout(session.finalizeTimer);
4155
+ this.flushAgentState(session);
4156
+ }
4157
+ this.staging.clear();
4158
+ this.store.close();
4159
+ }
4160
+ };
3241
4161
  var PERMISSION_TIMEOUT_MS = 5 * 60 * 1e3;
3242
4162
  var HookEventManager = class extends EventEmitter3 {
3243
4163
  stateManager;
@@ -3288,7 +4208,7 @@ var HookEventManager = class extends EventEmitter3 {
3288
4208
  // Private
3289
4209
  // ---------------------------------------------------------------------------
3290
4210
  async handlePermissionRequest(event) {
3291
- const permissionId = randomUUID();
4211
+ const permissionId = randomUUID2();
3292
4212
  const permission = {
3293
4213
  permissionId,
3294
4214
  sessionId: event.session_id,
@@ -3398,17 +4318,19 @@ async function main() {
3398
4318
  const app = Fastify({ logger: { level: "info" } });
3399
4319
  await app.register(cors, { origin: true });
3400
4320
  await app.register(websocket);
3401
- const clientDist = join10(__dirname, "..", "..", "client", "dist");
4321
+ const clientDist = join11(__dirname, "..", "..", "client", "dist");
3402
4322
  await app.register(fastifyStatic, {
3403
4323
  root: clientDist,
3404
4324
  prefix: "/",
3405
4325
  wildcard: false
3406
4326
  });
3407
4327
  const stateManager = new AgentStateManager();
4328
+ const sessionRecorder = new SessionRecorder(stateManager);
3408
4329
  const hookManager = new HookEventManager(stateManager);
3409
4330
  const broadcaster = new Broadcaster(stateManager, hookManager);
3410
4331
  registerWsHandler(app, stateManager, broadcaster, hookManager);
3411
4332
  registerApiRoutes(app, stateManager);
4333
+ registerSessionRoutes(app, sessionRecorder, stateManager);
3412
4334
  app.post("/hook", {
3413
4335
  config: { rawBody: false }
3414
4336
  }, async (req, reply) => {
@@ -3456,6 +4378,7 @@ async function main() {
3456
4378
  console.log("Shutting down...");
3457
4379
  for (const w of watchers)
3458
4380
  w.stop();
4381
+ sessionRecorder.dispose();
3459
4382
  hookManager.dispose();
3460
4383
  broadcaster.dispose();
3461
4384
  stateManager.dispose();