@lightcone-ai/daemon 0.15.76 → 0.15.77

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.76",
3
+ "version": "0.15.77",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import path from 'node:path';
3
- import { mkdir, rm, writeFile, access } from 'node:fs/promises';
3
+ import { mkdir, rm, writeFile, access, stat as statAsync } from 'node:fs/promises';
4
4
  import { constants as fsConstants } from 'node:fs';
5
5
  import os from 'node:os';
6
6
  import { randomUUID } from 'node:crypto';
@@ -407,7 +407,18 @@ export async function composeVideoV2({
407
407
  }
408
408
 
409
409
  const totalDuration = await probeDurationSec(outPath);
410
- return { path: outPath, duration_ms: Math.round(totalDuration * 1000) };
410
+
411
+ // Stat the final file before returning so the caller can rely on size and
412
+ // so we can detect the (rare but observed) case where ffmpeg's `close`
413
+ // arrived but the kernel writeback wasn't complete. A 0-byte / tiny mp4
414
+ // here means the burn-subtitles pass produced nothing usable — fail loudly
415
+ // instead of letting a broken file flow into write_workspace_file / submit.
416
+ const finalStat = await statAsync(outPath);
417
+ const sizeBytes = Number(finalStat.size ?? 0);
418
+ if (!Number.isFinite(sizeBytes) || sizeBytes < 1024) {
419
+ throw new Error(`compose_video_v2 produced an invalid output: ${outPath} size=${sizeBytes} bytes`);
420
+ }
421
+ return { path: outPath, duration_ms: Math.round(totalDuration * 1000), size_bytes: sizeBytes };
411
422
  } finally {
412
423
  await rm(tmpDir, { recursive: true, force: true });
413
424
  }
@@ -103,6 +103,7 @@ export async function runComposeVideoV2Tool({ segments, outro_paths, format, res
103
103
  'compose_video_v2 completed.',
104
104
  `path=${result.path}`,
105
105
  `duration_ms=${result.duration_ms}`,
106
+ `size_bytes=${result.size_bytes ?? 'unknown'}`,
106
107
  `segments=${segments.length}`,
107
108
  `outro_clips=${(outro_paths ?? []).length}`,
108
109
  ];
@@ -1,7 +1,31 @@
1
1
  import path, { extname } from 'node:path';
2
- import { readFileSync, createReadStream } from 'node:fs';
2
+ import { readFileSync, createReadStream, statSync } from 'node:fs';
3
3
  import { createHash } from 'node:crypto';
4
4
 
5
+ // Wait for a file to stop growing before we read it. compose_video_v2 / ffmpeg
6
+ // occasionally finishes the wrapping tool-call promise a fraction earlier than
7
+ // the kernel finishes the last writeback for the output mp4 (observed: codex
8
+ // reports item.completed at T, file mtime is T+60s, and write_workspace_file
9
+ // reads a half-finished file in between). We stat → sleep → stat; if size or
10
+ // mtime moved, the file is still being written and we retry. Cheap (200-400ms
11
+ // for stable files; bounded retries for actively-growing ones).
12
+ async function waitForFileStable(localPath, { intervalMs = 300, attempts = 10 } = {}) {
13
+ let lastSize = -1;
14
+ let lastMtime = -1;
15
+ for (let i = 0; i < attempts; i += 1) {
16
+ const st = statSync(localPath);
17
+ if (st.size === lastSize && st.mtimeMs === lastMtime) {
18
+ return { size: st.size };
19
+ }
20
+ lastSize = st.size;
21
+ lastMtime = st.mtimeMs;
22
+ await new Promise(r => setTimeout(r, intervalMs));
23
+ }
24
+ // Returned best-effort: caller can still proceed, but mismatch may surface
25
+ // at /storage/confirm size check.
26
+ return { size: lastSize };
27
+ }
28
+
5
29
  export const WORKSPACE_BINARY_MIME = {
6
30
  '.mp4': 'video/mp4',
7
31
  '.png': 'image/png',
@@ -48,6 +72,11 @@ async function uploadBinaryFile({
48
72
  presign, // async fn({ workspaceId, path, size, mime, sha256 }) → { uploadUrl, objectKey, alreadyExists }
49
73
  confirmUpload, // async fn({ workspaceId, path, objectKey }) → void
50
74
  }) {
75
+ // Wait until the source file is no longer being written. Without this we
76
+ // sometimes read a partial mp4 from a still-running ffmpeg, upload that
77
+ // partial buffer, and end up serving a truncated video downstream.
78
+ await waitForFileStable(localPath);
79
+
51
80
  const buf = readFileSync(localPath);
52
81
  const size = buf.length;
53
82
  const sha256 = sha256ofBuffer(buf);