@lightcone-ai/daemon 0.14.16 → 0.14.18

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,90 @@
1
+ import { readFileSync } from 'fs';
2
+
3
+ function isPlainObject(value) {
4
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
5
+ }
6
+
7
+ function normalizeToken(value) {
8
+ return String(value ?? '').trim();
9
+ }
10
+
11
+ function normalizeId(value) {
12
+ return normalizeToken(value).toLowerCase();
13
+ }
14
+
15
+ function normalizeToolList(value) {
16
+ if (Array.isArray(value)) {
17
+ return value
18
+ .map(item => normalizeToken(item).toLowerCase())
19
+ .filter(Boolean);
20
+ }
21
+ const direct = normalizeToken(value).toLowerCase();
22
+ return direct ? [direct] : [];
23
+ }
24
+
25
+ function normalizeRule(rawRule) {
26
+ if (!isPlainObject(rawRule)) return null;
27
+
28
+ const workspaceId = normalizeId(rawRule.workspace_id ?? rawRule.workspaceId);
29
+ const agentId = normalizeId(rawRule.agent_id ?? rawRule.agentId);
30
+ const tools = normalizeToolList(
31
+ rawRule.tools
32
+ ?? rawRule.tool_names
33
+ ?? rawRule.toolNames
34
+ ?? rawRule.tool
35
+ );
36
+ const message = normalizeToken(rawRule.message ?? rawRule.error ?? rawRule.reason);
37
+
38
+ if (!workspaceId || !agentId || tools.length === 0 || !message) {
39
+ return null;
40
+ }
41
+
42
+ return {
43
+ workspaceId,
44
+ agentId,
45
+ tools,
46
+ message,
47
+ };
48
+ }
49
+
50
+ export function loadToolBlockRulesFromManifest(manifestUrl) {
51
+ if (!manifestUrl) return [];
52
+ let rawManifest = null;
53
+ try {
54
+ rawManifest = JSON.parse(readFileSync(manifestUrl, 'utf8'));
55
+ } catch {
56
+ return [];
57
+ }
58
+ if (!isPlainObject(rawManifest)) return [];
59
+
60
+ const rawRules = Array.isArray(rawManifest.tool_block_rules)
61
+ ? rawManifest.tool_block_rules
62
+ : Array.isArray(rawManifest.toolBlockRules)
63
+ ? rawManifest.toolBlockRules
64
+ : [];
65
+
66
+ return rawRules
67
+ .map(rule => normalizeRule(rule))
68
+ .filter(Boolean);
69
+ }
70
+
71
+ export function findToolBlockRule(rules, {
72
+ toolName = '',
73
+ workspaceId = '',
74
+ agentId = '',
75
+ } = {}) {
76
+ if (!Array.isArray(rules) || rules.length === 0) return null;
77
+
78
+ const targetTool = normalizeToken(toolName).toLowerCase();
79
+ const targetWorkspaceId = normalizeId(workspaceId);
80
+ const targetAgentId = normalizeId(agentId);
81
+ if (!targetTool || !targetWorkspaceId || !targetAgentId) return null;
82
+
83
+ for (const rule of rules) {
84
+ if (!rule || rule.workspaceId !== targetWorkspaceId || rule.agentId !== targetAgentId) continue;
85
+ if (rule.tools.includes('*') || rule.tools.includes(targetTool)) {
86
+ return rule;
87
+ }
88
+ }
89
+ return null;
90
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.14.16",
3
+ "version": "0.14.18",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -11,6 +11,7 @@ import {
11
11
  stopFfmpegCapture,
12
12
  waitForProcessExit,
13
13
  } from './ffmpeg-runner.js';
14
+ import { estimatePlanDurationMs } from './plan-estimator.js';
14
15
  import { executePlanPhases, normalizePlanPhases } from './plan-executor.js';
15
16
 
16
17
  const DEFAULT_VIEWPORT = Object.freeze({ width: 1080, height: 1920 });
@@ -64,47 +65,6 @@ function resolveUrl({ url, plan }) {
64
65
  throw error;
65
66
  }
66
67
 
67
- function estimatePlanDurationMs(plan = {}) {
68
- let phases = [];
69
- try {
70
- phases = normalizePlanPhases(plan);
71
- } catch {
72
- phases = [];
73
- }
74
-
75
- return phases.reduce((total, phase) => {
76
- const action = String(phase?.action ?? phase?.visual_action?.type ?? '').trim().toLowerCase();
77
- const durationMs = Number(phase?.duration_ms);
78
- const dwellMs = Number(phase?.dwell_ms);
79
- const transitionMs = Number(phase?.transition_ms ?? phase?.visual_action?.transition_ms);
80
- const effectiveHoldMs = Number.isFinite(dwellMs) && dwellMs > 0
81
- ? dwellMs
82
- : durationMs;
83
-
84
- if (action === 'hold' && Number.isFinite(effectiveHoldMs) && effectiveHoldMs > 0) {
85
- return total + effectiveHoldMs;
86
- }
87
- if (action === 'linear_scroll_during') {
88
- if (Number.isFinite(effectiveHoldMs) && effectiveHoldMs > 0) return total + effectiveHoldMs;
89
- return total + 1200;
90
- }
91
- if (action === 'scroll_to_dwell' || action === 'cursor_focus' || action === 'scroll_back') {
92
- let next = total;
93
- if (Number.isFinite(transitionMs) && transitionMs > 0) next += transitionMs;
94
- if (Number.isFinite(effectiveHoldMs) && effectiveHoldMs > 0) next += effectiveHoldMs;
95
- if (next === total) next += 1200;
96
- return next;
97
- }
98
- if (Number.isFinite(transitionMs) && transitionMs > 0) {
99
- return total + transitionMs;
100
- }
101
- if (Number.isFinite(durationMs) && durationMs > 0) {
102
- return total + durationMs;
103
- }
104
- return total + 800;
105
- }, 0);
106
- }
107
-
108
68
  function createXvfbExitError({ code, signal, stderr }) {
109
69
  const error = new Error(`xvfb_exited_unexpectedly:code=${code ?? 'null'}:signal=${signal ?? 'none'}`);
110
70
  error.code = 'XVFB_EXITED_UNEXPECTEDLY';
@@ -1,3 +1,5 @@
1
+ import { resolveDurationMs } from './phase-duration.js';
2
+
1
3
  function normalizeText(value) {
2
4
  if (typeof value !== 'string') return '';
3
5
  return value.trim();
@@ -85,16 +87,6 @@ function nowMs(getNowMs) {
85
87
  return Number(getNowMs?.()) || Date.now();
86
88
  }
87
89
 
88
- function resolveDurationMs(phase, fallback = 0) {
89
- const parsed = normalizeInteger(phase?.duration_ms, null);
90
- if (parsed !== null && parsed >= 0) return parsed;
91
- const dwellMs = normalizeInteger(phase?.dwell_ms, null);
92
- if (dwellMs !== null && dwellMs >= 0) return dwellMs;
93
- const secs = Number(phase?.duration_s);
94
- if (Number.isFinite(secs) && secs >= 0) return Math.round(secs * 1000);
95
- return fallback;
96
- }
97
-
98
90
  function resolveTransitionMs(phase, fallback) {
99
91
  const parsed = normalizeInteger(phase?.transition_ms, null);
100
92
  if (parsed !== null && parsed >= 0) return parsed;
@@ -16,6 +16,7 @@ import { injectWorkspaceContext } from './drivers/claude.js';
16
16
  import { parseKimiLine, encodeKimiStdin } from './drivers/kimi.js';
17
17
  import { startSession, stopSession, stopAllSessions } from './browser-login.js';
18
18
  import { markInvalidatedLeases } from './governance-state.js';
19
+ import { resolveExitOfflineDetail, resolveLifecycleExitState } from './lifecycle-protocol.js';
19
20
  import { runPublishJob } from './publish-job-runner.js';
20
21
 
21
22
  const KIMI_SYSTEM_PROMPT_FILE = '.lightcone-kimi-system.md';
@@ -115,15 +116,6 @@ function runtimeMissingDetail(runtime) {
115
116
  return `cli_missing:${runtime}`;
116
117
  }
117
118
 
118
- function resolveExitOfflineDetail({ code, signal, stopCause }) {
119
- if (stopCause === 'manual_stop') return '';
120
- if (stopCause === 'credential_revoked') return 'credential_revoked';
121
- if (code === 0) return '';
122
- if (signal === 'SIGTERM') return '';
123
- if (signal === 'SIGKILL') return 'agent_timeout';
124
- return 'spawn_session_crashed';
125
- }
126
-
127
119
  function normalizeObject(value) {
128
120
  return isPlainObject(value) ? value : {};
129
121
  }
@@ -218,6 +210,31 @@ export class AgentManager {
218
210
  return `New message in ${message.workspace_type === 'dm' ? 'dm from' : `#${message.workspace_name} from`} ${message.sender_name}: ${message.content}`;
219
211
  }
220
212
 
213
+ _emitLifecycle(connection, {
214
+ agentId,
215
+ workspaceId,
216
+ runtime,
217
+ reachability,
218
+ availability,
219
+ runtimeState,
220
+ reason = null,
221
+ sessionId = null,
222
+ }) {
223
+ const normalizedRuntime = String(runtime ?? '').trim().toLowerCase() || 'claude';
224
+ const normalizedReason = String(reason ?? '').trim() || null;
225
+ connection.send({
226
+ type: 'agent:lifecycle',
227
+ agentId,
228
+ workspaceId,
229
+ reachability,
230
+ availability,
231
+ runtimeState,
232
+ reason: normalizedReason,
233
+ sessionId: sessionId ?? null,
234
+ runtime: normalizedRuntime,
235
+ });
236
+ }
237
+
221
238
  _takePendingMessage(key) {
222
239
  if (!this._pendingMessages) return null;
223
240
  const pending = this._pendingMessages.get(key);
@@ -227,6 +244,16 @@ export class AgentManager {
227
244
  return msg;
228
245
  }
229
246
 
247
+ _enqueuePendingMessage(key, msg, { front = false } = {}) {
248
+ if (!msg) return 0;
249
+ if (!this._pendingMessages) this._pendingMessages = new Map();
250
+ const pending = this._pendingMessages.get(key) ?? [];
251
+ if (front) pending.unshift(msg);
252
+ else pending.push(msg);
253
+ this._pendingMessages.set(key, pending);
254
+ return pending.length;
255
+ }
256
+
230
257
  _prepareStartupMessage(key, runtime) {
231
258
  if (runtime === 'codex') {
232
259
  const startupMsg = this._takePendingMessage(key);
@@ -575,9 +602,21 @@ export class AgentManager {
575
602
  this._workspaceRootDir(workspaceId);
576
603
  const workspaceDir = this._workspaceDir(agentId, workspaceId);
577
604
  const chatBridgePath = new URL('./chat-bridge.js', import.meta.url).pathname;
578
- const failStart = (reason) => {
605
+ let rollbackStartupMessage = null;
606
+ const failStart = (reason, runtimeHint = requestedRuntime) => {
607
+ rollbackStartupMessage?.(reason ?? 'spawn_failed');
579
608
  this._clearDirectiveRefresh(key);
580
609
  this.starting.delete(key);
610
+ this._emitLifecycle(connection, {
611
+ agentId,
612
+ workspaceId,
613
+ runtime: runtimeHint,
614
+ reachability: 'unreachable',
615
+ availability: 'unreachable',
616
+ runtimeState: 'crashed',
617
+ reason: reason ?? 'spawn_failed',
618
+ sessionId: config.sessionId ?? null,
619
+ });
581
620
  connection.send({ type: 'agent:status', agentId, workspaceId, status: 'inactive' });
582
621
  connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'offline', detail: reason ?? '', entries: [] });
583
622
  };
@@ -620,6 +659,22 @@ export class AgentManager {
620
659
  }
621
660
 
622
661
  const { startupMsg } = this._prepareStartupMessage(key, runtime);
662
+ let startupMsgRestored = false;
663
+ const restoreStartupMessage = (reason) => {
664
+ if (runtime !== 'codex' || !startupMsg || startupMsgRestored) return;
665
+ startupMsgRestored = true;
666
+ const queueSize = this._enqueuePendingMessage(key, startupMsg, { front: true });
667
+ const seq = startupMsg.seq ?? 'n/a';
668
+ console.log(
669
+ `[AgentManager] Restored codex startup message seq=${seq} workspace=${workspaceName ?? workspaceId ?? 'none'} reason=${reason ?? 'unknown'} pending=${queueSize}`
670
+ );
671
+ };
672
+ if (runtime === 'codex' && startupMsg) {
673
+ console.log(
674
+ `[AgentManager] Claimed codex startup message seq=${startupMsg.seq ?? 'n/a'} workspace=${workspaceName ?? workspaceId ?? 'none'}`
675
+ );
676
+ }
677
+ rollbackStartupMessage = restoreStartupMessage;
623
678
 
624
679
  // Keep .skills materialization for runtime compatibility, but no longer use skills to build prompt/env/mcp.
625
680
  let skills = [];
@@ -732,9 +787,20 @@ export class AgentManager {
732
787
  const reportSpawnFailure = (detail) => {
733
788
  if (spawnErrorReported) return;
734
789
  spawnErrorReported = true;
790
+ restoreStartupMessage(detail ?? 'spawn_failed');
735
791
  this._clearDirectiveRefresh(key);
736
792
  this.starting.delete(key);
737
793
  this.agents.delete(key);
794
+ this._emitLifecycle(connection, {
795
+ agentId,
796
+ workspaceId,
797
+ runtime,
798
+ reachability: 'unreachable',
799
+ availability: 'unreachable',
800
+ runtimeState: 'crashed',
801
+ reason: detail ?? 'spawn_failed',
802
+ sessionId: config.sessionId ?? null,
803
+ });
738
804
  connection.send({ type: 'agent:status', agentId, workspaceId, status: 'inactive' });
739
805
  connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'offline', detail: detail ?? 'spawn_failed', entries: [] });
740
806
  };
@@ -864,6 +930,10 @@ export class AgentManager {
864
930
  this.agents.delete(key);
865
931
 
866
932
  if (code === 0 && runtime === 'codex' && this._pendingMessages?.get(key)?.length) {
933
+ const pendingCount = this._pendingMessages?.get(key)?.length ?? 0;
934
+ console.log(
935
+ `[AgentManager] Codex exited cleanly with pending=${pendingCount}; restarting next turn for ${config.displayName ?? agentId}`
936
+ );
867
937
  const restartConfig = { ...config, sessionId: agent?.sessionId ?? config.sessionId ?? null };
868
938
  this._startAgent({ agentId, workspaceId, config: restartConfig }, connection);
869
939
  return;
@@ -873,6 +943,7 @@ export class AgentManager {
873
943
  if (code !== 0 && config.sessionId && !this._retried?.has(key)) {
874
944
  if (!this._retried) this._retried = new Set();
875
945
  this._retried.add(key);
946
+ restoreStartupMessage('session_retry_without_session');
876
947
  console.log(`[AgentManager] Retrying ${agentId} workspace=${workspaceId} without session (session may not exist locally)`);
877
948
  const retryConfig = { ...config, sessionId: null };
878
949
  this._startAgent({ agentId, workspaceId, config: retryConfig }, connection);
@@ -885,6 +956,23 @@ export class AgentManager {
885
956
  stopCause: agent?.stopCause ?? null,
886
957
  });
887
958
 
959
+ const lifecycleExit = resolveLifecycleExitState({
960
+ runtime,
961
+ code,
962
+ signal,
963
+ stopCause: agent?.stopCause ?? null,
964
+ });
965
+ this._emitLifecycle(connection, {
966
+ agentId,
967
+ workspaceId,
968
+ runtime,
969
+ reachability: lifecycleExit.reachability,
970
+ availability: lifecycleExit.availability,
971
+ runtimeState: lifecycleExit.runtimeState,
972
+ reason: lifecycleExit.reason,
973
+ sessionId: agent?.sessionId ?? config.sessionId ?? null,
974
+ });
975
+
888
976
  connection.send({ type: 'agent:status', agentId, workspaceId, status: 'inactive' });
889
977
  connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'offline', detail: offlineDetail, entries: [] });
890
978
  });
@@ -897,6 +985,17 @@ export class AgentManager {
897
985
  }
898
986
 
899
987
  console.log(`[AgentManager] Agent ${config.displayName ?? agentId} is now active (workspace=${workspaceName ?? workspaceId ?? 'none'})`);
988
+ const startedAgent = this.agents.get(key);
989
+ this._emitLifecycle(connection, {
990
+ agentId,
991
+ workspaceId,
992
+ runtime,
993
+ reachability: 'reachable',
994
+ availability: 'available',
995
+ runtimeState: 'running',
996
+ reason: 'spawned',
997
+ sessionId: startedAgent?.sessionId ?? config.sessionId ?? null,
998
+ });
900
999
  connection.send({ type: 'agent:status', agentId, workspaceId, status: 'active' });
901
1000
  connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'online', detail: '', entries: [] });
902
1001
 
@@ -1166,10 +1265,8 @@ export class AgentManager {
1166
1265
  if (!this.agents.has(key) && !this.starting.has(key)) {
1167
1266
  // Agent not running — queue the message and request config to spawn it
1168
1267
  console.log(`[AgentManager] Agent ${agentId.slice(0,8)} workspace=${message.workspace_name ?? workspaceId} not running, requesting start for seq=${seq}`);
1169
- if (!this._pendingMessages) this._pendingMessages = new Map();
1170
- const pending = this._pendingMessages.get(key) ?? [];
1171
- pending.push(msg);
1172
- this._pendingMessages.set(key, pending);
1268
+ const queueSize = this._enqueuePendingMessage(key, msg);
1269
+ console.log(`[AgentManager] Queued seq=${seq} for start request pending=${queueSize}`);
1173
1270
  connection.send({ type: 'agent:request_start', agentId, workspaceId });
1174
1271
  return;
1175
1272
  }
@@ -1177,10 +1274,8 @@ export class AgentManager {
1177
1274
  if (this.starting.has(key)) {
1178
1275
  // Spawn in progress — queue the message for delivery after start
1179
1276
  console.log(`[AgentManager] Agent ${agentId.slice(0,8)} workspace=${message.workspace_name ?? workspaceId} still starting, queuing seq=${seq}`);
1180
- if (!this._pendingMessages) this._pendingMessages = new Map();
1181
- const pending = this._pendingMessages.get(key) ?? [];
1182
- pending.push(msg);
1183
- this._pendingMessages.set(key, pending);
1277
+ const queueSize = this._enqueuePendingMessage(key, msg);
1278
+ console.log(`[AgentManager] Queued seq=${seq} while starting pending=${queueSize}`);
1184
1279
  return;
1185
1280
  }
1186
1281
 
@@ -1188,10 +1283,8 @@ export class AgentManager {
1188
1283
  const agent = this.agents.get(key);
1189
1284
  console.log(`[AgentManager] Delivering seq=${seq} to agent ${agent?.config?.displayName ?? agentId.slice(0,8)} workspace=${message.workspace_name ?? workspaceId}`);
1190
1285
  if (agent?.runtime === 'codex') {
1191
- if (!this._pendingMessages) this._pendingMessages = new Map();
1192
- const pending = this._pendingMessages.get(key) ?? [];
1193
- pending.push(msg);
1194
- this._pendingMessages.set(key, pending);
1286
+ const queueSize = this._enqueuePendingMessage(key, msg);
1287
+ console.log(`[AgentManager] Queued seq=${seq} for codex next turn pending=${queueSize}`);
1195
1288
  return;
1196
1289
  }
1197
1290
  this._write(key, text);
@@ -1253,13 +1346,43 @@ export class AgentManager {
1253
1346
  }
1254
1347
  break;
1255
1348
  case 'thinking':
1349
+ this._emitLifecycle(connection, {
1350
+ agentId,
1351
+ workspaceId,
1352
+ runtime: 'kimi',
1353
+ reachability: 'reachable',
1354
+ availability: 'busy',
1355
+ runtimeState: 'running',
1356
+ reason: 'thinking',
1357
+ sessionId: agent.sessionId ?? null,
1358
+ });
1256
1359
  connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'thinking', detail: '', entries: [] });
1257
1360
  break;
1258
1361
  case 'tool_call':
1362
+ this._emitLifecycle(connection, {
1363
+ agentId,
1364
+ workspaceId,
1365
+ runtime: 'kimi',
1366
+ reachability: 'reachable',
1367
+ availability: 'busy',
1368
+ runtimeState: 'running',
1369
+ reason: `tool:${evt.name ?? 'unknown_tool'}`,
1370
+ sessionId: agent.sessionId ?? null,
1371
+ });
1259
1372
  connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'working', detail: evt.name, entries: [] });
1260
1373
  break;
1261
1374
  case 'turn_end':
1262
1375
  agent.kimiIdle = true;
1376
+ this._emitLifecycle(connection, {
1377
+ agentId,
1378
+ workspaceId,
1379
+ runtime: 'kimi',
1380
+ reachability: 'reachable',
1381
+ availability: 'available',
1382
+ runtimeState: 'running',
1383
+ reason: 'turn_end',
1384
+ sessionId: agent.sessionId ?? null,
1385
+ });
1263
1386
  connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'online', detail: '', entries: [] });
