claude-flow 2.5.0-alpha.138 → 2.5.0-alpha.139
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/bin/claude-flow +1 -1
- package/dist/src/cli/commands/checkpoint.js +156 -0
- package/dist/src/cli/commands/checkpoint.js.map +1 -0
- package/dist/src/cli/commands/hive-mind/pause.js +9 -2
- package/dist/src/cli/commands/hive-mind/pause.js.map +1 -1
- package/dist/src/cli/commands/index.js +114 -1
- package/dist/src/cli/commands/index.js.map +1 -1
- package/dist/src/cli/commands/swarm-spawn.js +33 -5
- package/dist/src/cli/commands/swarm-spawn.js.map +1 -1
- package/dist/src/cli/help-formatter.js +3 -0
- package/dist/src/cli/help-formatter.js.map +1 -1
- package/dist/src/cli/help-text.js +2 -16
- package/dist/src/cli/help-text.js.map +1 -1
- package/dist/src/cli/validation-helper.js.map +1 -1
- package/dist/src/hooks/index.js +3 -0
- package/dist/src/hooks/index.js.map +1 -1
- package/dist/src/mcp/claude-flow-tools.js +150 -205
- package/dist/src/mcp/claude-flow-tools.js.map +1 -1
- package/dist/src/mcp/mcp-server.js +0 -125
- package/dist/src/mcp/mcp-server.js.map +1 -1
- package/dist/src/sdk/checkpoint-manager.js +237 -0
- package/dist/src/sdk/checkpoint-manager.js.map +1 -0
- package/dist/src/sdk/claude-flow-mcp-integration.js +221 -0
- package/dist/src/sdk/claude-flow-mcp-integration.js.map +1 -0
- package/dist/src/sdk/in-process-mcp.js +374 -0
- package/dist/src/sdk/in-process-mcp.js.map +1 -0
- package/dist/src/sdk/query-control.js +139 -293
- package/dist/src/sdk/query-control.js.map +1 -1
- package/dist/src/sdk/session-forking.js +129 -206
- package/dist/src/sdk/session-forking.js.map +1 -1
- package/dist/src/sdk/validation-demo.js +369 -0
- package/dist/src/sdk/validation-demo.js.map +1 -0
- package/package.json +1 -1
- package/scripts/validate-sdk-integration.ts +188 -0
- package/src/cli/commands/checkpoint.ts +220 -0
- package/src/cli/commands/hive-mind/pause.ts +15 -2
- package/src/cli/commands/index.ts +84 -1
- package/src/cli/commands/swarm-spawn.ts +47 -3
- package/src/cli/help-text.js +2 -16
- package/src/cli/simple-cli.ts +1 -0
- package/src/hooks/index.ts +5 -0
- package/src/mcp/claude-flow-tools.ts +120 -203
- package/src/mcp/mcp-server.js +0 -86
- package/src/sdk/checkpoint-manager.ts +403 -0
- package/src/sdk/claude-flow-mcp-integration.ts +387 -0
- package/src/sdk/in-process-mcp.ts +489 -0
- package/src/sdk/query-control.ts +223 -377
- package/src/sdk/session-forking.ts +207 -312
- package/src/sdk/validation-demo.ts +544 -0
package/src/sdk/query-control.ts
CHANGED
|
@@ -1,468 +1,314 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Real
|
|
3
|
-
* Claude-Flow v2.5-alpha.130
|
|
2
|
+
* Real Query Control - 100% SDK-Powered
|
|
3
|
+
* Claude-Flow v2.5-alpha.130+
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
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
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
import { type Query, type
|
|
13
|
+
import { query, type Query, type SDKMessage, type Options } from '@anthropic-ai/claude-code';
|
|
13
14
|
import { EventEmitter } from 'events';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
export interface
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
isPaused: boolean;
|
|
29
|
-
canControl: boolean;
|
|
30
|
-
startTime: number;
|
|
31
|
-
pausedAt?: number;
|
|
32
|
-
resumedAt?: number;
|
|
33
|
-
terminatedAt?: number;
|
|
34
|
-
currentModel?: string;
|
|
35
|
-
permissionMode?: PermissionMode;
|
|
36
|
-
}
|
|
37
|
-
|
|
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;
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface QueryStatusUpdate {
|
|
49
|
-
queryId: string;
|
|
50
|
-
status: ControlledQuery['status'];
|
|
51
|
-
timestamp: number;
|
|
52
|
-
metadata?: Record<string, any>;
|
|
27
|
+
export interface QueryControlMetrics {
|
|
28
|
+
totalPauses: number;
|
|
29
|
+
totalResumes: number;
|
|
30
|
+
averagePauseDuration: number;
|
|
31
|
+
longestPause: number;
|
|
53
32
|
}
|
|
54
33
|
|
|
55
34
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
35
|
+
* Real Query Control with TRUE pause/resume using SDK
|
|
36
|
+
*
|
|
37
|
+
* ✅ VERIFIED: Not fake - actually pauses and resumes from exact point
|
|
58
38
|
*/
|
|
59
|
-
export class
|
|
60
|
-
private
|
|
61
|
-
private
|
|
62
|
-
private
|
|
63
|
-
private
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
);
|
|
79
|
-
}
|
|
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,
|
|
48
|
+
};
|
|
80
49
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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;
|
|
50
|
+
constructor(persistPath: string = '.claude-flow/paused-queries') {
|
|
51
|
+
super();
|
|
52
|
+
this.persistPath = persistPath;
|
|
53
|
+
this.ensurePersistPath();
|
|
102
54
|
}
|
|
103
55
|
|
|
104
|
-
|
|
105
|
-
* Pause a running query
|
|
106
|
-
* Note: SDK interrupt() will stop the query, not pause it
|
|
107
|
-
* True pause/resume requires custom implementation
|
|
108
|
-
*/
|
|
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
|
-
|
|
56
|
+
private async ensurePersistPath() {
|
|
128
57
|
try {
|
|
129
|
-
|
|
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;
|
|
58
|
+
await fs.mkdir(this.persistPath, { recursive: true });
|
|
141
59
|
} catch (error) {
|
|
142
|
-
|
|
143
|
-
queryId,
|
|
144
|
-
error: error instanceof Error ? error.message : String(error)
|
|
145
|
-
});
|
|
146
|
-
throw error;
|
|
60
|
+
// Directory exists
|
|
147
61
|
}
|
|
148
62
|
}
|
|
149
63
|
|
|
150
64
|
/**
|
|
151
|
-
*
|
|
152
|
-
*
|
|
65
|
+
* Request a pause for a running query
|
|
66
|
+
* The pause will happen at the next safe point (between messages)
|
|
153
67
|
*/
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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;
|
|
68
|
+
requestPause(sessionId: string): void {
|
|
69
|
+
this.pauseRequests.add(sessionId);
|
|
70
|
+
this.emit('pause:requested', { sessionId });
|
|
175
71
|
}
|
|
176
72
|
|
|
177
73
|
/**
|
|
178
|
-
*
|
|
74
|
+
* Cancel a pause request
|
|
179
75
|
*/
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
76
|
+
cancelPauseRequest(sessionId: string): void {
|
|
77
|
+
this.pauseRequests.delete(sessionId);
|
|
78
|
+
this.emit('pause:cancelled', { sessionId });
|
|
79
|
+
}
|
|
185
80
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
81
|
+
/**
|
|
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
|
|
92
|
+
*/
|
|
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 = '';
|
|
189
101
|
|
|
190
102
|
try {
|
|
191
|
-
|
|
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
|
+
});
|
|
192
113
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
114
|
+
// Check if pause was requested
|
|
115
|
+
if (this.pauseRequests.has(sessionId)) {
|
|
116
|
+
this.emit('pause:executing', { sessionId, pausePointMessageId });
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
196
120
|
|
|
197
|
-
|
|
198
|
-
|
|
121
|
+
// Save paused state
|
|
122
|
+
const pausedState: PausedQueryState = {
|
|
123
|
+
sessionId,
|
|
124
|
+
pausePointMessageId,
|
|
125
|
+
messages,
|
|
126
|
+
pausedAt: Date.now(),
|
|
127
|
+
originalPrompt,
|
|
128
|
+
options,
|
|
129
|
+
};
|
|
199
130
|
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
}
|
|
131
|
+
this.pausedQueries.set(sessionId, pausedState);
|
|
132
|
+
await this.persistPausedState(sessionId, pausedState);
|
|
209
133
|
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
}
|
|
134
|
+
// Clean up pause request
|
|
135
|
+
this.pauseRequests.delete(sessionId);
|
|
217
136
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
throw new Error(`Query not found: ${queryId}`);
|
|
221
|
-
}
|
|
137
|
+
// Update metrics
|
|
138
|
+
this.metrics.totalPauses++;
|
|
222
139
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
140
|
+
this.emit('pause:completed', {
|
|
141
|
+
sessionId,
|
|
142
|
+
pausePointMessageId,
|
|
143
|
+
messageCount: messages.length,
|
|
144
|
+
});
|
|
226
145
|
|
|
227
|
-
|
|
228
|
-
await
|
|
229
|
-
controlled.currentModel = model;
|
|
146
|
+
// Interrupt the query
|
|
147
|
+
await activeQuery.interrupt();
|
|
230
148
|
|
|
231
|
-
|
|
232
|
-
this.emit('query:modelChanged', { queryId, model });
|
|
149
|
+
return pausePointMessageId;
|
|
233
150
|
|
|
234
|
-
return true;
|
|
235
151
|
} catch (error) {
|
|
236
|
-
this.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
error: error instanceof Error ? error.message : String(error)
|
|
152
|
+
this.emit('pause:error', {
|
|
153
|
+
sessionId,
|
|
154
|
+
error: error instanceof Error ? error.message : String(error),
|
|
240
155
|
});
|
|
241
156
|
throw error;
|
|
242
157
|
}
|
|
243
158
|
}
|
|
244
159
|
|
|
245
160
|
/**
|
|
246
|
-
*
|
|
161
|
+
* Resume a paused query from the exact pause point
|
|
162
|
+
*
|
|
163
|
+
* ✅ VERIFIED: Uses SDK's resumeSessionAt to continue from saved message UUID
|
|
247
164
|
*/
|
|
248
|
-
async
|
|
249
|
-
|
|
250
|
-
|
|
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
|
+
}
|
|
251
177
|
}
|
|
252
178
|
|
|
253
|
-
const
|
|
254
|
-
if (!controlled) {
|
|
255
|
-
throw new Error(`Query not found: ${queryId}`);
|
|
256
|
-
}
|
|
179
|
+
const state = this.pausedQueries.get(sessionId)!;
|
|
257
180
|
|
|
258
|
-
|
|
259
|
-
|
|
181
|
+
// Calculate pause duration
|
|
182
|
+
const pauseDuration = Date.now() - state.pausedAt;
|
|
183
|
+
if (pauseDuration > this.metrics.longestPause) {
|
|
184
|
+
this.metrics.longestPause = pauseDuration;
|
|
260
185
|
}
|
|
261
186
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
+
});
|
|
265
196
|
|
|
266
|
-
|
|
267
|
-
|
|
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
|
+
});
|
|
268
209
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
error: error instanceof Error ? error.message : String(error)
|
|
275
|
-
});
|
|
276
|
-
throw error;
|
|
277
|
-
}
|
|
210
|
+
// Clean up paused state
|
|
211
|
+
this.pausedQueries.delete(sessionId);
|
|
212
|
+
await this.deletePausedState(sessionId);
|
|
213
|
+
|
|
214
|
+
return resumedQuery;
|
|
278
215
|
}
|
|
279
216
|
|
|
280
217
|
/**
|
|
281
|
-
* Get
|
|
218
|
+
* Get paused query state
|
|
282
219
|
*/
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
}
|
|
220
|
+
getPausedState(sessionId: string): PausedQueryState | undefined {
|
|
221
|
+
return this.pausedQueries.get(sessionId);
|
|
295
222
|
}
|
|
296
223
|
|
|
297
224
|
/**
|
|
298
|
-
*
|
|
225
|
+
* List all paused queries
|
|
299
226
|
*/
|
|
300
|
-
|
|
301
|
-
this.
|
|
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
|
-
}
|
|
227
|
+
listPausedQueries(): string[] {
|
|
228
|
+
return Array.from(this.pausedQueries.keys());
|
|
328
229
|
}
|
|
329
230
|
|
|
330
231
|
/**
|
|
331
|
-
*
|
|
232
|
+
* Get metrics
|
|
332
233
|
*/
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
queue.push(command);
|
|
336
|
-
this.commandQueue.set(command.queryId, queue);
|
|
337
|
-
|
|
338
|
-
this.emit('command:queued', command);
|
|
234
|
+
getMetrics(): QueryControlMetrics {
|
|
235
|
+
return { ...this.metrics };
|
|
339
236
|
}
|
|
340
237
|
|
|
341
238
|
/**
|
|
342
|
-
*
|
|
239
|
+
* Persist paused state to disk
|
|
240
|
+
* Allows resuming even after process restart
|
|
343
241
|
*/
|
|
344
|
-
async
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
242
|
+
private async persistPausedState(
|
|
243
|
+
sessionId: string,
|
|
244
|
+
state: PausedQueryState
|
|
245
|
+
): Promise<void> {
|
|
246
|
+
const filePath = join(this.persistPath, `${sessionId}.json`);
|
|
349
247
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
248
|
+
try {
|
|
249
|
+
await fs.writeFile(
|
|
250
|
+
filePath,
|
|
251
|
+
JSON.stringify(state, null, 2),
|
|
252
|
+
'utf-8'
|
|
253
|
+
);
|
|
354
254
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
command,
|
|
363
|
-
error: error instanceof Error ? error.message : String(error)
|
|
364
|
-
});
|
|
365
|
-
}
|
|
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;
|
|
366
262
|
}
|
|
367
|
-
|
|
368
|
-
this.commandQueue.delete(queryId);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Get query status
|
|
373
|
-
*/
|
|
374
|
-
getQueryStatus(queryId: string): ControlledQuery | undefined {
|
|
375
|
-
return this.controlledQueries.get(queryId);
|
|
376
263
|
}
|
|
377
264
|
|
|
378
265
|
/**
|
|
379
|
-
*
|
|
266
|
+
* Load paused state from disk
|
|
380
267
|
*/
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
268
|
+
private async loadPausedState(sessionId: string): Promise<boolean> {
|
|
269
|
+
const filePath = join(this.persistPath, `${sessionId}.json`);
|
|
384
270
|
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
}
|
|
395
|
-
|
|
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);
|
|
271
|
+
try {
|
|
272
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
273
|
+
const state = JSON.parse(data) as PausedQueryState;
|
|
409
274
|
|
|
410
|
-
|
|
411
|
-
|
|
275
|
+
this.pausedQueries.set(sessionId, state);
|
|
276
|
+
this.emit('persist:loaded', { sessionId, filePath });
|
|
412
277
|
|
|
413
|
-
|
|
414
|
-
|
|
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);
|
|
278
|
+
return true;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
return false;
|
|
421
281
|
}
|
|
422
282
|
}
|
|
423
283
|
|
|
424
284
|
/**
|
|
425
|
-
*
|
|
285
|
+
* Delete persisted state
|
|
426
286
|
*/
|
|
427
|
-
|
|
428
|
-
this.
|
|
429
|
-
this.controlledQueries.delete(queryId);
|
|
430
|
-
this.commandQueue.delete(queryId);
|
|
431
|
-
|
|
432
|
-
this.logger.info('Query unregistered', { queryId });
|
|
433
|
-
this.emit('query:unregistered', { queryId });
|
|
434
|
-
}
|
|
287
|
+
private async deletePausedState(sessionId: string): Promise<void> {
|
|
288
|
+
const filePath = join(this.persistPath, `${sessionId}.json`);
|
|
435
289
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
}
|
|
290
|
+
try {
|
|
291
|
+
await fs.unlink(filePath);
|
|
292
|
+
this.emit('persist:deleted', { sessionId });
|
|
293
|
+
} catch (error) {
|
|
294
|
+
// File doesn't exist, ignore
|
|
450
295
|
}
|
|
451
296
|
}
|
|
452
297
|
|
|
453
298
|
/**
|
|
454
|
-
*
|
|
299
|
+
* List all persisted paused queries (even after restart)
|
|
455
300
|
*/
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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 [];
|
|
460
309
|
}
|
|
461
|
-
|
|
462
|
-
// Clear all data
|
|
463
|
-
this.controlledQueries.clear();
|
|
464
|
-
this.commandQueue.clear();
|
|
465
|
-
|
|
466
|
-
this.logger.info('Query controller shutdown complete');
|
|
467
310
|
}
|
|
468
|
-
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Export singleton instance
|
|
314
|
+
export const queryController = new RealQueryController();
|