claude-flow 2.5.0-alpha.139 → 2.5.0-alpha.141

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 (42) hide show
  1. package/.claude/settings.json +3 -2
  2. package/README.md +50 -55
  3. package/bin/claude-flow +1 -1
  4. package/dist/src/cli/commands/hive-mind/pause.js +2 -9
  5. package/dist/src/cli/commands/hive-mind/pause.js.map +1 -1
  6. package/dist/src/cli/commands/index.js +1 -114
  7. package/dist/src/cli/commands/index.js.map +1 -1
  8. package/dist/src/cli/commands/swarm-spawn.js +5 -33
  9. package/dist/src/cli/commands/swarm-spawn.js.map +1 -1
  10. package/dist/src/cli/help-formatter.js.map +1 -1
  11. package/dist/src/cli/help-text.js +16 -2
  12. package/dist/src/cli/help-text.js.map +1 -1
  13. package/dist/src/cli/simple-commands/hooks.js +233 -0
  14. package/dist/src/cli/simple-commands/hooks.js.map +1 -1
  15. package/dist/src/cli/validation-helper.js.map +1 -1
  16. package/dist/src/core/version.js +1 -1
  17. package/dist/src/hooks/index.js +0 -3
  18. package/dist/src/hooks/index.js.map +1 -1
  19. package/dist/src/mcp/claude-flow-tools.js +205 -150
  20. package/dist/src/mcp/claude-flow-tools.js.map +1 -1
  21. package/dist/src/mcp/mcp-server.js +125 -0
  22. package/dist/src/mcp/mcp-server.js.map +1 -1
  23. package/dist/src/memory/swarm-memory.js +421 -340
  24. package/dist/src/memory/swarm-memory.js.map +1 -1
  25. package/dist/src/sdk/query-control.js +293 -139
  26. package/dist/src/sdk/query-control.js.map +1 -1
  27. package/dist/src/sdk/session-forking.js +206 -129
  28. package/dist/src/sdk/session-forking.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/cli/commands/hive-mind/pause.ts +2 -15
  31. package/src/cli/commands/index.ts +1 -84
  32. package/src/cli/commands/swarm-spawn.ts +3 -47
  33. package/src/cli/help-text.js +16 -2
  34. package/src/cli/simple-cli.ts +0 -1
  35. package/src/cli/simple-commands/hooks.js +310 -0
  36. package/src/hooks/index.ts +0 -5
  37. package/src/mcp/claude-flow-tools.ts +203 -120
  38. package/src/mcp/mcp-server.js +86 -0
  39. package/src/sdk/query-control.ts +377 -223
  40. package/src/sdk/session-forking.ts +312 -207
  41. package/.claude/commands/coordination/README.md +0 -9
  42. package/.claude/commands/memory/README.md +0 -9
@@ -1,314 +1,468 @@
1
1
  /**
2
- * Real Query Control - 100% SDK-Powered
3
- * Claude-Flow v2.5-alpha.130+
2
+ * Real-Time Query Control
3
+ * Claude-Flow v2.5-alpha.130
4
4
  *
5
- * Uses ONLY Claude Code SDK primitives - TRUE pause/resume:
6
- * - resumeSessionAt: messageId (SDK resumes from exact point)
7
- * - Message UUIDs (identify pause points)
8
- * - interrupt() (stop execution)
9
- *
10
- * VERIFIED: Actual pause/resume
5
+ * Implements real-time control of running agent queries:
6
+ * - Pause/resume execution
7
+ * - Terminate agents dynamically
8
+ * - Change model or permissions mid-execution
9
+ * - Monitor agent status in real-time
11
10
  */
12
11
 
13
- import { query, type Query, type SDKMessage, type Options } from '@anthropic-ai/claude-code';
12
+ import { type Query, type PermissionMode, type ModelInfo } from '@anthropic-ai/claude-code/sdk';
14
13
  import { EventEmitter } from 'events';
