clearctx 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/LICENSE +21 -0
- package/README.md +1006 -0
- package/STRATEGY.md +485 -0
- package/bin/cli.js +1756 -0
- package/bin/continuity-hook.js +118 -0
- package/bin/mcp.js +27 -0
- package/bin/setup.js +929 -0
- package/package.json +56 -0
- package/src/artifact-store.js +710 -0
- package/src/atomic-io.js +99 -0
- package/src/briefing-generator.js +451 -0
- package/src/continuity-hooks.js +253 -0
- package/src/contract-store.js +525 -0
- package/src/decision-journal.js +229 -0
- package/src/delegate.js +348 -0
- package/src/dependency-resolver.js +453 -0
- package/src/diff-engine.js +473 -0
- package/src/file-lock.js +161 -0
- package/src/index.js +61 -0
- package/src/lineage-graph.js +402 -0
- package/src/manager.js +510 -0
- package/src/mcp-server.js +3501 -0
- package/src/pattern-registry.js +221 -0
- package/src/pipeline-engine.js +618 -0
- package/src/prompts.js +1217 -0
- package/src/safety-net.js +170 -0
- package/src/session-snapshot.js +508 -0
- package/src/snapshot-engine.js +490 -0
- package/src/stale-detector.js +169 -0
- package/src/store.js +131 -0
- package/src/stream-session.js +463 -0
- package/src/team-hub.js +615 -0
package/src/manager.js
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionManager — Multi-session orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Manages multiple StreamSession instances. Provides a high-level API for:
|
|
5
|
+
* - Spawning new sessions (stream or resume mode)
|
|
6
|
+
* - Sending messages to sessions
|
|
7
|
+
* - Pausing, resuming, forking, killing sessions
|
|
8
|
+
* - Tracking costs and interaction history
|
|
9
|
+
* - Persisting session state for restart recovery
|
|
10
|
+
*
|
|
11
|
+
* Usage (programmatic):
|
|
12
|
+
* const { SessionManager } = require('clearctx');
|
|
13
|
+
* const mgr = new SessionManager();
|
|
14
|
+
*
|
|
15
|
+
* // Spawn and interact
|
|
16
|
+
* const session = await mgr.spawn('fix-auth', { prompt: 'Fix the auth bug', model: 'sonnet' });
|
|
17
|
+
* const r1 = await mgr.send('fix-auth', 'Also add input validation');
|
|
18
|
+
* const r2 = await mgr.send('fix-auth', 'Now write tests');
|
|
19
|
+
*
|
|
20
|
+
* // Parallel tasks
|
|
21
|
+
* await Promise.all([
|
|
22
|
+
* mgr.spawn('task-a', { prompt: 'Build login page' }),
|
|
23
|
+
* mgr.spawn('task-b', { prompt: 'Build signup page' }),
|
|
24
|
+
* ]);
|
|
25
|
+
*
|
|
26
|
+
* // Check status
|
|
27
|
+
* console.log(mgr.list());
|
|
28
|
+
* mgr.stopAll();
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const StreamSession = require('./stream-session');
|
|
32
|
+
const Store = require('./store');
|
|
33
|
+
const { spawn: spawnProcess, execSync } = require('child_process');
|
|
34
|
+
|
|
35
|
+
class SessionManager {
|
|
36
|
+
/**
|
|
37
|
+
* @param {object} [options]
|
|
38
|
+
* @param {string} [options.dataDir] - Override store data directory
|
|
39
|
+
* @param {string} [options.defaultModel='sonnet'] - Default model for new sessions
|
|
40
|
+
* @param {string} [options.defaultPermissionMode='default'] - Default permission mode
|
|
41
|
+
* @param {number} [options.defaultTimeout=600000] - Default response timeout
|
|
42
|
+
*/
|
|
43
|
+
constructor(options = {}) {
|
|
44
|
+
this.store = new Store(options.dataDir);
|
|
45
|
+
this.defaultModel = options.defaultModel || 'sonnet';
|
|
46
|
+
this.defaultPermissionMode = options.defaultPermissionMode || 'default';
|
|
47
|
+
this.defaultTimeout = options.defaultTimeout || 600000;
|
|
48
|
+
|
|
49
|
+
// Live sessions (in-memory StreamSession instances)
|
|
50
|
+
this.sessions = new Map();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Spawn a new session and optionally send the first message.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} name - Unique session name
|
|
57
|
+
* @param {object} options - Session config
|
|
58
|
+
* @param {string} [options.prompt] - Initial prompt (if provided, sends immediately)
|
|
59
|
+
* @param {string} [options.model] - Model to use
|
|
60
|
+
* @param {string} [options.workDir] - Working directory
|
|
61
|
+
* @param {string} [options.permissionMode] - Permission mode
|
|
62
|
+
* @param {string[]} [options.allowedTools] - Restrict tools
|
|
63
|
+
* @param {string} [options.systemPrompt] - System prompt append
|
|
64
|
+
* @param {number} [options.maxBudget] - Max budget USD
|
|
65
|
+
* @param {string} [options.agent] - Agent to use
|
|
66
|
+
* @param {number} [options.timeout] - Response timeout
|
|
67
|
+
* @param {string[]} [options.tags] - Tags for organizing
|
|
68
|
+
* @returns {Promise<object>} { session, response? } — response is included if prompt was given
|
|
69
|
+
*/
|
|
70
|
+
async spawn(name, options = {}) {
|
|
71
|
+
// Check for name collision
|
|
72
|
+
if (this.sessions.has(name)) {
|
|
73
|
+
throw new Error(`Session "${name}" already exists and is alive. Kill it first or use a different name.`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const existingRecord = this.store.get(name);
|
|
77
|
+
if (existingRecord && !['stopped', 'killed', 'failed'].includes(existingRecord.status)) {
|
|
78
|
+
throw new Error(`Session "${name}" exists in store with status "${existingRecord.status}". Kill or delete it first.`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Create StreamSession
|
|
82
|
+
const session = new StreamSession({
|
|
83
|
+
name,
|
|
84
|
+
model: options.model || this.defaultModel,
|
|
85
|
+
workDir: options.workDir || process.cwd(),
|
|
86
|
+
permissionMode: options.permissionMode || this.defaultPermissionMode,
|
|
87
|
+
allowedTools: options.allowedTools || [],
|
|
88
|
+
systemPrompt: options.systemPrompt || '',
|
|
89
|
+
maxBudget: options.maxBudget || null,
|
|
90
|
+
agent: options.agent || null,
|
|
91
|
+
timeout: options.timeout || this.defaultTimeout,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Wire up events for persistence
|
|
95
|
+
this._wireEvents(session);
|
|
96
|
+
|
|
97
|
+
// Start the process (just spawns it, no waiting)
|
|
98
|
+
session.start();
|
|
99
|
+
|
|
100
|
+
// Store in memory
|
|
101
|
+
this.sessions.set(name, session);
|
|
102
|
+
|
|
103
|
+
// Persist to store
|
|
104
|
+
this._persist(name);
|
|
105
|
+
|
|
106
|
+
// If a prompt was given, send it right away
|
|
107
|
+
// The first send() triggers init + gets the response
|
|
108
|
+
let response = null;
|
|
109
|
+
if (options.prompt) {
|
|
110
|
+
response = await session.send(options.prompt);
|
|
111
|
+
this._persist(name);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { session: session.toJSON(), initData: session.initData, response };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Send a message to an existing session.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} name - Session name
|
|
121
|
+
* @param {string} message - The message to send
|
|
122
|
+
* @param {number} [timeout] - Override timeout
|
|
123
|
+
* @returns {Promise<object>} Response { text, cost, turns, duration, sessionId }
|
|
124
|
+
*/
|
|
125
|
+
async send(name, message, timeout) {
|
|
126
|
+
const session = this._getSession(name);
|
|
127
|
+
|
|
128
|
+
// If session is paused, auto-resume
|
|
129
|
+
if (session.status === 'paused') {
|
|
130
|
+
session.resume();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const response = await session.send(message, timeout);
|
|
134
|
+
this._persist(name);
|
|
135
|
+
return response;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Pause a session (stops accepting messages but keeps process alive).
|
|
140
|
+
*/
|
|
141
|
+
pause(name) {
|
|
142
|
+
const session = this._getSession(name);
|
|
143
|
+
session.pause();
|
|
144
|
+
this._persist(name);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resume a paused session (can optionally send a message).
|
|
149
|
+
*
|
|
150
|
+
* @param {string} name - Session name
|
|
151
|
+
* @param {string} [message] - Optional message to send after resuming
|
|
152
|
+
* @returns {Promise<object|null>} Response if message was sent
|
|
153
|
+
*/
|
|
154
|
+
async resume(name, message) {
|
|
155
|
+
// First check if session is alive in memory
|
|
156
|
+
if (this.sessions.has(name)) {
|
|
157
|
+
const session = this.sessions.get(name);
|
|
158
|
+
if (session.status === 'paused') {
|
|
159
|
+
session.resume();
|
|
160
|
+
}
|
|
161
|
+
if (message) {
|
|
162
|
+
const response = await session.send(message);
|
|
163
|
+
this._persist(name);
|
|
164
|
+
return response;
|
|
165
|
+
}
|
|
166
|
+
this._persist(name);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Session not alive — try to restore from store using --resume
|
|
171
|
+
const record = this.store.get(name);
|
|
172
|
+
if (!record) {
|
|
173
|
+
throw new Error(`Session "${name}" not found.`);
|
|
174
|
+
}
|
|
175
|
+
if (!record.claudeSessionId) {
|
|
176
|
+
throw new Error(`Session "${name}" has no session ID. Cannot restore.`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Spawn a new StreamSession that resumes the old one
|
|
180
|
+
const session = new StreamSession({
|
|
181
|
+
name,
|
|
182
|
+
model: record.model || this.defaultModel,
|
|
183
|
+
workDir: record.workDir || process.cwd(),
|
|
184
|
+
permissionMode: record.permissionMode || this.defaultPermissionMode,
|
|
185
|
+
allowedTools: record.allowedTools || [],
|
|
186
|
+
systemPrompt: record.systemPrompt || '',
|
|
187
|
+
maxBudget: record.maxBudget || null,
|
|
188
|
+
agent: record.agent || null,
|
|
189
|
+
sessionId: record.claudeSessionId, // Resume existing conversation
|
|
190
|
+
timeout: record.timeout || this.defaultTimeout,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
this._wireEvents(session);
|
|
194
|
+
session.start(); // Just spawns process, no await needed
|
|
195
|
+
this.sessions.set(name, session);
|
|
196
|
+
|
|
197
|
+
// Merge interaction history from store
|
|
198
|
+
session.interactions = record.interactions || [];
|
|
199
|
+
session.totalCostUsd = record.totalCostUsd || 0;
|
|
200
|
+
session.totalTurns = record.totalTurns || 0;
|
|
201
|
+
|
|
202
|
+
if (message) {
|
|
203
|
+
const response = await session.send(message);
|
|
204
|
+
this._persist(name);
|
|
205
|
+
return response;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this._persist(name);
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Fork a session — creates a new session with the parent's context.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} sourceName - Source session name
|
|
216
|
+
* @param {string} newName - New session name
|
|
217
|
+
* @param {object} [options] - Override options for the fork
|
|
218
|
+
* @param {string} [options.message] - Initial message for the fork
|
|
219
|
+
* @returns {Promise<object>} { session, response? }
|
|
220
|
+
*/
|
|
221
|
+
async fork(sourceName, newName, options = {}) {
|
|
222
|
+
// Get the source session ID (from live session or store)
|
|
223
|
+
let sourceSessionId;
|
|
224
|
+
let sourceRecord;
|
|
225
|
+
|
|
226
|
+
if (this.sessions.has(sourceName)) {
|
|
227
|
+
const source = this.sessions.get(sourceName);
|
|
228
|
+
sourceSessionId = source.id;
|
|
229
|
+
sourceRecord = source.toJSON();
|
|
230
|
+
} else {
|
|
231
|
+
sourceRecord = this.store.get(sourceName);
|
|
232
|
+
if (!sourceRecord) {
|
|
233
|
+
throw new Error(`Source session "${sourceName}" not found.`);
|
|
234
|
+
}
|
|
235
|
+
sourceSessionId = sourceRecord.claudeSessionId;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!sourceSessionId) {
|
|
239
|
+
throw new Error(`Source session "${sourceName}" has no session ID.`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Use the resume-mode approach for fork (--resume + --fork-session)
|
|
243
|
+
// because stream mode doesn't support --fork-session natively
|
|
244
|
+
const model = options.model || sourceRecord.model || this.defaultModel;
|
|
245
|
+
const workDir = options.workDir || sourceRecord.workDir || process.cwd();
|
|
246
|
+
const message = options.message || 'Continue from the forked conversation.';
|
|
247
|
+
|
|
248
|
+
// Use single-shot resume with fork
|
|
249
|
+
const args = [
|
|
250
|
+
'-p',
|
|
251
|
+
'--output-format', 'json',
|
|
252
|
+
'--model', model,
|
|
253
|
+
'--resume', sourceSessionId,
|
|
254
|
+
'--fork-session',
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
if (sourceRecord.permissionMode && sourceRecord.permissionMode !== 'default') {
|
|
258
|
+
args.push('--permission-mode', sourceRecord.permissionMode);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
args.push(message);
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const result = execSync(
|
|
265
|
+
`claude ${args.map(a => `"${a.replace(/"/g, '\\"')}"`).join(' ')}`,
|
|
266
|
+
{ cwd: workDir, encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024, timeout: 600000, windowsHide: true }
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const parsed = JSON.parse(result);
|
|
270
|
+
|
|
271
|
+
// Now spawn a new stream session that resumes the forked ID
|
|
272
|
+
const forked = new StreamSession({
|
|
273
|
+
name: newName,
|
|
274
|
+
model,
|
|
275
|
+
workDir,
|
|
276
|
+
permissionMode: sourceRecord.permissionMode || this.defaultPermissionMode,
|
|
277
|
+
sessionId: parsed.session_id, // The forked session's ID
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
this._wireEvents(forked);
|
|
281
|
+
forked.start(); // Just spawns process
|
|
282
|
+
this.sessions.set(newName, forked);
|
|
283
|
+
|
|
284
|
+
// Record the fork's first interaction
|
|
285
|
+
forked.interactions.push({
|
|
286
|
+
prompt: message,
|
|
287
|
+
response: parsed.result || '',
|
|
288
|
+
cost: parsed.cost_usd || 0,
|
|
289
|
+
turns: parsed.num_turns || 0,
|
|
290
|
+
duration: parsed.duration_ms || 0,
|
|
291
|
+
timestamp: new Date().toISOString(),
|
|
292
|
+
type: 'fork',
|
|
293
|
+
});
|
|
294
|
+
forked.totalCostUsd = parsed.cost_usd || 0;
|
|
295
|
+
forked.totalTurns = parsed.num_turns || 0;
|
|
296
|
+
|
|
297
|
+
this._persist(newName);
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
session: forked.toJSON(),
|
|
301
|
+
response: {
|
|
302
|
+
text: parsed.result || '',
|
|
303
|
+
cost: parsed.cost_usd || 0,
|
|
304
|
+
turns: parsed.num_turns || 0,
|
|
305
|
+
duration: parsed.duration_ms || 0,
|
|
306
|
+
sessionId: parsed.session_id,
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
} catch (err) {
|
|
311
|
+
throw new Error(`Fork failed: ${err.message}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Stop a session gracefully (closes stdin, process exits).
|
|
317
|
+
*/
|
|
318
|
+
stop(name) {
|
|
319
|
+
const session = this._getSession(name);
|
|
320
|
+
session.stop();
|
|
321
|
+
this._persist(name);
|
|
322
|
+
this.sessions.delete(name);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Force kill a session.
|
|
327
|
+
*/
|
|
328
|
+
kill(name) {
|
|
329
|
+
if (this.sessions.has(name)) {
|
|
330
|
+
const session = this.sessions.get(name);
|
|
331
|
+
session.kill();
|
|
332
|
+
this.sessions.delete(name);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Update store
|
|
336
|
+
const record = this.store.get(name);
|
|
337
|
+
if (record) {
|
|
338
|
+
record.status = 'killed';
|
|
339
|
+
this.store.set(name, record);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Stop all live sessions.
|
|
345
|
+
*/
|
|
346
|
+
stopAll() {
|
|
347
|
+
for (const [name, session] of this.sessions) {
|
|
348
|
+
session.stop();
|
|
349
|
+
this._persist(name);
|
|
350
|
+
}
|
|
351
|
+
this.sessions.clear();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get status of a session.
|
|
356
|
+
*/
|
|
357
|
+
status(name) {
|
|
358
|
+
// Check live session first
|
|
359
|
+
if (this.sessions.has(name)) {
|
|
360
|
+
return this.sessions.get(name).toJSON();
|
|
361
|
+
}
|
|
362
|
+
// Fall back to store
|
|
363
|
+
const record = this.store.get(name);
|
|
364
|
+
if (!record) {
|
|
365
|
+
throw new Error(`Session "${name}" not found.`);
|
|
366
|
+
}
|
|
367
|
+
return record;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* List all sessions (live + stored).
|
|
372
|
+
* @param {string} [statusFilter]
|
|
373
|
+
* @returns {object[]}
|
|
374
|
+
*/
|
|
375
|
+
list(statusFilter) {
|
|
376
|
+
const result = new Map();
|
|
377
|
+
|
|
378
|
+
// Load from store first
|
|
379
|
+
const stored = this.store.list();
|
|
380
|
+
for (const record of stored) {
|
|
381
|
+
result.set(record.name, record);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Override with live session data
|
|
385
|
+
for (const [name, session] of this.sessions) {
|
|
386
|
+
result.set(name, session.toJSON());
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let list = Array.from(result.values());
|
|
390
|
+
if (statusFilter) {
|
|
391
|
+
list = list.filter(s => s.status === statusFilter);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return list;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Get interaction history for a session.
|
|
399
|
+
*/
|
|
400
|
+
history(name) {
|
|
401
|
+
if (this.sessions.has(name)) {
|
|
402
|
+
return this.sessions.get(name).interactions;
|
|
403
|
+
}
|
|
404
|
+
const record = this.store.get(name);
|
|
405
|
+
if (!record) {
|
|
406
|
+
throw new Error(`Session "${name}" not found.`);
|
|
407
|
+
}
|
|
408
|
+
return record.interactions || [];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Get the last response from a session.
|
|
413
|
+
*/
|
|
414
|
+
lastOutput(name) {
|
|
415
|
+
const hist = this.history(name);
|
|
416
|
+
if (hist.length === 0) return null;
|
|
417
|
+
return hist[hist.length - 1];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Delete a session from store permanently.
|
|
422
|
+
*/
|
|
423
|
+
delete(name) {
|
|
424
|
+
if (this.sessions.has(name)) {
|
|
425
|
+
this.sessions.get(name).kill();
|
|
426
|
+
this.sessions.delete(name);
|
|
427
|
+
}
|
|
428
|
+
this.store.delete(name);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Clean up old sessions.
|
|
433
|
+
*/
|
|
434
|
+
cleanup(days = 7) {
|
|
435
|
+
return this.store.cleanup(days);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Batch spawn — start multiple sessions from a spec array.
|
|
440
|
+
*
|
|
441
|
+
* @param {object[]} specs - Array of { name, prompt, model?, ... }
|
|
442
|
+
* @returns {Promise<object[]>} Results for each spawn
|
|
443
|
+
*/
|
|
444
|
+
async batch(specs) {
|
|
445
|
+
// Spawn all in parallel
|
|
446
|
+
const promises = specs.map(spec =>
|
|
447
|
+
this.spawn(spec.name, spec).catch(err => ({
|
|
448
|
+
session: { name: spec.name, status: 'failed' },
|
|
449
|
+
error: err.message,
|
|
450
|
+
}))
|
|
451
|
+
);
|
|
452
|
+
return Promise.all(promises);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ===========================================================================
|
|
456
|
+
// Private
|
|
457
|
+
// ===========================================================================
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Get a live session or throw.
|
|
461
|
+
*/
|
|
462
|
+
_getSession(name) {
|
|
463
|
+
const session = this.sessions.get(name);
|
|
464
|
+
if (!session) {
|
|
465
|
+
throw new Error(`Session "${name}" is not alive. Use resume() to restore it.`);
|
|
466
|
+
}
|
|
467
|
+
return session;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Wire session events for logging/persistence.
|
|
472
|
+
*/
|
|
473
|
+
_wireEvents(session) {
|
|
474
|
+
session.on('error', (err) => {
|
|
475
|
+
this._persist(session.name);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
session.on('close', (code) => {
|
|
479
|
+
this._persist(session.name);
|
|
480
|
+
this.sessions.delete(session.name);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Persist a session's current state to store.
|
|
486
|
+
*/
|
|
487
|
+
_persist(name) {
|
|
488
|
+
const session = this.sessions.get(name);
|
|
489
|
+
if (!session) return;
|
|
490
|
+
|
|
491
|
+
this.store.set(name, {
|
|
492
|
+
name: session.name,
|
|
493
|
+
claudeSessionId: session.id,
|
|
494
|
+
status: session.status,
|
|
495
|
+
model: session.model,
|
|
496
|
+
workDir: session.workDir,
|
|
497
|
+
permissionMode: session.permissionMode,
|
|
498
|
+
allowedTools: session.allowedTools,
|
|
499
|
+
systemPrompt: session.systemPrompt,
|
|
500
|
+
maxBudget: session.maxBudget,
|
|
501
|
+
agent: session.agent,
|
|
502
|
+
totalCostUsd: session.totalCostUsd,
|
|
503
|
+
totalTurns: session.totalTurns,
|
|
504
|
+
interactions: session.interactions,
|
|
505
|
+
created: session.interactions[0]?.timestamp || new Date().toISOString(),
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
module.exports = SessionManager;
|