@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.
- package/package.json +1 -1
- package/packages/client/dist/assets/{BufferResource-Dl7AyA-b.js → BufferResource-Dhljy8H8.js} +1 -1
- package/packages/client/dist/assets/{CanvasRenderer-CoGUh0dQ.js → CanvasRenderer-Bpr11iOT.js} +1 -1
- package/packages/client/dist/assets/{Filter-DNvhyFhm.js → Filter-DL2yN3-o.js} +1 -1
- package/packages/client/dist/assets/{RenderTargetSystem-C8Ap8tmK.js → RenderTargetSystem-BTwylEdr.js} +1 -1
- package/packages/client/dist/assets/{WebGLRenderer-DGZDe07g.js → WebGLRenderer-wH1P7d1x.js} +1 -1
- package/packages/client/dist/assets/{WebGPURenderer-D3zU-ngm.js → WebGPURenderer-C7n8jUXC.js} +1 -1
- package/packages/client/dist/assets/{browserAll-Dw8CUBpx.js → browserAll-CgAMpWnT.js} +1 -1
- package/packages/client/dist/assets/index-DG7HqEmM.js +1338 -0
- package/packages/client/dist/assets/{index-edh-N9Dm.css → index-Nz5TZeB1.css} +1 -1
- package/packages/client/dist/assets/{webworkerAll-XKfx082n.js → webworkerAll-wrP2P1GC.js} +1 -1
- package/packages/client/dist/index.html +7 -2
- package/packages/server/dist/index.d.ts.map +1 -1
- package/packages/server/dist/index.js +954 -31
- package/packages/server/dist/index.js.map +1 -1
- package/packages/server/dist/routes/sessions-api.d.ts +5 -0
- package/packages/server/dist/routes/sessions-api.d.ts.map +1 -0
- package/packages/server/dist/routes/sessions-api.js +88 -0
- package/packages/server/dist/routes/sessions-api.js.map +1 -0
- package/packages/server/dist/state/activity-processor.d.ts.map +1 -1
- package/packages/server/dist/state/activity-processor.js +0 -2
- package/packages/server/dist/state/activity-processor.js.map +1 -1
- package/packages/server/dist/state/agent-state-manager.d.ts.map +1 -1
- package/packages/server/dist/state/agent-state-manager.js +3 -5
- package/packages/server/dist/state/agent-state-manager.js.map +1 -1
- package/packages/server/dist/state/identity-manager.d.ts.map +1 -1
- package/packages/server/dist/state/identity-manager.js +0 -3
- package/packages/server/dist/state/identity-manager.js.map +1 -1
- package/packages/server/dist/storage/session-recorder.d.ts +38 -0
- package/packages/server/dist/storage/session-recorder.d.ts.map +1 -0
- package/packages/server/dist/storage/session-recorder.js +941 -0
- package/packages/server/dist/storage/session-recorder.js.map +1 -0
- package/packages/server/dist/storage/session-store.d.ts +60 -0
- package/packages/server/dist/storage/session-store.d.ts.map +1 -0
- package/packages/server/dist/storage/session-store.js +330 -0
- package/packages/server/dist/storage/session-store.js.map +1 -0
- package/packages/server/dist/watcher/opencode/opencode-watcher.d.ts +15 -0
- package/packages/server/dist/watcher/opencode/opencode-watcher.d.ts.map +1 -1
- package/packages/server/dist/watcher/opencode/opencode-watcher.js +61 -4
- package/packages/server/dist/watcher/opencode/opencode-watcher.js.map +1 -1
- package/packages/server/dist/ws/broadcaster.d.ts.map +1 -1
- package/packages/server/dist/ws/broadcaster.js +3 -18
- package/packages/server/dist/ws/broadcaster.js.map +1 -1
- package/packages/shared/dist/constants/tools.d.ts +4 -0
- package/packages/shared/dist/constants/tools.d.ts.map +1 -1
- package/packages/shared/dist/constants/tools.js +4 -0
- package/packages/shared/dist/constants/tools.js.map +1 -1
- package/packages/shared/dist/index.d.ts +2 -1
- package/packages/shared/dist/index.d.ts.map +1 -1
- package/packages/shared/dist/index.js +1 -1
- package/packages/shared/dist/index.js.map +1 -1
- package/packages/shared/dist/types/session-record.d.ts +87 -0
- package/packages/shared/dist/types/session-record.d.ts.map +1 -0
- package/packages/shared/dist/types/session-record.js +2 -0
- package/packages/shared/dist/types/session-record.js.map +1 -0
- package/packages/shared/dist/types/websocket.d.ts +3 -0
- package/packages/shared/dist/types/websocket.d.ts.map +1 -1
- 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
|
|
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(
|
|
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.
|
|
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(
|
|
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 =
|
|
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 =
|
|
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();
|