@lightcone-ai/daemon 0.15.64 → 0.15.66

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.15.64",
3
+ "version": "0.15.66",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1629,6 +1629,17 @@ export class AgentManager {
1629
1629
  }
1630
1630
  }
1631
1631
 
1632
+ _reportTurnUsage(connection, { agentId, workspaceId, agent, usage = null }) {
1633
+ connection.send({
1634
+ type: 'agent:turn_usage',
1635
+ agentId,
1636
+ workspaceId,
1637
+ sessionId: agent?.sessionId ?? null,
1638
+ runtime: agent?.runtime ?? null,
1639
+ usage,
1640
+ });
1641
+ }
1642
+
1632
1643
  async _postInternalActionComplete({ agentId, actionId, ok, result, error }) {
1633
1644
  const url = `${this.serverUrl.replace(/\/$/, '')}/internal/agent/${encodeURIComponent(agentId)}/actions/${encodeURIComponent(actionId)}/complete`;
1634
1645
  const res = await fetch(url, {
@@ -1985,6 +1996,7 @@ export class AgentManager {
1985
1996
  case 'turn_end':
1986
1997
  agent.kimiIdle = true;
1987
1998
  this._resetVisibleReplyTracking(agent);
1999
+ this._reportTurnUsage(connection, { agentId, workspaceId, agent });
1988
2000
  this._emitLifecycle(connection, {
1989
2001
  agentId,
1990
2002
  workspaceId,
@@ -2077,6 +2089,7 @@ export class AgentManager {
2077
2089
  agent.codexVisibleTextSeen = false;
2078
2090
  agent.codexSendMessageUsed = false;
2079
2091
  this._resetVisibleReplyTracking(agent);
2092
+ this._reportTurnUsage(connection, { agentId, workspaceId, agent, usage: evt.usage ?? null });
2080
2093
  this._emitLifecycle(connection, {
2081
2094
  agentId,
2082
2095
  workspaceId,
@@ -2195,6 +2208,7 @@ export class AgentManager {
2195
2208
  }
2196
2209
  this._resetVisibleReplyTracking(agent);
2197
2210
  console.log(`[AgentManager][${displayName}] turn done (stop_reason=${event.stop_reason ?? '?'})`);
2211
+ this._reportTurnUsage(connection, { agentId, workspaceId, agent, usage: event.usage ?? null });
2198
2212
  this._emitLifecycle(connection, {
2199
2213
  agentId,
2200
2214
  workspaceId,
@@ -306,7 +306,7 @@ export function parseCodexLine(line) {
306
306
  break;
307
307
  }
308
308
  case 'turn.completed':
309
- events.push({ kind: 'turn_end' });
309
+ events.push({ kind: 'turn_end', usage: event.usage && typeof event.usage === 'object' ? event.usage : null });
310
310
  break;
311
311
  case 'turn.failed':
312
312
  if (event.error?.message) events.push({ kind: 'error', message: event.error.message });
package/src/mcp-config.js CHANGED
@@ -62,25 +62,40 @@ function resolveSkillArg(arg, config) {
62
62
  return arg;
63
63
  }
64
64
 
65
+ // MCP servers that talk to the lightcone server at runtime — either a full
66
+ // server-backed MCP (mysql / publisher / platform / domain-data services) or a
67
+ // thin-proxy (`daemon/mcp-servers/_thin-proxy/forward.js`) whose tools forward
68
+ // to /internal/agent/:agentId/mcp/:serverId/:toolName. All of these need the
69
+ // SERVER_URL / MACHINE_API_KEY / AGENT_ID triple injected, or the proxy throws
70
+ // "thin-proxy missing SERVER_URL / MACHINE_API_KEY / AGENT_ID env".
71
+ const SERVER_BACKED_MCP_SERVERS = new Set([
72
+ 'mysql',
73
+ 'publisher',
74
+ 'platform',
75
+ 'research-fetch',
76
+ 'wechat-mp-fetch',
77
+ 'market-data-query',
78
+ 'company-fundamentals',
79
+ 'industry-report',
80
+ 'risk-metrics',
81
+ 'compliance-check',
82
+ 'portfolio-read',
83
+ 'portfolio-analysis',
84
+ // thin-proxy MCP services migrated to the server (roadmap §4) — every one of
85
+ // these uses startThinProxy() and requires the SERVER_URL/MACHINE_API_KEY/AGENT_ID triple.
86
+ 'video-narration-planner',
87
+ 'page-understanding',
88
+ 'platform-policy-db',
89
+ 'keyword-research',
90
+ 'audience-research',
91
+ 'hook-pattern-library',
92
+ ]);
93
+
65
94
  function baseEnvForServer(serverKey, { serverUrl, authToken, agentId, workspaceId, workspaceDir }) {
66
95
  if (serverKey === 'workspace-migrate') {
67
96
  return { SERVER_URL: serverUrl, MACHINE_API_KEY: authToken, AGENT_ID: agentId };
68
97
  }
69
- if (
70
- serverKey === 'mysql'
71
- || serverKey === 'publisher'
72
- || serverKey === 'platform'
73
- || serverKey === 'research-fetch'
74
- || serverKey === 'wechat-mp-fetch'
75
- || serverKey === 'market-data-query'
76
- || serverKey === 'company-fundamentals'
77
- || serverKey === 'industry-report'
78
- || serverKey === 'risk-metrics'
79
- || serverKey === 'compliance-check'
80
- || serverKey === 'portfolio-read'
81
- || serverKey === 'portfolio-analysis'
82
- || serverKey === 'video-narration-planner'
83
- ) {
98
+ if (SERVER_BACKED_MCP_SERVERS.has(serverKey)) {
84
99
  return {
85
100
  SERVER_URL: serverUrl,
86
101
  MACHINE_API_KEY: authToken,
@@ -1,5 +1,6 @@
1
1
  import path from 'path';
2
2
  import os from 'os';
3
+ import fs from 'fs';
3
4
  import { randomUUID } from 'crypto';
4
5
  import { composeVideoV2 } from '../_vendor/video/composer-v2/index.js';
5
6
 
@@ -11,11 +12,22 @@ function toolError(text) {
11
12
  return { isError: true, content: [{ type: 'text', text }] };
12
13
  }
13
14
 
15
+ // A real screenshot / rendered card / video frame is at least tens of KB. A
16
+ // few-KB image is almost certainly a blank or broken capture (e.g. the page
17
+ // hadn't finished loading when screenshotted). Composing from those produces a
18
+ // near-blank video, so reject up front rather than emit garbage.
19
+ const MIN_IMAGE_BYTES = 4 * 1024;
20
+
21
+ function statSizeOrNull(p) {
22
+ try { return fs.statSync(p).size; } catch { return null; }
23
+ }
24
+
14
25
  export async function runComposeVideoV2Tool({ segments, outro_paths, format, resolution, output_path, workspaceDir }) {
15
26
  if (!Array.isArray(segments) || segments.length === 0) {
16
27
  return toolError('segments must be a non-empty array.');
17
28
  }
18
29
 
30
+ const imagePaths = [];
19
31
  for (let i = 0; i < segments.length; i++) {
20
32
  const seg = segments[i];
21
33
  const kind = seg.visual_kind;
@@ -30,6 +42,30 @@ export async function runComposeVideoV2Tool({ segments, outro_paths, format, res
30
42
  if (kind !== 'carousel' && !seg.visual_path) {
31
43
  return toolError(`segments[${i}]: visual_path required for kind=${kind}.`);
32
44
  }
45
+ if (kind === 'image' && seg.visual_path) imagePaths.push(seg.visual_path);
46
+ if (kind === 'carousel' && Array.isArray(seg.visual_paths)) imagePaths.push(...seg.visual_paths.filter(Boolean));
47
+ }
48
+
49
+ // Garbage-in guard: refuse to compose from blank/broken source images.
50
+ for (const p of new Set(imagePaths)) {
51
+ const size = statSizeOrNull(p);
52
+ if (size === null) {
53
+ return toolError(`Source image not found: ${p}. Re-capture/render it before composing.`);
54
+ }
55
+ if (size < MIN_IMAGE_BYTES) {
56
+ return toolError(
57
+ `Source image ${p} is only ${size} bytes — almost certainly a blank or broken capture `
58
+ + `(e.g. the page hadn't finished loading when screenshotted). Re-capture with the page fully loaded, then retry.`
59
+ );
60
+ }
61
+ }
62
+ // Heuristic warning: a multi-segment image video that reuses one single image
63
+ // will look near-static — usually a sign the source page didn't render and the
64
+ // agent fell back to one blank screenshot.
65
+ let warning = null;
66
+ if (imagePaths.length >= 2 && new Set(imagePaths).size === 1) {
67
+ warning = `WARNING: all ${imagePaths.length} image segments reuse the same file (${imagePaths[0]}). `
68
+ + 'The output will be near-static — verify the source page actually rendered before submitting this video.';
33
69
  }
34
70
 
35
71
  const outDir = workspaceDir
@@ -46,13 +82,15 @@ export async function runComposeVideoV2Tool({ segments, outro_paths, format, res
46
82
  output_path: outPath,
47
83
  });
48
84
 
49
- return toolText([
85
+ const lines = [
50
86
  'compose_video_v2 completed.',
51
87
  `path=${result.path}`,
52
88
  `duration_ms=${result.duration_ms}`,
53
89
  `segments=${segments.length}`,
54
90
  `outro_clips=${(outro_paths ?? []).length}`,
55
- ].join('\n'));
91
+ ];
92
+ if (warning) lines.push(warning);
93
+ return toolText(lines.join('\n'));
56
94
  } catch (error) {
57
95
  return toolError(`compose_video_v2 failed: ${error.message}`);
58
96
  }
@@ -20,8 +20,6 @@ const TEXT_EXTS = new Set([
20
20
  '.csv', '.xml', '.yaml', '.yml', '.sh',
21
21
  ]);
22
22
 
23
- const LARGE_FILE_THRESHOLD = 5 * 1024 * 1024; // 5MB
24
-
25
23
  export function resolveWorkspaceFileUploadPlan({ localPath, workspacePath }) {
26
24
  const target = String(workspacePath ?? '').trim() || String(localPath ?? '').trim();
27
25
  const ext = extname(target).toLowerCase();
@@ -36,9 +34,11 @@ function sha256ofBuffer(buf) {
36
34
  }
37
35
 
38
36
  /**
39
- * Upload a local file to the workspace via the new presign → PUT → confirm flow.
40
- * For large files (>= 5MB), presign returns immediately (DB status = uploading)
41
- * and the PUT + confirm happen in the background.
37
+ * Upload a local file to the workspace via the presign → PUT → confirm flow.
38
+ * The PUT + confirm are awaited: a fire-and-forget background upload could be
39
+ * abandoned when the agent run / daemon process ends, leaving the DB row stuck
40
+ * at object_status='uploading' forever (the file then 202s "uploading" on every
41
+ * read). Blocking the tool call for a few seconds is cheaper than that.
42
42
  */
43
43
  async function uploadBinaryFile({
44
44
  localPath,
@@ -64,25 +64,13 @@ async function uploadBinaryFile({
64
64
  return { mode: 'object-storage', mime, bytes: size, objectKey, deduped: true };
65
65
  }
66
66
 
67
- const isLarge = size >= LARGE_FILE_THRESHOLD;
68
-
69
- const doPut = async () => {
70
- const resp = await fetch(uploadUrl, {
71
- method: 'PUT',
72
- body: buf,
73
- headers: { 'Content-Type': mime, 'Content-Length': String(size) },
74
- });
75
- if (!resp.ok) throw new Error(`COS PUT failed: ${resp.status} ${await resp.text()}`);
76
- await confirmUpload({ workspaceId, path: workspacePath, objectKey });
77
- };
78
-
79
- if (isLarge) {
80
- // Fire-and-forget for large files; server already wrote `uploading` status
81
- doPut().catch(err => console.error('[workspace-upload] large file background upload failed:', err?.message));
82
- return { mode: 'object-storage', mime, bytes: size, objectKey, async: true };
83
- }
84
-
85
- await doPut();
67
+ const resp = await fetch(uploadUrl, {
68
+ method: 'PUT',
69
+ body: buf,
70
+ headers: { 'Content-Type': mime, 'Content-Length': String(size) },
71
+ });
72
+ if (!resp.ok) throw new Error(`COS PUT failed: ${resp.status} ${await resp.text()}`);
73
+ await confirmUpload({ workspaceId, path: workspacePath, objectKey });
86
74
  return { mode: 'object-storage', mime, bytes: size, objectKey, async: false };
87
75
  }
88
76