1264
1387
  break;
1265
1388
  case 'error':
@@ -1282,12 +1405,42 @@ export class AgentManager {
1282
1405
  }
1283
1406
  break;
1284
1407
  case 'thinking':
1408
+ this._emitLifecycle(connection, {
1409
+ agentId,
1410
+ workspaceId,
1411
+ runtime: 'codex',
1412
+ reachability: 'reachable',
1413
+ availability: 'busy',
1414
+ runtimeState: 'running',
1415
+ reason: 'thinking',
1416
+ sessionId: agent.sessionId ?? null,
1417
+ });
1285
1418
  connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'thinking', detail: '', entries: [] });
1286
1419
  break;
1287
1420
  case 'tool_call':
1421
+ this._emitLifecycle(connection, {
1422
+ agentId,
1423
+ workspaceId,
1424
+ runtime: 'codex',
1425
+ reachability: 'reachable',
1426
+ availability: 'busy',
1427
+ runtimeState: 'running',
1428
+ reason: `tool:${evt.name ?? 'unknown_tool'}`,
1429
+ sessionId: agent.sessionId ?? null,
1430
+ });
1288
1431
  connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'working', detail: evt.name, entries: [] });
1289
1432
  break;
1290
1433
  case 'turn_end':
1434
+ this._emitLifecycle(connection, {
1435
+ agentId,
1436
+ workspaceId,
1437
+ runtime: 'codex',
1438
+ reachability: 'reachable',
1439
+ availability: 'available',
1440
+ runtimeState: 'running',
1441
+ reason: 'turn_end',
1442
+ sessionId: agent.sessionId ?? null,
1443
+ });
1291
1444
  connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'online', detail: '', entries: [] });
