agentgui 1.0.715 → 1.0.716

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.
@@ -0,0 +1,93 @@
1
+ import { checkToolViaBunx } from './tool-version.js';
2
+
3
+ let updateCheckInterval = null;
4
+ const UPDATE_CHECK_INTERVAL = 6 * 60 * 60 * 1000;
5
+
6
+ export async function autoProvision(TOOLS, statusCache, install, broadcast) {
7
+ const log = (msg) => console.log('[TOOLS-AUTO] ' + msg);
8
+ log('Starting background tool provisioning...');
9
+ for (const tool of TOOLS) {
10
+ try {
11
+ const status = await checkToolViaBunx(tool.pkg, tool.pluginId, tool.category, tool.frameWork, true, TOOLS);
12
+ statusCache.set(tool.id, { ...status, toolId: tool.id, timestamp: Date.now() });
13
+ if (!status.installed) {
14
+ log(`${tool.id} not installed, installing...`);
15
+ broadcast({ type: 'tool_install_started', toolId: tool.id });
16
+ const result = await install(tool.id, (msg) => {
17
+ broadcast({ type: 'tool_install_progress', toolId: tool.id, data: msg });
18
+ });
19
+ if (result.success) {
20
+ log(`${tool.id} installed v${result.version}`);
21
+ broadcast({ type: 'tool_install_complete', toolId: tool.id, data: result });
22
+ } else {
23
+ log(`${tool.id} install failed: ${result.error}`);
24
+ broadcast({ type: 'tool_install_failed', toolId: tool.id, data: result });
25
+ }
26
+ } else if (status.upgradeNeeded) {
27
+ log(`${tool.id} needs update (${status.installedVersion} -> ${status.publishedVersion})`);
28
+ broadcast({ type: 'tool_install_started', toolId: tool.id });
29
+ const { update } = await import('./tool-spawner.js').then(m => m);
30
+ const result = await install(tool.id, (msg) => {
31
+ broadcast({ type: 'tool_update_progress', toolId: tool.id, data: msg });
32
+ });
33
+ if (result.success) {
34
+ log(`${tool.id} updated to v${result.version}`);
35
+ broadcast({ type: 'tool_update_complete', toolId: tool.id, data: result });
36
+ } else {
37
+ log(`${tool.id} update failed: ${result.error}`);
38
+ broadcast({ type: 'tool_update_failed', toolId: tool.id, data: result });
39
+ }
40
+ } else {
41
+ log(`${tool.id} v${status.installedVersion} up-to-date`);
42
+ broadcast({ type: 'tool_status_update', toolId: tool.id, data: { installed: true, isUpToDate: true, installedVersion: status.installedVersion, status: 'installed' } });
43
+ }
44
+ } catch (err) {
45
+ log(`${tool.id} error: ${err.message}`);
46
+ }
47
+ }
48
+ log('Provisioning complete');
49
+ }
50
+
51
+ export function startPeriodicUpdateCheck(TOOLS, statusCache, update, broadcast) {
52
+ const log = (msg) => console.log('[TOOLS-PERIODIC] ' + msg);
53
+ if (updateCheckInterval) { log('Update check already running'); return; }
54
+ log('Starting periodic tool update checker (every 6 hours)');
55
+
56
+ const check = async () => {
57
+ log('Checking for tool updates...');
58
+ for (const tool of TOOLS) {
59
+ try {
60
+ const status = await checkToolViaBunx(tool.pkg, tool.pluginId, tool.category, tool.frameWork, false, TOOLS);
61
+ if (status.upgradeNeeded) {
62
+ log(`Update available for ${tool.id}: ${status.installedVersion} -> ${status.publishedVersion}`);
63
+ broadcast({ type: 'tool_update_available', toolId: tool.id, data: { installedVersion: status.installedVersion, publishedVersion: status.publishedVersion } });
64
+ log(`Auto-updating ${tool.id}...`);
65
+ const result = await update(tool.id, (msg) => {
66
+ broadcast({ type: 'tool_update_progress', toolId: tool.id, data: msg });
67
+ });
68
+ if (result.success) {
69
+ log(`${tool.id} auto-updated to v${result.version}`);
70
+ broadcast({ type: 'tool_update_complete', toolId: tool.id, data: { ...result, autoUpdated: true } });
71
+ } else {
72
+ log(`${tool.id} auto-update failed: ${result.error}`);
73
+ broadcast({ type: 'tool_update_failed', toolId: tool.id, data: { ...result, autoUpdated: true } });
74
+ }
75
+ }
76
+ } catch (err) {
77
+ log(`Error checking ${tool.id}: ${err.message}`);
78
+ }
79
+ }
80
+ log('Update check complete');
81
+ };
82
+
83
+ setImmediate(() => check().catch(err => log(`Initial check failed: ${err.message}`)));
84
+ updateCheckInterval = setInterval(() => check().catch(err => log(`Periodic check failed: ${err.message}`)), UPDATE_CHECK_INTERVAL);
85
+ }
86
+
87
+ export function stopPeriodicUpdateCheck() {
88
+ if (updateCheckInterval) {
89
+ clearInterval(updateCheckInterval);
90
+ updateCheckInterval = null;
91
+ console.log('[TOOLS-PERIODIC] Update check stopped');
92
+ }
93
+ }
@@ -0,0 +1,121 @@
1
+ import { spawn } from 'child_process';
2
+ import os from 'os';
3
+ import { getCliVersion, getInstalledVersion, clearVersionCache, checkToolViaBunx } from './tool-version.js';
4
+
5
+ const isWindows = os.platform() === 'win32';
6
+
7
+ const spawnNpmInstall = (pkg, onProgress) => new Promise((resolve) => {
8
+ const cmd = isWindows ? 'npm.cmd' : 'npm';
9
+ let completed = false, stderr = '', stdout = '';
10
+ let proc;
11
+ try {
12
+ proc = spawn(cmd, ['install', '-g', pkg], { stdio: ['pipe', 'pipe', 'pipe'], timeout: 300000, shell: isWindows, windowsHide: true });
13
+ } catch (err) {
14
+ return resolve({ success: false, error: `Failed to spawn npm install: ${err.message}` });
15
+ }
16
+ if (!proc) return resolve({ success: false, error: 'Failed to spawn npm process' });
17
+ const timer = setTimeout(() => { if (!completed) { completed = true; try { proc.kill('SIGKILL'); } catch (_) {} resolve({ success: false, error: 'Timeout (5min)' }); } }, 300000);
18
+ const onData = (d) => { if (onProgress) onProgress({ type: 'progress', data: d.toString() }); };
19
+ if (proc.stdout) proc.stdout.on('data', (d) => { stdout += d.toString(); onData(d); });
20
+ if (proc.stderr) proc.stderr.on('data', (d) => { stderr += d.toString(); onData(d); });
21
+ proc.on('close', (code) => {
22
+ clearTimeout(timer);
23
+ if (completed) return;
24
+ completed = true;
25
+ resolve(code === 0 ? { success: true, error: null, pkg } : { success: false, error: (stdout + stderr).substring(0, 1000) || 'Failed' });
26
+ });
27
+ proc.on('error', (err) => { clearTimeout(timer); if (!completed) { completed = true; resolve({ success: false, error: err.message }); } });
28
+ });
29
+
30
+ const spawnBunxProc = (pkg, onProgress) => new Promise((resolve) => {
31
+ const cmd = isWindows ? 'bun.cmd' : 'bun';
32
+ let completed = false, stderr = '', stdout = '';
33
+ let lastDataTime = Date.now();
34
+ let proc;
35
+ try {
36
+ proc = spawn(cmd, ['x', pkg], { stdio: ['pipe', 'pipe', 'pipe'], timeout: 300000, shell: isWindows, windowsHide: true });
37
+ } catch (err) {
38
+ return resolve({ success: false, error: `Failed to spawn bun x: ${err.message}` });
39
+ }
40
+ if (!proc) return resolve({ success: false, error: 'Failed to spawn bun x process' });
41
+
42
+ const timer = setTimeout(() => {
43
+ if (!completed) { completed = true; try { proc.kill('SIGKILL'); } catch (_) {} resolve({ success: false, error: 'Timeout (5min)' }); }
44
+ }, 300000);
45
+
46
+ const heartbeatTimer = setInterval(() => {
47
+ if (completed) { clearInterval(heartbeatTimer); return; }
48
+ const elapsed = Date.now() - lastDataTime;
49
+ if (elapsed > 30000) console.warn(`[tool-manager] No output from bun x ${pkg} for ${elapsed}ms - process may be hung`);
50
+ }, 30000);
51
+
52
+ const onData = (d) => { lastDataTime = Date.now(); if (onProgress) onProgress({ type: 'progress', data: d.toString() }); };
53
+ if (proc.stdout) proc.stdout.on('data', (d) => { stdout += d.toString(); onData(d); });
54
+ if (proc.stderr) proc.stderr.on('data', (d) => { stderr += d.toString(); onData(d); });
55
+
56
+ proc.on('close', (code) => {
57
+ clearTimeout(timer);
58
+ clearInterval(heartbeatTimer);
59
+ if (completed) return;
60
+ completed = true;
61
+ const output = stdout + stderr;
62
+ const ok = [code === 0, output.includes('upgraded'), output.includes('registered'),
63
+ output.includes('Hooks registered'), output.includes('successfully'),
64
+ output.includes('Done'), code === 0 && !output.includes('error')].some(Boolean);
65
+ resolve(ok ? { success: true, error: null, pkg } : { success: false, error: output.substring(0, 1000) || 'Failed' });
66
+ });
67
+
68
+ proc.on('error', (err) => {
69
+ clearTimeout(timer);
70
+ clearInterval(heartbeatTimer);
71
+ if (!completed) { completed = true; resolve({ success: false, error: `Process error: ${err.message}` }); }
72
+ });
73
+ });
74
+
75
+ function spawnForTool(tool, onProgress) {
76
+ const pkg = tool.installPkg || tool.pkg;
77
+ return tool.category === 'cli' ? spawnNpmInstall(pkg, onProgress) : spawnBunxProc(pkg, onProgress);
78
+ }
79
+
80
+ async function postInstallRefresh(tool, statusCache, checkToolStatusAsync) {
81
+ await new Promise(r => setTimeout(r, 500));
82
+ statusCache.delete(tool.id);
83
+ clearVersionCache();
84
+ const version = tool.category === 'cli' ? getCliVersion(tool.pkg) : getInstalledVersion(tool.pkg, tool.pluginId, tool.frameWork);
85
+ const freshStatus = await checkToolStatusAsync(tool.id);
86
+ return { success: true, error: null, version: version || freshStatus.publishedVersion || 'unknown', ...freshStatus };
87
+ }
88
+
89
+ export function createInstaller(getTool, installLocks, statusCache, checkToolStatusAsync) {
90
+ async function install(toolId, onProgress) {
91
+ const tool = getTool(toolId);
92
+ if (!tool) return { success: false, error: 'Tool not found' };
93
+ if (installLocks.get(toolId)) return { success: false, error: 'Install in progress' };
94
+ installLocks.set(toolId, true);
95
+ try {
96
+ const result = await spawnForTool(tool, onProgress);
97
+ if (result.success) return await postInstallRefresh(tool, statusCache, checkToolStatusAsync);
98
+ return result;
99
+ } finally {
100
+ installLocks.delete(toolId);
101
+ }
102
+ }
103
+
104
+ async function update(toolId, onProgress) {
105
+ const tool = getTool(toolId);
106
+ if (!tool) return { success: false, error: 'Tool not found' };
107
+ const current = await checkToolStatusAsync(toolId);
108
+ if (!current?.installed) return { success: false, error: 'Tool not installed' };
109
+ if (installLocks.get(toolId)) return { success: false, error: 'Install in progress' };
110
+ installLocks.set(toolId, true);
111
+ try {
112
+ const result = await spawnForTool(tool, onProgress);
113
+ if (result.success) return await postInstallRefresh(tool, statusCache, checkToolStatusAsync);
114
+ return result;
115
+ } finally {
116
+ installLocks.delete(toolId);
117
+ }
118
+ }
119
+
120
+ return { install, update };
121
+ }
@@ -0,0 +1,196 @@
1
+ import os from 'os';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { execSync } from 'child_process';
5
+
6
+ const isWindows = os.platform() === 'win32';
7
+ const homeDir = os.homedir();
8
+
9
+ const BIN_MAP = {
10
+ '@anthropic-ai/claude-code': 'claude',
11
+ 'opencode-ai': 'opencode',
12
+ '@google/gemini-cli': 'gemini',
13
+ '@kilocode/cli': 'kilo',
14
+ '@openai/codex': 'codex',
15
+ 'agent-browser': 'agent-browser',
16
+ };
17
+
18
+ const FRAMEWORK_PATHS = {
19
+ claude: {
20
+ pluginDir: (pluginId) => path.join(homeDir, '.claude', 'plugins', pluginId),
21
+ versionFile: (pluginId) => path.join(homeDir, '.claude', 'plugins', pluginId, 'plugin.json'),
22
+ parseVersion: (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf-8')).version,
23
+ },
24
+ opencode: {
25
+ pluginDir: (pluginId) => path.join(homeDir, '.config', 'opencode', 'agents', pluginId),
26
+ markerFile: (pluginId) => path.join(homeDir, '.config', 'opencode', 'agents', pluginId + '.md'),
27
+ versionFile: (pluginId) => path.join(homeDir, '.config', 'opencode', 'agents', pluginId, 'plugin.json'),
28
+ parseVersion: (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf-8')).version,
29
+ fallbackInstalled: true,
30
+ },
31
+ gemini: {
32
+ pluginDir: (pluginId) => path.join(homeDir, '.gemini', 'extensions', pluginId),
33
+ versionFile: (pluginId) => path.join(homeDir, '.gemini', 'extensions', pluginId, 'gemini-extension.json'),
34
+ parseVersion: (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf-8')).version,
35
+ },
36
+ kilo: {
37
+ pluginDir: (pluginId) => path.join(homeDir, '.config', 'kilo', 'agents', pluginId),
38
+ markerFile: (pluginId) => path.join(homeDir, '.config', 'kilo', 'agents', pluginId + '.md'),
39
+ versionFile: (pluginId) => path.join(homeDir, '.config', 'kilo', 'agents', pluginId, 'plugin.json'),
40
+ parseVersion: (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf-8')).version,
41
+ fallbackInstalled: true,
42
+ },
43
+ codex: {
44
+ pluginDir: (pluginId) => path.join(homeDir, '.codex', 'plugins', pluginId),
45
+ versionFile: (pluginId) => path.join(homeDir, '.codex', 'plugins', pluginId, 'plugin.json'),
46
+ parseVersion: (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf-8')).version,
47
+ fallbackInstalled: true,
48
+ },
49
+ };
50
+
51
+ function tryReadVersion(fwConfig, pluginId) {
52
+ try {
53
+ const vFile = fwConfig.versionFile(pluginId);
54
+ if (fs.existsSync(vFile)) {
55
+ const ver = fwConfig.parseVersion(vFile);
56
+ if (ver) return ver;
57
+ }
58
+ } catch (_) {}
59
+ return null;
60
+ }
61
+
62
+ function tryBunCache(pkg) {
63
+ try {
64
+ const cachePath = path.join(homeDir, '.gmweb/cache/.bun/install/cache');
65
+ const dirs = fs.readdirSync(cachePath).filter(d => d.startsWith(pkg + '@'));
66
+ const latest = dirs.sort().reverse()[0];
67
+ if (latest) {
68
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(cachePath, latest, 'package.json'), 'utf-8'));
69
+ if (pkgJson.version) return pkgJson.version;
70
+ }
71
+ } catch (_) {}
72
+ return null;
73
+ }
74
+
75
+ export function getInstalledVersion(pkg, pluginId = null, frameWork = null, tools = []) {
76
+ try {
77
+ const tool = tools.find(t => t.pkg === pkg);
78
+ const actualPluginId = pluginId || tool?.pluginId || pkg;
79
+ const actualFrameWork = frameWork || tool?.frameWork;
80
+
81
+ const frameworks = actualFrameWork ? [actualFrameWork] : Object.keys(FRAMEWORK_PATHS);
82
+ for (const fw of frameworks) {
83
+ const fwConfig = FRAMEWORK_PATHS[fw];
84
+ if (!fwConfig) continue;
85
+
86
+ const version = tryReadVersion(fwConfig, actualPluginId);
87
+ if (version) return version;
88
+
89
+ if (fwConfig.fallbackInstalled) {
90
+ const hasMarker = fwConfig.markerFile && fs.existsSync(fwConfig.markerFile(actualPluginId));
91
+ const hasDir = fs.existsSync(fwConfig.pluginDir(actualPluginId));
92
+ if (hasMarker || hasDir) {
93
+ const cached = tryBunCache(pkg);
94
+ if (cached) return cached;
95
+ return 'installed';
96
+ }
97
+ }
98
+ }
99
+ } catch (_) {}
100
+ return null;
101
+ }
102
+
103
+ export function checkCliInstalled(pkg) {
104
+ try {
105
+ const cmd = isWindows ? 'where' : 'which';
106
+ const bin = BIN_MAP[pkg];
107
+ if (bin) {
108
+ execSync(`${cmd} ${bin}`, { stdio: 'pipe', timeout: 3000, windowsHide: true });
109
+ return true;
110
+ }
111
+ } catch (_) {}
112
+ return false;
113
+ }
114
+
115
+ export function getCliVersion(pkg) {
116
+ try {
117
+ const bin = BIN_MAP[pkg];
118
+ if (!bin) return null;
119
+ try {
120
+ const versionFlag = pkg === 'agent-browser' ? '-V' : '--version';
121
+ const out = execSync(`${bin} ${versionFlag}`, { stdio: 'pipe', timeout: 1000, encoding: 'utf8', windowsHide: true });
122
+ const match = out.match(/(\d+\.\d+\.\d+)/);
123
+ if (match) {
124
+ console.log(`[tool-manager] CLI ${pkg} (${bin}) version: ${match[1]}`);
125
+ return match[1];
126
+ }
127
+ } catch (err) {
128
+ console.log(`[tool-manager] CLI ${pkg} (${bin}) version detection failed: ${err.message.split('\n')[0]}`);
129
+ }
130
+ } catch (err) {
131
+ console.log(`[tool-manager] Error in getCliVersion for ${pkg}:`, err.message);
132
+ }
133
+ return null;
134
+ }
135
+
136
+ export function checkToolInstalled(pluginId, frameWork = null) {
137
+ try {
138
+ const frameworks = frameWork ? [frameWork] : Object.keys(FRAMEWORK_PATHS);
139
+ for (const fw of frameworks) {
140
+ const fwConfig = FRAMEWORK_PATHS[fw];
141
+ if (!fwConfig) continue;
142
+ if (fs.existsSync(fwConfig.pluginDir(pluginId))) return true;
143
+ if (fwConfig.markerFile && fs.existsSync(fwConfig.markerFile(pluginId))) return true;
144
+ }
145
+ } catch (_) {}
146
+ return false;
147
+ }
148
+
149
+ export function compareVersions(v1, v2) {
150
+ if (!v1 || !v2) return false;
151
+ const parts1 = v1.split('.').map(Number);
152
+ const parts2 = v2.split('.').map(Number);
153
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
154
+ const p1 = parts1[i] || 0;
155
+ const p2 = parts2[i] || 0;
156
+ if (p1 < p2) return true;
157
+ if (p1 > p2) return false;
158
+ }
159
+ return false;
160
+ }
161
+
162
+ const versionCache = new Map();
163
+
164
+ export function getPublishedVersion(pkg) {
165
+ const cacheKey = `published-${pkg}`;
166
+ const cached = versionCache.get(cacheKey);
167
+ if (cached && Date.now() - cached.timestamp < 86400000) {
168
+ return cached.version;
169
+ }
170
+ return null;
171
+ }
172
+
173
+ export function clearVersionCache() {
174
+ versionCache.clear();
175
+ }
176
+
177
+ export async function checkToolViaBunx(pkg, pluginId = null, category = 'plugin', frameWork = null, skipPublishedVersion = false, tools = []) {
178
+ try {
179
+ const isCli = category === 'cli';
180
+ const installed = isCli ? checkCliInstalled(pkg) : checkToolInstalled(pluginId || pkg, frameWork);
181
+ const installedVersion = isCli ? getCliVersion(pkg) : getInstalledVersion(pkg, pluginId, frameWork, tools);
182
+ let publishedVersion = null;
183
+ if (!skipPublishedVersion) {
184
+ publishedVersion = getPublishedVersion(pkg);
185
+ }
186
+ const needsUpdate = installed && publishedVersion && compareVersions(installedVersion, publishedVersion);
187
+ const isUpToDate = installed && !needsUpdate;
188
+ return { installed, isUpToDate, upgradeNeeded: needsUpdate, output: 'version-check', installedVersion, publishedVersion };
189
+ } catch (err) {
190
+ console.log(`[tool-manager] Error checking ${pkg}:`, err.message);
191
+ const isCli = category === 'cli';
192
+ const installed = isCli ? checkCliInstalled(pkg) : checkToolInstalled(pluginId || pkg, frameWork);
193
+ const installedVersion = isCli ? getCliVersion(pkg) : getInstalledVersion(pkg, pluginId, frameWork, tools);
194
+ return { installed, isUpToDate: false, upgradeNeeded: false, output: '', installedVersion, publishedVersion: null };
195
+ }
196
+ }
@@ -6,54 +6,21 @@ import * as execMachine from './execution-machine.js';
6
6
  function fail(code, message) { const e = new Error(message); e.code = code; throw e; }
7
7
  function notFound(msg = 'Not found') { fail(404, msg); }
8
8
  function expandTilde(p) { return p && p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : p; }
9
-
10
9
  function validate(schema, params) {
11
10
  const result = schema.safeParse(params);
12
11
  if (!result.success) fail(400, result.error.issues.map(i => i.message).join('; '));
13
12
  return result.data;
14
13
  }
15
14
 
16
- const ConvNewSchema = z.object({
17
- agentId: z.string().optional(),
18
- title: z.string().optional(),
19
- workingDirectory: z.string().optional(),
20
- model: z.string().optional(),
21
- subAgent: z.string().optional(),
22
- }).passthrough();
23
-
24
- const ConvUpdSchema = z.object({
25
- id: z.string().min(1, 'id required'),
26
- }).passthrough();
27
-
28
- const MsgStreamSchema = z.object({
29
- id: z.string().min(1, 'conversation id required'),
30
- content: z.union([z.string(), z.any()]).optional(),
31
- message: z.union([z.string(), z.any()]).optional(),
32
- agentId: z.string().optional(),
33
- model: z.string().optional(),
34
- subAgent: z.string().optional(),
35
- }).passthrough();
36
-
37
- const ConvSteerSchema = z.object({
38
- id: z.string().min(1, 'conversation id required'),
39
- content: z.union([z.string(), z.record(z.any())]).refine(v => v !== undefined && v !== null && v !== '', { message: 'content required' }),
40
- }).passthrough();
15
+ const ConvNewSchema = z.object({ agentId: z.string().optional(), title: z.string().optional(), workingDirectory: z.string().optional(), model: z.string().optional(), subAgent: z.string().optional() }).passthrough();
16
+ const ConvUpdSchema = z.object({ id: z.string().min(1, 'id required') }).passthrough();
17
+ const ConvSteerSchema = z.object({ id: z.string().min(1, 'conversation id required'), content: z.union([z.string(), z.record(z.any())]).refine(v => v !== undefined && v !== null && v !== '', { message: 'content required' }) }).passthrough();
41
18
 
42
19
  export function register(router, deps) {
43
- const { queries, activeExecutions, messageQueues, rateLimitState,
44
- broadcastSync, processMessageWithStreaming, cleanupExecution, logError = () => {},
20
+ const { queries, activeExecutions, rateLimitState,
21
+ broadcastSync, processMessageWithStreaming, cleanupExecution,
45
22
  getJsonlWatcher = () => null } = deps;
46
23
 
47
- // Per-conversation queue seq counter for event ordering
48
- const queueSeqByConv = new Map();
49
-
50
- function getNextQueueSeq(conversationId) {
51
- const current = queueSeqByConv.get(conversationId) || 0;
52
- const next = current + 1;
53
- queueSeqByConv.set(conversationId, next);
54
- return next;
55
- }
56
-
57
24
  router.handle('conv.ls', () => {
58
25
  const conversations = queries.getConversationsList();
59
26
  for (const c of conversations) { if (c.isStreaming && !execMachine.isActive(c.id)) c.isStreaming = 0; }
@@ -205,164 +172,4 @@ export function register(router, deps) {
205
172
  return { ok: true, steered: true, conversationId: p.id, messageId: message.id };
206
173
  });
207
174
 
208
- router.handle('msg.ls', (p) => {
209
- return queries.getPaginatedMessages(p.id, Math.min(p.limit || 50, 100), Math.max(p.offset || 0, 0));
210
- });
211
-
212
- router.handle('msg.ls.earlier', (p) => {
213
- if (!p.id) fail(400, 'Missing conversation id');
214
- if (!p.before) fail(400, 'Missing before messageId parameter');
215
- const limit = Math.min(p.limit || 50, 100);
216
- const result = queries.getMessagesBefore(p.id, p.before, limit);
217
- return { ok: true, messages: result.messages, total: result.total, hasMore: result.hasMore, limit: result.limit };
218
- });
219
-
220
- function startExecution(convId, message, agentId, model, content, subAgent) {
221
- const session = queries.createSession(convId);
222
- queries.createEvent('session.created', { messageId: message.id, sessionId: session.id }, convId, session.id);
223
- // Machine is authoritative — START event sets sessionId in context
224
- execMachine.send(convId, { type: 'START', sessionId: session.id });
225
- activeExecutions.set(convId, { pid: null, startTime: Date.now(), sessionId: session.id, lastActivity: Date.now() });
226
- queries.setIsStreaming(convId, true);
227
- broadcastSync({ type: 'streaming_start', sessionId: session.id, conversationId: convId, messageId: message.id, agentId, timestamp: Date.now() });
228
- processMessageWithStreaming(convId, message.id, session.id, content, agentId, model, subAgent).catch(e => logError('startExecution', e, { convId }));
229
- return session;
230
- }
231
-
232
- function enqueue(convId, content, agentId, model, messageId, subAgent) {
233
- const item = { content, agentId, model, messageId, subAgent };
234
- // Machine is authoritative for queue — ENQUEUE event adds to machine context.queue
235
- execMachine.send(convId, { type: 'ENQUEUE', item });
236
- // Keep messageQueues in sync for legacy REST endpoints
237
- if (!messageQueues.has(convId)) messageQueues.set(convId, []);
238
- messageQueues.get(convId).push(item);
239
- const queueLength = execMachine.getQueue(convId).length;
240
- broadcastSync({ type: 'queue_status', conversationId: convId, queueLength, messageId, timestamp: Date.now() });
241
- return queueLength;
242
- }
243
-
244
- router.handle('msg.send', (p) => {
245
- const conv = queries.getConversation(p.id);
246
- if (!conv) notFound('Conversation not found');
247
- const agentId = p.agentId || conv.agentType || conv.agentId || 'claude-code';
248
- const model = p.model || conv.model || null;
249
- const subAgent = p.subAgent || conv.subAgent || null;
250
- const idempotencyKey = p.idempotencyKey || null;
251
- const rawContent = p.content;
252
- if (rawContent !== undefined && typeof rawContent !== 'string') {
253
- console.warn(`[ws-validation] msg.send content is ${typeof rawContent}: ${JSON.stringify(rawContent).slice(0, 200)}`);
254
- logError('ws-content-type', new TypeError(`non-string content in msg.send`), { method: 'msg.send', type: typeof rawContent });
255
- }
256
- p.content = typeof rawContent === 'string' ? rawContent : (rawContent ? JSON.stringify(rawContent) : '');
257
- const message = queries.createMessage(p.id, 'user', p.content, idempotencyKey);
258
- queries.createEvent('message.created', { role: 'user', messageId: message.id }, p.id);
259
-
260
- // Machine is authoritative: gate on machine state, not Map
261
- if (!execMachine.isActive(p.id)) {
262
- broadcastSync({ type: 'message_created', conversationId: p.id, message, timestamp: Date.now() });
263
- const session = startExecution(p.id, message, agentId, model, p.content, subAgent);
264
- return { message, session, idempotencyKey };
265
- }
266
-
267
- const qp = enqueue(p.id, p.content, agentId, model, message.id, subAgent);
268
- return { message, queued: true, queuePosition: qp, idempotencyKey };
269
- });
270
-
271
- router.handle('msg.get', (p) => {
272
- const msg = queries.getMessage(p.messageId);
273
- if (!msg || msg.conversationId !== p.id) notFound();
274
- return { message: msg };
275
- });
276
-
277
- router.handle('msg.stream', (p) => {
278
- p = validate(MsgStreamSchema, p);
279
- const conv = queries.getConversation(p.id);
280
- if (!conv) notFound('Conversation not found');
281
- const rawContent = p.content || p.message;
282
- if (rawContent !== undefined && typeof rawContent !== 'string') {
283
- console.warn(`[ws-validation] msg.stream content is ${typeof rawContent}: ${JSON.stringify(rawContent).slice(0, 200)}`);
284
- logError('ws-content-type', new TypeError(`non-string content in msg.stream`), { method: 'msg.stream', type: typeof rawContent });
285
- }
286
- const prompt = typeof rawContent === 'string' ? rawContent : (rawContent ? JSON.stringify(rawContent) : '');
287
- const agentId = p.agentId || conv.agentType || conv.agentId || 'claude-code';
288
- const model = p.model || conv.model || null;
289
- const subAgent = p.subAgent || conv.subAgent || null;
290
- const userMessage = queries.createMessage(p.id, 'user', prompt);
291
- queries.createEvent('message.created', { role: 'user', messageId: userMessage.id }, p.id);
292
-
293
- // Machine is authoritative: gate on machine state, not Map
294
- if (!execMachine.isActive(p.id)) {
295
- broadcastSync({ type: 'message_created', conversationId: p.id, message: userMessage, timestamp: Date.now() });
296
- const session = startExecution(p.id, userMessage, agentId, model, prompt, subAgent);
297
- return { message: userMessage, session, streamId: session.id };
298
- }
299
-
300
- const qp = enqueue(p.id, prompt, agentId, model, userMessage.id, subAgent);
301
- const seq = getNextQueueSeq(p.id);
302
- broadcastSync({
303
- type: 'queue_status',
304
- conversationId: p.id,
305
- queueLength: execMachine.getQueue(p.id).length,
306
- seq,
307
- timestamp: Date.now()
308
- });
309
- return { message: userMessage, queued: true, queuePosition: qp };
310
- });
311
-
312
- router.handle('q.ls', (p) => {
313
- if (!queries.getConversation(p.id)) notFound('Conversation not found');
314
- // Read queue from machine context (authoritative), fall back to Map for compatibility
315
- const machineQueue = execMachine.getQueue(p.id);
316
- return { queue: machineQueue.length > 0 ? machineQueue : (messageQueues.get(p.id) || []) };
317
- });
318
-
319
- router.handle('q.del', (p) => {
320
- const machineQueue = execMachine.getQueue(p.id);
321
- const mapQueue = messageQueues.get(p.id);
322
- if (!machineQueue.length && !mapQueue) notFound('Queue not found');
323
- // Remove from both machine and Map
324
- const idx = machineQueue.findIndex(q => q.messageId === p.messageId);
325
- if (idx === -1 && (!mapQueue || mapQueue.findIndex(q => q.messageId === p.messageId) === -1)) notFound('Queued message not found');
326
- // Update machine queue via direct send
327
- if (idx !== -1) {
328
- const newQueue = [...machineQueue];
329
- newQueue.splice(idx, 1);
330
- execMachine.send(p.id, { type: 'SET_QUEUE', queue: newQueue });
331
- }
332
- if (mapQueue) {
333
- const mi = mapQueue.findIndex(q => q.messageId === p.messageId);
334
- if (mi !== -1) mapQueue.splice(mi, 1);
335
- if (mapQueue.length === 0) messageQueues.delete(p.id);
336
- }
337
- const seq = getNextQueueSeq(p.id);
338
- broadcastSync({
339
- type: 'queue_status',
340
- conversationId: p.id,
341
- queueLength: execMachine.getQueue(p.id).length,
342
- seq,
343
- timestamp: Date.now()
344
- });
345
- return { deleted: true };
346
- });
347
-
348
- router.handle('q.upd', (p) => {
349
- const machineQueue = execMachine.getQueue(p.id);
350
- const mapQueue = messageQueues.get(p.id);
351
- if (!machineQueue.length && !mapQueue) notFound('Queue not found');
352
- const item = machineQueue.find(q => q.messageId === p.messageId) || mapQueue?.find(q => q.messageId === p.messageId);
353
- if (!item) notFound('Queued message not found');
354
- if (p.content !== undefined) item.content = p.content;
355
- if (p.agentId !== undefined) item.agentId = p.agentId;
356
- const seq = getNextQueueSeq(p.id);
357
- broadcastSync({
358
- type: 'queue_updated',
359
- conversationId: p.id,
360
- messageId: p.messageId,
361
- content: item.content,
362
- agentId: item.agentId,
363
- seq,
364
- timestamp: Date.now()
365
- });
366
- return { updated: true, item };
367
- });
368
175
  }