@lightcone-ai/daemon 0.14.19 → 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/package.json +1 -1
- 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/lifecycle-protocol.js +18 -0
- package/src/mcp-config.js +25 -14
- package/src/runtime-config.js +19 -0
package/package.json
CHANGED
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
|
+
);
|
package/src/drivers/claude.js
CHANGED
|
@@ -40,34 +40,46 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
|
|
|
40
40
|
|
|
41
41
|
CRITICAL RULES:
|
|
42
42
|
- Always communicate through ${t("send_message")}. This is your only output method.
|
|
43
|
+
- Plain assistant text is not a user-visible reply. If you need the human or another agent to see something, you must call ${t("send_message")}.
|
|
43
44
|
- Use only the provided MCP tools for messaging — they are already available and ready.
|
|
44
45
|
- Always claim a task via ${t("claim_tasks")} before starting work on it. If the claim fails, move on to a different task.
|
|
45
46
|
|
|
46
47
|
## Startup sequence
|
|
47
48
|
|
|
48
|
-
1. If this turn already includes a concrete incoming message
|
|
49
|
+
1. If this turn already includes a concrete incoming message with \`from=user\`, first decide whether it needs a visible acknowledgment, blocker question, or ownership signal. If it does, send it early with ${t("send_message")} before deep context gathering.
|
|
49
50
|
2. Read MEMORY.md (via ${t("read_memory")}) and then only the additional memory/notes files you need to handle the current turn well.
|
|
50
51
|
3. If there is no concrete incoming message to handle, stop and wait. New messages will be delivered to you automatically via stdin.
|
|
51
|
-
4. When you receive a message,
|
|
52
|
+
4. When you receive a message, first apply the audience-aware rules below before deciding whether to call ${t("send_message")}. Do not treat plain narrative output as a substitute for a visible reply.
|
|
52
53
|
5. **Complete ALL your work before stopping.** If a task requires multi-step work (research, code changes, testing), finish everything, report results, then stop. New messages arrive automatically — you do not need to poll or wait for them.
|
|
53
54
|
|
|
54
55
|
## Messaging
|
|
55
56
|
|
|
56
|
-
Messages you receive have a single
|
|
57
|
+
Messages you receive have a single structured data header followed by the sender/content payload:
|
|
57
58
|
|
|
58
59
|
\`\`\`
|
|
59
|
-
[target=#general msg=a1b2c3d4 time=2026-03-15T01:00:00] @richard: hello everyone
|
|
60
|
-
[target=#general msg=e5f6a7b8 time=2026-03-15T01:00:01
|
|
61
|
-
[target=dm:@richard msg=c9d0e1f2 time=2026-03-15T01:00:02] @richard: hey, can you help?
|
|
62
|
-
[target=#general
|
|
63
|
-
[target=dm:@richard:x9y8z7a0 msg=d7e8f9a0 time=2026-03-15T01:00:04] @richard: DM thread reply
|
|
60
|
+
[from=user user=richard target=#general msg=a1b2c3d4 time=2026-03-15T01:00:00] @richard: hello everyone
|
|
61
|
+
[from=peer-agent agent=Alice target=#general msg=e5f6a7b8 time=2026-03-15T01:00:01] @Alice: hi there
|
|
62
|
+
[from=user user=richard target=dm:@richard msg=c9d0e1f2 time=2026-03-15T01:00:02] @richard: hey, can you help?
|
|
63
|
+
[from=system reason=startup target=#general msg=f3a4b5c6 time=2026-03-15T01:00:03] Agent resumed
|
|
64
64
|
\`\`\`
|
|
65
65
|
|
|
66
66
|
Header fields:
|
|
67
|
+
- \`from=\` — one of \`user\` / \`peer-agent\` / \`system\`.
|
|
67
68
|
- \`target=\` — where the message came from. Reuse as the \`target\` parameter when replying.
|
|
68
69
|
- \`msg=\` — message short ID (first 8 chars of UUID). Use as thread suffix to start/reply in a thread.
|
|
69
70
|
- \`time=\` — timestamp.
|
|
70
|
-
|
|
71
|
+
|
|
72
|
+
## Audience-aware messaging
|
|
73
|
+
|
|
74
|
+
Each incoming message has a \`from=\` tag and should be handled as follows:
|
|
75
|
+
|
|
76
|
+
- **from=user**: this is a real user request. You MUST acknowledge with ${t("send_message")} before deep context-gathering, even if the acknowledgment is short.
|
|
77
|
+
- **from=peer-agent**: default is **no reply**. Reply only when at least one condition holds:
|
|
78
|
+
1. You were explicitly @mentioned.
|
|
79
|
+
2. You have materially new information that changes execution decisions.
|
|
80
|
+
3. The peer explicitly asked you a concrete question.
|
|
81
|
+
Pure receipts like "收到/已对齐/分工确认" are not requests and should not trigger a reply.
|
|
82
|
+
- **from=system**: internal lifecycle/governance/startup signal. Do not reply unless explicitly instructed.
|
|
71
83
|
|
|
72
84
|
### Sending messages
|
|
73
85
|
|
|
@@ -123,6 +135,12 @@ Only top-level workspace / DM messages can become tasks. Messages inside threads
|
|
|
123
135
|
4. When done, set status to \`in_review\` so a human can validate
|
|
124
136
|
5. After approval (e.g. "looks good", "merge it"), set status to \`done\`
|
|
125
137
|
|
|
138
|
+
**Primary-agent dispatch hard rule (fail-closed):**
|
|
139
|
+
- If your role is the workspace primary agent/owner and a user sends an execution request, you MUST call \`${t("create_tasks")}\` first and include an explicit \`scenario_type\` before routing work via \`${t("send_message")}\`.
|
|
140
|
+
- Execution requests include requests like content writing, short-video scripting, research, design/asset production, implementation, or any request that requires downstream execution instead of a simple answer.
|
|
141
|
+
- Use \`scenario_type\` values declared by your scenario manifest/dispatch protocol (for example: \`trend_scan\`, \`topic_research\`, \`research\`, \`graphic_writing\`, \`short_video_scripting\`, \`publish\`).
|
|
142
|
+
- Do not route execution work with only \`${t("send_message")}\`: skipping \`${t("create_tasks")}\` can cause downstream \`${t("claim_tasks")}\` failures and deadlock the workflow.
|
|
143
|
+
|
|
126
144
|
**What \`${t("create_tasks")}\` really means:**
|
|
127
145
|
- Tasks live in the same chat flow as messages. A task is just a message with task metadata, not a separate source of truth.
|
|
128
146
|
- \`${t("create_tasks")}\` is a convenience helper for a specific sequence: create a brand-new message, then publish that new message as a task-message.
|
package/src/drivers/codex.js
CHANGED
|
@@ -39,6 +39,14 @@ function quote(value) {
|
|
|
39
39
|
return JSON.stringify(value);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function formatCodexServerKey(serverKey) {
|
|
43
|
+
const normalizedKey = String(serverKey ?? '').trim();
|
|
44
|
+
if (!normalizedKey) return null;
|
|
45
|
+
return /^[A-Za-z0-9_-]+$/.test(normalizedKey)
|
|
46
|
+
? normalizedKey
|
|
47
|
+
: quote(normalizedKey);
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
function normalizeCodexModel(model) {
|
|
43
51
|
if (!model) return 'gpt-5.2';
|
|
44
52
|
const normalized = String(model).trim().toLowerCase();
|
|
@@ -71,7 +79,6 @@ export function adaptCodexSystemPrompt(sourcePrompt) {
|
|
|
71
79
|
if (!basePrompt.trim()) return '';
|
|
72
80
|
|
|
73
81
|
let prompt = basePrompt
|
|
74
|
-
.replaceAll('mcp__chat__', '')
|
|
75
82
|
.replace(
|
|
76
83
|
'3. If there is no concrete incoming message to handle, stop and wait. New messages will be delivered to you automatically via stdin.',
|
|
77
84
|
'3. If there is no concrete incoming message to handle, stop. The daemon will restart you when new messages arrive.'
|
|
@@ -117,22 +124,24 @@ function normalizeDirectiveMcpServers(value) {
|
|
|
117
124
|
function buildDirectiveMcpFlags(mcpServers) {
|
|
118
125
|
const args = [];
|
|
119
126
|
for (const [serverKey, mc] of Object.entries(mcpServers)) {
|
|
127
|
+
const keyExpr = formatCodexServerKey(serverKey);
|
|
128
|
+
if (!keyExpr) continue;
|
|
120
129
|
const envPairs = Object.entries(mc.env ?? {}).map(([k, v]) => `${k}=${v ?? ''}`);
|
|
121
130
|
if (envPairs.length > 0) {
|
|
122
131
|
args.push(
|
|
123
|
-
'-c', `mcp_servers.${
|
|
124
|
-
'-c', `mcp_servers.${
|
|
125
|
-
'-c', `mcp_servers.${
|
|
132
|
+
'-c', `mcp_servers.${keyExpr}.command=${quote('env')}`,
|
|
133
|
+
'-c', `mcp_servers.${keyExpr}.args=${quote([...envPairs, mc.command, ...(mc.args ?? [])])}`,
|
|
134
|
+
'-c', `mcp_servers.${keyExpr}.enabled=true`
|
|
126
135
|
);
|
|
127
136
|
} else {
|
|
128
137
|
args.push(
|
|
129
|
-
'-c', `mcp_servers.${
|
|
130
|
-
'-c', `mcp_servers.${
|
|
131
|
-
'-c', `mcp_servers.${
|
|
138
|
+
'-c', `mcp_servers.${keyExpr}.command=${quote(mc.command)}`,
|
|
139
|
+
'-c', `mcp_servers.${keyExpr}.args=${quote(mc.args ?? [])}`,
|
|
140
|
+
'-c', `mcp_servers.${keyExpr}.enabled=true`
|
|
132
141
|
);
|
|
133
142
|
}
|
|
134
143
|
if (mc.required) {
|
|
135
|
-
args.push('-c', `mcp_servers.${
|
|
144
|
+
args.push('-c', `mcp_servers.${keyExpr}.required=true`);
|
|
136
145
|
}
|
|
137
146
|
}
|
|
138
147
|
return args;
|
|
@@ -206,19 +215,21 @@ export function buildCodexSpawn({
|
|
|
206
215
|
});
|
|
207
216
|
|
|
208
217
|
for (const [serverKey, mc] of Object.entries(skillMcpServers)) {
|
|
218
|
+
const keyExpr = formatCodexServerKey(serverKey);
|
|
219
|
+
if (!keyExpr) continue;
|
|
209
220
|
const envPairs = Object.entries(mc.env ?? {}).map(([k, v]) => `${k}=${v ?? ''}`);
|
|
210
221
|
if (envPairs.length > 0) {
|
|
211
222
|
args.push(
|
|
212
|
-
'-c', `mcp_servers.${
|
|
213
|
-
'-c', `mcp_servers.${
|
|
214
|
-
'-c', `mcp_servers.${
|
|
223
|
+
'-c', `mcp_servers.${keyExpr}.command=${quote('env')}`,
|
|
224
|
+
'-c', `mcp_servers.${keyExpr}.args=${quote([...envPairs, mc.command, ...(mc.args ?? [])])}`,
|
|
225
|
+
'-c', `mcp_servers.${keyExpr}.enabled=true`
|
|
215
226
|
);
|
|
216
227
|
continue;
|
|
217
228
|
}
|
|
218
229
|
args.push(
|
|
219
|
-
'-c', `mcp_servers.${
|
|
220
|
-
'-c', `mcp_servers.${
|
|
221
|
-
'-c', `mcp_servers.${
|
|
230
|
+
'-c', `mcp_servers.${keyExpr}.command=${quote(mc.command)}`,
|
|
231
|
+
'-c', `mcp_servers.${keyExpr}.args=${quote(mc.args ?? [])}`,
|
|
232
|
+
'-c', `mcp_servers.${keyExpr}.enabled=true`
|
|
222
233
|
);
|
|
223
234
|
}
|
|
224
235
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export function resolveExitOfflineDetail({ code, signal, stopCause }) {
|
|
2
2
|
if (stopCause === 'manual_stop') return '';
|
|
3
3
|
if (stopCause === 'credential_revoked') return 'credential_revoked';
|
|
4
|
+
if (stopCause === 'contract_violation') return 'contract_violation:visible_text_without_send_message';
|
|
5
|
+
if (stopCause === 'outbound_rate_limited') return 'outbound_rate_limited';
|
|
4
6
|
if (code === 0) return '';
|
|
5
7
|
if (signal === 'SIGTERM') return '';
|
|
6
8
|
if (signal === 'SIGKILL') return 'agent_timeout';
|
|
@@ -25,6 +27,22 @@ export function resolveLifecycleExitState({ runtime, code, signal, stopCause })
|
|
|
25
27
|
reason: 'manual_stop',
|
|
26
28
|
};
|
|
27
29
|
}
|
|
30
|
+
if (stopCause === 'contract_violation') {
|
|
31
|
+
return {
|
|
32
|
+
reachability: 'reachable',
|
|
33
|
+
availability: 'available',
|
|
34
|
+
runtimeState: 'standby',
|
|
35
|
+
reason: 'contract_violation:visible_text_without_send_message',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (stopCause === 'outbound_rate_limited') {
|
|
39
|
+
return {
|
|
40
|
+
reachability: 'reachable',
|
|
41
|
+
availability: 'paused',
|
|
42
|
+
runtimeState: 'stopped',
|
|
43
|
+
reason: 'outbound_rate_limited',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
28
46
|
if (normalizedRuntime === 'codex' && code === 0) {
|
|
29
47
|
return {
|
|
30
48
|
reachability: 'reachable',
|
package/src/mcp-config.js
CHANGED
|
@@ -23,8 +23,14 @@ function resolveMcpPathToken(arg, mcpPaths = {}) {
|
|
|
23
23
|
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) return null;
|
|
24
24
|
|
|
25
25
|
const legacyId = LEGACY_MCP_PATH_TOKENS[trimmed] ?? null;
|
|
26
|
+
const legacyUnderscoreMatch = trimmed.match(/^\{([a-z0-9_]+)_mcp_path\}$/i);
|
|
27
|
+
const legacyUnderscoreId = legacyUnderscoreMatch
|
|
28
|
+
? legacyUnderscoreMatch[1].toLowerCase().replaceAll('_', '-')
|
|
29
|
+
: null;
|
|
26
30
|
const dynamicMatch = trimmed.match(/^\{mcp_path:([a-z0-9][a-z0-9_-]*)\}$/i);
|
|
27
|
-
const serverId = legacyId
|
|
31
|
+
const serverId = legacyId
|
|
32
|
+
?? legacyUnderscoreId
|
|
33
|
+
?? (dynamicMatch ? dynamicMatch[1].toLowerCase() : null);
|
|
28
34
|
if (!serverId) return null;
|
|
29
35
|
|
|
30
36
|
const runtimeMcpPath = typeof mcpPaths?.[serverId] === 'string'
|
|
@@ -99,20 +105,25 @@ export function buildSkillMcpServers({
|
|
|
99
105
|
const mc = skill.mcpConfig;
|
|
100
106
|
if (mcpServers[mc.server]) continue;
|
|
101
107
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
try {
|
|
109
|
+
const resolvedArgs = (mc.args ?? []).map(arg => resolveSkillArg(arg, config));
|
|
110
|
+
const resolvedEnv = {};
|
|
111
|
+
for (const envKey of (mc.env ?? [])) {
|
|
112
|
+
resolvedEnv[envKey] = agentEnv[envKey] ?? process.env[envKey] ?? '';
|
|
113
|
+
}
|
|
107
114
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
mcpServers[mc.server] = {
|
|
116
|
+
command: mc.command,
|
|
117
|
+
args: resolvedArgs,
|
|
118
|
+
env: {
|
|
119
|
+
...resolvedEnv,
|
|
120
|
+
...baseEnvForServer(mc.server, { serverUrl, authToken, agentId, workspaceId, workspaceDir }),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
} catch (error) {
|
|
124
|
+
const message = error instanceof Error ? error.message : String(error ?? 'unknown_error');
|
|
125
|
+
console.warn(`[mcp-config] Skipping MCP server '${mc.server}' due to invalid skill config: ${message}`);
|
|
126
|
+
}
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
return mcpServers;
|
package/src/runtime-config.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
const DEFAULT_LIGHTCONE_PORT = 9779;
|
|
2
|
+
const DEFAULT_OUTBOUND_WINDOW_MS = 60_000;
|
|
3
|
+
const DEFAULT_OUTBOUND_SOFT_CAP = 5;
|
|
4
|
+
const DEFAULT_OUTBOUND_HARD_CAP = 10;
|
|
5
|
+
const DEFAULT_OUTBOUND_COOLDOWN_MS = 5 * 60_000;
|
|
6
|
+
|
|
7
|
+
function resolvePositiveInt(value, fallback) {
|
|
8
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
9
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
2
12
|
|
|
3
13
|
export function resolveLightconeServerUrl(env = process.env) {
|
|
4
14
|
return env.SERVER_URL || `http://localhost:${DEFAULT_LIGHTCONE_PORT}`;
|
|
5
15
|
}
|
|
16
|
+
|
|
17
|
+
export function resolveOutboundRateLimitConfig(env = process.env) {
|
|
18
|
+
return {
|
|
19
|
+
windowMs: resolvePositiveInt(env.AGENT_OUTBOUND_RATE_WINDOW_MS, DEFAULT_OUTBOUND_WINDOW_MS),
|
|
20
|
+
softCap: resolvePositiveInt(env.AGENT_OUTBOUND_RATE_SOFT_CAP, DEFAULT_OUTBOUND_SOFT_CAP),
|
|
21
|
+
hardCap: resolvePositiveInt(env.AGENT_OUTBOUND_RATE_HARD_CAP, DEFAULT_OUTBOUND_HARD_CAP),
|
|
22
|
+
cooldownMs: resolvePositiveInt(env.AGENT_OUTBOUND_RATE_COOLDOWN_MS, DEFAULT_OUTBOUND_COOLDOWN_MS),
|
|
23
|
+
};
|
|
24
|
+
}
|