1292
1445
  break;
1293
1446
  case 'error':
@@ -1322,10 +1475,30 @@ export class AgentManager {
1322
1475
  console.log(`[AgentManager][${displayName}] <text> ${block.text?.slice(0, 500)}`);
1323
1476
  } else if (block.type === 'tool_use') {
1324
1477
  console.log(`[AgentManager][${displayName}] <tool_use> ${block.name} params=${JSON.stringify(block.input ?? {}).slice(0, 500)}`);
1478
+ this._emitLifecycle(connection, {
1479
+ agentId,
1480
+ workspaceId,
1481
+ runtime: 'claude',
1482
+ reachability: 'reachable',
1483
+ availability: 'busy',
1484
+ runtimeState: 'running',
1485
+ reason: `tool:${block.name ?? 'unknown_tool'}`,
1486
+ sessionId: this.agents.get(key)?.sessionId ?? null,
1487
+ });
1325
1488
  connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'working', detail: block.name, entries: [] });
1326
1489
  }
1327
1490
  }
1328
1491
  if (!content.some(c => c.type === 'tool_use')) {
1492
+ this._emitLifecycle(connection, {
1493
+ agentId,
1494
+ workspaceId,
1495
+ runtime: 'claude',
1496
+ reachability: 'reachable',
1497
+ availability: 'busy',
1498
+ runtimeState: 'running',
1499
+ reason: 'thinking',
1500
+ sessionId: this.agents.get(key)?.sessionId ?? null,
1501
+ });
1329
1502
  connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'thinking', detail: '', entries: [] });
