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.
- package/.claude/settings.json +3 -2
- package/README.md +50 -55
- package/bin/claude-flow +1 -1
- package/dist/src/cli/commands/hive-mind/pause.js +2 -9
- package/dist/src/cli/commands/hive-mind/pause.js.map +1 -1
- package/dist/src/cli/commands/index.js +1 -114
- package/dist/src/cli/commands/index.js.map +1 -1
- package/dist/src/cli/commands/swarm-spawn.js +5 -33
- package/dist/src/cli/commands/swarm-spawn.js.map +1 -1
- package/dist/src/cli/help-formatter.js.map +1 -1
- package/dist/src/cli/help-text.js +16 -2
- package/dist/src/cli/help-text.js.map +1 -1
- package/dist/src/cli/simple-commands/hooks.js +233 -0
- package/dist/src/cli/simple-commands/hooks.js.map +1 -1
- package/dist/src/cli/validation-helper.js.map +1 -1
- package/dist/src/core/version.js +1 -1
- package/dist/src/hooks/index.js +0 -3
- package/dist/src/hooks/index.js.map +1 -1
- package/dist/src/mcp/claude-flow-tools.js +205 -150
- package/dist/src/mcp/claude-flow-tools.js.map +1 -1
- package/dist/src/mcp/mcp-server.js +125 -0
- package/dist/src/mcp/mcp-server.js.map +1 -1
- package/dist/src/memory/swarm-memory.js +421 -340
- package/dist/src/memory/swarm-memory.js.map +1 -1
- package/dist/src/sdk/query-control.js +293 -139
- package/dist/src/sdk/query-control.js.map +1 -1
- package/dist/src/sdk/session-forking.js +206 -129
- package/dist/src/sdk/session-forking.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/hive-mind/pause.ts +2 -15
- package/src/cli/commands/index.ts +1 -84
- package/src/cli/commands/swarm-spawn.ts +3 -47
- package/src/cli/help-text.js +16 -2
- package/src/cli/simple-cli.ts +0 -1
- package/src/cli/simple-commands/hooks.js +310 -0
- package/src/hooks/index.ts +0 -5
- package/src/mcp/claude-flow-tools.ts +203 -120
- package/src/mcp/mcp-server.js +86 -0
- package/src/sdk/query-control.ts +377 -223
- package/src/sdk/session-forking.ts +312 -207
- package/.claude/commands/coordination/README.md +0 -9
- package/.claude/commands/memory/README.md +0 -9
package/src/sdk/query-control.ts
CHANGED
|
@@ -1,314 +1,468 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Real Query Control
|
|
3
|
-
* Claude-Flow v2.5-alpha.130
|
|
2
|
+
* Real-Time Query Control
|
|
3
|
+
* Claude-Flow v2.5-alpha.130
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
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 {
|
|
12
|
+
import { type Query, type PermissionMode, type ModelInfo } from '@anthropic-ai/claude-code/sdk';
|
|
14
13
|
import { EventEmitter } from 'events';
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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.
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
*
|
|
66
|
-
*
|
|
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
|
-
|
|
69
|
-
this.
|
|
70
|
-
|
|
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
|
-
*
|
|
151
|
+
* Resume a paused query
|
|
152
|
+
* Note: Actual resume requires storing state and restarting
|
|
75
153
|
*/
|
|
76
|
-
|
|
77
|
-
this.
|
|
78
|
-
|
|
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
|
-
*
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
break;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
193
|
+
controlled.status = 'terminated';
|
|
194
|
+
controlled.terminatedAt = Date.now();
|
|
195
|
+
this.stopMonitoring(queryId);
|
|
120
196
|
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
|
|
218
|
+
const controlled = this.controlledQueries.get(queryId);
|
|
219
|
+
if (!controlled) {
|
|
220
|
+
throw new Error(`Query not found: ${queryId}`);
|
|
221
|
+
}
|
|
139
222
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
147
|
-
await
|
|
227
|
+
try {
|
|
228
|
+
await controlled.query.setModel(model);
|
|
229
|
+
controlled.currentModel = model;
|
|
148
230
|
|
|
149
|
-
|
|
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.
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
*
|
|
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
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
|
281
|
+
* Get supported models for a query
|
|
219
282
|
*/
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
*
|
|
298
|
+
* Execute a control command
|
|
226
299
|
*/
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
*
|
|
331
|
+
* Queue a command for execution
|
|
233
332
|
*/
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
*
|
|
240
|
-
* Allows resuming even after process restart
|
|
342
|
+
* Process queued commands for a query
|
|
241
343
|
*/
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
'utf-8'
|
|
253
|
-
);
|
|
350
|
+
this.logger.debug('Processing queued commands', {
|
|
351
|
+
queryId,
|
|
352
|
+
commandCount: queue.length
|
|
353
|
+
});
|
|
254
354
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
*
|
|
372
|
+
* Get query status
|
|
267
373
|
*/
|
|
268
|
-
|
|
269
|
-
|
|
374
|
+
getQueryStatus(queryId: string): ControlledQuery | undefined {
|
|
375
|
+
return this.controlledQueries.get(queryId);
|
|
376
|
+
}
|
|
270
377
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
378
|
+
/**
|
|
379
|
+
* Get all controlled queries
|
|
380
|
+
*/
|
|
381
|
+
getAllQueries(): Map<string, ControlledQuery> {
|
|
382
|
+
return new Map(this.controlledQueries);
|
|
383
|
+
}
|
|
274
384
|
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
*
|
|
425
|
+
* Unregister a query
|
|
286
426
|
*/
|
|
287
|
-
|
|
288
|
-
|
|
427
|
+
unregisterQuery(queryId: string): void {
|
|
428
|
+
this.stopMonitoring(queryId);
|
|
429
|
+
this.controlledQueries.delete(queryId);
|
|
430
|
+
this.commandQueue.delete(queryId);
|
|
289
431
|
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
*
|
|
437
|
+
* Cleanup completed queries
|
|
300
438
|
*/
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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
|
+
}
|