chainlesschain 0.45.70 → 0.45.74

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 (89) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/Analytics-B4OM8S8X.css +1 -0
  4. package/src/assets/web-panel/assets/Analytics-sBrYoc3A.js +3 -0
  5. package/src/assets/web-panel/assets/AppLayout-BhJ3YFWt.js +1 -0
  6. package/src/assets/web-panel/assets/AppLayout-Cr2lWhF-.css +1 -0
  7. package/src/assets/web-panel/assets/Backup-D68fenbD.js +1 -0
  8. package/src/assets/web-panel/assets/Backup-fZqtfC1m.css +1 -0
  9. package/src/assets/web-panel/assets/{Chat-DXtvKoM0.js → Chat-DaxTP3x8.js} +1 -1
  10. package/src/assets/web-panel/assets/{Cron-BJ4ODHOy.js → Cron-CNs03iHJ.js} +2 -2
  11. package/src/assets/web-panel/assets/{Dashboard-BZd4wDPQ.js → Dashboard-CjlX4CrX.js} +2 -2
  12. package/src/assets/web-panel/assets/Git-CCMVr3Y8.js +2 -0
  13. package/src/assets/web-panel/assets/Git-DGcuBXST.css +1 -0
  14. package/src/assets/web-panel/assets/{Logs-CSeKZEG_.js → Logs-BY6A0UNG.js} +2 -2
  15. package/src/assets/web-panel/assets/{McpTools-BYQAK11r.js → McpTools-CrBVYlg6.js} +2 -2
  16. package/src/assets/web-panel/assets/{Memory-gkUAPyuZ.js → Memory-CWx3SpUt.js} +2 -2
  17. package/src/assets/web-panel/assets/{Notes-bjNrQgAo.js → Notes-1LcGD49x.js} +2 -2
  18. package/src/assets/web-panel/assets/Organization-DdOOM4ic.css +1 -0
  19. package/src/assets/web-panel/assets/Organization-Dx2DhbkM.js +4 -0
  20. package/src/assets/web-panel/assets/P2P-B16fjqfJ.js +2 -0
  21. package/src/assets/web-panel/assets/P2P-OEzOeMZX.css +1 -0
  22. package/src/assets/web-panel/assets/Permissions-BQbC9FzG.js +4 -0
  23. package/src/assets/web-panel/assets/Permissions-C9WlkGl-.css +1 -0
  24. package/src/assets/web-panel/assets/Projects-CjhZbNYm.js +2 -0
  25. package/src/assets/web-panel/assets/Projects-DxKelI5h.css +1 -0
  26. package/src/assets/web-panel/assets/Providers-BEakqcO5.css +1 -0
  27. package/src/assets/web-panel/assets/Providers-ivOAQtHM.js +2 -0
  28. package/src/assets/web-panel/assets/RssFeed-BlFC20eg.css +1 -0
  29. package/src/assets/web-panel/assets/RssFeed-BrsErdrU.js +3 -0
  30. package/src/assets/web-panel/assets/Security-DnEvJU5h.js +4 -0
  31. package/src/assets/web-panel/assets/Security-Dwxw7rfP.css +1 -0
  32. package/src/assets/web-panel/assets/{Services-CS0oMdxh.js → Services-7jQywNbl.js} +2 -2
  33. package/src/assets/web-panel/assets/Skills-BCvgBkD3.js +1 -0
  34. package/src/assets/web-panel/assets/{Tasks-qULws8pc.js → Tasks-CmJBC1cf.js} +1 -1
  35. package/src/assets/web-panel/assets/Templates-DOY_oZnm.css +1 -0
  36. package/src/assets/web-panel/assets/Templates-RXT8-DNk.js +1 -0
  37. package/src/assets/web-panel/assets/Wallet-3iYASEx_.js +4 -0
  38. package/src/assets/web-panel/assets/Wallet-DnIumafl.css +1 -0
  39. package/src/assets/web-panel/assets/WebAuthn-CNPl2VQR.css +1 -0
  40. package/src/assets/web-panel/assets/WebAuthn-s3Hzd9db.js +5 -0
  41. package/src/assets/web-panel/assets/{antd-CJSBocer.js → antd-gZyc63Qr.js} +114 -114
  42. package/src/assets/web-panel/assets/chat-BmwHBi9M.js +1 -0
  43. package/src/assets/web-panel/assets/index-DrmEk9S3.js +2 -0
  44. package/src/assets/web-panel/assets/{markdown-Bo5cVN4u.js → markdown-Bv7nG63L.js} +1 -1
  45. package/src/assets/web-panel/assets/ws-CU7Gvoom.js +1 -0
  46. package/src/assets/web-panel/index.html +2 -2
  47. package/src/commands/doctor.js +33 -151
  48. package/src/commands/mcp.js +1 -1
  49. package/src/commands/plugin.js +1 -1
  50. package/src/commands/session.js +106 -7
  51. package/src/commands/status.js +39 -69
  52. package/src/gateways/ws/session-protocol.js +1 -1
  53. package/src/gateways/ws/ws-agent-handler.js +484 -0
  54. package/src/gateways/ws/ws-server.js +758 -4
  55. package/src/gateways/ws/ws-session-gateway.js +1432 -1
  56. package/src/harness/mcp-client.js +417 -0
  57. package/src/harness/mock-llm-provider.js +167 -0
  58. package/src/harness/plugin-manager.js +434 -0
  59. package/src/lib/agent-core.js +25 -1902
  60. package/src/lib/hashline.js +208 -0
  61. package/src/lib/jsonl-session-store.js +11 -0
  62. package/src/lib/mcp-client.js +14 -412
  63. package/src/lib/plugin-manager.js +29 -428
  64. package/src/lib/prompt-compressor.js +11 -0
  65. package/src/lib/session-hooks.js +61 -0
  66. package/src/lib/skill-loader.js +4 -0
  67. package/src/lib/skill-mcp.js +190 -0
  68. package/src/lib/workflow-state-reader.js +94 -0
  69. package/src/lib/ws-agent-handler.js +8 -472
  70. package/src/lib/ws-server.js +12 -756
  71. package/src/lib/ws-session-manager.js +8 -1417
  72. package/src/repl/agent-repl.js +27 -3
  73. package/src/runtime/agent-core.js +1760 -0
  74. package/src/runtime/agent-runtime.js +3 -1
  75. package/src/runtime/coding-agent-contract-shared.cjs +496 -0
  76. package/src/runtime/coding-agent-contract.js +49 -229
  77. package/src/runtime/coding-agent-policy.cjs +54 -5
  78. package/src/runtime/diagnostics.js +317 -0
  79. package/src/runtime/index.js +3 -0
  80. package/src/tools/index.js +3 -0
  81. package/src/tools/legacy-agent-tools.js +5 -0
  82. package/src/assets/web-panel/assets/AppLayout-B_tkw3Pn.js +0 -1
  83. package/src/assets/web-panel/assets/AppLayout-CFP4dGIJ.css +0 -1
  84. package/src/assets/web-panel/assets/Providers-Brm-S_hS.css +0 -1
  85. package/src/assets/web-panel/assets/Providers-Dbf57Tbv.js +0 -1
  86. package/src/assets/web-panel/assets/Skills-B2fgruv8.js +0 -1
  87. package/src/assets/web-panel/assets/chat-DnH09sSR.js +0 -1
  88. package/src/assets/web-panel/assets/index-IK-oro0g.js +0 -2
  89. package/src/assets/web-panel/assets/ws-DjelKkD6.js +0 -1