1330
1503
  }
1331
1504
  } else if (event.type === 'tool') {
@@ -1345,6 +1518,16 @@ export class AgentManager {
1345
1518
  }
1346
1519
  } else if (event.type === 'result') {
1347
1520
  console.log(`[AgentManager][${displayName}] turn done (stop_reason=${event.stop_reason ?? '?'})`);
1521
+ this._emitLifecycle(connection, {
1522
+ agentId,
1523
+ workspaceId,
1524
+ runtime: 'claude',
1525
+ reachability: 'reachable',
1526
+ availability: 'available',
1527
+ runtimeState: 'running',
1528
+ reason: 'turn_end',
1529
+ sessionId: this.agents.get(key)?.sessionId ?? null,
1530
+ });
1348
1531
  connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'online', detail: '', entries: [] });
1349
1532
  }
1350
1533
  }
@@ -59,6 +59,14 @@ let currentWorkspaceId = WORKSPACE_ID;
59
59
  const VOICEOVER_LOCAL_DIR = path.join(WORKSPACE_DIR, 'artifacts', 'audio');
60
60
  const VIDEO_COMPOSE_LOCAL_DIR = path.join(WORKSPACE_DIR, 'artifacts', 'video');
61
61
  const DEFAULT_OUTRO_PATH = path.join(homedir(), '.lightcone', 'assets', 'outros', 'default.mp4');
