@lightcone-ai/daemon 0.14.18 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mcp-servers/official/page-understanding/index.js +1 -1
- package/package.json +3 -1
- package/src/_vendor/video/understanding/analyze-page.js +684 -0
- package/src/_vendor/video/understanding/heuristics.js +826 -0
- package/src/_vendor/video/understanding/index.js +11 -0
- package/src/_vendor/video/understanding/llm-client.js +261 -0
- package/src/_vendor/video/understanding/schema.js +254 -0
- package/src/_vendor/video/understanding/site-selectors.js +47 -0
- package/src/agent-manager.js +371 -4
- package/src/capability-probe.js +113 -0
- package/src/chat-bridge.js +15 -1
- package/src/drivers/claude.js +27 -9
- package/src/drivers/codex.js +25 -14
- package/src/index.js +1 -1
- package/src/lifecycle-protocol.js +18 -0
- package/src/mcp-config.js +25 -14
- package/src/runtime-config.js +24 -0
package/src/agent-manager.js
CHANGED
|
@@ -18,10 +18,14 @@ import { startSession, stopSession, stopAllSessions } from './browser-login.js';
|
|
|
18
18
|
import { markInvalidatedLeases } from './governance-state.js';
|
|
19
19
|
import { resolveExitOfflineDetail, resolveLifecycleExitState } from './lifecycle-protocol.js';
|
|
20
20
|
import { runPublishJob } from './publish-job-runner.js';
|
|
21
|
+
import { resolveOutboundRateLimitConfig } from './runtime-config.js';
|
|
22
|
+
import { probeChatBridgeCapability } from './capability-probe.js';
|
|
21
23
|
|
|
22
24
|
const KIMI_SYSTEM_PROMPT_FILE = '.lightcone-kimi-system.md';
|
|
23
25
|
const KIMI_AGENT_FILE = '.lightcone-kimi-agent.yaml';
|
|
24
26
|
const KIMI_MCP_FILE = '.lightcone-kimi-mcp.json';
|
|
27
|
+
const CHAT_CAPABILITY_ID = 'chat';
|
|
28
|
+
const CAPABILITY_PROBE_TIMEOUT_MS = 8000;
|
|
25
29
|
const LOCAL_FILE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
26
30
|
const LOCAL_MCP_ROOTS = Object.freeze([
|
|
27
31
|
path.resolve(LOCAL_FILE_DIR, '../mcp-servers'),
|
|
@@ -129,6 +133,10 @@ function replacePlaceholders(text, replacements) {
|
|
|
129
133
|
return next;
|
|
130
134
|
}
|
|
131
135
|
|
|
136
|
+
function previewText(value, max = 500) {
|
|
137
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim().slice(0, max);
|
|
138
|
+
}
|
|
139
|
+
|
|
132
140
|
export class AgentManager {
|
|
133
141
|
constructor({ serverUrl, machineApiKey }) {
|
|
134
142
|
this.serverUrl = serverUrl;
|
|
@@ -138,6 +146,10 @@ export class AgentManager {
|
|
|
138
146
|
// key → true (spawn in progress)
|
|
139
147
|
this.starting = new Set();
|
|
140
148
|
this.directiveRefreshTimers = new Map();
|
|
149
|
+
this.outboundRateLimit = resolveOutboundRateLimitConfig();
|
|
150
|
+
this.outboundHistory = new Map();
|
|
151
|
+
this.outboundCooldownUntil = new Map();
|
|
152
|
+
this.inboxPauseUntil = new Map();
|
|
141
153
|
}
|
|
142
154
|
|
|
143
155
|
_key(agentId, workspaceId) {
|
|
@@ -189,6 +201,111 @@ export class AgentManager {
|
|
|
189
201
|
return dir;
|
|
190
202
|
}
|
|
191
203
|
|
|
204
|
+
_nowMs() {
|
|
205
|
+
return Date.now();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
_trimOutboundHistory(key, nowMs = this._nowMs()) {
|
|
209
|
+
const history = this.outboundHistory.get(key) ?? [];
|
|
210
|
+
const cutoff = nowMs - this.outboundRateLimit.windowMs;
|
|
211
|
+
const trimmed = history.filter(ts => Number.isFinite(ts) && ts > cutoff);
|
|
212
|
+
if (trimmed.length > 0) this.outboundHistory.set(key, trimmed);
|
|
213
|
+
else this.outboundHistory.delete(key);
|
|
214
|
+
return trimmed;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
_getRemainingCooldownMs(key, nowMs = this._nowMs()) {
|
|
218
|
+
const cooldownUntil = Number(this.outboundCooldownUntil.get(key) ?? 0);
|
|
219
|
+
if (!Number.isFinite(cooldownUntil) || cooldownUntil <= nowMs) {
|
|
220
|
+
this.outboundCooldownUntil.delete(key);
|
|
221
|
+
this.inboxPauseUntil.delete(key);
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
return cooldownUntil - nowMs;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
_isInboundFlushPaused(key, nowMs = this._nowMs()) {
|
|
228
|
+
const pauseUntil = Number(this.inboxPauseUntil.get(key) ?? 0);
|
|
229
|
+
if (!Number.isFinite(pauseUntil) || pauseUntil <= nowMs) {
|
|
230
|
+
this.inboxPauseUntil.delete(key);
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_emitOutboundRateLimitEvent(connection, {
|
|
237
|
+
agentId,
|
|
238
|
+
workspaceId,
|
|
239
|
+
historyCount,
|
|
240
|
+
nowMs,
|
|
241
|
+
cooldownUntilMs,
|
|
242
|
+
}) {
|
|
243
|
+
connection.send({
|
|
244
|
+
type: 'agent:governance_event',
|
|
245
|
+
agentId,
|
|
246
|
+
workspaceId,
|
|
247
|
+
eventType: 'agent_outbound_rate_limit_triggered',
|
|
248
|
+
reason: 'hard_cap',
|
|
249
|
+
payload: {
|
|
250
|
+
count: historyCount,
|
|
251
|
+
window_ms: this.outboundRateLimit.windowMs,
|
|
252
|
+
soft_cap: this.outboundRateLimit.softCap,
|
|
253
|
+
hard_cap: this.outboundRateLimit.hardCap,
|
|
254
|
+
cooldown_ms: this.outboundRateLimit.cooldownMs,
|
|
255
|
+
triggered_at: new Date(nowMs).toISOString(),
|
|
256
|
+
cooldown_until: new Date(cooldownUntilMs).toISOString(),
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
_recordSuccessfulOutboundMessage({
|
|
262
|
+
key,
|
|
263
|
+
agent,
|
|
264
|
+
agentId,
|
|
265
|
+
workspaceId,
|
|
266
|
+
connection,
|
|
267
|
+
nowMs = this._nowMs(),
|
|
268
|
+
}) {
|
|
269
|
+
const history = this._trimOutboundHistory(key, nowMs);
|
|
270
|
+
history.push(nowMs);
|
|
271
|
+
this.outboundHistory.set(key, history);
|
|
272
|
+
|
|
273
|
+
if (history.length > this.outboundRateLimit.softCap) {
|
|
274
|
+
console.warn(
|
|
275
|
+
`[WARN] [AgentManager] outbound rate high agent=${agent?.config?.displayName ?? agentId.slice(0, 8)} `
|
|
276
|
+
+ `workspace=${workspaceId ?? 'none'} count_60s=${history.length}`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
if (history.length <= this.outboundRateLimit.hardCap) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const cooldownUntilMs = nowMs + this.outboundRateLimit.cooldownMs;
|
|
284
|
+
this.outboundCooldownUntil.set(key, cooldownUntilMs);
|
|
285
|
+
this.inboxPauseUntil.set(key, cooldownUntilMs);
|
|
286
|
+
|
|
287
|
+
console.warn(
|
|
288
|
+
`[WARN] [AgentManager] outbound hard cap triggered agent=${agent?.config?.displayName ?? agentId.slice(0, 8)} `
|
|
289
|
+
+ `workspace=${workspaceId ?? 'none'} count_60s=${history.length} cooldown_until=${new Date(cooldownUntilMs).toISOString()}`
|
|
290
|
+
);
|
|
291
|
+
this._emitOutboundRateLimitEvent(connection, {
|
|
292
|
+
agentId,
|
|
293
|
+
workspaceId,
|
|
294
|
+
historyCount: history.length,
|
|
295
|
+
nowMs,
|
|
296
|
+
cooldownUntilMs,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
if (agent?.proc) {
|
|
300
|
+
agent.stopCause = 'outbound_rate_limited';
|
|
301
|
+
try {
|
|
302
|
+
agent.proc.kill();
|
|
303
|
+
} catch (err) {
|
|
304
|
+
console.warn(`[AgentManager] Failed to stop rate-limited agent ${agentId}: ${err.message}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
192
309
|
_materializeSkills(workspaceDir, skills) {
|
|
193
310
|
const skillsDir = path.join(workspaceDir, '.skills');
|
|
194
311
|
mkdirSync(skillsDir, { recursive: true });
|
|
@@ -206,8 +323,64 @@ export class AgentManager {
|
|
|
206
323
|
}
|
|
207
324
|
}
|
|
208
325
|
|
|
326
|
+
_normalizeDeliverySenderType(senderType) {
|
|
327
|
+
const normalized = String(senderType ?? '').trim().toLowerCase();
|
|
328
|
+
if (normalized === 'user') return 'user';
|
|
329
|
+
if (normalized === 'agent') return 'peer-agent';
|
|
330
|
+
return 'system';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
_formatDeliveryHeaderValue(value) {
|
|
334
|
+
const normalized = String(value ?? '').trim();
|
|
335
|
+
if (!normalized) return 'unknown';
|
|
336
|
+
if (/^[A-Za-z0-9_:@#./-]+$/.test(normalized)) return normalized;
|
|
337
|
+
return JSON.stringify(normalized);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
_formatDeliveryTarget(message) {
|
|
341
|
+
const workspaceType = String(message?.workspace_type ?? '').trim().toLowerCase();
|
|
342
|
+
const workspaceName = String(message?.workspace_name ?? '').trim();
|
|
343
|
+
const parentWorkspaceName = String(message?.parent_workspace_name ?? '').trim();
|
|
344
|
+
const parentWorkspaceType = String(message?.parent_workspace_type ?? '').trim().toLowerCase();
|
|
345
|
+
|
|
346
|
+
if (workspaceType === 'dm' || workspaceType === 'lightcone_dm') {
|
|
347
|
+
return `dm:@${workspaceName || 'unknown'}`;
|
|
348
|
+
}
|
|
349
|
+
if (workspaceType === 'thread') {
|
|
350
|
+
if (parentWorkspaceType === 'dm' || parentWorkspaceType === 'lightcone_dm') {
|
|
351
|
+
return `dm:@${parentWorkspaceName || workspaceName || 'unknown'}`;
|
|
352
|
+
}
|
|
353
|
+
return `#${parentWorkspaceName || workspaceName || 'unknown'}`;
|
|
354
|
+
}
|
|
355
|
+
return `#${workspaceName || 'all'}`;
|
|
356
|
+
}
|
|
357
|
+
|
|
209
358
|
_formatDeliveryText(message) {
|
|
210
|
-
|
|
359
|
+
const fromType = this._normalizeDeliverySenderType(message?.sender_type);
|
|
360
|
+
const senderName = String(message?.sender_name ?? '').trim();
|
|
361
|
+
const target = this._formatDeliveryTarget(message);
|
|
362
|
+
const messageIdShort = String(message?.message_id ?? '').trim().slice(0, 8);
|
|
363
|
+
const time = String(message?.timestamp ?? '').trim();
|
|
364
|
+
const content = String(message?.content ?? '').trim();
|
|
365
|
+
|
|
366
|
+
const fields = [`from=${fromType}`];
|
|
367
|
+
if (fromType === 'user') {
|
|
368
|
+
fields.push(`user=${this._formatDeliveryHeaderValue(senderName || 'unknown')}`);
|
|
369
|
+
} else if (fromType === 'peer-agent') {
|
|
370
|
+
fields.push(`agent=${this._formatDeliveryHeaderValue(senderName || 'unknown')}`);
|
|
371
|
+
} else {
|
|
372
|
+
const reason = String(message?.reason ?? message?.type ?? 'delivery').trim() || 'delivery';
|
|
373
|
+
fields.push(`reason=${this._formatDeliveryHeaderValue(reason)}`);
|
|
374
|
+
}
|
|
375
|
+
fields.push(`target=${this._formatDeliveryHeaderValue(target)}`);
|
|
376
|
+
if (messageIdShort) fields.push(`msg=${this._formatDeliveryHeaderValue(messageIdShort)}`);
|
|
377
|
+
if (time) fields.push(`time=${this._formatDeliveryHeaderValue(time)}`);
|
|
378
|
+
|
|
379
|
+
if (fromType === 'system') {
|
|
380
|
+
return `[${fields.join(' ')}] ${content}`;
|
|
381
|
+
}
|
|
382
|
+
const speaker = senderName ? `@${senderName}` : '@unknown';
|
|
383
|
+
return `[${fields.join(' ')}] ${speaker}: ${content}`;
|
|
211
384
|
}
|
|
212
385
|
|
|
213
386
|
_emitLifecycle(connection, {
|
|
@@ -235,6 +408,35 @@ export class AgentManager {
|
|
|
235
408
|
});
|
|
236
409
|
}
|
|
237
410
|
|
|
411
|
+
_emitCapabilityProbe(connection, {
|
|
412
|
+
agentId,
|
|
413
|
+
workspaceId,
|
|
414
|
+
capabilityId = CHAT_CAPABILITY_ID,
|
|
415
|
+
state = 'unknown',
|
|
416
|
+
reason = null,
|
|
417
|
+
runtime = null,
|
|
418
|
+
details = null,
|
|
419
|
+
latencyMs = null,
|
|
420
|
+
probedAt = new Date().toISOString(),
|
|
421
|
+
}) {
|
|
422
|
+
const normalizedCapabilityId = String(capabilityId ?? '').trim().toLowerCase() || CHAT_CAPABILITY_ID;
|
|
423
|
+
const normalizedState = String(state ?? '').trim().toLowerCase() || 'unknown';
|
|
424
|
+
const normalizedReason = String(reason ?? '').trim() || null;
|
|
425
|
+
const normalizedRuntime = String(runtime ?? '').trim().toLowerCase() || null;
|
|
426
|
+
connection.send({
|
|
427
|
+
type: 'agent:capability_probe',
|
|
428
|
+
agentId,
|
|
429
|
+
workspaceId,
|
|
430
|
+
capabilityId: normalizedCapabilityId,
|
|
431
|
+
state: normalizedState,
|
|
432
|
+
reason: normalizedReason,
|
|
433
|
+
runtime: normalizedRuntime,
|
|
434
|
+
details: details && typeof details === 'object' ? details : null,
|
|
435
|
+
latencyMs: Number.isFinite(Number(latencyMs)) ? Number(latencyMs) : null,
|
|
436
|
+
probedAt,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
238
440
|
_takePendingMessage(key) {
|
|
239
441
|
if (!this._pendingMessages) return null;
|
|
240
442
|
const pending = this._pendingMessages.get(key);
|
|
@@ -320,10 +522,14 @@ export class AgentManager {
|
|
|
320
522
|
_buildCodexMcpArgs(mcpServers) {
|
|
321
523
|
const args = [];
|
|
322
524
|
for (const [serverKey, server] of Object.entries(mcpServers)) {
|
|
525
|
+
const normalizedKey = String(serverKey ?? '').trim();
|
|
526
|
+
if (!normalizedKey) continue;
|
|
527
|
+
const keyExpr = /^[A-Za-z0-9_-]+$/.test(normalizedKey)
|
|
528
|
+
? normalizedKey
|
|
529
|
+
: JSON.stringify(normalizedKey);
|
|
323
530
|
const serverArgs = Array.isArray(server.args) ? server.args.map((item) => String(item)) : [];
|
|
324
531
|
const envVars = normalizeObject(server.env);
|
|
325
532
|
const envPairs = Object.entries(envVars).map(([k, v]) => `${k}=${String(v ?? '')}`);
|
|
326
|
-
const keyExpr = JSON.stringify(serverKey);
|
|
327
533
|
const commandExpr = JSON.stringify(envPairs.length > 0 ? 'env' : server.command);
|
|
328
534
|
const argsExpr = JSON.stringify(envPairs.length > 0 ? [...envPairs, server.command, ...serverArgs] : serverArgs);
|
|
329
535
|
args.push(
|
|
@@ -373,7 +579,6 @@ export class AgentManager {
|
|
|
373
579
|
const codexPrompt = startupMsg
|
|
374
580
|
? `${codexBasePrompt}\n\nNew message received:\n\n${this._formatDeliveryText(startupMsg.message)}\n\nRespond as appropriate. Complete all required work before stopping.`
|
|
375
581
|
: codexBasePrompt;
|
|
376
|
-
|
|
377
582
|
const authToken = config?.authToken || this.machineApiKey;
|
|
378
583
|
const userId = config?.userId ?? 'default';
|
|
379
584
|
const profileRoot = path.join(homedir(), '.lightcone', 'chrome-profiles');
|
|
@@ -390,6 +595,17 @@ export class AgentManager {
|
|
|
390
595
|
};
|
|
391
596
|
const mcpServers = this._resolveDirectiveMcpServers(directive, baseReplacements);
|
|
392
597
|
|
|
598
|
+
if (runtime === 'codex') {
|
|
599
|
+
const mcpKeys = Object.keys(mcpServers);
|
|
600
|
+
console.log(
|
|
601
|
+
`[AgentManager][codex][spawn] agent=${config?.displayName ?? agentId} workspace=${workspaceId ?? 'none'} `
|
|
602
|
+
+ `startup_seq=${startupMsg?.seq ?? 'none'} prompt_len=${codexPrompt.length} `
|
|
603
|
+
+ `session=${config?.sessionId ?? 'none'} `
|
|
604
|
+
+ `mcp=${mcpKeys.join(',') || 'none'} `
|
|
605
|
+
+ `startup_preview="${previewText(startupMsg?.message?.content ?? '')}"`
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
393
609
|
let kimiFiles = null;
|
|
394
610
|
if (runtime === 'kimi') {
|
|
395
611
|
kimiFiles = this._createKimiConfigFiles(workspaceDir, {
|
|
@@ -596,6 +812,28 @@ export class AgentManager {
|
|
|
596
812
|
console.log(`[AgentManager] Agent ${config?.displayName ?? agentId} in workspace ${workspaceName ?? workspaceId} already registered`);
|
|
597
813
|
return;
|
|
598
814
|
}
|
|
815
|
+
const remainingCooldownMs = this._getRemainingCooldownMs(key);
|
|
816
|
+
if (remainingCooldownMs > 0) {
|
|
817
|
+
const remainingSeconds = Math.ceil(remainingCooldownMs / 1000);
|
|
818
|
+
const reason = `outbound_rate_limited_cooldown:${remainingSeconds}s`;
|
|
819
|
+
console.warn(
|
|
820
|
+
`[WARN] [AgentManager] Rejecting start for rate-limited agent=${config?.displayName ?? agentId} `
|
|
821
|
+
+ `workspace=${workspaceName ?? workspaceId ?? 'none'} cooldown_remaining_s=${remainingSeconds}`
|
|
822
|
+
);
|
|
823
|
+
this._emitLifecycle(connection, {
|
|
824
|
+
agentId,
|
|
825
|
+
workspaceId,
|
|
826
|
+
runtime: config?.runtime ?? 'claude',
|
|
827
|
+
reachability: 'reachable',
|
|
828
|
+
availability: 'paused',
|
|
829
|
+
runtimeState: 'stopped',
|
|
830
|
+
reason,
|
|
831
|
+
sessionId: config?.sessionId ?? null,
|
|
832
|
+
});
|
|
833
|
+
connection.send({ type: 'agent:status', agentId, workspaceId, status: 'inactive' });
|
|
834
|
+
connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'offline', detail: reason, entries: [] });
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
599
837
|
this.starting.add(key);
|
|
600
838
|
|
|
601
839
|
const requestedRuntime = String(config.runtime ?? 'claude').trim().toLowerCase() || 'claude';
|
|
@@ -607,6 +845,15 @@ export class AgentManager {
|
|
|
607
845
|
rollbackStartupMessage?.(reason ?? 'spawn_failed');
|
|
608
846
|
this._clearDirectiveRefresh(key);
|
|
609
847
|
this.starting.delete(key);
|
|
848
|
+
this._emitCapabilityProbe(connection, {
|
|
849
|
+
agentId,
|
|
850
|
+
workspaceId,
|
|
851
|
+
capabilityId: CHAT_CAPABILITY_ID,
|
|
852
|
+
state: 'unready',
|
|
853
|
+
reason: reason ?? 'spawn_failed',
|
|
854
|
+
runtime: runtimeHint,
|
|
855
|
+
details: { stage: 'fail_start' },
|
|
856
|
+
});
|
|
610
857
|
this._emitLifecycle(connection, {
|
|
611
858
|
agentId,
|
|
612
859
|
workspaceId,
|
|
@@ -773,6 +1020,40 @@ export class AgentManager {
|
|
|
773
1020
|
return;
|
|
774
1021
|
}
|
|
775
1022
|
|
|
1023
|
+
this._emitCapabilityProbe(connection, {
|
|
1024
|
+
agentId,
|
|
1025
|
+
workspaceId,
|
|
1026
|
+
capabilityId: CHAT_CAPABILITY_ID,
|
|
1027
|
+
state: 'unknown',
|
|
1028
|
+
reason: 'probe_pending',
|
|
1029
|
+
runtime,
|
|
1030
|
+
details: { stage: 'spawn_preflight' },
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
const capabilityProbe = await probeChatBridgeCapability({
|
|
1034
|
+
chatBridgePath,
|
|
1035
|
+
cwd: workspaceDir,
|
|
1036
|
+
timeoutMs: CAPABILITY_PROBE_TIMEOUT_MS,
|
|
1037
|
+
env: {
|
|
1038
|
+
...spawnPlan.env,
|
|
1039
|
+
SERVER_URL: spawnPlan.env?.SERVER_URL ?? this.serverUrl,
|
|
1040
|
+
MACHINE_API_KEY: spawnPlan.env?.MACHINE_API_KEY ?? runtimeConfig.authToken ?? this.machineApiKey,
|
|
1041
|
+
AGENT_ID: spawnPlan.env?.AGENT_ID ?? agentId,
|
|
1042
|
+
WORKSPACE_ID: spawnPlan.env?.WORKSPACE_ID ?? (workspaceId ?? ''),
|
|
1043
|
+
WORKSPACE_DIR: spawnPlan.env?.WORKSPACE_DIR ?? workspaceDir,
|
|
1044
|
+
},
|
|
1045
|
+
});
|
|
1046
|
+
this._emitCapabilityProbe(connection, {
|
|
1047
|
+
agentId,
|
|
1048
|
+
workspaceId,
|
|
1049
|
+
capabilityId: CHAT_CAPABILITY_ID,
|
|
1050
|
+
state: capabilityProbe.state,
|
|
1051
|
+
reason: capabilityProbe.reason,
|
|
1052
|
+
runtime,
|
|
1053
|
+
details: capabilityProbe.details,
|
|
1054
|
+
latencyMs: capabilityProbe.latencyMs,
|
|
1055
|
+
});
|
|
1056
|
+
|
|
776
1057
|
console.log(`[AgentManager] Spawning ${runtime} for ${config.displayName ?? agentId} workspace=${workspaceName ?? workspaceId ?? 'none'} directive=${directive.directive_id ?? 'n/a'}`);
|
|
777
1058
|
const stdio = runtime === 'codex'
|
|
778
1059
|
? ['ignore', 'pipe', 'pipe']
|
|
@@ -791,6 +1072,15 @@ export class AgentManager {
|
|
|
791
1072
|
this._clearDirectiveRefresh(key);
|
|
792
1073
|
this.starting.delete(key);
|
|
793
1074
|
this.agents.delete(key);
|
|
1075
|
+
this._emitCapabilityProbe(connection, {
|
|
1076
|
+
agentId,
|
|
1077
|
+
workspaceId,
|
|
1078
|
+
capabilityId: CHAT_CAPABILITY_ID,
|
|
1079
|
+
state: 'unready',
|
|
1080
|
+
reason: detail ?? 'spawn_failed',
|
|
1081
|
+
runtime,
|
|
1082
|
+
details: { stage: 'spawn_error' },
|
|
1083
|
+
});
|
|
794
1084
|
this._emitLifecycle(connection, {
|
|
795
1085
|
agentId,
|
|
796
1086
|
workspaceId,
|
|
@@ -828,6 +1118,7 @@ export class AgentManager {
|
|
|
828
1118
|
kimiIdle: false,
|
|
829
1119
|
directive,
|
|
830
1120
|
requiredCredentials,
|
|
1121
|
+
lastStartupMessage: startupMsg?.message ?? null,
|
|
831
1122
|
stopCause: null,
|
|
832
1123
|
});
|
|
833
1124
|
this.starting.delete(key);
|
|
@@ -875,8 +1166,11 @@ export class AgentManager {
|
|
|
875
1166
|
sessionId: config.sessionId ?? null,
|
|
876
1167
|
proc,
|
|
877
1168
|
runtime: 'codex',
|
|
1169
|
+
codexVisibleTextSeen: false,
|
|
1170
|
+
codexSendMessageUsed: false,
|
|
878
1171
|
directive,
|
|
879
1172
|
requiredCredentials,
|
|
1173
|
+
lastStartupMessage: startupMsg?.message ?? null,
|
|
880
1174
|
stopCause: null,
|
|
881
1175
|
});
|
|
882
1176
|
this.starting.delete(key);
|
|
@@ -901,6 +1195,7 @@ export class AgentManager {
|
|
|
901
1195
|
runtime: 'claude',
|
|
902
1196
|
directive,
|
|
903
1197
|
requiredCredentials,
|
|
1198
|
+
lastStartupMessage: startupMsg?.message ?? null,
|
|
904
1199
|
stopCause: null,
|
|
905
1200
|
});
|
|
906
1201
|
this.starting.delete(key);
|
|
@@ -929,7 +1224,12 @@ export class AgentManager {
|
|
|
929
1224
|
this._clearDirectiveRefresh(key);
|
|
930
1225
|
this.agents.delete(key);
|
|
931
1226
|
|
|
932
|
-
if (code === 0
|
|
1227
|
+
if (code === 0
|
|
1228
|
+
&& runtime === 'codex'
|
|
1229
|
+
&& agent?.stopCause !== 'contract_violation'
|
|
1230
|
+
&& agent?.stopCause !== 'outbound_rate_limited'
|
|
1231
|
+
&& this._getRemainingCooldownMs(key) <= 0
|
|
1232
|
+
&& this._pendingMessages?.get(key)?.length) {
|
|
933
1233
|
const pendingCount = this._pendingMessages?.get(key)?.length ?? 0;
|
|
934
1234
|
console.log(
|
|
935
1235
|
`[AgentManager] Codex exited cleanly with pending=${pendingCount}; restarting next turn for ${config.displayName ?? agentId}`
|
|
@@ -1293,6 +1593,13 @@ export class AgentManager {
|
|
|
1293
1593
|
_flushPending(key, connection) {
|
|
1294
1594
|
if (!this._pendingMessages) return;
|
|
1295
1595
|
if (this.agents.get(key)?.runtime === 'codex') return;
|
|
1596
|
+
if (this._isInboundFlushPaused(key)) {
|
|
1597
|
+
const pendingCount = this._pendingMessages.get(key)?.length ?? 0;
|
|
1598
|
+
if (pendingCount > 0) {
|
|
1599
|
+
console.warn(`[WARN] [AgentManager] Inbox flush paused key=${key} pending=${pendingCount}`);
|
|
1600
|
+
}
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1296
1603
|
const pending = this._pendingMessages.get(key);
|
|
1297
1604
|
if (!pending || pending.length === 0) return;
|
|
1298
1605
|
this._pendingMessages.delete(key);
|
|
@@ -1358,7 +1665,21 @@ export class AgentManager {
|
|
|
1358
1665
|
});
|
|
1359
1666
|
connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'thinking', detail: '', entries: [] });
|
|
1360
1667
|
break;
|
|
1668
|
+
case 'text':
|
|
1669
|
+
agent.codexVisibleTextSeen = true;
|
|
1670
|
+
console.log(`[AgentManager][${agent.config?.displayName ?? agentId.slice(0, 8)}][codex] <text> ${String(evt.text ?? '').slice(0, 500)}`);
|
|
1671
|
+
break;
|
|
1361
1672
|
case 'tool_call':
|
|
1673
|
+
if (evt.name === 'send_message') {
|
|
1674
|
+
agent.codexSendMessageUsed = true;
|
|
1675
|
+
this._recordSuccessfulOutboundMessage({
|
|
1676
|
+
key,
|
|
1677
|
+
agent,
|
|
1678
|
+
agentId,
|
|
1679
|
+
workspaceId,
|
|
1680
|
+
connection,
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1362
1683
|
this._emitLifecycle(connection, {
|
|
1363
1684
|
agentId,
|
|
1364
1685
|
workspaceId,
|
|
@@ -1395,6 +1716,9 @@ export class AgentManager {
|
|
|
1395
1716
|
_parseCodexLine(key, agentId, workspaceId, line, connection) {
|
|
1396
1717
|
const agent = this.agents.get(key);
|
|
1397
1718
|
if (!agent) return;
|
|
1719
|
+
console.log(
|
|
1720
|
+
`[AgentManager][${agent.config?.displayName ?? agentId.slice(0, 8)}][codex][raw] ${String(line).slice(0, 1200)}`
|
|
1721
|
+
);
|
|
1398
1722
|
const events = parseCodexLine(line);
|
|
1399
1723
|
for (const evt of events) {
|
|
1400
1724
|
switch (evt.kind) {
|
|
@@ -1417,7 +1741,26 @@ export class AgentManager {
|
|
|
1417
1741
|
});
|
|
1418
1742
|
connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'thinking', detail: '', entries: [] });
|
|
1419
1743
|
break;
|
|
1744
|
+
case 'text':
|
|
1745
|
+
agent.codexVisibleTextSeen = true;
|
|
1746
|
+
console.log(
|
|
1747
|
+
`[AgentManager][${agent.config?.displayName ?? agentId.slice(0, 8)}][codex][text] ${String(evt.text ?? '').slice(0, 1200)}`
|
|
1748
|
+
);
|
|
1749
|
+
break;
|
|
1420
1750
|
case 'tool_call':
|
|
1751
|
+
if (evt.name === 'send_message') {
|
|
1752
|
+
agent.codexSendMessageUsed = true;
|
|
1753
|
+
this._recordSuccessfulOutboundMessage({
|
|
1754
|
+
key,
|
|
1755
|
+
agent,
|
|
1756
|
+
agentId,
|
|
1757
|
+
workspaceId,
|
|
1758
|
+
connection,
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
console.log(
|
|
1762
|
+
`[AgentManager][${agent.config?.displayName ?? agentId.slice(0, 8)}][codex][tool] ${evt.name ?? 'unknown_tool'}`
|
|
1763
|
+
);
|
|
1421
1764
|
this._emitLifecycle(connection, {
|
|
1422
1765
|
agentId,
|
|
1423
1766
|
workspaceId,
|
|
@@ -1431,6 +1774,15 @@ export class AgentManager {
|
|
|
1431
1774
|
connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'working', detail: evt.name, entries: [] });
|
|
1432
1775
|
break;
|
|
1433
1776
|
case 'turn_end':
|
|
1777
|
+
const startupSenderType = String(agent.lastStartupMessage?.sender_type ?? '').trim().toLowerCase();
|
|
1778
|
+
if (startupSenderType === 'user' && agent.codexVisibleTextSeen && !agent.codexSendMessageUsed) {
|
|
1779
|
+
agent.stopCause = 'contract_violation';
|
|
1780
|
+
console.warn(
|
|
1781
|
+
`[AgentManager][${agent.config?.displayName ?? agentId.slice(0, 8)}][codex] contract_violation: visible text emitted without send_message workspace=${workspaceId ?? 'none'}`
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
agent.codexVisibleTextSeen = false;
|
|
1785
|
+
agent.codexSendMessageUsed = false;
|
|
1434
1786
|
this._emitLifecycle(connection, {
|
|
1435
1787
|
agentId,
|
|
1436
1788
|
workspaceId,
|
|
@@ -1444,6 +1796,9 @@ export class AgentManager {
|
|
|
1444
1796
|
connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'online', detail: '', entries: [] });
|
|
1445
1797
|
break;
|
|
1446
1798
|
case 'error':
|
|
1799
|
+
console.error(
|
|
1800
|
+
`[AgentManager][${agent.config?.displayName ?? agentId.slice(0, 8)}][codex][error] ${evt.message}`
|
|
1801
|
+
);
|
|
1447
1802
|
console.error(`[AgentManager][codex][${agentId}] Error: ${evt.message}`);
|
|
1448
1803
|
break;
|
|
1449
1804
|
}
|
|
@@ -1475,6 +1830,18 @@ export class AgentManager {
|
|
|
1475
1830
|
console.log(`[AgentManager][${displayName}] <text> ${block.text?.slice(0, 500)}`);
|
|
1476
1831
|
} else if (block.type === 'tool_use') {
|
|
1477
1832
|
console.log(`[AgentManager][${displayName}] <tool_use> ${block.name} params=${JSON.stringify(block.input ?? {}).slice(0, 500)}`);
|
|
1833
|
+
if (block.name === 'send_message') {
|
|
1834
|
+
const agent = this.agents.get(key);
|
|
1835
|
+
if (agent) {
|
|
1836
|
+
this._recordSuccessfulOutboundMessage({
|
|
1837
|
+
key,
|
|
1838
|
+
agent,
|
|
1839
|
+
agentId,
|
|
1840
|
+
workspaceId,
|
|
1841
|
+
connection,
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1478
1845
|
this._emitLifecycle(connection, {
|
|
1479
1846
|
agentId,
|
|
1480
1847
|
workspaceId,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
|
+
|
|
4
|
+
const REQUIRED_CHAT_TOOLS = Object.freeze(['check_messages', 'send_message']);
|
|
5
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 8000;
|
|
6
|
+
|
|
7
|
+
function toErrorCode(error, fallback = 'probe_failed') {
|
|
8
|
+
const message = String(error?.message ?? error ?? '').trim();
|
|
9
|
+
if (!message) return fallback;
|
|
10
|
+
return message.toLowerCase().replace(/[^a-z0-9:_-]+/g, '_').slice(0, 120) || fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
14
|
+
const effectiveTimeoutMs = Math.max(1000, Number(timeoutMs) || DEFAULT_PROBE_TIMEOUT_MS);
|
|
15
|
+
let timeout = null;
|
|
16
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
17
|
+
timeout = setTimeout(() => {
|
|
18
|
+
const error = new Error(label);
|
|
19
|
+
error.code = label;
|
|
20
|
+
reject(error);
|
|
21
|
+
}, effectiveTimeoutMs);
|
|
22
|
+
});
|
|
23
|
+
return Promise.race([
|
|
24
|
+
promise,
|
|
25
|
+
timeoutPromise,
|
|
26
|
+
]).finally(() => {
|
|
27
|
+
if (timeout) clearTimeout(timeout);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeEnv(env = {}) {
|
|
32
|
+
const normalized = {};
|
|
33
|
+
for (const [key, value] of Object.entries(env)) {
|
|
34
|
+
if (value == null) continue;
|
|
35
|
+
normalized[key] = String(value);
|
|
36
|
+
}
|
|
37
|
+
return normalized;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function summarizeMissingTools(toolNames) {
|
|
41
|
+
return REQUIRED_CHAT_TOOLS.filter((toolName) => !toolNames.has(toolName));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function probeChatBridgeCapability({
|
|
45
|
+
chatBridgePath,
|
|
46
|
+
cwd,
|
|
47
|
+
env = {},
|
|
48
|
+
timeoutMs = DEFAULT_PROBE_TIMEOUT_MS,
|
|
49
|
+
} = {}) {
|
|
50
|
+
const startedAt = Date.now();
|
|
51
|
+
const normalizedEnv = normalizeEnv(env);
|
|
52
|
+
const client = new Client({
|
|
53
|
+
name: 'lightcone-capability-probe',
|
|
54
|
+
version: '1.0.0',
|
|
55
|
+
});
|
|
56
|
+
const transport = new StdioClientTransport({
|
|
57
|
+
command: process.execPath,
|
|
58
|
+
args: [chatBridgePath],
|
|
59
|
+
cwd,
|
|
60
|
+
env: normalizedEnv,
|
|
61
|
+
stderr: 'pipe',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await withTimeout(client.connect(transport), timeoutMs, 'probe_connect_timeout');
|
|
66
|
+
const listed = await withTimeout(client.listTools(), timeoutMs, 'probe_list_tools_timeout');
|
|
67
|
+
const tools = Array.isArray(listed?.tools) ? listed.tools : [];
|
|
68
|
+
const toolNames = new Set(
|
|
69
|
+
tools
|
|
70
|
+
.map((tool) => String(tool?.name ?? '').trim())
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
);
|
|
73
|
+
const missingTools = summarizeMissingTools(toolNames);
|
|
74
|
+
const latencyMs = Date.now() - startedAt;
|
|
75
|
+
if (missingTools.length > 0) {
|
|
76
|
+
return {
|
|
77
|
+
state: 'unready',
|
|
78
|
+
reason: `missing_tools:${missingTools.join(',')}`,
|
|
79
|
+
latencyMs,
|
|
80
|
+
details: {
|
|
81
|
+
toolCount: tools.length,
|
|
82
|
+
missingTools,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
state: 'ready',
|
|
88
|
+
reason: null,
|
|
89
|
+
latencyMs,
|
|
90
|
+
details: {
|
|
91
|
+
toolCount: tools.length,
|
|
92
|
+
missingTools: [],
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return {
|
|
97
|
+
state: 'unready',
|
|
98
|
+
reason: toErrorCode(error, 'probe_failed'),
|
|
99
|
+
latencyMs: Date.now() - startedAt,
|
|
100
|
+
details: {
|
|
101
|
+
toolCount: null,
|
|
102
|
+
missingTools: [...REQUIRED_CHAT_TOOLS],
|
|
103
|
+
error: String(error?.message ?? error ?? 'unknown_error').slice(0, 500),
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
} finally {
|
|
107
|
+
try {
|
|
108
|
+
await withTimeout(client.close(), 1200, 'probe_close_timeout');
|
|
109
|
+
} catch {
|
|
110
|
+
// noop: closing a failed/half-open client may throw.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/chat-bridge.js
CHANGED
|
@@ -53,6 +53,12 @@ const GOVERNANCE_TIMEOUT_MS = Number(process.env.GOVERNANCE_TIMEOUT_MS ?? 5000);
|
|
|
53
53
|
const LEASE_GRACE_MS = Number(process.env.GOVERNANCE_LEASE_GRACE_MS ?? 5000);
|
|
54
54
|
const BUNDLE_EVENT_FLUSH_MS = Number(process.env.GOVERNANCE_BUNDLE_FLUSH_MS ?? 2000);
|
|
55
55
|
|
|
56
|
+
function redactTokenPrefix(token) {
|
|
57
|
+
const normalized = String(token ?? '').trim();
|
|
58
|
+
if (!normalized) return 'missing';
|
|
59
|
+
return normalized.slice(0, 18);
|
|
60
|
+
}
|
|
61
|
+
|
|
56
62
|
// Current active workspaceId for memory isolation (defaults to spawn-time WORKSPACE_ID)
|
|
57
63
|
let currentWorkspaceId = WORKSPACE_ID;
|
|
58
64
|
|
|
@@ -2266,4 +2272,12 @@ if (IS_HOST_AGENT) {
|
|
|
2266
2272
|
// ── start ─────────────────────────────────────────────────────────────────────
|
|
2267
2273
|
const transport = new StdioServerTransport();
|
|
2268
2274
|
await server.connect(transport);
|
|
2269
|
-
console.error(
|
|
2275
|
+
console.error(
|
|
2276
|
+
`[chat-bridge] MCP Server started `
|
|
2277
|
+
+ `agentId=${AGENT_ID || 'missing'} `
|
|
2278
|
+
+ `workspaceId=${currentWorkspaceId || 'none'} `
|
|
2279
|
+
+ `host=${IS_HOST_AGENT ? 'true' : 'false'} `
|
|
2280
|
+
+ `serverUrl=${SERVER_URL || 'missing'} `
|
|
2281
|
+
+ `machineKey=${redactTokenPrefix(MACHINE_API_KEY)} `
|
|
2282
|
+
+ `governanceBundle=${GOVERNANCE_SPAWN_BUNDLE_ID || 'none'}`
|
|
2283
|
+
);
|