@@ -1,1421 +1,12 @@
1
1
  /**
2
- * WebSocket Session Manager
2
+ * @deprecated canonical implementation lives in
3
+ * `../gateways/ws/ws-session-gateway.js` as of the CLI Runtime
4
+ * Convergence roadmap (Phase 6a, 2026-04-09). This file is retained
5
+ * as a re-export shim for backwards compatibility and will be
6
+ * removed once all external consumers have migrated.
3
7
  *
4
- * Registry and lifecycle management for stateful agent/chat sessions
5
- * accessed over WebSocket. Each session maintains its own message history,
6
- * context engine, permanent memory, plan manager, and LLM configuration.
8
+ * Please import `WSSessionManager` directly from
9
+ * `packages/cli/src/gateways/ws/ws-session-gateway.js` in new code.
7
10
  */
8
11
 
9
- import { createHash } from "crypto";
10
- import fs from "fs";
11
- import path from "path";
12
- import { ExecutionPlan, PlanModeManager, PlanState } from "./plan-mode.js";
13
- import { CLIContextEngineering } from "./cli-context-engineering.js";
14
- import { CLIPermanentMemory } from "./permanent-memory.js";
15
- import {
16
- createTrustedMcpServerMap,
17
- resolveMcpServerPolicy,
18
- normalizeRiskLevel,
19
- normalizeBoolean,
20
- selectHigherRiskLevel,
21
- } from "../runtime/coding-agent-managed-tool-policy.cjs";
22
- import {
23
- createSession as dbCreateSession,
24
- saveMessages as dbSaveMessages,
25
- getSession as dbGetSession,
26
- listSessions as dbListSessions,
27
- updateSession as dbUpdateSession,
28
- } from "./session-manager.js";
29
- import { buildSystemPrompt } from "./agent-core.js";
30
- import { SubAgentRegistry } from "./sub-agent-registry.js";
31
- import { createWorktree, removeWorktree } from "./worktree-isolator.js";
32
- import { isGitRepo } from "./git-integration.js";
33
- import {
34
- CODING_AGENT_MVP_TOOL_NAMES,
35
- listCodingAgentToolNames,
36
- } from "../runtime/coding-agent-contract.js";
37
-
38
- /**
39
- * @typedef {object} Session
40
- * @property {string} id
41
- * @property {"agent"|"chat"} type
42
- * @property {"active"|"closed"} status
43
- * @property {Array} messages
44
- * @property {string} provider
45
- * @property {string} model
46
- * @property {string|null} apiKey
47
- * @property {string|null} baseUrl
48
- * @property {string} projectRoot
49
- * @property {string} baseProjectRoot
50
- * @property {string|null} rulesContent
51
- * @property {string[]} enabledToolNames
52
- * @property {object|null} hostManagedToolPolicy
53
- * @property {Array<object>} externalToolDefinitions
54
- * @property {object} externalToolDescriptors
55
- * @property {object} externalToolExecutors
56
- * @property {boolean} worktreeIsolation
57
- * @property {object|null} worktree
58
- * @property {PlanModeManager} planManager
59
- * @property {CLIContextEngineering|null} contextEngine
60
- * @property {CLIPermanentMemory|null} permanentMemory
61
- * @property {import("./interaction-adapter.js").WebSocketInteractionAdapter|null} interaction
62
- * @property {string} createdAt
63
- * @property {string} lastActivity
64
- */
65
-
66
- export class WSSessionManager {
67
- /**
68
- * @param {object} options
69
- * @param {object} [options.db] - Database instance
70
- * @param {object} [options.config] - Config object
71
- * @param {string} [options.defaultProjectRoot] - Default project root
72
- */
73
- constructor(options = {}) {
74
- this.db = options.db || null;
75
- this.config = options.config || {};
76
- this.defaultProjectRoot = options.defaultProjectRoot || process.cwd();
77
- this.mcpClient = options.mcpClient || null;
78
- this.allowedMcpServerNames = Array.isArray(options.allowedMcpServerNames)
79
- ? options.allowedMcpServerNames
80
- : null;
81
- this.allowHighRiskMcpServers = options.allowHighRiskMcpServers === true;
82
- this.trustedMcpServers = createTrustedMcpServerMap(
83
- options.mcpServerRegistry || null,
84
- );
85
-
86
- /** @type {Map<string, Session>} */
87
- this.sessions = new Map();
88
- }
89
-
90
- _normalizeEnabledToolNames(enabledToolNames) {
91
- const knownToolNames = new Set(listCodingAgentToolNames());
92
- const requested = Array.isArray(enabledToolNames)
93
- ? enabledToolNames
94
- .map((name) => String(name || "").trim())
95
- .filter(Boolean)
96
- : [];
97
-
98
- const filtered = requested.filter((name) => knownToolNames.has(name));
99
- if (filtered.length > 0) {
100
- return [...new Set(filtered)];
101
- }
102
-
103
- return [...CODING_AGENT_MVP_TOOL_NAMES];
104
- }
105
-
106
- _buildSessionExternalTools() {
107
- if (
108
- !this.mcpClient ||
109
- !(this.mcpClient.servers instanceof Map) ||
110
- typeof this.mcpClient.listTools !== "function"
111
- ) {
112
- return {
113
- definitions: [],
114
- descriptors: {},
115
- executors: {},
116
- };
117
- }
118
-
119
- const definitions = [];
120
- const descriptors = {};
121
- const executors = {};
122
- const seenNames = new Set();
123
-
124
- for (const [serverName, serverState] of this.mcpClient.servers.entries()) {
125
- const serverPolicy = resolveMcpServerPolicy(serverName, serverState, {
126
- allowedMcpServerNames: this.allowedMcpServerNames,
127
- trustedMcpServers: this.trustedMcpServers,
128
- allowHighRiskMcpServers: this.allowHighRiskMcpServers,
129
- });
130
-
131
- if (!serverPolicy.allowed) {
132
- continue;
133
- }
134
-
135
- const serverTools = Array.isArray(serverState?.tools)
136
- ? serverState.tools
137
- : this.mcpClient.listTools(serverName);
138
-
139
- for (const mcpTool of Array.isArray(serverTools) ? serverTools : []) {
140
- const parsedSchema = this._parseToolSchema(mcpTool?.inputSchema) ||
141
- this._parseToolSchema(mcpTool?.input_schema) ||
142
- this._parseToolSchema(mcpTool?.parameters_schema) || {
143
- type: "object",
144
- properties: {},
145
- };
146
- const riskLevel = selectHigherRiskLevel(
147
- serverPolicy.securityLevel,
148
- normalizeRiskLevel(mcpTool?.risk_level, null),
149
- );
150
- const isReadOnly =
151
- normalizeBoolean(mcpTool?.isReadOnly, false) ||
152
- normalizeBoolean(mcpTool?.is_read_only, false) ||
153
- riskLevel === "low";
154
-
155
- let toolName = `mcp_${serverName}_${mcpTool?.name || "tool"}`;
156
- if (seenNames.has(toolName)) {
157
- let index = 2;
158
- let candidate = `${toolName}_${index}`;
159
- while (seenNames.has(candidate)) {
160
- index += 1;
161
- candidate = `${toolName}_${index}`;
162
- }
163
- toolName = candidate;
164
- }
165
- seenNames.add(toolName);
166
-
167
- const descriptor = {
168
- name: toolName,
169
- description: mcpTool?.description || `MCP tool from ${serverName}.`,
170
- inputSchema: parsedSchema,
171
- isReadOnly,
172
- riskLevel,
173
- source: `mcp:${serverName}`,
174
- mcpMetadata: {
175
- serverName,
176
- trusted: serverPolicy.trusted === true,
177
- securityLevel: serverPolicy.securityLevel,
178
- requiredPermissions: serverPolicy.requiredPermissions || [],
179
- capabilities: serverPolicy.capabilities || [],
180
- originalToolName: mcpTool?.name || null,
181
- tool: mcpTool || null,
182
- },
183
- };
184
-
185
- definitions.push({
186
- type: "function",
187
- function: {
188
- name: descriptor.name,
189
- description: descriptor.description,
190
- parameters: JSON.parse(JSON.stringify(descriptor.inputSchema)),
191
- },
192
- });
193
- descriptors[descriptor.name] = descriptor;
194
- executors[descriptor.name] = {
195
- kind: "mcp",
196
- serverName,
197
- toolName: mcpTool?.name || null,
198
- };
199
- }
200
- }
201
-
202
- return {
203
- definitions,
204
- descriptors,
205
- executors,
206
- };
207
- }
208
-
209
- _parseToolSchema(value) {
210
- if (!value) {
211
- return null;
212
- }
213
-
214
- if (typeof value === "object") {
215
- return value;
216
- }
217
-
218
- if (typeof value !== "string") {
219
- return null;
220
- }
221
-
222
- try {
223
- return JSON.parse(value);
224
- } catch (_err) {
225
- return null;
226
- }
227
- }
228
-
229
- /**
230
- * Generate a unique session ID
231
- */
232
- _generateId() {
233
- const hash = createHash("sha256")
234
- .update(Math.random().toString() + Date.now().toString())
235
- .digest("hex")
236
- .slice(0, 8);
237
- return `ws-session-${Date.now()}-${hash}`;
238
- }
239
-
240
- /**
241
- * Create a new session.
242
- *
243
- * @param {object} options
244
- * @param {"agent"|"chat"} [options.type="agent"]
245
- * @param {string} [options.projectRoot]
246
- * @param {string} [options.provider="ollama"]
247
- * @param {string} [options.model]
248
- * @param {string} [options.apiKey]
249
- * @param {string} [options.baseUrl]
250
- * @param {object} [options.hostManagedToolPolicy]
251
- * @returns {{ sessionId: string }}
252
- */
253
- createSession(options = {}) {
254
- const sessionId = this._generateId();
255
- const type = options.type || "agent";
256
- const baseProjectRoot = options.projectRoot || this.defaultProjectRoot;
257
- const cfgLlm = this.config?.llm || {};
258
- const provider = options.provider || cfgLlm.provider || "ollama";
259
- const model =
260
- options.model ||
261
- cfgLlm.model ||
262
- (provider === "ollama" ? "qwen2.5:7b" : null);
263
- const baseUrl =
264
- options.baseUrl || cfgLlm.baseUrl || "http://localhost:11434";
265
- const apiKey = options.apiKey || cfgLlm.apiKey || null;
266
- const worktreeIsolationRequested = options.worktreeIsolation === true;
267
- const enabledToolNames = this._normalizeEnabledToolNames(
268
- options.enabledToolNames,
269
- );
270
- const externalTools = this._buildSessionExternalTools();
271
- const isolatedWorkspace = this._prepareSessionWorkspace(
272
- baseProjectRoot,
273
- sessionId,
274
- {
275
- worktreeIsolation: worktreeIsolationRequested,
276
- },
277
- );
278
- const projectRoot = isolatedWorkspace.projectRoot;
279
- const worktree = isolatedWorkspace.worktree;
280
-
281
- // Project context (rules.md, persona) is now loaded by buildSystemPrompt()
282
-
283
- // Create plan manager (non-singleton, per-session)
284
- const planManager = new PlanModeManager();
285
-
286
- // Create context engine
287
- let contextEngine = null;
288
- let permanentMemory = null;
289
- try {
290
- const memoryDir = path.join(projectRoot, "memory");
291
- permanentMemory = new CLIPermanentMemory({
292
- db: this.db,
293
- memoryDir,
294
- });
295
- permanentMemory.initialize();
296
- } catch (_err) {
297
- // Non-critical
298
- }
299
-
300
- try {
301
- contextEngine = new CLIContextEngineering({
302
- db: this.db,
303
- permanentMemory,
304
- });
305
- } catch (_err) {
306
- // Non-critical
307
- }
308
-
309
- // Build initial system prompt (includes persona + rules.md)
310
- const systemPrompt = buildSystemPrompt(projectRoot);
311
-
312
- const messages = [{ role: "system", content: systemPrompt }];
313
-
314
- // Persist to DB
315
- if (this.db) {
316
- try {
317
- dbCreateSession(this.db, {
318
- id: sessionId,
319
- title: `WS ${type} ${new Date().toISOString().slice(0, 10)}`,
320
- provider,
321
- model: model || "",
322
- messages,
323
- });
324
- } catch (_err) {
325
- // Non-critical
326
- }
327
- }
328
-
329
- const session = {
330
- id: sessionId,
331
- type,
332
- status: "active",
333
- messages,
334
- provider,
335
- model,
336
- apiKey,
337
- baseUrl,
338
- mcpClient: this.mcpClient,
339
- enabledToolNames,
340
- hostManagedToolPolicy: options.hostManagedToolPolicy || null,
341
- externalToolDefinitions: externalTools.definitions,
342
- externalToolDescriptors: externalTools.descriptors,
343
- externalToolExecutors: externalTools.executors,
344
- projectRoot,
345
- baseProjectRoot,
346
- rulesContent: null,
347
- worktreeIsolation: worktreeIsolationRequested,
348
- worktree,
349
- planManager,
350
- contextEngine,
351
- permanentMemory,
352
- reviewState: null,
353
- pendingPatches: new Map(),
354
- patchHistory: [],
355
- taskGraph: null,
356
- interaction: null, // Set by ws-server after creation
357
- createdAt: new Date().toISOString(),
358
- lastActivity: new Date().toISOString(),
359
- };
360
-
361
- if (this.db) {
362
- try {
363
- dbUpdateSession(this.db, sessionId, {
364
- metadata: this._serializeSessionMetadata(session),
365
- });
366
- } catch (_err) {
367
- // Non-critical
368
- }
369
- }
370
-
371
- this._bindPlanManagerPersistence(session);
372
- this.sessions.set(sessionId, session);
373
-
374
- return { sessionId };
375
- }
376
-
377
- /**
378
- * Resume an existing session from DB.
379
- *
380
- * @param {string} sessionId
381
- * @returns {Session|null}
382
- */
383
- resumeSession(sessionId) {
384
- // Check in-memory first
385
- if (this.sessions.has(sessionId)) {
386
- const session = this.sessions.get(sessionId);
387
- session.status = "active";
388
- session.lastActivity = new Date().toISOString();
389
- return session;
390
- }
391
-
392
- // Try loading from DB
393
- if (!this.db) return null;
394
-
395
- try {
396
- const dbSession = dbGetSession(this.db, sessionId);
397
- if (!dbSession) return null;
398
-
399
- const messages =
400
- typeof dbSession.messages === "string"
401
- ? JSON.parse(dbSession.messages)
402
- : dbSession.messages || [];
403
- const metadata = this._normalizeSessionMetadata(dbSession.metadata);
404
- const baseProjectRoot =
405
- metadata.baseProjectRoot ||
406
- metadata.projectRoot ||
407
- this.defaultProjectRoot;
408
- const workspace = this._restoreSessionWorkspace(
409
- dbSession.id,
410
- baseProjectRoot,
411
- metadata,
412
- );
413
- const planManager = this._hydratePlanManager(metadata.planSnapshot);
414
- const externalTools = this._buildSessionExternalTools();
415
- let contextEngine = null;
416
- let permanentMemory = null;
417
-
418
- try {
419
- const memoryDir = path.join(workspace.projectRoot, "memory");
420
- permanentMemory = new CLIPermanentMemory({
421
- db: this.db,
422
- memoryDir,
423
- });
424
- permanentMemory.initialize();
425
- } catch (_err) {
426
- // Non-critical
427
- }
428
-
429
- try {
430
- contextEngine = new CLIContextEngineering({
431
- db: this.db,
432
- permanentMemory,
433
- });
434
- } catch (_err) {
435
- // Non-critical
436
- }
437
-
438
- const session = {
439
- id: dbSession.id,
440
- type: metadata.sessionType || "agent",
441
- status: "active",
442
- messages,
443
- provider: dbSession.provider || "ollama",
444
- model: dbSession.model || null,
445
- apiKey: null,
446
- baseUrl: metadata.baseUrl || "http://localhost:11434",
447
- mcpClient: this.mcpClient,
448
- enabledToolNames: this._normalizeEnabledToolNames(
449
- metadata.enabledToolNames,
450
- ),
451
- hostManagedToolPolicy: metadata.hostManagedToolPolicy || null,
452
- externalToolDefinitions: externalTools.definitions,
453
- externalToolDescriptors: externalTools.descriptors,
454
- externalToolExecutors: externalTools.executors,
455
- projectRoot: workspace.projectRoot,
456
- baseProjectRoot,
457
- rulesContent: null,
458
- worktreeIsolation: metadata.worktreeIsolation === true,
459
- worktree: workspace.worktree,
460
- planManager,
461
- contextEngine,
462
- permanentMemory,
463
- reviewState: metadata.reviewState || null,
464
- pendingPatches: this._hydratePendingPatches(metadata.pendingPatches),
465
- patchHistory: Array.isArray(metadata.patchHistory)
466
- ? metadata.patchHistory
467
- : [],
468
- taskGraph: this._hydrateTaskGraph(metadata.taskGraph),
469
- interaction: null,
470
- createdAt: dbSession.created_at,
471
- lastActivity: new Date().toISOString(),
472
- };
473
-
474
- this._bindPlanManagerPersistence(session);
475
- this.sessions.set(session.id, session);
476
- return session;
477
- } catch (_err) {
478
- return null;
479
- }
480
- }
481
-
482
- /**
483
- * Close a session and persist final state.
484
- *
485
- * @param {string} sessionId
486
- */
487
- closeSession(sessionId) {
488
- const session = this.sessions.get(sessionId);
489
- if (!session) return;
490
-
491
- session.status = "closed";
492
-
493
- // Persist messages to DB
494
- this._persistSessionState(sessionId);
495
-
496
- // Auto-summarize into permanent memory
497
- if (session.permanentMemory && session.messages.length > 4) {
498
- try {
499
- session.permanentMemory.autoSummarize(session.messages);
500
- } catch (_err) {
501
- // Non-critical
502
- }
503
- }
504
-
505
- // Force-complete any active sub-agents for this session
506
- try {
507
- SubAgentRegistry.getInstance().forceCompleteAll(sessionId);
508
- } catch (_err) {
509
- // Non-critical
510
- }
511
-
512
- // Clean up plan manager listeners
513
- if (session.planManager) {
514
- if (typeof session._planPersistenceCleanup === "function") {
515
- try {
516
- session._planPersistenceCleanup();
517
- } catch (_err) {
518
- // Non-critical.
519
- }
520
- }
521
- session.planManager.removeAllListeners();
522
- }
523
-
524
- if (session.worktree?.path && session.baseProjectRoot) {
525
- try {
526
- removeWorktree(session.baseProjectRoot, session.worktree.path, {
527
- deleteBranch: true,
528
- });
529
- } catch (_err) {
530
- // Best-effort cleanup.
531
- }
532
- }
533
-
534
- this.sessions.delete(sessionId);
535
- }
536
-
537
- /**
538
- * List all sessions (in-memory + DB).
539
- *
540
- * @returns {Array<{id, type, status, createdAt, lastActivity}>}
541
- */
542
- listSessions() {
543
- const results = [];
544
-
545
- // In-memory active sessions
546
- for (const [, session] of this.sessions) {
547
- results.push({
548
- id: session.id,
549
- type: session.type,
550
- status: session.status,
551
- provider: session.provider,
552
- model: session.model,
553
- messageCount: session.messages.length,
554
- enabledToolNames: session.enabledToolNames || [],
555
- baseProjectRoot: session.baseProjectRoot,
556
- worktreeIsolation: session.worktreeIsolation === true,
557
- worktree: session.worktree || null,
558
- createdAt: session.createdAt,
559
- lastActivity: session.lastActivity,
560
- });
561
- }
562
-
563
- // DB sessions (exclude already-listed in-memory ones)
564
- if (this.db) {
565
- try {
566
- const dbSessions = dbListSessions(this.db, { limit: 20 });
567
- const inMemoryIds = new Set(this.sessions.keys());
568
- for (const dbs of dbSessions) {
569
- const metadata = this._normalizeSessionMetadata(dbs.metadata);
570
- if (!inMemoryIds.has(dbs.id)) {
571
- results.push({
572
- id: dbs.id,
573
- type: metadata.sessionType || "unknown",
574
- status: "persisted",
575
- provider: dbs.provider,
576
- model: dbs.model,
577
- messageCount: dbs.message_count,
578
- enabledToolNames: Array.isArray(metadata.enabledToolNames)
579
- ? metadata.enabledToolNames
580
- : [],
581
- baseProjectRoot: metadata.baseProjectRoot || null,
582
- worktreeIsolation: metadata.worktreeIsolation === true,
583
- worktree: metadata.worktree || null,
584
- createdAt: dbs.created_at,
585
- lastActivity: dbs.updated_at,
586
- });
587
- }
588
- }
589
- } catch (_err) {
590
- // Non-critical
591
- }
592
- }
593
-
594
- return results;
595
- }
596
-
597
- /**
598
- * Get a session by ID.
599
- *
600
- * @param {string} sessionId
601
- * @returns {Session|null}
602
- */
603
- getSession(sessionId) {
604
- return this.sessions.get(sessionId) || null;
605
- }
606
-
607
- /**
608
- * Update host-managed tool policy for an active session.
609
- *
610
- * @param {string} sessionId
611
- * @param {object|null} hostManagedToolPolicy
612
- * @returns {Session|null}
613
- */
614
- updateSessionPolicy(sessionId, hostManagedToolPolicy) {
615
- const session = this.sessions.get(sessionId);
616
- if (!session) return null;
617
-
618
- session.hostManagedToolPolicy = hostManagedToolPolicy || null;
619
- session.lastActivity = new Date().toISOString();
620
- this._persistSessionState(sessionId);
621
- return session;
622
- }
623
-
624
- /**
625
- * Enter explicit review mode for a session. While in review, handlers
626
- * MUST gate new sendMessage calls until the review is resolved. Reviewer
627
- * sub-agents and human reviewers both feed into the same `comments` /
628
- * `checklist` arrays.
629
- *
630
- * @param {string} sessionId
631
- * @param {{
632
- * reason?: string,
633
- * requestedBy?: string,
634
- * checklist?: Array<{ id?: string, title: string, note?: string }>,
635
- * blocking?: boolean,
636
- * }} [options]
637
- */
638
- enterReview(sessionId, options = {}) {
639
- const session = this.sessions.get(sessionId);
640
- if (!session) return null;
641
-
642
- // If already in pending review, return the existing state unchanged so
643
- // callers can retry safely.
644
- if (session.reviewState && session.reviewState.status === "pending") {
645
- return session.reviewState;
646
- }
647
-
648
- const reviewId = `review-${this._generateId()}`;
649
- const now = new Date().toISOString();
650
- const checklist = Array.isArray(options.checklist)
651
- ? options.checklist.map((item, index) => ({
652
- id: item.id || `chk-${index}-${Date.now()}`,
653
- title: item.title || `Item ${index + 1}`,
654
- note: item.note || null,
655
- done: false,
656
- }))
657
- : [];
658
-
659
- session.reviewState = {
660
- reviewId,
661
- status: "pending",
662
- reason: options.reason || null,
663
- requestedBy: options.requestedBy || "user",
664
- requestedAt: now,
665
- resolvedAt: null,
666
- resolvedBy: null,
667
- decision: null,
668
- blocking: options.blocking !== false,
669
- comments: [],
670
- checklist,
671
- };
672
- session.lastActivity = now;
673
- this._persistSessionState(sessionId);
674
- return session.reviewState;
675
- }
676
-
677
- /**
678
- * Submit an incremental update to the active review — append a comment
679
- * and/or toggle a checklist item. Returns the updated reviewState, or null
680
- * if the session has no active review.
681
- *
682
- * @param {string} sessionId
683
- * @param {{
684
- * comment?: { author?: string, content: string },
685
- * checklistItemId?: string,
686
- * checklistItemDone?: boolean,
687
- * checklistItemNote?: string,
688
- * }} update
689
- */
690
- submitReviewComment(sessionId, update = {}) {
691
- const session = this.sessions.get(sessionId);
692
- if (!session || !session.reviewState) return null;
693
- if (session.reviewState.status !== "pending") return null;
694
-
695
- const now = new Date().toISOString();
696
-
697
- if (update.comment && update.comment.content) {
698
- session.reviewState.comments.push({
699
- id: `cmt-${session.reviewState.comments.length}-${Date.now()}`,
700
- author: update.comment.author || "user",
701
- content: String(update.comment.content),
702
- timestamp: now,
703
- });
704
- }
705
-
706
- if (update.checklistItemId) {
707
- const item = session.reviewState.checklist.find(
708
- (c) => c.id === update.checklistItemId,
709
- );
710
- if (item) {
711
- if (typeof update.checklistItemDone === "boolean") {
712
- item.done = update.checklistItemDone;
713
- }
714
- if (typeof update.checklistItemNote === "string") {
715
- item.note = update.checklistItemNote;
716
- }
717
- }
718
- }
719
-
720
- session.lastActivity = now;
721
- this._persistSessionState(sessionId);
722
- return session.reviewState;
723
- }
724
-
725
- /**
726
- * Resolve the active review with an approved/rejected decision. After
727
- * resolve the session can accept new messages again (reviewState becomes
728
- * non-blocking but is retained for audit).
729
- *
730
- * @param {string} sessionId
731
- * @param {{ decision: "approved"|"rejected", resolvedBy?: string, summary?: string }} payload
732
- */
733
- resolveReview(sessionId, payload = {}) {
734
- const session = this.sessions.get(sessionId);
735
- if (!session || !session.reviewState) return null;
736
- if (session.reviewState.status !== "pending") {
737
- return session.reviewState;
738
- }
739
-
740
- const decision =
741
- payload.decision === "approved" || payload.decision === "rejected"
742
- ? payload.decision
743
- : "approved";
744
-
745
- session.reviewState.status = decision;
746
- session.reviewState.decision = decision;
747
- session.reviewState.resolvedAt = new Date().toISOString();
748
- session.reviewState.resolvedBy = payload.resolvedBy || "user";
749
- session.reviewState.blocking = false;
750
- if (payload.summary) {
751
- session.reviewState.summary = String(payload.summary);
752
- }
753
-
754
- session.lastActivity = session.reviewState.resolvedAt;
755
- this._persistSessionState(sessionId);
756
- return session.reviewState;
757
- }
758
-
759
- /**
760
- * Returns true when the session currently has a blocking review gate
761
- * open. Callers (e.g. handleSessionMessage) should short-circuit with a
762
- * REVIEW_BLOCKING error instead of running the agent turn.
763
- */
764
- isReviewBlocking(sessionId) {
765
- const session = this.sessions.get(sessionId);
766
- if (!session || !session.reviewState) return false;
767
- return (
768
- session.reviewState.status === "pending" &&
769
- session.reviewState.blocking === true
770
- );
771
- }
772
-
773
- getReviewState(sessionId) {
774
- const session = this.sessions.get(sessionId);
775
- return session ? session.reviewState || null : null;
776
- }
777
-
778
- /**
779
- * Record a proposed patch on the session. Accepts one or more file hunks
780
- * that a tool wanted to write but should be previewed before they land.
781
- *
782
- * @param {string} sessionId
783
- * @param {{
784
- * files: Array<{
785
- * path: string,
786
- * op?: "create"|"modify"|"delete",
787
- * before?: string|null,
788
- * after?: string|null,
789
- * diff?: string|null,
790
- * stats?: { added?: number, removed?: number }
791
- * }>,
792
- * origin?: string,
793
- * reason?: string,
794
- * requestId?: string|null
795
- * }} payload
796
- * @returns {object|null} patch record, or null if the session is missing
797
- */
798
- proposePatch(sessionId, payload = {}) {
799
- const session = this.sessions.get(sessionId);
800
- if (!session) return null;
801
-
802
- const files = Array.isArray(payload.files) ? payload.files : [];
803
- if (files.length === 0) return null;
804
-
805
- const patchId = `patch-${this._generateId()}`;
806
- const now = new Date().toISOString();
807
- const normalizedFiles = files.map((file, index) => {
808
- const op = file.op || (file.before == null ? "create" : "modify");
809
- const stats = this._computePatchStats(file);
810
- return {
811
- index,
812
- path: file.path || `unknown-${index}`,
813
- op,
814
- before: file.before == null ? null : String(file.before),
815
- after: file.after == null ? null : String(file.after),
816
- diff: file.diff == null ? null : String(file.diff),
817
- stats,
818
- };
819
- });
820
-
821
- const totalStats = normalizedFiles.reduce(
822
- (acc, file) => ({
823
- added: acc.added + (file.stats.added || 0),
824
- removed: acc.removed + (file.stats.removed || 0),
825
- }),
826
- { added: 0, removed: 0 },
827
- );
828
-
829
- const patch = {
830
- patchId,
831
- status: "pending",
832
- origin: payload.origin || "tool",
833
- reason: payload.reason || null,
834
- requestId: payload.requestId || null,
835
- proposedAt: now,
836
- resolvedAt: null,
837
- resolvedBy: null,
838
- files: normalizedFiles,
839
- stats: {
840
- fileCount: normalizedFiles.length,
841
- added: totalStats.added,
842
- removed: totalStats.removed,
843
- },
844
- };
845
-
846
- if (!(session.pendingPatches instanceof Map)) {
847
- session.pendingPatches = new Map();
848
- }
849
- session.pendingPatches.set(patchId, patch);
850
- session.lastActivity = now;
851
- this._persistSessionState(sessionId);
852
- return patch;
853
- }
854
-
855
- /**
856
- * Mark a pending patch as applied. Moves the record to patchHistory so it
857
- * is still visible in the summary view but no longer counts as pending.
858
- */
859
- applyPatch(sessionId, patchId, options = {}) {
860
- const session = this.sessions.get(sessionId);
861
- if (!session || !(session.pendingPatches instanceof Map)) return null;
862
- const patch = session.pendingPatches.get(patchId);
863
- if (!patch) return null;
864
-
865
- patch.status = "applied";
866
- patch.resolvedAt = new Date().toISOString();
867
- patch.resolvedBy = options.resolvedBy || "user";
868
- if (options.note) {
869
- patch.note = String(options.note);
870
- }
871
-
872
- session.pendingPatches.delete(patchId);
873
- if (!Array.isArray(session.patchHistory)) {
874
- session.patchHistory = [];
875
- }
876
- session.patchHistory.push(patch);
877
- session.lastActivity = patch.resolvedAt;
878
- this._persistSessionState(sessionId);
879
- return patch;
880
- }
881
-
882
- /**
883
- * Discard a pending patch. Same bookkeeping as applyPatch but records a
884
- * "rejected" decision instead.
885
- */
886
- rejectPatch(sessionId, patchId, options = {}) {
887
- const session = this.sessions.get(sessionId);
888
- if (!session || !(session.pendingPatches instanceof Map)) return null;
889
- const patch = session.pendingPatches.get(patchId);
890
- if (!patch) return null;
891
-
892
- patch.status = "rejected";
893
- patch.resolvedAt = new Date().toISOString();
894
- patch.resolvedBy = options.resolvedBy || "user";
895
- if (options.reason) {
896
- patch.rejectionReason = String(options.reason);
897
- }
898
-
899
- session.pendingPatches.delete(patchId);
900
- if (!Array.isArray(session.patchHistory)) {
901
- session.patchHistory = [];
902
- }
903
- session.patchHistory.push(patch);
904
- session.lastActivity = patch.resolvedAt;
905
- this._persistSessionState(sessionId);
906
- return patch;
907
- }
908
-
909
- /**
910
- * Return a flattened summary of all pending + resolved patches on the
911
- * session. Shape matches what the renderer strip consumes:
912
- * { pending: [...], history: [...], totals: { added, removed, fileCount } }
913
- */
914
- getPatchSummary(sessionId) {
915
- const session = this.sessions.get(sessionId);
916
- if (!session) return null;
917
-
918
- const pending =
919
- session.pendingPatches instanceof Map
920
- ? Array.from(session.pendingPatches.values())
921
- : [];
922
- const history = Array.isArray(session.patchHistory)
923
- ? session.patchHistory
924
- : [];
925
-
926
- const totals = [...pending, ...history].reduce(
927
- (acc, patch) => ({
928
- fileCount: acc.fileCount + (patch.stats?.fileCount || 0),
929
- added: acc.added + (patch.stats?.added || 0),
930
- removed: acc.removed + (patch.stats?.removed || 0),
931
- }),
932
- { fileCount: 0, added: 0, removed: 0 },
933
- );
934
-
935
- return { pending, history, totals };
936
- }
937
-
938
- hasPendingPatches(sessionId) {
939
- const session = this.sessions.get(sessionId);
940
- if (!session || !(session.pendingPatches instanceof Map)) return false;
941
- return session.pendingPatches.size > 0;
942
- }
943
-
944
- _computePatchStats(file) {
945
- if (file && file.stats && typeof file.stats === "object") {
946
- return {
947
- added: Number(file.stats.added) || 0,
948
- removed: Number(file.stats.removed) || 0,
949
- };
950
- }
951
- const before = file && typeof file.before === "string" ? file.before : "";
952
- const after = file && typeof file.after === "string" ? file.after : "";
953
- const beforeLines = before ? before.split(/\r?\n/).length : 0;
954
- const afterLines = after ? after.split(/\r?\n/).length : 0;
955
- // Rough heuristic when no explicit diff is provided: full replace counts
956
- // the entire file as added/removed.
957
- if (!before && after) return { added: afterLines, removed: 0 };
958
- if (before && !after) return { added: 0, removed: beforeLines };
959
- return {
960
- added: Math.max(0, afterLines - beforeLines),
961
- removed: Math.max(0, beforeLines - afterLines),
962
- };
963
- }
964
-
965
- /**
966
- * Create or replace the task graph for a session. A graph is a DAG of
967
- * `nodes` keyed by id; each node has `{ id, title, status, dependsOn[],
968
- * metadata }`. Returns the serialized graph.
969
- */
970
- createTaskGraph(sessionId, payload = {}) {
971
- const session = this.sessions.get(sessionId);
972
- if (!session) return null;
973
-
974
- const graphId = payload.graphId || `graph-${this._generateId()}`;
975
- const now = new Date().toISOString();
976
- const nodes = {};
977
- const incomingNodes = Array.isArray(payload.nodes) ? payload.nodes : [];
978
- for (const raw of incomingNodes) {
979
- if (!raw || !raw.id) continue;
980
- nodes[raw.id] = this._normalizeTaskNode(raw, now);
981
- }
982
-
983
- const graph = {
984
- graphId,
985
- title: payload.title || null,
986
- description: payload.description || null,
987
- status: "active",
988
- createdAt: now,
989
- updatedAt: now,
990
- completedAt: null,
991
- nodes,
992
- order: Object.keys(nodes),
993
- };
994
-
995
- session.taskGraph = graph;
996
- session.lastActivity = now;
997
- this._persistSessionState(sessionId);
998
- return this._cloneTaskGraph(graph);
999
- }
1000
-
1001
- /**
1002
- * Add a node to the existing task graph. Fails if no graph exists or if
1003
- * the node id already exists.
1004
- */
1005
- addTaskNode(sessionId, payload = {}) {
1006
- const session = this.sessions.get(sessionId);
1007
- if (!session || !session.taskGraph) return null;
1008
- if (!payload || !payload.id) return null;
1009
- const graph = session.taskGraph;
1010
- if (graph.nodes[payload.id]) return null;
1011
-
1012
- const now = new Date().toISOString();
1013
- graph.nodes[payload.id] = this._normalizeTaskNode(payload, now);
1014
- graph.order = [...(graph.order || []), payload.id];
1015
- graph.updatedAt = now;
1016
- session.lastActivity = now;
1017
- this._persistSessionState(sessionId);
1018
- return this._cloneTaskGraph(graph);
1019
- }
1020
-
1021
- /**
1022
- * Update a node's status / metadata. Valid statuses: pending, ready,
1023
- * running, completed, failed, skipped.
1024
- */
1025
- updateTaskNode(sessionId, nodeId, updates = {}) {
1026
- const session = this.sessions.get(sessionId);
1027
- if (!session || !session.taskGraph) return null;
1028
- const graph = session.taskGraph;
1029
- const node = graph.nodes[nodeId];
1030
- if (!node) return null;
1031
-
1032
- const now = new Date().toISOString();
1033
- if (updates.status) {
1034
- node.status = String(updates.status);
1035
- if (node.status === "running" && !node.startedAt) {
1036
- node.startedAt = now;
1037
- }
1038
- if (
1039
- node.status === "completed" ||
1040
- node.status === "failed" ||
1041
- node.status === "skipped"
1042
- ) {
1043
- node.completedAt = now;
1044
- }
1045
- }
1046
- if (updates.title !== undefined) node.title = updates.title;
1047
- if (updates.result !== undefined) node.result = updates.result;
1048
- if (updates.error !== undefined) node.error = updates.error;
1049
- if (updates.metadata !== undefined) {
1050
- node.metadata = { ...(node.metadata || {}), ...(updates.metadata || {}) };
1051
- }
1052
- node.updatedAt = now;
1053
- graph.updatedAt = now;
1054
-
1055
- // Check graph completion
1056
- const allDone = Object.values(graph.nodes).every((n) =>
1057
- ["completed", "failed", "skipped"].includes(n.status),
1058
- );
1059
- if (allDone) {
1060
- graph.status = Object.values(graph.nodes).some(
1061
- (n) => n.status === "failed",
1062
- )
1063
- ? "failed"
1064
- : "completed";
1065
- graph.completedAt = now;
1066
- }
1067
-
1068
- session.lastActivity = now;
1069
- this._persistSessionState(sessionId);
1070
- return this._cloneTaskGraph(graph);
1071
- }
1072
-
1073
- /**
1074
- * Advance the task graph: mark any `pending` node whose dependencies are
1075
- * all `completed` (or `skipped`) as `ready`. Returns the list of node ids
1076
- * that became ready and the updated graph snapshot.
1077
- */
1078
- advanceTaskGraph(sessionId) {
1079
- const session = this.sessions.get(sessionId);
1080
- if (!session || !session.taskGraph) return null;
1081
- const graph = session.taskGraph;
1082
-
1083
- const becameReady = [];
1084
- for (const node of Object.values(graph.nodes)) {
1085
- if (node.status !== "pending") continue;
1086
- const deps = Array.isArray(node.dependsOn) ? node.dependsOn : [];
1087
- const blocked = deps.some((depId) => {
1088
- const dep = graph.nodes[depId];
1089
- if (!dep) return true;
1090
- return dep.status !== "completed" && dep.status !== "skipped";
1091
- });
1092
- if (!blocked) {
1093
- node.status = "ready";
1094
- node.updatedAt = new Date().toISOString();
1095
- becameReady.push(node.id);
1096
- }
1097
- }
1098
-
1099
- if (becameReady.length > 0) {
1100
- graph.updatedAt = new Date().toISOString();
1101
- session.lastActivity = graph.updatedAt;
1102
- this._persistSessionState(sessionId);
1103
- }
1104
-
1105
- return {
1106
- graph: this._cloneTaskGraph(graph),
1107
- becameReady,
1108
- };
1109
- }
1110
-
1111
- getTaskGraph(sessionId) {
1112
- const session = this.sessions.get(sessionId);
1113
- if (!session || !session.taskGraph) return null;
1114
- return this._cloneTaskGraph(session.taskGraph);
1115
- }
1116
-
1117
- clearTaskGraph(sessionId) {
1118
- const session = this.sessions.get(sessionId);
1119
- if (!session) return false;
1120
- session.taskGraph = null;
1121
- session.lastActivity = new Date().toISOString();
1122
- this._persistSessionState(sessionId);
1123
- return true;
1124
- }
1125
-
1126
- _normalizeTaskNode(raw, now) {
1127
- const status = raw.status || "pending";
1128
- return {
1129
- id: raw.id,
1130
- title: raw.title || raw.id,
1131
- description: raw.description || null,
1132
- status,
1133
- dependsOn: Array.isArray(raw.dependsOn)
1134
- ? raw.dependsOn.filter((x) => typeof x === "string")
1135
- : [],
1136
- metadata:
1137
- raw.metadata && typeof raw.metadata === "object" ? raw.metadata : {},
1138
- createdAt: raw.createdAt || now,
1139
- updatedAt: raw.updatedAt || now,
1140
- startedAt: raw.startedAt || null,
1141
- completedAt: raw.completedAt || null,
1142
- result: raw.result || null,
1143
- error: raw.error || null,
1144
- };
1145
- }
1146
-
1147
- _cloneTaskGraph(graph) {
1148
- if (!graph) return null;
1149
- return {
1150
- graphId: graph.graphId,
1151
- title: graph.title,
1152
- description: graph.description,
1153
- status: graph.status,
1154
- createdAt: graph.createdAt,
1155
- updatedAt: graph.updatedAt,
1156
- completedAt: graph.completedAt,
1157
- order: Array.isArray(graph.order)
1158
- ? [...graph.order]
1159
- : Object.keys(graph.nodes || {}),
1160
- nodes: Object.fromEntries(
1161
- Object.entries(graph.nodes || {}).map(([id, node]) => [
1162
- id,
1163
- {
1164
- ...node,
1165
- dependsOn: [...(node.dependsOn || [])],
1166
- metadata: { ...(node.metadata || {}) },
1167
- },
1168
- ]),
1169
- ),
1170
- };
1171
- }
1172
-
1173
- _hydrateTaskGraph(data) {
1174
- if (!data || typeof data !== "object") return null;
1175
- if (!data.graphId || !data.nodes) return null;
1176
- const nodes = {};
1177
- for (const [id, node] of Object.entries(data.nodes)) {
1178
- nodes[id] = this._normalizeTaskNode(
1179
- { ...node, id },
1180
- node.createdAt || new Date().toISOString(),
1181
- );
1182
- }
1183
- return {
1184
- graphId: data.graphId,
1185
- title: data.title || null,
1186
- description: data.description || null,
1187
- status: data.status || "active",
1188
- createdAt: data.createdAt || new Date().toISOString(),
1189
- updatedAt: data.updatedAt || new Date().toISOString(),
1190
- completedAt: data.completedAt || null,
1191
- order: Array.isArray(data.order) ? data.order : Object.keys(nodes),
1192
- nodes,
1193
- };
1194
- }
1195
-
1196
- _serializeTaskGraph(graph) {
1197
- if (!graph) return null;
1198
- return this._cloneTaskGraph(graph);
1199
- }
1200
-
1201
- /**
1202
- * Persist current messages for a session.
1203
- */
1204
- persistMessages(sessionId) {
1205
- const session = this.sessions.get(sessionId);
1206
- if (!session || !this.db) return;
1207
-
1208
- try {
1209
- dbSaveMessages(
1210
- this.db,
1211
- sessionId,
1212
- session.messages,
1213
- this._serializeSessionMetadata(session),
1214
- );
1215
- } catch (_err) {
1216
- // Non-critical
1217
- }
1218
-
1219
- session.lastActivity = new Date().toISOString();
1220
- }
1221
-
1222
- _prepareSessionWorkspace(projectRoot, sessionId, options = {}) {
1223
- if (options.worktreeIsolation !== true) {
1224
- return {
1225
- projectRoot,
1226
- worktree: null,
1227
- };
1228
- }
1229
-
1230
- if (!isGitRepo(projectRoot)) {
1231
- throw new Error(
1232
- `Worktree isolation requires a git repository: ${projectRoot}`,
1233
- );
1234
- }
1235
-
1236
- const branchName = `coding-agent/${sessionId}`;
1237
- const worktree = createWorktree(projectRoot, branchName);
1238
-
1239
- return {
1240
- projectRoot: worktree.path,
1241
- worktree: {
1242
- branch: worktree.branch,
1243
- path: worktree.path,
1244
- baseProjectRoot: projectRoot,
1245
- },
1246
- };
1247
- }
1248
-
1249
- _persistSessionState(sessionId) {
1250
- const session = this.sessions.get(sessionId);
1251
- if (!session || !this.db) return;
1252
-
1253
- try {
1254
- dbSaveMessages(
1255
- this.db,
1256
- sessionId,
1257
- session.messages,
1258
- this._serializeSessionMetadata(session),
1259
- );
1260
- } catch (_err) {
1261
- // Non-critical
1262
- }
1263
-
1264
- session.lastActivity = new Date().toISOString();
1265
- }
1266
-
1267
- _serializeSessionMetadata(session) {
1268
- return {
1269
- version: 1,
1270
- sessionType: session.type || "agent",
1271
- projectRoot: session.projectRoot || null,
1272
- baseProjectRoot: session.baseProjectRoot || session.projectRoot || null,
1273
- baseUrl: session.baseUrl || null,
1274
- hostManagedToolPolicy: session.hostManagedToolPolicy || null,
1275
- enabledToolNames: session.enabledToolNames || [],
1276
- worktreeIsolation: session.worktreeIsolation === true,
1277
- worktree: session.worktree || null,
1278
- planSnapshot: this._serializePlanManager(session.planManager),
1279
- reviewState: session.reviewState || null,
1280
- pendingPatches:
1281
- session.pendingPatches instanceof Map
1282
- ? Array.from(session.pendingPatches.values())
1283
- : [],
1284
- patchHistory: Array.isArray(session.patchHistory)
1285
- ? session.patchHistory
1286
- : [],
1287
- taskGraph: this._serializeTaskGraph(session.taskGraph),
1288
- };
1289
- }
1290
-
1291
- _hydratePendingPatches(list) {
1292
- const map = new Map();
1293
- if (Array.isArray(list)) {
1294
- for (const patch of list) {
1295
- if (patch && patch.patchId) {
1296
- map.set(patch.patchId, patch);
1297
- }
1298
- }
1299
- }
1300
- return map;
1301
- }
1302
-
1303
- _serializePlanManager(planManager) {
1304
- if (!planManager) {
1305
- return null;
1306
- }
1307
-
1308
- return {
1309
- state: planManager.state || PlanState.INACTIVE,
1310
- currentPlan: planManager.currentPlan || null,
1311
- history: Array.isArray(planManager.history) ? planManager.history : [],
1312
- blockedToolLog: Array.isArray(planManager.blockedToolLog)
1313
- ? planManager.blockedToolLog
1314
- : [],
1315
- };
1316
- }
1317
-
1318
- _normalizeSessionMetadata(metadata) {
1319
- if (!metadata) {
1320
- return {};
1321
- }
1322
-
1323
- if (typeof metadata === "string") {
1324
- try {
1325
- return JSON.parse(metadata);
1326
- } catch (_err) {
1327
- return {};
1328
- }
1329
- }
1330
-
1331
- return typeof metadata === "object" ? metadata : {};
1332
- }
1333
-
1334
- _hydratePlanManager(snapshot) {
1335
- const planManager = new PlanModeManager();
1336
- if (!snapshot || typeof snapshot !== "object") {
1337
- return planManager;
1338
- }
1339
-
1340
- planManager.state = snapshot.state || PlanState.INACTIVE;
1341
- planManager.currentPlan = snapshot.currentPlan
1342
- ? new ExecutionPlan(snapshot.currentPlan)
1343
- : null;
1344
- planManager.history = Array.isArray(snapshot.history)
1345
- ? snapshot.history.map((plan) => new ExecutionPlan(plan))
1346
- : [];
1347
- planManager.blockedToolLog = Array.isArray(snapshot.blockedToolLog)
1348
- ? [...snapshot.blockedToolLog]
1349
- : [];
1350
- return planManager;
1351
- }
1352
-
1353
- _restoreSessionWorkspace(sessionId, baseProjectRoot, metadata = {}) {
1354
- const requestedWorktreeIsolation = metadata.worktreeIsolation === true;
1355
- const persistedWorktreePath = metadata.worktree?.path || null;
1356
-
1357
- if (!requestedWorktreeIsolation) {
1358
- return {
1359
- projectRoot: metadata.projectRoot || baseProjectRoot,
1360
- worktree: null,
1361
- };
1362
- }
1363
-
1364
- if (persistedWorktreePath && fs.existsSync(persistedWorktreePath)) {
1365
- return {
1366
- projectRoot: persistedWorktreePath,
1367
- worktree: {
1368
- ...(metadata.worktree || {}),
1369
- baseProjectRoot,
1370
- },
1371
- };
1372
- }
1373
-
1374
- try {
1375
- return this._prepareSessionWorkspace(baseProjectRoot, sessionId, {
1376
- worktreeIsolation: true,
1377
- });
1378
- } catch (_err) {
1379
- return {
1380
- projectRoot: baseProjectRoot,
1381
- worktree: null,
1382
- };
1383
- }
1384
- }
1385
-
1386
- _bindPlanManagerPersistence(session) {
1387
- if (
1388
- !session?.id ||
1389
- !session.planManager ||
1390
- typeof session.planManager.on !== "function"
1391
- ) {
1392
- return;
1393
- }
1394
-
1395
- if (typeof session._planPersistenceCleanup === "function") {
1396
- session._planPersistenceCleanup();
1397
- }
1398
-
1399
- const persist = () => this._persistSessionState(session.id);
1400
- const events = [
1401
- "enter",
1402
- "exit",
1403
- "item-added",
1404
- "plan-ready",
1405
- "plan-approved",
1406
- "tool-blocked",
1407
- ];
1408
-
1409
- for (const eventName of events) {
1410
- session.planManager.on(eventName, persist);
1411
- }
1412
-
1413
- session._planPersistenceCleanup = () => {
1414
- if (typeof session.planManager.off === "function") {
1415
- for (const eventName of events) {
1416
- session.planManager.off(eventName, persist);
1417
- }
1418
- }
1419
- };
1420
- }
1421
- }
12
+ export { WSSessionManager } from "../gateways/ws/ws-session-gateway.js";