62
+ const CVMAX_WORKSPACE_ID = 'ae63cc9e-feff-4d7e-a62e-a7a7c5fd69d9';
63
+ const CVMAX_EDITOR_IN_CHIEF_AGENT_ID = '91a45fd7-ce5f-4da6-9b27-e34bf7b7c0e2';
64
+ const CVMAX_EDITOR_BLOCKED_VIDEO_TOOLS = new Set([
65
+ 'generate_voiceover',
66
+ 'record_url_narration',
67
+ 'compose_video',
68
+ 'submit_to_library',
69
+ ]);
62
70
 
63
71
  function dataUrlSummary(content) {
64
72
  if (typeof content !== 'string' || !content.startsWith('data:')) return null;
@@ -83,6 +91,25 @@ function formatBytes(bytes) {
83
91
  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
84
92
  }
85
93
 
94
+ function isBlockedCvmaxEditorVideoTool(toolName) {
95
+ return currentWorkspaceId === CVMAX_WORKSPACE_ID
96
+ && AGENT_ID === CVMAX_EDITOR_IN_CHIEF_AGENT_ID
97
+ && CVMAX_EDITOR_BLOCKED_VIDEO_TOOLS.has(toolName);
98
+ }
99
+
100
+ function cvmaxEditorVideoToolError(toolName) {
101
+ return {
102
+ isError: true,
103
+ content: [{
104
+ type: 'text',
105
+ text:
106
+ `Error: ${toolName} blocked for editor_in_chief in CvMax. ` +
107
+ 'In this workspace, @short_video_scripter owns video production. ' +
108
+ 'editor_in_chief may route, review, or assist with OCR/verification, but must not run video production tools directly.',
109
+ }],
110
+ };
111
+ }
112
+
86
113
  function normalizeVoiceFormat(value) {
87
114
  const normalized = String(value ?? '').trim().toLowerCase();
88
115
  if (!normalized) return 'mp3';
@@ -1345,6 +1372,9 @@ server.tool('generate_voiceover',
1345
1372
  credential_id: z.string().optional().describe('Optional explicit credential id. If omitted, uses latest granted tts_provider credential.'),
1346
1373
  },
1347
1374
  async ({ workspace_id, text, voice_preset, speed, format, credential_id }) => {
1375
+ if (isBlockedCvmaxEditorVideoTool('generate_voiceover')) {
1376
+ return cvmaxEditorVideoToolError('generate_voiceover');
1377
+ }
1348
1378
  const targetWorkspaceId = (workspace_id ?? currentWorkspaceId ?? WORKSPACE_ID ?? '').trim();
1349
1379
  if (!targetWorkspaceId) {
1350
1380
  return { isError: true, content: [{ type: 'text', text: 'workspace_id is required (no current workspace context).' }] };
@@ -1425,13 +1455,18 @@ server.tool('record_url_narration',
1425
1455
  fps: z.number().optional().describe('Default 30. Do not change unless needed.'),
1426
1456
  settle_ms: z.number().optional().describe('Default 4000. Settle wait after navigation before recording starts.'),
1427
1457
  },
1428
- async (args) => runRecordUrlNarrationTool({
1429
- args,
1430
- currentWorkspaceId,
1431
- workspaceDir: WORKSPACE_DIR,
1432
- runMandatoryLocalToolFn: runMandatoryLocalTool,
1433
- recordUrlNarrationFn: recordUrlNarration,
1434
- })
1458
+ async (args) => {
1459
+ if (isBlockedCvmaxEditorVideoTool('record_url_narration')) {
1460
+ return cvmaxEditorVideoToolError('record_url_narration');
1461
+ }
1462
+ return runRecordUrlNarrationTool({
1463
+ args,
1464
+ currentWorkspaceId,
1465
+ workspaceDir: WORKSPACE_DIR,
1466
+ runMandatoryLocalToolFn: runMandatoryLocalTool,
1467
+ recordUrlNarrationFn: recordUrlNarration,
1468
+ });
1469
+ }
1435
1470
  );
1436
1471
 
1437
1472
  // ── compose_video ───────────────────────────────────────────────────────────────
@@ -1449,6 +1484,9 @@ server.tool('compose_video',
1449
1484
  target: z.enum(['short_video_cn', 'douyin', 'xhs']).optional().describe('Transcode target profile. Defaults to short_video_cn.'),
1450
1485
  },
1451
1486
  async ({ video_path, audio_segments, events_log, outro_path, target }) => {
1487
+ if (isBlockedCvmaxEditorVideoTool('compose_video')) {
1488
+ return cvmaxEditorVideoToolError('compose_video');
1489
+ }
1452
1490
  const composeInput = { video_path, audio_segments, events_log, outro_path, target };
1453
1491
  try {
1454
1492
  const result = await runMandatoryLocalTool({
@@ -1547,11 +1585,16 @@ server.tool('submit_to_library',
1547
1585
  understanding: z.record(z.any()).optional().describe('analyze_page 输出'),
1548
1586
  plan: z.record(z.any()).optional().describe('plan_video / detail_sections 输出'),
1549
1587
  },
1550
- async (args) => runSubmitToLibraryTool({
1551
- args,
1552
- currentWorkspaceId,
1553
- apiFn: api,
1554
- })
1588
+ async (args) => {
1589
+ if (isBlockedCvmaxEditorVideoTool('submit_to_library')) {
1590
+ return cvmaxEditorVideoToolError('submit_to_library');
1591
+ }
1592
+ return runSubmitToLibraryTool({
1593
+ args,
1594
+ currentWorkspaceId,
1595
+ apiFn: api,
1596
+ });
1597
+ }
1555
1598
  );
1556
1599
 
1557
1600
  // ── register_data_source ───────────────────────────────────────────────────────
package/src/index.js CHANGED
@@ -4,6 +4,7 @@ import { createRequire } from 'module';
4
4
  import { DaemonConnection } from './connection.js';
5
5
  import { AgentManager } from './agent-manager.js';
6
6
  import { releaseProfileLocksForProcess } from './profile-lock.js';
7
+ import { resolveLightconeServerUrl } from '../../src/runtime/config.js';
7
8
 
8
9
  const { version } = createRequire(import.meta.url)('../package.json');
9
10
 
@@ -39,7 +40,7 @@ if (opts['--help'] || opts['-h']) {
39
40
  process.exit(0);
40
41
  }
41
42
 
42
- const SERVER_URL = String(opts['--server-url'] || process.env.SERVER_URL || 'http://localhost:9779').trim();
43
+ const SERVER_URL = String(opts['--server-url'] || resolveLightconeServerUrl()).trim();
43
44
  const MACHINE_API_KEY = String(opts['--api-key'] || process.env.MACHINE_API_KEY || '').trim();
44
45
 
45
46
  if (!MACHINE_API_KEY) {