chainlesschain 0.45.64 → 0.45.65
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/src/gateways/ws/session-protocol.js +162 -105
- package/src/gateways/ws/worktree-protocol.js +134 -141
- package/src/lib/agent-core.js +173 -13
- package/src/lib/interaction-adapter.js +45 -0
- package/src/lib/plan-mode.js +3 -1
- package/src/lib/session-manager.js +45 -8
- package/src/lib/web-ui-envelope.js +94 -0
- package/src/lib/web-ui-server.js +13 -1
- package/src/lib/ws-agent-handler.js +8 -1
- package/src/lib/ws-session-manager.js +388 -20
- package/src/runtime/agent-runtime.js +140 -37
- package/src/runtime/coding-agent-contract.js +294 -0
- package/src/runtime/coding-agent-events.cjs +372 -0
- package/src/runtime/coding-agent-managed-tool-policy.cjs +294 -0
- package/src/runtime/coding-agent-policy.cjs +354 -0
- package/src/runtime/coding-agent-shell-policy.cjs +233 -0
- package/src/runtime/contracts/session-record.js +13 -0
- package/src/runtime/index.js +14 -0
- package/src/runtime/runtime-events.js +27 -0
- package/src/tools/index.js +12 -0
- package/src/tools/legacy-agent-tools.js +12 -157
|
@@ -83,7 +83,14 @@ export class WSAgentHandler {
|
|
|
83
83
|
contextEngine: session.contextEngine,
|
|
84
84
|
hookDb: this.db,
|
|
85
85
|
cwd: session.projectRoot,
|
|
86
|
+
sessionId: session.id,
|
|
87
|
+
planManager: session.planManager,
|
|
88
|
+
enabledToolNames: session.enabledToolNames || null,
|
|
86
89
|
hostManagedToolPolicy: session.hostManagedToolPolicy || null,
|
|
90
|
+
extraToolDefinitions: session.externalToolDefinitions || [],
|
|
91
|
+
externalToolDescriptors: session.externalToolDescriptors || {},
|
|
92
|
+
externalToolExecutors: session.externalToolExecutors || {},
|
|
93
|
+
mcpClient: session.mcpClient || null,
|
|
87
94
|
slotFiller,
|
|
88
95
|
interaction: this.interaction,
|
|
89
96
|
};
|
|
@@ -372,7 +379,7 @@ export class WSAgentHandler {
|
|
|
372
379
|
planManager.approvePlan();
|
|
373
380
|
this.session.messages.push({
|
|
374
381
|
role: "system",
|
|
375
|
-
content: `[PLAN APPROVED] The user has approved your plan with ${planManager.currentPlan.items.length} items. You can now use all tools including write_file, edit_file, run_shell, and run_skill. Execute the plan items in order.`,
|
|
382
|
+
content: `[PLAN APPROVED] The user has approved your plan with ${planManager.currentPlan.items.length} items. You can now use all tools including write_file, edit_file, run_shell, git, and run_skill. Execute the plan items in order.`,
|
|
376
383
|
});
|
|
377
384
|
this.interaction.emit("command-response", {
|
|
378
385
|
requestId,
|
|
@@ -9,19 +9,31 @@
|
|
|
9
9
|
import { createHash } from "crypto";
|
|
10
10
|
import fs from "fs";
|
|
11
11
|
import path from "path";
|
|
12
|
-
import { PlanModeManager } from "./plan-mode.js";
|
|
12
|
+
import { ExecutionPlan, PlanModeManager, PlanState } from "./plan-mode.js";
|
|
13
13
|
import { CLIContextEngineering } from "./cli-context-engineering.js";
|
|
14
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";
|
|
15
22
|
import {
|
|
16
23
|
createSession as dbCreateSession,
|
|
17
24
|
saveMessages as dbSaveMessages,
|
|
18
25
|
getSession as dbGetSession,
|
|
19
26
|
listSessions as dbListSessions,
|
|
27
|
+
updateSession as dbUpdateSession,
|
|
20
28
|
} from "./session-manager.js";
|
|
21
29
|
import { buildSystemPrompt } from "./agent-core.js";
|
|
22
30
|
import { SubAgentRegistry } from "./sub-agent-registry.js";
|
|
23
31
|
import { createWorktree, removeWorktree } from "./worktree-isolator.js";
|
|
24
32
|
import { isGitRepo } from "./git-integration.js";
|
|
33
|
+
import {
|
|
34
|
+
CODING_AGENT_MVP_TOOL_NAMES,
|
|
35
|
+
listCodingAgentToolNames,
|
|
36
|
+
} from "../runtime/coding-agent-contract.js";
|
|
25
37
|
|
|
26
38
|
/**
|
|
27
39
|
* @typedef {object} Session
|
|
@@ -36,7 +48,11 @@ import { isGitRepo } from "./git-integration.js";
|
|
|
36
48
|
* @property {string} projectRoot
|
|
37
49
|
* @property {string} baseProjectRoot
|
|
38
50
|
* @property {string|null} rulesContent
|
|
51
|
+
* @property {string[]} enabledToolNames
|
|
39
52
|
* @property {object|null} hostManagedToolPolicy
|
|
53
|
+
* @property {Array<object>} externalToolDefinitions
|
|
54
|
+
* @property {object} externalToolDescriptors
|
|
55
|
+
* @property {object} externalToolExecutors
|
|
40
56
|
* @property {boolean} worktreeIsolation
|
|
41
57
|
* @property {object|null} worktree
|
|
42
58
|
* @property {PlanModeManager} planManager
|
|
@@ -58,11 +74,158 @@ export class WSSessionManager {
|
|
|
58
74
|
this.db = options.db || null;
|
|
59
75
|
this.config = options.config || {};
|
|
60
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
|
+
);
|
|
61
85
|
|
|
62
86
|
/** @type {Map<string, Session>} */
|
|
63
87
|
this.sessions = new Map();
|
|
64
88
|
}
|
|
65
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
|
+
|
|
66
229
|
/**
|
|
67
230
|
* Generate a unique session ID
|
|
68
231
|
*/
|
|
@@ -101,6 +264,10 @@ export class WSSessionManager {
|
|
|
101
264
|
options.baseUrl || cfgLlm.baseUrl || "http://localhost:11434";
|
|
102
265
|
const apiKey = options.apiKey || cfgLlm.apiKey || null;
|
|
103
266
|
const worktreeIsolationRequested = options.worktreeIsolation === true;
|
|
267
|
+
const enabledToolNames = this._normalizeEnabledToolNames(
|
|
268
|
+
options.enabledToolNames,
|
|
269
|
+
);
|
|
270
|
+
const externalTools = this._buildSessionExternalTools();
|
|
104
271
|
const isolatedWorkspace = this._prepareSessionWorkspace(
|
|
105
272
|
baseProjectRoot,
|
|
106
273
|
sessionId,
|
|
@@ -168,7 +335,12 @@ export class WSSessionManager {
|
|
|
168
335
|
model,
|
|
169
336
|
apiKey,
|
|
170
337
|
baseUrl,
|
|
338
|
+
mcpClient: this.mcpClient,
|
|
339
|
+
enabledToolNames,
|
|
171
340
|
hostManagedToolPolicy: options.hostManagedToolPolicy || null,
|
|
341
|
+
externalToolDefinitions: externalTools.definitions,
|
|
342
|
+
externalToolDescriptors: externalTools.descriptors,
|
|
343
|
+
externalToolExecutors: externalTools.executors,
|
|
172
344
|
projectRoot,
|
|
173
345
|
baseProjectRoot,
|
|
174
346
|
rulesContent: null,
|
|
@@ -182,6 +354,17 @@ export class WSSessionManager {
|
|
|
182
354
|
lastActivity: new Date().toISOString(),
|
|
183
355
|
};
|
|
184
356
|
|
|
357
|
+
if (this.db) {
|
|
358
|
+
try {
|
|
359
|
+
dbUpdateSession(this.db, sessionId, {
|
|
360
|
+
metadata: this._serializeSessionMetadata(session),
|
|
361
|
+
});
|
|
362
|
+
} catch (_err) {
|
|
363
|
+
// Non-critical
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
this._bindPlanManagerPersistence(session);
|
|
185
368
|
this.sessions.set(sessionId, session);
|
|
186
369
|
|
|
187
370
|
return { sessionId };
|
|
@@ -213,13 +396,23 @@ export class WSSessionManager {
|
|
|
213
396
|
typeof dbSession.messages === "string"
|
|
214
397
|
? JSON.parse(dbSession.messages)
|
|
215
398
|
: dbSession.messages || [];
|
|
216
|
-
|
|
217
|
-
const
|
|
399
|
+
const metadata = this._normalizeSessionMetadata(dbSession.metadata);
|
|
400
|
+
const baseProjectRoot =
|
|
401
|
+
metadata.baseProjectRoot ||
|
|
402
|
+
metadata.projectRoot ||
|
|
403
|
+
this.defaultProjectRoot;
|
|
404
|
+
const workspace = this._restoreSessionWorkspace(
|
|
405
|
+
dbSession.id,
|
|
406
|
+
baseProjectRoot,
|
|
407
|
+
metadata,
|
|
408
|
+
);
|
|
409
|
+
const planManager = this._hydratePlanManager(metadata.planSnapshot);
|
|
410
|
+
const externalTools = this._buildSessionExternalTools();
|
|
218
411
|
let contextEngine = null;
|
|
219
412
|
let permanentMemory = null;
|
|
220
413
|
|
|
221
414
|
try {
|
|
222
|
-
const memoryDir = path.join(
|
|
415
|
+
const memoryDir = path.join(workspace.projectRoot, "memory");
|
|
223
416
|
permanentMemory = new CLIPermanentMemory({
|
|
224
417
|
db: this.db,
|
|
225
418
|
memoryDir,
|
|
@@ -240,19 +433,26 @@ export class WSSessionManager {
|
|
|
240
433
|
|
|
241
434
|
const session = {
|
|
242
435
|
id: dbSession.id,
|
|
243
|
-
type: "agent",
|
|
436
|
+
type: metadata.sessionType || "agent",
|
|
244
437
|
status: "active",
|
|
245
438
|
messages,
|
|
246
439
|
provider: dbSession.provider || "ollama",
|
|
247
440
|
model: dbSession.model || null,
|
|
248
441
|
apiKey: null,
|
|
249
|
-
baseUrl: "http://localhost:11434",
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
442
|
+
baseUrl: metadata.baseUrl || "http://localhost:11434",
|
|
443
|
+
mcpClient: this.mcpClient,
|
|
444
|
+
enabledToolNames: this._normalizeEnabledToolNames(
|
|
445
|
+
metadata.enabledToolNames,
|
|
446
|
+
),
|
|
447
|
+
hostManagedToolPolicy: metadata.hostManagedToolPolicy || null,
|
|
448
|
+
externalToolDefinitions: externalTools.definitions,
|
|
449
|
+
externalToolDescriptors: externalTools.descriptors,
|
|
450
|
+
externalToolExecutors: externalTools.executors,
|
|
451
|
+
projectRoot: workspace.projectRoot,
|
|
452
|
+
baseProjectRoot,
|
|
253
453
|
rulesContent: null,
|
|
254
|
-
worktreeIsolation:
|
|
255
|
-
worktree:
|
|
454
|
+
worktreeIsolation: metadata.worktreeIsolation === true,
|
|
455
|
+
worktree: workspace.worktree,
|
|
256
456
|
planManager,
|
|
257
457
|
contextEngine,
|
|
258
458
|
permanentMemory,
|
|
@@ -261,6 +461,7 @@ export class WSSessionManager {
|
|
|
261
461
|
lastActivity: new Date().toISOString(),
|
|
262
462
|
};
|
|
263
463
|
|
|
464
|
+
this._bindPlanManagerPersistence(session);
|
|
264
465
|
this.sessions.set(session.id, session);
|
|
265
466
|
return session;
|
|
266
467
|
} catch (_err) {
|
|
@@ -280,13 +481,7 @@ export class WSSessionManager {
|
|
|
280
481
|
session.status = "closed";
|
|
281
482
|
|
|
282
483
|
// Persist messages to DB
|
|
283
|
-
|
|
284
|
-
try {
|
|
285
|
-
dbSaveMessages(this.db, sessionId, session.messages);
|
|
286
|
-
} catch (_err) {
|
|
287
|
-
// Non-critical
|
|
288
|
-
}
|
|
289
|
-
}
|
|
484
|
+
this._persistSessionState(sessionId);
|
|
290
485
|
|
|
291
486
|
// Auto-summarize into permanent memory
|
|
292
487
|
if (session.permanentMemory && session.messages.length > 4) {
|
|
@@ -306,6 +501,13 @@ export class WSSessionManager {
|
|
|
306
501
|
|
|
307
502
|
// Clean up plan manager listeners
|
|
308
503
|
if (session.planManager) {
|
|
504
|
+
if (typeof session._planPersistenceCleanup === "function") {
|
|
505
|
+
try {
|
|
506
|
+
session._planPersistenceCleanup();
|
|
507
|
+
} catch (_err) {
|
|
508
|
+
// Non-critical.
|
|
509
|
+
}
|
|
510
|
+
}
|
|
309
511
|
session.planManager.removeAllListeners();
|
|
310
512
|
}
|
|
311
513
|
|
|
@@ -339,6 +541,7 @@ export class WSSessionManager {
|
|
|
339
541
|
provider: session.provider,
|
|
340
542
|
model: session.model,
|
|
341
543
|
messageCount: session.messages.length,
|
|
544
|
+
enabledToolNames: session.enabledToolNames || [],
|
|
342
545
|
baseProjectRoot: session.baseProjectRoot,
|
|
343
546
|
worktreeIsolation: session.worktreeIsolation === true,
|
|
344
547
|
worktree: session.worktree || null,
|
|
@@ -353,14 +556,21 @@ export class WSSessionManager {
|
|
|
353
556
|
const dbSessions = dbListSessions(this.db, { limit: 20 });
|
|
354
557
|
const inMemoryIds = new Set(this.sessions.keys());
|
|
355
558
|
for (const dbs of dbSessions) {
|
|
559
|
+
const metadata = this._normalizeSessionMetadata(dbs.metadata);
|
|
356
560
|
if (!inMemoryIds.has(dbs.id)) {
|
|
357
561
|
results.push({
|
|
358
562
|
id: dbs.id,
|
|
359
|
-
type: "unknown",
|
|
563
|
+
type: metadata.sessionType || "unknown",
|
|
360
564
|
status: "persisted",
|
|
361
565
|
provider: dbs.provider,
|
|
362
566
|
model: dbs.model,
|
|
363
567
|
messageCount: dbs.message_count,
|
|
568
|
+
enabledToolNames: Array.isArray(metadata.enabledToolNames)
|
|
569
|
+
? metadata.enabledToolNames
|
|
570
|
+
: [],
|
|
571
|
+
baseProjectRoot: metadata.baseProjectRoot || null,
|
|
572
|
+
worktreeIsolation: metadata.worktreeIsolation === true,
|
|
573
|
+
worktree: metadata.worktree || null,
|
|
364
574
|
createdAt: dbs.created_at,
|
|
365
575
|
lastActivity: dbs.updated_at,
|
|
366
576
|
});
|
|
@@ -397,6 +607,7 @@ export class WSSessionManager {
|
|
|
397
607
|
|
|
398
608
|
session.hostManagedToolPolicy = hostManagedToolPolicy || null;
|
|
399
609
|
session.lastActivity = new Date().toISOString();
|
|
610
|
+
this._persistSessionState(sessionId);
|
|
400
611
|
return session;
|
|
401
612
|
}
|
|
402
613
|
|
|
@@ -408,7 +619,12 @@ export class WSSessionManager {
|
|
|
408
619
|
if (!session || !this.db) return;
|
|
409
620
|
|
|
410
621
|
try {
|
|
411
|
-
dbSaveMessages(
|
|
622
|
+
dbSaveMessages(
|
|
623
|
+
this.db,
|
|
624
|
+
sessionId,
|
|
625
|
+
session.messages,
|
|
626
|
+
this._serializeSessionMetadata(session),
|
|
627
|
+
);
|
|
412
628
|
} catch (_err) {
|
|
413
629
|
// Non-critical
|
|
414
630
|
}
|
|
@@ -442,4 +658,156 @@ export class WSSessionManager {
|
|
|
442
658
|
},
|
|
443
659
|
};
|
|
444
660
|
}
|
|
661
|
+
|
|
662
|
+
_persistSessionState(sessionId) {
|
|
663
|
+
const session = this.sessions.get(sessionId);
|
|
664
|
+
if (!session || !this.db) return;
|
|
665
|
+
|
|
666
|
+
try {
|
|
667
|
+
dbSaveMessages(
|
|
668
|
+
this.db,
|
|
669
|
+
sessionId,
|
|
670
|
+
session.messages,
|
|
671
|
+
this._serializeSessionMetadata(session),
|
|
672
|
+
);
|
|
673
|
+
} catch (_err) {
|
|
674
|
+
// Non-critical
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
session.lastActivity = new Date().toISOString();
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
_serializeSessionMetadata(session) {
|
|
681
|
+
return {
|
|
682
|
+
version: 1,
|
|
683
|
+
sessionType: session.type || "agent",
|
|
684
|
+
projectRoot: session.projectRoot || null,
|
|
685
|
+
baseProjectRoot: session.baseProjectRoot || session.projectRoot || null,
|
|
686
|
+
baseUrl: session.baseUrl || null,
|
|
687
|
+
hostManagedToolPolicy: session.hostManagedToolPolicy || null,
|
|
688
|
+
enabledToolNames: session.enabledToolNames || [],
|
|
689
|
+
worktreeIsolation: session.worktreeIsolation === true,
|
|
690
|
+
worktree: session.worktree || null,
|
|
691
|
+
planSnapshot: this._serializePlanManager(session.planManager),
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
_serializePlanManager(planManager) {
|
|
696
|
+
if (!planManager) {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return {
|
|
701
|
+
state: planManager.state || PlanState.INACTIVE,
|
|
702
|
+
currentPlan: planManager.currentPlan || null,
|
|
703
|
+
history: Array.isArray(planManager.history) ? planManager.history : [],
|
|
704
|
+
blockedToolLog: Array.isArray(planManager.blockedToolLog)
|
|
705
|
+
? planManager.blockedToolLog
|
|
706
|
+
: [],
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
_normalizeSessionMetadata(metadata) {
|
|
711
|
+
if (!metadata) {
|
|
712
|
+
return {};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (typeof metadata === "string") {
|
|
716
|
+
try {
|
|
717
|
+
return JSON.parse(metadata);
|
|
718
|
+
} catch (_err) {
|
|
719
|
+
return {};
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return typeof metadata === "object" ? metadata : {};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
_hydratePlanManager(snapshot) {
|
|
727
|
+
const planManager = new PlanModeManager();
|
|
728
|
+
if (!snapshot || typeof snapshot !== "object") {
|
|
729
|
+
return planManager;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
planManager.state = snapshot.state || PlanState.INACTIVE;
|
|
733
|
+
planManager.currentPlan = snapshot.currentPlan
|
|
734
|
+
? new ExecutionPlan(snapshot.currentPlan)
|
|
735
|
+
: null;
|
|
736
|
+
planManager.history = Array.isArray(snapshot.history)
|
|
737
|
+
? snapshot.history.map((plan) => new ExecutionPlan(plan))
|
|
738
|
+
: [];
|
|
739
|
+
planManager.blockedToolLog = Array.isArray(snapshot.blockedToolLog)
|
|
740
|
+
? [...snapshot.blockedToolLog]
|
|
741
|
+
: [];
|
|
742
|
+
return planManager;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
_restoreSessionWorkspace(sessionId, baseProjectRoot, metadata = {}) {
|
|
746
|
+
const requestedWorktreeIsolation = metadata.worktreeIsolation === true;
|
|
747
|
+
const persistedWorktreePath = metadata.worktree?.path || null;
|
|
748
|
+
|
|
749
|
+
if (!requestedWorktreeIsolation) {
|
|
750
|
+
return {
|
|
751
|
+
projectRoot: metadata.projectRoot || baseProjectRoot,
|
|
752
|
+
worktree: null,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (persistedWorktreePath && fs.existsSync(persistedWorktreePath)) {
|
|
757
|
+
return {
|
|
758
|
+
projectRoot: persistedWorktreePath,
|
|
759
|
+
worktree: {
|
|
760
|
+
...(metadata.worktree || {}),
|
|
761
|
+
baseProjectRoot,
|
|
762
|
+
},
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
try {
|
|
767
|
+
return this._prepareSessionWorkspace(baseProjectRoot, sessionId, {
|
|
768
|
+
worktreeIsolation: true,
|
|
769
|
+
});
|
|
770
|
+
} catch (_err) {
|
|
771
|
+
return {
|
|
772
|
+
projectRoot: baseProjectRoot,
|
|
773
|
+
worktree: null,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
_bindPlanManagerPersistence(session) {
|
|
779
|
+
if (
|
|
780
|
+
!session?.id ||
|
|
781
|
+
!session.planManager ||
|
|
782
|
+
typeof session.planManager.on !== "function"
|
|
783
|
+
) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (typeof session._planPersistenceCleanup === "function") {
|
|
788
|
+
session._planPersistenceCleanup();
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const persist = () => this._persistSessionState(session.id);
|
|
792
|
+
const events = [
|
|
793
|
+
"enter",
|
|
794
|
+
"exit",
|
|
795
|
+
"item-added",
|
|
796
|
+
"plan-ready",
|
|
797
|
+
"plan-approved",
|
|
798
|
+
"tool-blocked",
|
|
799
|
+
];
|
|
800
|
+
|
|
801
|
+
for (const eventName of events) {
|
|
802
|
+
session.planManager.on(eventName, persist);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
session._planPersistenceCleanup = () => {
|
|
806
|
+
if (typeof session.planManager.off === "function") {
|
|
807
|
+
for (const eventName of events) {
|
|
808
|
+
session.planManager.off(eventName, persist);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
}
|
|
445
813
|
}
|