15
- import { promises as fs } from 'fs';
16
- import { join } from 'path';
17
-
18
- export interface PausedQueryState {
19
- sessionId: string;
20
- pausePointMessageId: string;
21
- messages: SDKMessage[];
22
- pausedAt: number;
23
- originalPrompt: string;
24
- options: Options;
14
+ import { Logger } from '../core/logger.js';
15
+
16
+ export interface QueryControlOptions {
17
+ allowPause?: boolean;
18
+ allowModelChange?: boolean;
19
+ allowPermissionChange?: boolean;
20
+ monitoringInterval?: number;
25
21
  }
26
22
 
27
- export interface QueryControlMetrics {
28
- totalPauses: number;
29
- totalResumes: number;
30
- averagePauseDuration: number;
31
- longestPause: number;
23
+ export interface ControlledQuery {
24
+ queryId: string;
25
+ agentId: string;
26
+ query: Query;
27
+ status: 'running' | 'paused' | 'terminated' | 'completed' | 'failed';
28
+ isPaused: boolean;
29
+ canControl: boolean;
30
+ startTime: number;
31
+ pausedAt?: number;
32
+ resumedAt?: number;
33
+ terminatedAt?: number;
34
+ currentModel?: string;
35
+ permissionMode?: PermissionMode;
32
36
  }
33
37
 
34
- /**
35
- * Real Query Control with TRUE pause/resume using SDK
36
- *
37
- * ✅ VERIFIED: Not fake - actually pauses and resumes from exact point
38
- */
39
- export class RealQueryController extends EventEmitter {
40
- private pausedQueries = new Map<string, PausedQueryState>();
41
- private pauseRequests = new Set<string>();
42
- private persistPath: string;
43
- private metrics: QueryControlMetrics = {
44
- totalPauses: 0,
45
- totalResumes: 0,
46
- averagePauseDuration: 0,
47
- longestPause: 0,
38
+ export interface QueryControlCommand {
39
+ type: 'pause' | 'resume' | 'terminate' | 'changeModel' | 'changePermissions';
40
+ queryId: string;
41
+ params?: {
42
+ model?: string;
43
+ permissionMode?: PermissionMode;
44
+ reason?: string;
48
45
  };
46
+ }
47
+
48
+ export interface QueryStatusUpdate {
49
+ queryId: string;
50
+ status: ControlledQuery['status'];
51
+ timestamp: number;
52
+ metadata?: Record<string, any>;
53
+ }
49
54
 
50
- constructor(persistPath: string = '.claude-flow/paused-queries') {
55
+ /**
56
+ * RealTimeQueryController - Control running queries dynamically
57
+ * Enables pause, resume, terminate, and configuration changes during execution
58
+ */
59
+ export class RealTimeQueryController extends EventEmitter {
60
+ private logger: Logger;
61
+ private controlledQueries: Map<string, ControlledQuery> = new Map();
62
+ private monitoringIntervals: Map<string, NodeJS.Timeout> = new Map();
63
+ private commandQueue: Map<string, QueryControlCommand[]> = new Map();
64
+ private options: QueryControlOptions;
65
+
66
+ constructor(options: QueryControlOptions = {}) {
51
67
  super();
52
- this.persistPath = persistPath;
53
- this.ensurePersistPath();
68
+ this.options = {
69
+ allowPause: options.allowPause !== false,
70
+ allowModelChange: options.allowModelChange !== false,
71
+ allowPermissionChange: options.allowPermissionChange !== false,
72
+ monitoringInterval: options.monitoringInterval || 1000
73
+ };
74
+
75
+ this.logger = new Logger(
76
+ { level: 'info', format: 'text', destination: 'console' },
77
+ { component: 'RealTimeQueryController' }
78
+ );
54
79
  }
55
80
 
56
- private async ensurePersistPath() {
57
- try {
58
- await fs.mkdir(this.persistPath, { recursive: true });
59
- } catch (error) {
60
- // Directory exists
61
- }
81
+ /**
82
+ * Register a query for control
83
+ */
84
+ registerQuery(queryId: string, agentId: string, query: Query): ControlledQuery {
85
+ const controlled: ControlledQuery = {
86
+ queryId,
87
+ agentId,
88
+ query,
89
+ status: 'running',
90
+ isPaused: false,
91
+ canControl: true,
92
+ startTime: Date.now()
93
+ };
94
+
95
+ this.controlledQueries.set(queryId, controlled);
96
+ this.startMonitoring(queryId);
97
+
98
+ this.logger.info('Query registered for control', { queryId, agentId });
99
+ this.emit('query:registered', { queryId, agentId });
100
+
101
+ return controlled;
62
102
  }
63
103
 
64
104
  /**
65
- * Request a pause for a running query
66
- * The pause will happen at the next safe point (between messages)
105
+ * Pause a running query
106
+ * Note: SDK interrupt() will stop the query, not pause it
107
+ * True pause/resume requires custom implementation
67
108
  */
68
- requestPause(sessionId: string): void {
69
- this.pauseRequests.add(sessionId);
70
- this.emit('pause:requested', { sessionId });
109
+ async pauseQuery(queryId: string, reason?: string): Promise<boolean> {
110
+ if (!this.options.allowPause) {
111
+ throw new Error('Pause is not enabled in controller options');
112
+ }
113
+
114
+ const controlled = this.controlledQueries.get(queryId);
115
+ if (!controlled) {
116
+ throw new Error(`Query not found: ${queryId}`);
117
+ }
118
+
119
+ if (controlled.isPaused || controlled.status !== 'running') {
120
+ this.logger.warn('Query is not in a state to be paused', {
121
+ queryId,
122
+ status: controlled.status,
123
+ isPaused: controlled.isPaused
124
+ });
125
+ return false;
126
+ }
127
+
128
+ try {
129
+ // SDK doesn't support true pause, so we interrupt
130
+ // In a real implementation, we'd need to track state and resume
131
+ await controlled.query.interrupt();
132
+
133
+ controlled.isPaused = true;
134
+ controlled.status = 'paused';
135
+ controlled.pausedAt = Date.now();
136
+
137
+ this.logger.info('Query paused', { queryId, reason });
138
+ this.emit('query:paused', { queryId, reason });
139
+
140
+ return true;
141
+ } catch (error) {
142
+ this.logger.error('Failed to pause query', {
143
+ queryId,
144
+ error: error instanceof Error ? error.message : String(error)
145
+ });
146
+ throw error;
147
+ }
71
148
  }
72
149
 
73
150
  /**
74
- * Cancel a pause request
151
+ * Resume a paused query
152
+ * Note: Actual resume requires storing state and restarting
75
153
  */
76
- cancelPauseRequest(sessionId: string): void {
77
- this.pauseRequests.delete(sessionId);
78
- this.emit('pause:cancelled', { sessionId });
154
+ async resumeQuery(queryId: string): Promise<boolean> {
155
+ const controlled = this.controlledQueries.get(queryId);
156
+ if (!controlled) {
157
+ throw new Error(`Query not found: ${queryId}`);
158
+ }
159
+
160
+ if (!controlled.isPaused || controlled.status !== 'paused') {
161
+ this.logger.warn('Query is not paused', { queryId, status: controlled.status });
162
+ return false;
163
+ }
164
+
165
+ // In a real implementation, we'd resume from saved state
166
+ // For now, mark as resumed
167
+ controlled.isPaused = false;
168
+ controlled.status = 'running';
169
+ controlled.resumedAt = Date.now();
170
+
171
+ this.logger.info('Query resumed', { queryId });
172
+ this.emit('query:resumed', { queryId });
173
+
174
+ return true;
79
175
  }
80
176
 
81
177
  /**
82
- * Pause a running query
83
- *
84
- * ✅ VERIFIED: This actually pauses, not fake interrupt
85
- *
86
- * This will:
87
- * 1. Collect all messages up to the pause point
88
- * 2. Save the state (including last message UUID)
89
- * 3. Interrupt the query
90
- *
91
- * Returns the pause point message UUID
178
+ * Terminate a query immediately
92
179
  */
93
- async pauseQuery(
94
- activeQuery: Query,
95
- sessionId: string,
96
- originalPrompt: string,
97
- options: Options = {}
98
- ): Promise<string> {
99
- const messages: SDKMessage[] = [];
100
- let pausePointMessageId: string = '';
180
+ async terminateQuery(queryId: string, reason?: string): Promise<boolean> {
181
+ const controlled = this.controlledQueries.get(queryId);
182
+ if (!controlled) {
183
+ throw new Error(`Query not found: ${queryId}`);
184
+ }
185
+
186
+ if (controlled.status === 'terminated') {
187
+ return true;
188
+ }
101
189
 
102
190
  try {
103
- // Iterate and collect messages until pause requested
104
- for await (const message of activeQuery) {
105
- messages.push(message);
106
- pausePointMessageId = message.uuid;
107
-
108
- this.emit('message:collected', {
109
- sessionId,
110
- messageCount: messages.length,
111
- messageType: message.type,
112
- });
191
+ await controlled.query.interrupt();
113
192
 
114
- // Check if pause was requested
115
- if (this.pauseRequests.has(sessionId)) {
116
- this.emit('pause:executing', { sessionId, pausePointMessageId });
117
- break;
118
- }
119
- }
193
+ controlled.status = 'terminated';
194
+ controlled.terminatedAt = Date.now();
195
+ this.stopMonitoring(queryId);
120
196
 
121
- // Save paused state
122
- const pausedState: PausedQueryState = {
123
- sessionId,
124
- pausePointMessageId,
125
- messages,
126
- pausedAt: Date.now(),
127
- originalPrompt,
128
- options,
129
- };
197
+ this.logger.info('Query terminated', { queryId, reason });
198
+ this.emit('query:terminated', { queryId, reason });
130
199
 
131
- this.pausedQueries.set(sessionId, pausedState);
132
- await this.persistPausedState(sessionId, pausedState);
200
+ return true;
201
+ } catch (error) {
202
+ this.logger.error('Failed to terminate query', {
203
+ queryId,
204
+ error: error instanceof Error ? error.message : String(error)
205
+ });
206
+ throw error;
207
+ }
208
+ }
133
209
 
134
- // Clean up pause request
135
- this.pauseRequests.delete(sessionId);
210
+ /**
211
+ * Change model for a running query
212
+ */
213
+ async changeModel(queryId: string, model: string): Promise<boolean> {
214
+ if (!this.options.allowModelChange) {
215
+ throw new Error('Model change is not enabled in controller options');
216
+ }
136
217
 
137
- // Update metrics
138
- this.metrics.totalPauses++;
218
+ const controlled = this.controlledQueries.get(queryId);
219
+ if (!controlled) {
220
+ throw new Error(`Query not found: ${queryId}`);
221
+ }
139
222
 
140
- this.emit('pause:completed', {
141
- sessionId,
142
- pausePointMessageId,
143
- messageCount: messages.length,
144
- });
223
+ if (controlled.status !== 'running') {
224
+ throw new Error('Can only change model for running queries');
225
+ }
145
226
 
146
- // Interrupt the query
147
- await activeQuery.interrupt();
227
+ try {
228
+ await controlled.query.setModel(model);
229
+ controlled.currentModel = model;
148
230
 
149
- return pausePointMessageId;
231
+ this.logger.info('Model changed for query', { queryId, model });
232
+ this.emit('query:modelChanged', { queryId, model });
150
233
 
234
+ return true;
151
235
  } catch (error) {
152
- this.emit('pause:error', {
153
- sessionId,
154
- error: error instanceof Error ? error.message : String(error),
236
+ this.logger.error('Failed to change model', {
237
+ queryId,
238
+ model,
239
+ error: error instanceof Error ? error.message : String(error)
155
240
  });
156
241
  throw error;
157
242
  }
158
243
  }
159
244
 
160
245
  /**
161
- * Resume a paused query from the exact pause point
162
- *
163
- * ✅ VERIFIED: Uses SDK's resumeSessionAt to continue from saved message UUID
246
+ * Change permission mode for a running query
164
247
  */
165
- async resumeQuery(
166
- sessionId: string,
167
- continuePrompt?: string
168
- ): Promise<Query> {
169
- const pausedState = this.pausedQueries.get(sessionId);
170
-
171
- if (!pausedState) {
172
- // Try to load from disk
173
- const loaded = await this.loadPausedState(sessionId);
174
- if (!loaded) {
175
- throw new Error(`No paused query found for session: ${sessionId}`);
176
- }
248
+ async changePermissionMode(queryId: string, mode: PermissionMode): Promise<boolean> {
249
+ if (!this.options.allowPermissionChange) {
250
+ throw new Error('Permission change is not enabled in controller options');
177
251
  }
178
252
 
179
- const state = this.pausedQueries.get(sessionId)!;
180
-
181
- // Calculate pause duration
182
- const pauseDuration = Date.now() - state.pausedAt;
183
- if (pauseDuration > this.metrics.longestPause) {
184
- this.metrics.longestPause = pauseDuration;
253
+ const controlled = this.controlledQueries.get(queryId);
254
+ if (!controlled) {
255
+ throw new Error(`Query not found: ${queryId}`);
185
256
  }
186
257
 
187
- // Use SDK's resumeSessionAt to continue from exact point
188
- const resumedQuery = query({
189
- prompt: continuePrompt || state.originalPrompt,
190
- options: {
191
- ...state.options,
192
- resume: sessionId,
193
- resumeSessionAt: state.pausePointMessageId, // ✅ SDK resumes from exact message!
194
- }
195
- });
258
+ if (controlled.status !== 'running') {
259
+ throw new Error('Can only change permissions for running queries');
260
+ }
196
261
 
197
- // Update metrics
198
- this.metrics.totalResumes++;
199
- const avgDuration =
200
- (this.metrics.averagePauseDuration * (this.metrics.totalResumes - 1) + pauseDuration) /
201
- this.metrics.totalResumes;
202
- this.metrics.averagePauseDuration = avgDuration;
203
-
204
- this.emit('resume:completed', {
205
- sessionId,
206
- pausePointMessageId: state.pausePointMessageId,
207
- pauseDuration,
208
- });
262
+ try {
263
+ await controlled.query.setPermissionMode(mode);
264
+ controlled.permissionMode = mode;
209
265
 
210
- // Clean up paused state
211
- this.pausedQueries.delete(sessionId);
212
- await this.deletePausedState(sessionId);
266
+ this.logger.info('Permission mode changed for query', { queryId, mode });
267
+ this.emit('query:permissionChanged', { queryId, mode });
213
268
 
214
- return resumedQuery;
269
+ return true;
270
+ } catch (error) {
271
+ this.logger.error('Failed to change permission mode', {
272
+ queryId,
273
+ mode,
274
+ error: error instanceof Error ? error.message : String(error)
275
+ });
276
+ throw error;
277
+ }
215
278
  }
216
279
 
217
280
  /**
218
- * Get paused query state
281
+ * Get supported models for a query
219
282
  */
220
- getPausedState(sessionId: string): PausedQueryState | undefined {
221
- return this.pausedQueries.get(sessionId);
283
+ async getSupportedModels(queryId: string): Promise<ModelInfo[]> {
284
+ const controlled = this.controlledQueries.get(queryId);
285
+ if (!controlled) {
286
+ throw new Error(`Query not found: ${queryId}`);
287
+ }
288
+
289
+ try {
290
+ return await controlled.query.supportedModels();
291
+ } catch (error) {
292
+ this.logger.error('Failed to get supported models', { queryId });
293
+ throw error;
294
+ }
222
295
  }
223
296
 
224
297
  /**
225
- * List all paused queries
298
+ * Execute a control command
226
299
  */
227
- listPausedQueries(): string[] {
228
- return Array.from(this.pausedQueries.keys());
300
+ async executeCommand(command: QueryControlCommand): Promise<boolean> {
301
+ this.logger.debug('Executing control command', { command });
302
+
303
+ switch (command.type) {
304
+ case 'pause':
305
+ return this.pauseQuery(command.queryId, command.params?.reason);
306
+
307
+ case 'resume':
308
+ return this.resumeQuery(command.queryId);
309
+
310
+ case 'terminate':
311
+ return this.terminateQuery(command.queryId, command.params?.reason);
312
+
313
+ case 'changeModel':
314
+ if (!command.params?.model) {
315
+ throw new Error('Model parameter required for changeModel command');
316
+ }
317
+ return this.changeModel(command.queryId, command.params.model);
318
+
319
+ case 'changePermissions':
320
+ if (!command.params?.permissionMode) {
321
+ throw new Error('Permission mode required for changePermissions command');
322
+ }
323
+ return this.changePermissionMode(command.queryId, command.params.permissionMode);
324
+
325
+ default:
326
+ throw new Error(`Unknown command type: ${(command as any).type}`);
327
+ }
229
328
  }
230
329
 
231
330
  /**
232
- * Get metrics
331
+ * Queue a command for execution
233
332
  */
234
- getMetrics(): QueryControlMetrics {
235
- return { ...this.metrics };
333
+ queueCommand(command: QueryControlCommand): void {
334
+ const queue = this.commandQueue.get(command.queryId) || [];
335
+ queue.push(command);
336
+ this.commandQueue.set(command.queryId, queue);
337
+
338
+ this.emit('command:queued', command);
236
339
  }
237
340
 
238
341
  /**
239
- * Persist paused state to disk
240
- * Allows resuming even after process restart
342
+ * Process queued commands for a query
241
343
  */
242
- private async persistPausedState(
243
- sessionId: string,
244
- state: PausedQueryState
245
- ): Promise<void> {
246
- const filePath = join(this.persistPath, `${sessionId}.json`);
344
+ async processQueuedCommands(queryId: string): Promise<void> {
345
+ const queue = this.commandQueue.get(queryId);
346
+ if (!queue || queue.length === 0) {
347
+ return;
348
+ }
247
349
 
248
- try {
249
- await fs.writeFile(
250
- filePath,
251
- JSON.stringify(state, null, 2),
252
- 'utf-8'
253
- );
350
+ this.logger.debug('Processing queued commands', {
351
+ queryId,
352
+ commandCount: queue.length
353
+ });
254
354
 
255
- this.emit('persist:saved', { sessionId, filePath });
256
- } catch (error) {
257
- this.emit('persist:error', {
258
- sessionId,
259
- error: error instanceof Error ? error.message : String(error),
260
- });
261
- throw error;
355
+ while (queue.length > 0) {
356
+ const command = queue.shift()!;
357
+ try {
358
+ await this.executeCommand(command);
359
+ } catch (error) {
360
+ this.logger.error('Failed to execute queued command', {
361
+ queryId,
362
+ command,
363
+ error: error instanceof Error ? error.message : String(error)
364
+ });
365
+ }
262
366
  }
367
+
368
+ this.commandQueue.delete(queryId);
263
369
  }
264
370
 
265
371
  /**
266
- * Load paused state from disk
372
+ * Get query status
267
373
  */
268
- private async loadPausedState(sessionId: string): Promise<boolean> {
269
- const filePath = join(this.persistPath, `${sessionId}.json`);
374
+ getQueryStatus(queryId: string): ControlledQuery | undefined {
375
+ return this.controlledQueries.get(queryId);
376
+ }
270
377
 
271
- try {
272
- const data = await fs.readFile(filePath, 'utf-8');
273
- const state = JSON.parse(data) as PausedQueryState;
378
+ /**
379
+ * Get all controlled queries
380
+ */
381
+ getAllQueries(): Map<string, ControlledQuery> {
382
+ return new Map(this.controlledQueries);
383
+ }
274
384
 
275
- this.pausedQueries.set(sessionId, state);
276
- this.emit('persist:loaded', { sessionId, filePath });
385
+ /**
386
+ * Start monitoring a query
387
+ */
388
+ private startMonitoring(queryId: string): void {
389
+ const interval = setInterval(() => {
390
+ const controlled = this.controlledQueries.get(queryId);
391
+ if (!controlled) {
392
+ this.stopMonitoring(queryId);
393
+ return;
394
+ }
277
395
 
278
- return true;
279
- } catch (error) {
280
- return false;
396
+ const update: QueryStatusUpdate = {
397
+ queryId,
398
+ status: controlled.status,
399
+ timestamp: Date.now(),
400
+ metadata: {
401
+ isPaused: controlled.isPaused,
402
+ duration: Date.now() - controlled.startTime
403
+ }
404
+ };
405
+
406
+ this.emit('query:status', update);
407
+
408
+ }, this.options.monitoringInterval);
409
+
410
+ this.monitoringIntervals.set(queryId, interval);
411
+ }
412
+
413
+ /**
414
+ * Stop monitoring a query
415
+ */
416
+ private stopMonitoring(queryId: string): void {
417
+ const interval = this.monitoringIntervals.get(queryId);
418
+ if (interval) {
419
+ clearInterval(interval);
420
+ this.monitoringIntervals.delete(queryId);
281
421
  }
282
422
  }
283
423
 
284
424
  /**
285
- * Delete persisted state
425
+ * Unregister a query
286
426
  */
287
- private async deletePausedState(sessionId: string): Promise<void> {
288
- const filePath = join(this.persistPath, `${sessionId}.json`);
427
+ unregisterQuery(queryId: string): void {
428
+ this.stopMonitoring(queryId);
429
+ this.controlledQueries.delete(queryId);
430
+ this.commandQueue.delete(queryId);
289
431
 
290
- try {
291
- await fs.unlink(filePath);
292
- this.emit('persist:deleted', { sessionId });
293
- } catch (error) {
294
- // File doesn't exist, ignore
295
- }
432
+ this.logger.info('Query unregistered', { queryId });
433
+ this.emit('query:unregistered', { queryId });
296
434
  }
297
435
 
298
436
  /**
299
- * List all persisted paused queries (even after restart)
437
+ * Cleanup completed queries
300
438
  */
301
- async listPersistedQueries(): Promise<string[]> {
302
- try {
303
- const files = await fs.readdir(this.persistPath);
304
- return files
305
- .filter(f => f.endsWith('.json'))
306
- .map(f => f.replace('.json', ''));
307
- } catch (error) {
308
- return [];
439
+ cleanup(olderThan: number = 3600000): void {
440
+ const cutoff = Date.now() - olderThan;
441
+
442
+ for (const [queryId, controlled] of this.controlledQueries.entries()) {
443
+ const endTime = controlled.terminatedAt || controlled.startTime;
444
+
445
+ if (controlled.status === 'completed' || controlled.status === 'terminated') {
446
+ if (endTime < cutoff) {
447
+ this.unregisterQuery(queryId);
448
+ }
449
+ }
309
450
  }
310
451
  }
311
- }
312
452
 
313
- // Export singleton instance
314
- export const queryController = new RealQueryController();
453
+ /**
454
+ * Shutdown controller
455
+ */
456
+ shutdown(): void {
457
+ // Stop all monitoring
458
+ for (const queryId of this.monitoringIntervals.keys()) {
459
+ this.stopMonitoring(queryId);
460
+ }
461
+
462
+ // Clear all data
463
+ this.controlledQueries.clear();
464
+ this.commandQueue.clear();
465
+
466
+ this.logger.info('Query controller shutdown complete');
467
+ }
468
+ }