@lightcone-ai/daemon 0.15.48 → 0.15.50
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/chat-bridge.js +14 -39
- package/src/drivers/claude.js +22 -16
- package/src/publish-job-runner.js +24 -12
- package/src/workspace-file-upload.js +85 -32
package/package.json
CHANGED
package/src/chat-bridge.js
CHANGED
|
@@ -6,11 +6,7 @@ import { createReadStream, existsSync, mkdirSync, readFileSync, writeFileSync }
|
|
|
6
6
|
import { createHash, randomUUID } from 'crypto';
|
|
7
7
|
import path, { extname } from 'path';
|
|
8
8
|
import { recordUrlNarration } from './_vendor/video/recorder/index.js';
|
|
9
|
-
import {
|
|
10
|
-
VIDEO_EXT,
|
|
11
|
-
VIDEO_MIME,
|
|
12
|
-
writeLocalFileToWorkspace,
|
|
13
|
-
} from './workspace-file-upload.js';
|
|
9
|
+
import { writeLocalFileToWorkspace } from './workspace-file-upload.js';
|
|
14
10
|
import { runRecordUrlNarrationTool } from './record-url-narration-tool.js';
|
|
15
11
|
import { runSubmitToLibraryTool } from './submit-to-library-tool.js';
|
|
16
12
|
import { runRenderTextToImageTool } from './tools/render-text-to-image.js';
|
|
@@ -491,7 +487,7 @@ async function directApi(method, apiPath, body) {
|
|
|
491
487
|
async function directApiVideoUpload(apiPath, {
|
|
492
488
|
localPath,
|
|
493
489
|
filename,
|
|
494
|
-
contentType =
|
|
490
|
+
contentType = 'video/mp4',
|
|
495
491
|
}) {
|
|
496
492
|
const url = `${SERVER_URL}/internal/agent/${AGENT_ID}${apiPath}`;
|
|
497
493
|
const headers = {
|
|
@@ -1090,44 +1086,22 @@ server.tool('write_workspace_file', 'Write a local file directly to the shared w
|
|
|
1090
1086
|
uploadWorkspaceMemory: async ({ workspacePath, workspaceId, content }) => {
|
|
1091
1087
|
await api('PUT', `/workspace-memory?path=${encodeURIComponent(workspacePath)}&workspaceId=${encodeURIComponent(workspaceId)}`, { content });
|
|
1092
1088
|
},
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
mime,
|
|
1099
|
-
}) => {
|
|
1100
|
-
const uploadResult = await runMandatoryLocalTool({
|
|
1101
|
-
toolName: 'write_workspace_file',
|
|
1102
|
-
toolInput: {
|
|
1103
|
-
file_path,
|
|
1104
|
-
path: workspacePath,
|
|
1105
|
-
upload_mode: 'upload_video',
|
|
1106
|
-
},
|
|
1107
|
-
executor: async () => directApiVideoUpload(
|
|
1108
|
-
`/upload-video?workspaceId=${encodeURIComponent(workspaceId)}&filename=${encodeURIComponent(filename)}&path=${encodeURIComponent(workspacePath)}`,
|
|
1109
|
-
{
|
|
1110
|
-
localPath: videoLocalPath,
|
|
1111
|
-
filename,
|
|
1112
|
-
contentType: mime,
|
|
1113
|
-
}
|
|
1114
|
-
),
|
|
1115
|
-
});
|
|
1116
|
-
return {
|
|
1117
|
-
mode: 'upload-video',
|
|
1118
|
-
mime: VIDEO_MIME,
|
|
1119
|
-
bytes: Number(uploadResult?.sizeBytes) || 0,
|
|
1120
|
-
};
|
|
1089
|
+
presign: async ({ workspaceId, path: filePath, size, mime, sha256 }) => {
|
|
1090
|
+
return api('POST', '/storage/presign', { workspaceId, path: filePath, size, mime, sha256 });
|
|
1091
|
+
},
|
|
1092
|
+
confirmUpload: async ({ workspaceId, path: filePath, objectKey }) => {
|
|
1093
|
+
await api('POST', '/storage/confirm', { workspaceId, path: filePath, objectKey });
|
|
1121
1094
|
},
|
|
1122
1095
|
});
|
|
1123
1096
|
|
|
1124
|
-
const finalMime = result?.mime ??
|
|
1097
|
+
const finalMime = result?.mime ?? 'application/octet-stream';
|
|
1125
1098
|
const finalBytes = Number.isFinite(result?.bytes) ? result.bytes : 0;
|
|
1126
1099
|
const sizeText = finalBytes > 0 ? formatBytes(finalBytes) : 'unknown size';
|
|
1100
|
+
const asyncNote = result?.async ? ' (uploading in background)' : '';
|
|
1127
1101
|
return {
|
|
1128
1102
|
content: [{
|
|
1129
1103
|
type: 'text',
|
|
1130
|
-
text: `Saved local file to workspace: ${path} (${finalMime}, ${sizeText})`,
|
|
1104
|
+
text: `Saved local file to workspace: ${path} (${finalMime}, ${sizeText})${asyncNote}`,
|
|
1131
1105
|
}],
|
|
1132
1106
|
};
|
|
1133
1107
|
});
|
|
@@ -1616,10 +1590,10 @@ server.tool('execute_approved_action',
|
|
|
1616
1590
|
|
|
1617
1591
|
// ── promote_context ───────────────────────────────────────────────────────────
|
|
1618
1592
|
server.tool('promote_context',
|
|
1619
|
-
'Submit a
|
|
1593
|
+
'Submit a candidate for human review to add to the workspace\'s persistent context. Use this (1) after finishing a task when you discover a stable fact, convention, or learning future agents should know, or (2) immediately after responding to a user message when the user expressed a persistent operational preference (e.g. "always add links", "use subtitles by default"). The candidate appears in the workspace owner\'s "My Context → Pending Proposals" panel; once confirmed, it is auto-injected into every future agent\'s system prompt under "## Workspace context". This is the only sanctioned path for shared knowledge or preference governance.',
|
|
1620
1594
|
{
|
|
1621
1595
|
workspace_id: z.string().optional().describe('Target workspace id. Defaults to your current workspace if omitted.'),
|
|
1622
|
-
type: z.enum(['knowledge', 'workspace_norm', 'memory']).optional().describe('Candidate type
|
|
1596
|
+
type: z.enum(['knowledge', 'workspace_norm', 'memory', 'preference']).optional().describe('Candidate type: "knowledge" for facts/learnings, "workspace_norm" for standing rules, "memory" for durable facts, "preference" for persistent user preferences expressed in conversation.'),
|
|
1623
1597
|
summary: z.string().describe('One-line title that reviewers will see in the Pending Proposals list.'),
|
|
1624
1598
|
content: z.string().describe('Full candidate text that future agents will read. Be concrete, citable, and self-contained.'),
|
|
1625
1599
|
source_message_id: z.string().optional().describe('Optional message id that motivated this candidate, for audit trail.'),
|
|
@@ -1648,11 +1622,12 @@ server.tool('promote_context',
|
|
|
1648
1622
|
reason,
|
|
1649
1623
|
});
|
|
1650
1624
|
const proposal = data?.proposal ?? {};
|
|
1625
|
+
const resolvedType = type ?? 'knowledge';
|
|
1651
1626
|
return {
|
|
1652
1627
|
content: [{
|
|
1653
1628
|
type: 'text',
|
|
1654
1629
|
text:
|
|
1655
|
-
|
|
1630
|
+
`${resolvedType === 'preference' ? 'Preference' : 'Knowledge'} candidate submitted.\n` +
|
|
1656
1631
|
`proposal_id=${proposal.id ?? 'unknown'} workspace=${proposal.workspaceId ?? targetWorkspaceId} status=${proposal.status ?? 'candidate'}\n` +
|
|
1657
1632
|
`It is now visible in the workspace owner's "My Context → Pending Proposals" panel; once confirmed, it will be injected into every future agent's "## Workspace context".`,
|
|
1658
1633
|
}],
|
package/src/drivers/claude.js
CHANGED
|
@@ -238,7 +238,7 @@ The active workspace context (Goal State, constraints, decisions, knowledge) is
|
|
|
238
238
|
|
|
239
239
|
**Write rule:**
|
|
240
240
|
- Personal learnings → \`${t("write_memory")}\`
|
|
241
|
-
- Workspace-level knowledge
|
|
241
|
+
- Workspace-level knowledge or persistent user preferences → \`${t("promote_context")}\` (see below). Do **not** dump shared knowledge into ad-hoc files inside the workspace shared workspace.
|
|
242
242
|
- **Any file you produce for a task** → \`${t("write_workspace")}({ path: "artifacts/your-file.ext", ... })\` or \`${t("write_workspace_file")}({ file_path, path: "artifacts/your-file.ext" })\`
|
|
243
243
|
|
|
244
244
|
Temporary local files belong under \`tmp/\` in your personal workspace. If you need to show an image in chat, first save the durable copy to \`artifacts/\`, then optionally call \`${t("upload_image")}\` for a temporary public preview URL.
|
|
@@ -258,8 +258,8 @@ Example: writing a web page → \`${t("write_workspace")}({ path: "artifacts/job
|
|
|
258
258
|
- \`${t("write_workspace_file")}({ file_path, path })\` — write a local file from your workspace to a workspace artifact without putting base64 in context
|
|
259
259
|
- \`${t("list_workspace")}()\` — list all files in the workspace
|
|
260
260
|
|
|
261
|
-
**Workspace knowledge governance:**
|
|
262
|
-
- \`${t("promote_context")}({ workspace_id, type, summary, content })\` — submit a
|
|
261
|
+
**Workspace knowledge & preference governance:**
|
|
262
|
+
- \`${t("promote_context")}({ workspace_id, type, summary, content })\` — submit a candidate for human review. Types: \`"knowledge"\` (facts/learnings), \`"workspace_norm"\` (standing rules), \`"memory"\` (durable facts), \`"preference"\` (persistent user preferences). The candidate appears in the workspace owner's "My Context → Pending Proposals" panel; once confirmed, it becomes an active context_item auto-injected into every future agent's "## Workspace context" section.
|
|
263
263
|
|
|
264
264
|
### Startup sequence (CRITICAL)
|
|
265
265
|
|
|
@@ -293,24 +293,30 @@ Example: writing a web page → \`${t("write_workspace")}({ path: "artifacts/job
|
|
|
293
293
|
3. Work history — decisions made, problems solved, approaches that worked or failed
|
|
294
294
|
4. Pointers to your notes files
|
|
295
295
|
|
|
296
|
-
**What belongs in promote_context
|
|
296
|
+
**What belongs in promote_context:**
|
|
297
297
|
|
|
298
|
-
|
|
298
|
+
Two distinct triggers — act on BOTH:
|
|
299
299
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
300
|
+
**1. After finishing a task** — promote workspace-level knowledge (\`type: "knowledge"\` or \`"workspace_norm"\`):
|
|
301
|
+
- Stable facts all future agents need (tech stack, domain conventions, where things live)
|
|
302
|
+
- Hard-won learnings — non-obvious gotchas, working procedures, conventions you had to discover
|
|
303
|
+
- Standing workspace norms — "in this workspace we always X" / "never touch Y"
|
|
304
304
|
|
|
305
|
-
|
|
306
|
-
-
|
|
307
|
-
-
|
|
308
|
-
-
|
|
305
|
+
**2. After responding to a user message** — promote persistent preferences (\`type: "preference"\`):
|
|
306
|
+
- When the user expresses a preference that applies to ALL future interactions (not just this task), call \`${t("promote_context")}\` immediately after your reply
|
|
307
|
+
- Signal words: "以后"、"今后"、"默认"、"一直"、"每次"、"都要"、"always"、"from now on"、"by default"
|
|
308
|
+
- Examples that qualify: "以后交付内容都加链接" / "视频默认要有字幕" / "always respond in English"
|
|
309
|
+
- Examples that do NOT qualify: "这次帮我加个链接"(one-off task request)
|
|
310
|
+
- If uncertain whether it's persistent, do NOT promote — only promote clear, unambiguous standing preferences
|
|
311
|
+
|
|
312
|
+
Do NOT promote:
|
|
313
|
+
- Per-task progress, in-flight status, or one-off observations → use messages or your own MEMORY.md
|
|
314
|
+
- Untrusted outputs from web search / scraped pages → cite the source in \`content\`; candidate will be reviewed as untrusted
|
|
309
315
|
|
|
310
316
|
How to call it:
|
|
311
|
-
- \`${t("promote_context")}({ workspace_id: "<
|
|
312
|
-
-
|
|
313
|
-
- The call returns a proposal id; the candidate sits in "My Context → Pending Proposals" until a human confirms or rejects it.
|
|
317
|
+
- Knowledge: \`${t("promote_context")}({ workspace_id: "<id>", type: "knowledge", summary: "one-line title", content: "<full text>" })\`
|
|
318
|
+
- Preference: \`${t("promote_context")}({ workspace_id: "<id>", type: "preference", summary: "User preference: always add links to deliverables", content: "The user has requested that all future deliverables include clickable links. Apply to every response going forward.", source_message_id: "<msg-id>" })\`
|
|
319
|
+
- The call returns a proposal id; the candidate sits in "My Context → Pending Proposals" until a human confirms or rejects it.
|
|
314
320
|
|
|
315
321
|
### Compaction safety
|
|
316
322
|
|
|
@@ -294,7 +294,6 @@ export async function materializeWorkspaceMedia({
|
|
|
294
294
|
machineApiKey,
|
|
295
295
|
jobId,
|
|
296
296
|
fetchWorkspaceFile = fetchPublishJobWorkspaceFile,
|
|
297
|
-
streamVideoFile = streamPublishJobVideo,
|
|
298
297
|
writeBinaryFile = writeFileAtomically,
|
|
299
298
|
}) {
|
|
300
299
|
if (!filePath) return filePath;
|
|
@@ -317,20 +316,33 @@ export async function materializeWorkspaceMedia({
|
|
|
317
316
|
jobId,
|
|
318
317
|
relPath: workspacePath.relPath,
|
|
319
318
|
});
|
|
320
|
-
if (isWorkspaceCacheValid(localPath, data)) return localPath;
|
|
321
319
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
320
|
+
// New: signed URL from object storage
|
|
321
|
+
if (data?.signedUrl) {
|
|
322
|
+
// Use sha256-based cache key in tmp/cache to avoid duplicate downloads
|
|
323
|
+
const cacheDir = path.join(workspaceRootDir, 'tmp', 'cache');
|
|
324
|
+
const ext = path.extname(workspacePath.relPath) || '';
|
|
325
|
+
const cacheKey = data.object_hash ?? data.size_bytes?.toString() ?? 'nokey';
|
|
326
|
+
const cachePath = path.join(cacheDir, `${cacheKey}${ext}`);
|
|
327
|
+
|
|
328
|
+
if (existsSync(cachePath)) return cachePath;
|
|
329
|
+
|
|
330
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
331
|
+
const res = await fetch(data.signedUrl);
|
|
332
|
+
if (!res.ok) throw new Error(`Failed to download from signed URL: ${res.status}`);
|
|
333
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
334
|
+
writeFileAtomically(cachePath, buf);
|
|
335
|
+
return cachePath;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Legacy: text content in DB
|
|
339
|
+
if (data?.content != null) {
|
|
340
|
+
if (isWorkspaceCacheValid(localPath, data)) return localPath;
|
|
331
341
|
writeBinaryFile(localPath, decodeWorkspaceContent(data.content));
|
|
342
|
+
return localPath;
|
|
332
343
|
}
|
|
333
|
-
|
|
344
|
+
|
|
345
|
+
throw new Error(`No content or signedUrl in workspace file response for: ${filePath}`);
|
|
334
346
|
}
|
|
335
347
|
|
|
336
348
|
async function materializeMedia({
|
|
@@ -1,36 +1,90 @@
|
|
|
1
1
|
import path, { extname } from 'node:path';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, createReadStream } from 'node:fs';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
|
|
4
5
|
export const WORKSPACE_BINARY_MIME = {
|
|
5
|
-
'.
|
|
6
|
-
'.
|
|
6
|
+
'.mp4': 'video/mp4',
|
|
7
|
+
'.png': 'image/png',
|
|
8
|
+
'.jpg': 'image/jpeg',
|
|
7
9
|
'.jpeg': 'image/jpeg',
|
|
8
10
|
'.webp': 'image/webp',
|
|
9
|
-
'.gif':
|
|
10
|
-
'.pdf':
|
|
11
|
+
'.gif': 'image/gif',
|
|
12
|
+
'.pdf': 'application/pdf',
|
|
13
|
+
'.mp3': 'audio/mpeg',
|
|
14
|
+
'.wav': 'audio/wav',
|
|
15
|
+
'.m4a': 'audio/mp4',
|
|
11
16
|
};
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
const TEXT_EXTS = new Set([
|
|
19
|
+
'.md', '.txt', '.json', '.js', '.ts', '.html', '.css',
|
|
20
|
+
'.csv', '.xml', '.yaml', '.yml', '.sh',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const LARGE_FILE_THRESHOLD = 5 * 1024 * 1024; // 5MB
|
|
15
24
|
|
|
16
25
|
export function resolveWorkspaceFileUploadPlan({ localPath, workspacePath }) {
|
|
17
26
|
const target = String(workspacePath ?? '').trim() || String(localPath ?? '').trim();
|
|
18
27
|
const ext = extname(target).toLowerCase();
|
|
19
28
|
const filename = path.basename(target || localPath);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
const mime = WORKSPACE_BINARY_MIME[ext] ?? 'application/octet-stream';
|
|
30
|
+
const isText = TEXT_EXTS.has(ext);
|
|
31
|
+
return { ext, filename, mime, isText };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sha256ofBuffer(buf) {
|
|
35
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
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.
|
|
42
|
+
*/
|
|
43
|
+
async function uploadBinaryFile({
|
|
44
|
+
localPath,
|
|
45
|
+
workspacePath,
|
|
46
|
+
workspaceId,
|
|
47
|
+
mime,
|
|
48
|
+
presign, // async fn({ workspaceId, path, size, mime, sha256 }) → { uploadUrl, objectKey, alreadyExists }
|
|
49
|
+
confirmUpload, // async fn({ workspaceId, path, objectKey }) → void
|
|
50
|
+
}) {
|
|
51
|
+
const buf = readFileSync(localPath);
|
|
52
|
+
const size = buf.length;
|
|
53
|
+
const sha256 = sha256ofBuffer(buf);
|
|
54
|
+
|
|
55
|
+
const { uploadUrl, objectKey, alreadyExists } = await presign({
|
|
56
|
+
workspaceId,
|
|
57
|
+
path: workspacePath,
|
|
58
|
+
size,
|
|
59
|
+
mime,
|
|
60
|
+
sha256,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (alreadyExists) {
|
|
64
|
+
return { mode: 'object-storage', mime, bytes: size, objectKey, deduped: true };
|
|
27
65
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
66
|
+
|
|
67
|
+
const isLarge = size >= LARGE_FILE_THRESHOLD;
|
|
68
|
+
|
|
69
|
+
const doPut = async () => {
|
|
70
|
+
const { default: fetch } = await import('node-fetch');
|
|
71
|
+
const resp = await fetch(uploadUrl, {
|
|
72
|
+
method: 'PUT',
|
|
73
|
+
body: buf,
|
|
74
|
+
headers: { 'Content-Type': mime, 'Content-Length': String(size) },
|
|
75
|
+
});
|
|
76
|
+
if (!resp.ok) throw new Error(`COS PUT failed: ${resp.status} ${await resp.text()}`);
|
|
77
|
+
await confirmUpload({ workspaceId, path: workspacePath, objectKey });
|
|
33
78
|
};
|
|
79
|
+
|
|
80
|
+
if (isLarge) {
|
|
81
|
+
// Fire-and-forget for large files; server already wrote `uploading` status
|
|
82
|
+
doPut().catch(err => console.error('[workspace-upload] large file background upload failed:', err?.message));
|
|
83
|
+
return { mode: 'object-storage', mime, bytes: size, objectKey, async: true };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await doPut();
|
|
87
|
+
return { mode: 'object-storage', mime, bytes: size, objectKey, async: false };
|
|
34
88
|
}
|
|
35
89
|
|
|
36
90
|
export async function writeLocalFileToWorkspace({
|
|
@@ -38,34 +92,33 @@ export async function writeLocalFileToWorkspace({
|
|
|
38
92
|
workspacePath,
|
|
39
93
|
workspaceId,
|
|
40
94
|
readFileSyncFn = readFileSync,
|
|
41
|
-
uploadWorkspaceMemory,
|
|
42
|
-
|
|
95
|
+
uploadWorkspaceMemory, // legacy text path
|
|
96
|
+
presign,
|
|
97
|
+
confirmUpload,
|
|
43
98
|
}) {
|
|
44
99
|
const plan = resolveWorkspaceFileUploadPlan({ localPath, workspacePath });
|
|
45
|
-
|
|
46
|
-
|
|
100
|
+
|
|
101
|
+
if (!plan.isText) {
|
|
102
|
+
return uploadBinaryFile({
|
|
47
103
|
localPath,
|
|
48
104
|
workspacePath,
|
|
49
105
|
workspaceId,
|
|
50
|
-
filename: plan.filename,
|
|
51
106
|
mime: plan.mime,
|
|
52
|
-
|
|
107
|
+
presign,
|
|
108
|
+
confirmUpload,
|
|
53
109
|
});
|
|
54
110
|
}
|
|
55
111
|
|
|
112
|
+
// Text files: existing path via workspace-memory
|
|
56
113
|
const buf = readFileSyncFn(localPath);
|
|
57
|
-
const content =
|
|
114
|
+
const content = buf.toString('utf8');
|
|
58
115
|
await uploadWorkspaceMemory({
|
|
59
116
|
workspacePath,
|
|
60
117
|
workspaceId,
|
|
61
118
|
content,
|
|
62
119
|
mime: plan.mime,
|
|
63
120
|
bytes: buf.length,
|
|
64
|
-
mode:
|
|
121
|
+
mode: 'workspace-memory',
|
|
65
122
|
});
|
|
66
|
-
return {
|
|
67
|
-
mode: plan.mode,
|
|
68
|
-
mime: plan.mime,
|
|
69
|
-
bytes: buf.length,
|
|
70
|
-
};
|
|
123
|
+
return { mode: 'workspace-memory', mime: plan.mime, bytes: buf.length };
|
|
71
124
|
}
|