@prismer/runtime 2.0.3 → 2.0.5
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/dist/cli.cjs +2139 -295
- package/dist/cli.js +1978 -134
- package/dist/index.cjs +2107 -261
- package/dist/index.d.cts +533 -181
- package/dist/index.d.ts +533 -181
- package/dist/index.js +1992 -146
- package/package.json +2 -1
package/dist/cli.cjs
CHANGED
|
@@ -166,6 +166,42 @@ var init_skill_loader2 = __esm({
|
|
|
166
166
|
});
|
|
167
167
|
|
|
168
168
|
// src/adapters/hermes/index.ts
|
|
169
|
+
function readPlainRecord(value) {
|
|
170
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
171
|
+
}
|
|
172
|
+
function readNonEmptyString(value) {
|
|
173
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
174
|
+
}
|
|
175
|
+
function sanitizeRoleSkillSlug(value) {
|
|
176
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "template";
|
|
177
|
+
}
|
|
178
|
+
function renderRoleTemplateSkill(skillName, agents, roleTemplate) {
|
|
179
|
+
const mcpServers = Array.isArray(roleTemplate?.mcpServers) ? roleTemplate.mcpServers : [];
|
|
180
|
+
const tools = /* @__PURE__ */ new Set();
|
|
181
|
+
for (const server of mcpServers) {
|
|
182
|
+
const rec = readPlainRecord(server);
|
|
183
|
+
const allowlist = Array.isArray(rec?.toolsAllowlist) ? rec.toolsAllowlist : [];
|
|
184
|
+
for (const tool of allowlist) {
|
|
185
|
+
if (typeof tool === "string" && tool.trim()) tools.add(tool.trim());
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const allowedTools = [...tools].join(" ");
|
|
189
|
+
const roleTemplateSlug = JSON.stringify(readNonEmptyString(roleTemplate?.slug) ?? null);
|
|
190
|
+
return `---
|
|
191
|
+
name: ${skillName}
|
|
192
|
+
description: Role-template operating playbook projected from Prismer RoleTemplate.
|
|
193
|
+
${allowedTools ? `allowed-tools: ${allowedTools}
|
|
194
|
+
` : ""}metadata:
|
|
195
|
+
prismer:
|
|
196
|
+
source: role-template
|
|
197
|
+
roleTemplateSlug: ${roleTemplateSlug}
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
# Role Template Playbook
|
|
201
|
+
|
|
202
|
+
${agents.trim()}
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
169
205
|
function parsePrismerGoals(value) {
|
|
170
206
|
if (!Array.isArray(value)) return [];
|
|
171
207
|
return value.map((entry) => {
|
|
@@ -575,13 +611,14 @@ function failure(status, body) {
|
|
|
575
611
|
error: { code: "adapter_dispatch_failed", message: `Hermes ${status}: ${body || "<no body>"}` }
|
|
576
612
|
};
|
|
577
613
|
}
|
|
578
|
-
async function consumeSse(body, task) {
|
|
614
|
+
async function consumeSse(body, task, state) {
|
|
579
615
|
const reader = body.getReader();
|
|
580
616
|
const decoder = new TextDecoder();
|
|
581
617
|
let buf = "";
|
|
582
618
|
let deltas = "";
|
|
583
619
|
let finalOutput;
|
|
584
620
|
let lastProgress = 0;
|
|
621
|
+
let approvalRequested = false;
|
|
585
622
|
for (; ; ) {
|
|
586
623
|
const readStart = Date.now();
|
|
587
624
|
const { value, done } = await reader.read();
|
|
@@ -630,6 +667,10 @@ async function consumeSse(body, task) {
|
|
|
630
667
|
const next = Math.min(0.99, lastProgress + 0.05);
|
|
631
668
|
lastProgress = next;
|
|
632
669
|
const toolName = typeof payload.tool === "string" ? payload.tool : typeof payload.name === "string" ? payload.name : void 0;
|
|
670
|
+
if (eventName === "tool.started" && toolName === APPROVAL_TOOL_NAME) {
|
|
671
|
+
approvalRequested = true;
|
|
672
|
+
if (state) state.approvalRequested = true;
|
|
673
|
+
}
|
|
633
674
|
const previewStr = typeof payload.preview === "string" ? payload.preview : payload.preview != null ? JSON.stringify(payload.preview).slice(0, 200) : void 0;
|
|
634
675
|
task.onProgress?.({
|
|
635
676
|
progress: next,
|
|
@@ -659,7 +700,53 @@ async function consumeSse(body, task) {
|
|
|
659
700
|
}
|
|
660
701
|
}
|
|
661
702
|
}
|
|
662
|
-
return
|
|
703
|
+
return {
|
|
704
|
+
output: finalOutput && finalOutput.length > 0 ? finalOutput : deltas,
|
|
705
|
+
approvalRequested
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
async function consumeChatCompletionsSse(body, task) {
|
|
709
|
+
const reader = body.getReader();
|
|
710
|
+
const decoder = new TextDecoder();
|
|
711
|
+
let buf = "";
|
|
712
|
+
let output = "";
|
|
713
|
+
let lastProgress = 0;
|
|
714
|
+
for (; ; ) {
|
|
715
|
+
const { value, done } = await reader.read();
|
|
716
|
+
if (done) break;
|
|
717
|
+
buf += decoder.decode(value, { stream: true });
|
|
718
|
+
const events = buf.split("\n\n");
|
|
719
|
+
buf = events.pop() ?? "";
|
|
720
|
+
for (const ev of events) {
|
|
721
|
+
const dataLine = ev.match(/^data: (.+)$/m);
|
|
722
|
+
if (!dataLine) continue;
|
|
723
|
+
const raw = dataLine[1].trim();
|
|
724
|
+
if (raw === "[DONE]") continue;
|
|
725
|
+
let payload;
|
|
726
|
+
try {
|
|
727
|
+
payload = JSON.parse(raw);
|
|
728
|
+
} catch (err) {
|
|
729
|
+
process.stderr.write(
|
|
730
|
+
`[hermes-adapter] chat-completions SSE parse skipped: ${err.message}
|
|
731
|
+
`
|
|
732
|
+
);
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
const delta = payload.choices?.[0]?.delta?.content;
|
|
736
|
+
if (typeof delta === "string") {
|
|
737
|
+
output += delta;
|
|
738
|
+
} else if (Array.isArray(delta)) {
|
|
739
|
+
for (const part of delta) {
|
|
740
|
+
if (part?.type === "text" && typeof part.text === "string") output += part.text;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (output.length > 0 && lastProgress < 0.99) {
|
|
744
|
+
lastProgress = Math.min(0.99, lastProgress + 0.05);
|
|
745
|
+
task.onProgress?.({ progress: lastProgress, message: "streaming" });
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return output;
|
|
663
750
|
}
|
|
664
751
|
async function checkHealth(baseUrl, apiKey) {
|
|
665
752
|
try {
|
|
@@ -740,7 +827,7 @@ function pidAlive(pid) {
|
|
|
740
827
|
function escapeRegExp(value) {
|
|
741
828
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
742
829
|
}
|
|
743
|
-
var import_node_child_process3, import_node_fs2, import_node_module, import_node_os, import_node_path2, import_node_url, import_better_sqlite3, YAML, import_zod3, import_meta, PRISMER_IM_SKILL_NAME, PRISMER_IM_SKILL_CONTENT, HermesProfileConfigSchema, hermesAdapter, HermesService;
|
|
830
|
+
var import_node_child_process3, import_node_fs2, import_node_module, import_node_os, import_node_path2, import_node_url, import_better_sqlite3, YAML, import_zod3, import_meta, PRISMER_IM_SKILL_NAME, PRISMER_ROLE_SKILL_PREFIX, PRISMER_IM_SKILL_CONTENT, HermesProfileConfigSchema, hermesAdapter, HermesService, APPROVAL_TOOL_NAME;
|
|
744
831
|
var init_hermes = __esm({
|
|
745
832
|
"src/adapters/hermes/index.ts"() {
|
|
746
833
|
"use strict";
|
|
@@ -757,6 +844,7 @@ var init_hermes = __esm({
|
|
|
757
844
|
init_skill_loader2();
|
|
758
845
|
import_meta = {};
|
|
759
846
|
PRISMER_IM_SKILL_NAME = "prismer-im-collab";
|
|
847
|
+
PRISMER_ROLE_SKILL_PREFIX = "prismer-role";
|
|
760
848
|
PRISMER_IM_SKILL_CONTENT = `---
|
|
761
849
|
name: prismer-im-collab
|
|
762
850
|
description: Coordinate reliably in Prismer conversations, use workspace assets through bounded MCP tools, and keep task work on the board.
|
|
@@ -1069,6 +1157,7 @@ No @ needed; the human is the only other party.
|
|
|
1069
1157
|
const startedAt = Date.now();
|
|
1070
1158
|
let runId;
|
|
1071
1159
|
const sysPrompt = typeof task.metadata?.systemPrompt === "string" ? task.metadata.systemPrompt : void 0;
|
|
1160
|
+
this.installRoleTemplateSkill();
|
|
1072
1161
|
const skillPrompt = await this.skillLoader.loadSystemPromptFragment().catch((err) => {
|
|
1073
1162
|
process.stderr.write(
|
|
1074
1163
|
`[hermes-adapter] failed to read skills for ${this.profileName}: ${err.message}
|
|
@@ -1076,7 +1165,21 @@ No @ needed; the human is the only other party.
|
|
|
1076
1165
|
);
|
|
1077
1166
|
return void 0;
|
|
1078
1167
|
});
|
|
1079
|
-
const
|
|
1168
|
+
const outboxDir = typeof task.metadata?.prismerOutboxDir === "string" ? task.metadata.prismerOutboxDir : void 0;
|
|
1169
|
+
const outboxDirective = outboxDir ? [
|
|
1170
|
+
"[Outbox directive \u2014 MANDATORY, overrides any prior outbox path]",
|
|
1171
|
+
"",
|
|
1172
|
+
"For THIS turn ONLY, your active outbox is:",
|
|
1173
|
+
` ${outboxDir}`,
|
|
1174
|
+
"",
|
|
1175
|
+
"Any user-deliverable file (PDF, image, CSV, archive, doc) MUST be",
|
|
1176
|
+
"written to that EXACT absolute path. Do NOT reuse the outbox path",
|
|
1177
|
+
"from any previous turn \u2014 those directories no longer accept new",
|
|
1178
|
+
"uploads. Do NOT copy files into the previous outbox; copy them",
|
|
1179
|
+
"into the path above. Files outside this directory will not become",
|
|
1180
|
+
'chat attachments and the user will see "no files were attached".'
|
|
1181
|
+
].join("\n") : "";
|
|
1182
|
+
const instructions = [outboxDirective, sysPrompt, skillPrompt].filter(Boolean).join("\n\n");
|
|
1080
1183
|
if (sysPrompt) {
|
|
1081
1184
|
try {
|
|
1082
1185
|
const profileDir = getHermesProfileDir(this.profileName);
|
|
@@ -1100,8 +1203,15 @@ No @ needed; the human is the only other party.
|
|
|
1100
1203
|
`
|
|
1101
1204
|
);
|
|
1102
1205
|
}
|
|
1206
|
+
const sseState = { approvalRequested: false };
|
|
1103
1207
|
try {
|
|
1104
1208
|
const nativeBridgePatch = await this.prepareNativeBridgePatch(task);
|
|
1209
|
+
const imageRefs = (task.assetRefs ?? []).filter(
|
|
1210
|
+
(r) => r.mime?.startsWith("image/")
|
|
1211
|
+
);
|
|
1212
|
+
if (imageRefs.length > 0) {
|
|
1213
|
+
return await this.dispatchMultimodalChat(task, instructions, imageRefs, startedAt);
|
|
1214
|
+
}
|
|
1105
1215
|
const createRes = await fetch(`${this.baseUrl}/v1/runs`, {
|
|
1106
1216
|
method: "POST",
|
|
1107
1217
|
headers: {
|
|
@@ -1152,10 +1262,10 @@ No @ needed; the human is the only other party.
|
|
|
1152
1262
|
}
|
|
1153
1263
|
};
|
|
1154
1264
|
}
|
|
1155
|
-
const
|
|
1265
|
+
const sseResult = await consumeSse(eventsRes.body, task, sseState);
|
|
1156
1266
|
return {
|
|
1157
1267
|
ok: true,
|
|
1158
|
-
output,
|
|
1268
|
+
output: sseResult.output,
|
|
1159
1269
|
metrics: { durationMs: Date.now() - startedAt },
|
|
1160
1270
|
metadata: {
|
|
1161
1271
|
hermes: this.bridgeSnapshot("dispatched", {
|
|
@@ -1163,7 +1273,13 @@ No @ needed; the human is the only other party.
|
|
|
1163
1273
|
runId,
|
|
1164
1274
|
baseUrl: this.baseUrl,
|
|
1165
1275
|
model: this.config.model
|
|
1166
|
-
})
|
|
1276
|
+
}),
|
|
1277
|
+
// Surfaced to dispatch.ts so a subsequent reaper kill on this
|
|
1278
|
+
// task is reclassified as `awaiting_human_approval` rather than
|
|
1279
|
+
// `daemon_task_timeout`. Lives under a stable key (not under
|
|
1280
|
+
// `hermes.*`) so non-hermes adapters can adopt the same signal
|
|
1281
|
+
// later without bleeding hermes-specific metadata shape.
|
|
1282
|
+
...sseResult.approvalRequested ? { approvalRequested: true } : {}
|
|
1167
1283
|
}
|
|
1168
1284
|
};
|
|
1169
1285
|
} catch (err) {
|
|
@@ -1173,6 +1289,7 @@ No @ needed; the human is the only other party.
|
|
|
1173
1289
|
);
|
|
1174
1290
|
const categorized = categorizeDispatchError(err, task.signal);
|
|
1175
1291
|
const bridgeKind = categorized.error?.code === "task_cancelled" ? "cancelled" : "failed";
|
|
1292
|
+
const approvalSignal = sseState.approvalRequested ? { approvalRequested: true } : {};
|
|
1176
1293
|
return {
|
|
1177
1294
|
...categorized,
|
|
1178
1295
|
metadata: {
|
|
@@ -1181,13 +1298,87 @@ No @ needed; the human is the only other party.
|
|
|
1181
1298
|
baseUrl: this.baseUrl,
|
|
1182
1299
|
model: this.config.model,
|
|
1183
1300
|
...bridgeKind === "failed" ? { error: err.message } : {}
|
|
1184
|
-
})
|
|
1301
|
+
}),
|
|
1302
|
+
...approvalSignal
|
|
1185
1303
|
}
|
|
1186
1304
|
};
|
|
1187
1305
|
} finally {
|
|
1188
1306
|
if (runId && this.currentRunId === runId) this.currentRunId = void 0;
|
|
1189
1307
|
}
|
|
1190
1308
|
}
|
|
1309
|
+
/**
|
|
1310
|
+
* 14b rev.3 §3.0.4 — Hermes multimodal path.
|
|
1311
|
+
*
|
|
1312
|
+
* Builds an OpenAI-shape chat/completions body where the user message
|
|
1313
|
+
* `content` is a multipart array (`{type:'text'}` + N × `{type:'image_url'}`).
|
|
1314
|
+
* Hermes `_normalize_multimodal_content` (api_server.py:170) collapses the
|
|
1315
|
+
* blocks into its native multimodal representation before routing into
|
|
1316
|
+
* the agent pipeline.
|
|
1317
|
+
*
|
|
1318
|
+
* `image_url.url` is the cdn URL when the daemon's reachability probe said
|
|
1319
|
+
* the upstream LLM can see it (D35 path A); otherwise it's a `data:` URI
|
|
1320
|
+
* carrying the base64 bytes the daemon pre-fetched (D35 path B fallback).
|
|
1321
|
+
* The adapter doesn't decide — `resolveAssetRefs` does, and the adapter
|
|
1322
|
+
* just shuttles `cdnUrl ?? data:base64` into the wire.
|
|
1323
|
+
*/
|
|
1324
|
+
async dispatchMultimodalChat(task, instructions, imageRefs, startedAt) {
|
|
1325
|
+
const userContent = [{ type: "text", text: task.prompt }];
|
|
1326
|
+
for (const r of imageRefs) {
|
|
1327
|
+
const url = r.reachable === "cdn" && r.cdnUrl ? r.cdnUrl : r.base64 ? `data:${r.mime ?? "image/png"};base64,${r.base64}` : r.cdnUrl ?? "";
|
|
1328
|
+
if (!url) {
|
|
1329
|
+
userContent.push({
|
|
1330
|
+
type: "text",
|
|
1331
|
+
text: `[image attachment id=${r.assetId} mime=${r.mime ?? "unknown"} unavailable: cdn not reachable and bytes exceeded inline cap]`
|
|
1332
|
+
});
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
userContent.push({ type: "image_url", image_url: { url, detail: "auto" } });
|
|
1336
|
+
}
|
|
1337
|
+
const messages = [];
|
|
1338
|
+
if (instructions) messages.push({ role: "system", content: instructions });
|
|
1339
|
+
messages.push({ role: "user", content: userContent });
|
|
1340
|
+
const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
1341
|
+
method: "POST",
|
|
1342
|
+
headers: {
|
|
1343
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
1344
|
+
"Content-Type": "application/json"
|
|
1345
|
+
},
|
|
1346
|
+
body: JSON.stringify({
|
|
1347
|
+
model: this.config.model,
|
|
1348
|
+
messages,
|
|
1349
|
+
stream: true
|
|
1350
|
+
}),
|
|
1351
|
+
signal: task.signal
|
|
1352
|
+
});
|
|
1353
|
+
if (!res.ok) {
|
|
1354
|
+
const body = await res.text().catch(() => "");
|
|
1355
|
+
return failure(res.status, body);
|
|
1356
|
+
}
|
|
1357
|
+
if (!res.body) {
|
|
1358
|
+
return {
|
|
1359
|
+
ok: false,
|
|
1360
|
+
error: {
|
|
1361
|
+
code: "adapter_dispatch_failed",
|
|
1362
|
+
message: "Hermes /v1/chat/completions returned no body"
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
const output = await consumeChatCompletionsSse(res.body, task);
|
|
1367
|
+
return {
|
|
1368
|
+
ok: true,
|
|
1369
|
+
output,
|
|
1370
|
+
metrics: { durationMs: Date.now() - startedAt },
|
|
1371
|
+
metadata: {
|
|
1372
|
+
hermes: this.bridgeSnapshot("dispatched", {
|
|
1373
|
+
endpoint: "/v1/chat/completions",
|
|
1374
|
+
multimodal: true,
|
|
1375
|
+
imageRefs: imageRefs.length,
|
|
1376
|
+
baseUrl: this.baseUrl,
|
|
1377
|
+
model: this.config.model
|
|
1378
|
+
})
|
|
1379
|
+
}
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1191
1382
|
async shutdown() {
|
|
1192
1383
|
if (this.currentRunId) await this.stopRun(this.currentRunId);
|
|
1193
1384
|
}
|
|
@@ -1209,6 +1400,25 @@ No @ needed; the human is the only other party.
|
|
|
1209
1400
|
]);
|
|
1210
1401
|
return { ...kanbanPatch, ...goalsPatch };
|
|
1211
1402
|
}
|
|
1403
|
+
installRoleTemplateSkill() {
|
|
1404
|
+
const roleTemplate = readPlainRecord(this.config.roleTemplate);
|
|
1405
|
+
const hermesConfig = readPlainRecord(roleTemplate?.hermesConfig);
|
|
1406
|
+
const agents = readNonEmptyString(hermesConfig?.agents);
|
|
1407
|
+
if (!agents) return;
|
|
1408
|
+
const slug = sanitizeRoleSkillSlug(readNonEmptyString(roleTemplate?.slug) ?? "template");
|
|
1409
|
+
const skillName = `${PRISMER_ROLE_SKILL_PREFIX}-${slug}`;
|
|
1410
|
+
const content = renderRoleTemplateSkill(skillName, agents, roleTemplate);
|
|
1411
|
+
try {
|
|
1412
|
+
const skillDir = (0, import_node_path2.join)(this.skillLoader.getSkillsRoot(), skillName);
|
|
1413
|
+
(0, import_node_fs2.mkdirSync)(skillDir, { recursive: true });
|
|
1414
|
+
(0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(skillDir, "SKILL.md"), content, "utf8");
|
|
1415
|
+
} catch (err) {
|
|
1416
|
+
process.stderr.write(
|
|
1417
|
+
`[hermes-adapter] failed to install role-template skill ${skillName} for ${this.profileName}: ${err.message}
|
|
1418
|
+
`
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1212
1422
|
async prepareNativeKanbanPatch(task) {
|
|
1213
1423
|
const sourceKind = typeof task.metadata?.sourceKind === "string" ? task.metadata.sourceKind : null;
|
|
1214
1424
|
const parentTaskId = typeof task.metadata?.parentTaskId === "string" ? task.metadata.parentTaskId : null;
|
|
@@ -1317,6 +1527,7 @@ No @ needed; the human is the only other party.
|
|
|
1317
1527
|
};
|
|
1318
1528
|
}
|
|
1319
1529
|
};
|
|
1530
|
+
APPROVAL_TOOL_NAME = "prismer.approval.request_human_approval";
|
|
1320
1531
|
}
|
|
1321
1532
|
});
|
|
1322
1533
|
|
|
@@ -1348,6 +1559,58 @@ var init_skill_loader3 = __esm({
|
|
|
1348
1559
|
});
|
|
1349
1560
|
|
|
1350
1561
|
// src/adapters/openclaw/index.ts
|
|
1562
|
+
function readPlainRecord2(value) {
|
|
1563
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1564
|
+
}
|
|
1565
|
+
function readNonEmptyString2(value) {
|
|
1566
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1567
|
+
}
|
|
1568
|
+
function sanitizeRoleSkillSlug2(value) {
|
|
1569
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "template";
|
|
1570
|
+
}
|
|
1571
|
+
function renderRoleTemplateSkill2(skillName, agents, roleTemplate) {
|
|
1572
|
+
const mcpServers = Array.isArray(roleTemplate?.mcpServers) ? roleTemplate.mcpServers : [];
|
|
1573
|
+
const tools = /* @__PURE__ */ new Set();
|
|
1574
|
+
for (const server of mcpServers) {
|
|
1575
|
+
const rec = readPlainRecord2(server);
|
|
1576
|
+
const allowlist = Array.isArray(rec?.toolsAllowlist) ? rec.toolsAllowlist : [];
|
|
1577
|
+
for (const tool of allowlist) {
|
|
1578
|
+
if (typeof tool === "string" && tool.trim()) tools.add(tool.trim());
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
const allowedTools = [...tools].join(" ");
|
|
1582
|
+
const roleTemplateSlug = JSON.stringify(readNonEmptyString2(roleTemplate?.slug) ?? null);
|
|
1583
|
+
return `---
|
|
1584
|
+
name: ${skillName}
|
|
1585
|
+
description: Role-template operating playbook projected from Prismer RoleTemplate.
|
|
1586
|
+
${allowedTools ? `allowed-tools: ${allowedTools}
|
|
1587
|
+
` : ""}metadata:
|
|
1588
|
+
prismer:
|
|
1589
|
+
source: role-template
|
|
1590
|
+
roleTemplateSlug: ${roleTemplateSlug}
|
|
1591
|
+
---
|
|
1592
|
+
|
|
1593
|
+
# Role Template Playbook
|
|
1594
|
+
|
|
1595
|
+
${agents.trim()}
|
|
1596
|
+
`;
|
|
1597
|
+
}
|
|
1598
|
+
function pickResponsesSource(ref) {
|
|
1599
|
+
if (ref.reachable === "cdn" && ref.cdnUrl) {
|
|
1600
|
+
return { type: "url", url: ref.cdnUrl };
|
|
1601
|
+
}
|
|
1602
|
+
if (ref.base64) {
|
|
1603
|
+
return {
|
|
1604
|
+
type: "base64",
|
|
1605
|
+
data: ref.base64,
|
|
1606
|
+
media_type: ref.mime ?? "application/octet-stream"
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
if (ref.cdnUrl) {
|
|
1610
|
+
return { type: "url", url: ref.cdnUrl };
|
|
1611
|
+
}
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1351
1614
|
function getOpenClawSkillsDir(profile) {
|
|
1352
1615
|
const config = OpenClawProfileConfigSchema.parse(profile.config);
|
|
1353
1616
|
return resolveOpenClawSkillsDir(profile, config);
|
|
@@ -1368,15 +1631,17 @@ async function checkHealth2(baseUrl, apiKey) {
|
|
|
1368
1631
|
return false;
|
|
1369
1632
|
}
|
|
1370
1633
|
}
|
|
1371
|
-
var import_zod4, import_node_os2, import_node_path3, OpenClawProfileConfigSchema, openclawAdapter, OpenClawService;
|
|
1634
|
+
var import_zod4, import_node_fs3, import_node_os2, import_node_path3, PRISMER_ROLE_SKILL_PREFIX2, OpenClawProfileConfigSchema, openclawAdapter, OpenClawService;
|
|
1372
1635
|
var init_openclaw = __esm({
|
|
1373
1636
|
"src/adapters/openclaw/index.ts"() {
|
|
1374
1637
|
"use strict";
|
|
1375
1638
|
import_zod4 = require("zod");
|
|
1639
|
+
import_node_fs3 = require("fs");
|
|
1376
1640
|
import_node_os2 = require("os");
|
|
1377
1641
|
import_node_path3 = require("path");
|
|
1378
1642
|
init_contract();
|
|
1379
1643
|
init_skill_loader3();
|
|
1644
|
+
PRISMER_ROLE_SKILL_PREFIX2 = "prismer-role";
|
|
1380
1645
|
OpenClawProfileConfigSchema = import_zod4.z.object({
|
|
1381
1646
|
/** OpenClaw chat-completions HTTP port. */
|
|
1382
1647
|
port: import_zod4.z.number().int().min(1).max(65535).default(18789),
|
|
@@ -1421,23 +1686,25 @@ var init_openclaw = __esm({
|
|
|
1421
1686
|
);
|
|
1422
1687
|
}
|
|
1423
1688
|
const skillsDir = resolveOpenClawSkillsDir(profile, config);
|
|
1424
|
-
return new OpenClawService(baseUrl, config.apiKey, config.model, new OpenClawSkillLoader(skillsDir));
|
|
1689
|
+
return new OpenClawService(baseUrl, config.apiKey, config.model, config, new OpenClawSkillLoader(skillsDir));
|
|
1425
1690
|
},
|
|
1426
1691
|
async health() {
|
|
1427
1692
|
return { available: true };
|
|
1428
1693
|
}
|
|
1429
1694
|
};
|
|
1430
1695
|
OpenClawService = class {
|
|
1431
|
-
constructor(baseUrl, apiKey, model, skillLoader) {
|
|
1696
|
+
constructor(baseUrl, apiKey, model, config, skillLoader) {
|
|
1432
1697
|
this.baseUrl = baseUrl;
|
|
1433
1698
|
this.apiKey = apiKey;
|
|
1434
1699
|
this.model = model;
|
|
1700
|
+
this.config = config;
|
|
1435
1701
|
this.skillLoader = skillLoader;
|
|
1436
1702
|
this.id = baseUrl;
|
|
1437
1703
|
}
|
|
1438
1704
|
baseUrl;
|
|
1439
1705
|
apiKey;
|
|
1440
1706
|
model;
|
|
1707
|
+
config;
|
|
1441
1708
|
skillLoader;
|
|
1442
1709
|
id;
|
|
1443
1710
|
async healthy() {
|
|
@@ -1446,17 +1713,47 @@ var init_openclaw = __esm({
|
|
|
1446
1713
|
async dispatch(task) {
|
|
1447
1714
|
const startedAt = Date.now();
|
|
1448
1715
|
const systemPrompt = typeof task.metadata?.systemPrompt === "string" ? task.metadata.systemPrompt : void 0;
|
|
1716
|
+
this.installRoleTemplateSkill();
|
|
1449
1717
|
const skillPrompt = await this.skillLoader.loadSystemPromptFragment().catch((err) => {
|
|
1450
1718
|
process.stderr.write(`[openclaw-adapter] failed to read skills: ${err.message}
|
|
1451
1719
|
`);
|
|
1452
1720
|
return void 0;
|
|
1453
1721
|
});
|
|
1454
1722
|
const systemContent = [systemPrompt, skillPrompt].filter(Boolean).join("\n\n");
|
|
1455
|
-
const
|
|
1456
|
-
|
|
1457
|
-
|
|
1723
|
+
const refs = task.assetRefs ?? [];
|
|
1724
|
+
const imageRefs = refs.filter((r) => r.mime?.startsWith("image/"));
|
|
1725
|
+
const fileRefs = refs.filter((r) => r.mime && !r.mime.startsWith("image/"));
|
|
1726
|
+
const inputContent = [
|
|
1727
|
+
{ type: "input_text", text: task.prompt }
|
|
1728
|
+
];
|
|
1729
|
+
for (const r of imageRefs) {
|
|
1730
|
+
const source = pickResponsesSource(r);
|
|
1731
|
+
if (!source) {
|
|
1732
|
+
inputContent.push({
|
|
1733
|
+
type: "input_text",
|
|
1734
|
+
text: `[image attachment id=${r.assetId} unavailable: cdn unreachable and bytes exceeded inline cap]`
|
|
1735
|
+
});
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
inputContent.push({ type: "input_image", source });
|
|
1739
|
+
}
|
|
1740
|
+
for (const r of fileRefs) {
|
|
1741
|
+
const source = pickResponsesSource(r);
|
|
1742
|
+
if (!source) {
|
|
1743
|
+
inputContent.push({
|
|
1744
|
+
type: "input_text",
|
|
1745
|
+
text: `[file attachment id=${r.assetId} mime=${r.mime ?? "unknown"} unavailable]`
|
|
1746
|
+
});
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1749
|
+
if (r.filename && source.type === "base64") {
|
|
1750
|
+
source.filename = r.filename;
|
|
1751
|
+
}
|
|
1752
|
+
inputContent.push({ type: "input_file", source });
|
|
1753
|
+
}
|
|
1754
|
+
const input = [{ type: "message", role: "user", content: inputContent }];
|
|
1458
1755
|
try {
|
|
1459
|
-
const res = await fetch(`${this.baseUrl}/v1/
|
|
1756
|
+
const res = await fetch(`${this.baseUrl}/v1/responses`, {
|
|
1460
1757
|
method: "POST",
|
|
1461
1758
|
headers: {
|
|
1462
1759
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -1464,7 +1761,11 @@ var init_openclaw = __esm({
|
|
|
1464
1761
|
},
|
|
1465
1762
|
body: JSON.stringify({
|
|
1466
1763
|
model: this.model,
|
|
1467
|
-
|
|
1764
|
+
input,
|
|
1765
|
+
// OpenClaw `/v1/responses` accepts top-level `instructions` for
|
|
1766
|
+
// the system prompt — keeping it out of the user message
|
|
1767
|
+
// preserves cleanish channel-context separation.
|
|
1768
|
+
...systemContent ? { instructions: systemContent } : {},
|
|
1468
1769
|
stream: false
|
|
1469
1770
|
}),
|
|
1470
1771
|
signal: task.signal
|
|
@@ -1480,7 +1781,21 @@ var init_openclaw = __esm({
|
|
|
1480
1781
|
};
|
|
1481
1782
|
}
|
|
1482
1783
|
const json = await res.json();
|
|
1483
|
-
let output =
|
|
1784
|
+
let output = "";
|
|
1785
|
+
if (Array.isArray(json.output)) {
|
|
1786
|
+
for (const item of json.output) {
|
|
1787
|
+
if (item?.type === "message" && Array.isArray(item.content)) {
|
|
1788
|
+
for (const part of item.content) {
|
|
1789
|
+
if (part?.type === "output_text" && typeof part.text === "string") {
|
|
1790
|
+
output += part.text;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
if (!output) {
|
|
1797
|
+
output = json.choices?.[0]?.message?.content ?? "";
|
|
1798
|
+
}
|
|
1484
1799
|
const ERR_PREFIX_RE = /^Cannot read properties of undefined \(reading '[^']+'\)\s*\n+/;
|
|
1485
1800
|
if (ERR_PREFIX_RE.test(output)) {
|
|
1486
1801
|
process.stderr.write(
|
|
@@ -1498,6 +1813,25 @@ var init_openclaw = __esm({
|
|
|
1498
1813
|
return categorizeDispatchError(err, task.signal);
|
|
1499
1814
|
}
|
|
1500
1815
|
}
|
|
1816
|
+
installRoleTemplateSkill() {
|
|
1817
|
+
const roleTemplate = readPlainRecord2(this.config.roleTemplate);
|
|
1818
|
+
const openclawConfig = readPlainRecord2(roleTemplate?.openclawConfig);
|
|
1819
|
+
const agents = readNonEmptyString2(openclawConfig?.agents);
|
|
1820
|
+
if (!agents) return;
|
|
1821
|
+
const slug = sanitizeRoleSkillSlug2(readNonEmptyString2(roleTemplate?.slug) ?? "template");
|
|
1822
|
+
const skillName = `${PRISMER_ROLE_SKILL_PREFIX2}-${slug}`;
|
|
1823
|
+
const content = renderRoleTemplateSkill2(skillName, agents, roleTemplate);
|
|
1824
|
+
try {
|
|
1825
|
+
const skillDir = (0, import_node_path3.join)(this.skillLoader.getSkillsRoot(), skillName);
|
|
1826
|
+
(0, import_node_fs3.mkdirSync)(skillDir, { recursive: true });
|
|
1827
|
+
(0, import_node_fs3.writeFileSync)((0, import_node_path3.join)(skillDir, "SKILL.md"), content, "utf8");
|
|
1828
|
+
} catch (err) {
|
|
1829
|
+
process.stderr.write(
|
|
1830
|
+
`[openclaw-adapter] failed to install role-template skill ${skillName}: ${err.message}
|
|
1831
|
+
`
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1501
1835
|
};
|
|
1502
1836
|
}
|
|
1503
1837
|
});
|
|
@@ -1934,7 +2268,7 @@ var require_package = __commonJS({
|
|
|
1934
2268
|
"package.json"(exports2, module2) {
|
|
1935
2269
|
module2.exports = {
|
|
1936
2270
|
name: "@prismer/runtime",
|
|
1937
|
-
version: "2.0.
|
|
2271
|
+
version: "2.0.5",
|
|
1938
2272
|
description: "Prismer Cloud daemon runtime \u2014 TS-only adapter host for hosted IM agents",
|
|
1939
2273
|
type: "module",
|
|
1940
2274
|
main: "dist/index.js",
|
|
@@ -1968,6 +2302,7 @@ var require_package = __commonJS({
|
|
|
1968
2302
|
},
|
|
1969
2303
|
dependencies: {
|
|
1970
2304
|
"@iarna/toml": "^2.2.5",
|
|
2305
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
1971
2306
|
"better-sqlite3": "^11.8.0",
|
|
1972
2307
|
commander: "^12.1.0",
|
|
1973
2308
|
qrcode: "^1.5.4",
|
|
@@ -2125,21 +2460,21 @@ function pidFilePath(paths) {
|
|
|
2125
2460
|
return (0, import_node_path5.join)(paths.root, "daemon.pid");
|
|
2126
2461
|
}
|
|
2127
2462
|
function writePidFile(paths, pid) {
|
|
2128
|
-
(0,
|
|
2463
|
+
(0, import_node_fs5.writeFileSync)(pidFilePath(paths), `${pid}
|
|
2129
2464
|
`, "utf8");
|
|
2130
2465
|
}
|
|
2131
2466
|
function readPidFile(paths) {
|
|
2132
2467
|
const p = pidFilePath(paths);
|
|
2133
|
-
if (!(0,
|
|
2134
|
-
const raw = (0,
|
|
2468
|
+
if (!(0, import_node_fs5.existsSync)(p)) return void 0;
|
|
2469
|
+
const raw = (0, import_node_fs5.readFileSync)(p, "utf8").trim();
|
|
2135
2470
|
const pid = Number.parseInt(raw, 10);
|
|
2136
2471
|
return Number.isFinite(pid) ? pid : void 0;
|
|
2137
2472
|
}
|
|
2138
2473
|
function clearPidFile(paths) {
|
|
2139
2474
|
const p = pidFilePath(paths);
|
|
2140
|
-
if ((0,
|
|
2475
|
+
if ((0, import_node_fs5.existsSync)(p)) {
|
|
2141
2476
|
try {
|
|
2142
|
-
(0,
|
|
2477
|
+
(0, import_node_fs5.unlinkSync)(p);
|
|
2143
2478
|
} catch {
|
|
2144
2479
|
}
|
|
2145
2480
|
}
|
|
@@ -2152,11 +2487,11 @@ function pidAlive2(pid) {
|
|
|
2152
2487
|
return false;
|
|
2153
2488
|
}
|
|
2154
2489
|
}
|
|
2155
|
-
var
|
|
2490
|
+
var import_node_fs5, import_node_path5, DEFAULT_CLOUD_BASE_URL, ANSI, PKG_VERSION, RUNTIME_SUBTITLE;
|
|
2156
2491
|
var init_util = __esm({
|
|
2157
2492
|
"src/cli/util.ts"() {
|
|
2158
2493
|
"use strict";
|
|
2159
|
-
|
|
2494
|
+
import_node_fs5 = require("fs");
|
|
2160
2495
|
import_node_path5 = require("path");
|
|
2161
2496
|
init_ui();
|
|
2162
2497
|
DEFAULT_CLOUD_BASE_URL = "https://prismer.cloud";
|
|
@@ -2223,11 +2558,39 @@ async function syncAllAgentSkills(profiles, cloud, options = {}) {
|
|
|
2223
2558
|
}
|
|
2224
2559
|
async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, signal) {
|
|
2225
2560
|
if (!agentImUserId) return { synced: 0, skipped: 0, unchanged: 0 };
|
|
2226
|
-
|
|
2561
|
+
let data = await cloud.get(
|
|
2227
2562
|
`/api/im/skills/installed?agentId=${encodeURIComponent(agentImUserId)}`,
|
|
2228
2563
|
{ signal }
|
|
2229
2564
|
);
|
|
2230
|
-
|
|
2565
|
+
let entries = normalizeInstalledSkills(data);
|
|
2566
|
+
if (entries.length === 0 && (profile.adapterName === "hermes" || profile.adapterName === "openclaw")) {
|
|
2567
|
+
try {
|
|
2568
|
+
const backfill = await cloud.request(
|
|
2569
|
+
"POST",
|
|
2570
|
+
`/api/im/agents/${encodeURIComponent(agentImUserId)}/skills/install-builtins`,
|
|
2571
|
+
{ body: {}, signal }
|
|
2572
|
+
);
|
|
2573
|
+
if (backfill.ok && backfill.data && typeof backfill.data === "object" && "data" in backfill.data) {
|
|
2574
|
+
const installed = backfill.data.data?.installed ?? 0;
|
|
2575
|
+
if (installed > 0) {
|
|
2576
|
+
process.stdout.write(
|
|
2577
|
+
`[daemon] skill sync: backfilled ${installed} built-in skills for agent ${agentImUserId.slice(-8)}
|
|
2578
|
+
`
|
|
2579
|
+
);
|
|
2580
|
+
data = await cloud.get(
|
|
2581
|
+
`/api/im/skills/installed?agentId=${encodeURIComponent(agentImUserId)}`,
|
|
2582
|
+
{ signal }
|
|
2583
|
+
);
|
|
2584
|
+
entries = normalizeInstalledSkills(data);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
} catch (err) {
|
|
2588
|
+
process.stderr.write(
|
|
2589
|
+
`[daemon] skill sync: backfill skipped for ${agentImUserId.slice(-8)}: ${err.message}
|
|
2590
|
+
`
|
|
2591
|
+
);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2231
2594
|
const skillsRoot = resolveSkillsRoot(profile);
|
|
2232
2595
|
if (!skillsRoot) return { synced: 0, skipped: 0, unchanged: 0 };
|
|
2233
2596
|
let synced = 0;
|
|
@@ -2268,7 +2631,7 @@ async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, sig
|
|
|
2268
2631
|
continue;
|
|
2269
2632
|
}
|
|
2270
2633
|
const skillDir = (0, import_node_path12.join)(skillsRoot, slug);
|
|
2271
|
-
await
|
|
2634
|
+
await import_node_fs12.promises.mkdir(skillDir, { recursive: true });
|
|
2272
2635
|
const localFiles = await walkLocalDir(skillDir);
|
|
2273
2636
|
let dirty = false;
|
|
2274
2637
|
let perFileFailures = 0;
|
|
@@ -2323,14 +2686,14 @@ async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, sig
|
|
|
2323
2686
|
perFileFailures++;
|
|
2324
2687
|
continue;
|
|
2325
2688
|
}
|
|
2326
|
-
await
|
|
2327
|
-
await
|
|
2689
|
+
await import_node_fs12.promises.mkdir((0, import_node_path12.dirname)(targetPath), { recursive: true });
|
|
2690
|
+
await import_node_fs12.promises.writeFile(targetPath, bytes);
|
|
2328
2691
|
localFiles.delete(file.path);
|
|
2329
2692
|
dirty = true;
|
|
2330
2693
|
}
|
|
2331
2694
|
for (const orphan of localFiles.keys()) {
|
|
2332
2695
|
try {
|
|
2333
|
-
await
|
|
2696
|
+
await import_node_fs12.promises.unlink((0, import_node_path12.join)(skillDir, orphan));
|
|
2334
2697
|
dirty = true;
|
|
2335
2698
|
} catch (err) {
|
|
2336
2699
|
if (err.code !== "ENOENT") {
|
|
@@ -2443,7 +2806,7 @@ async function walkLocalDir(root) {
|
|
|
2443
2806
|
async function walk2(dir) {
|
|
2444
2807
|
let entries;
|
|
2445
2808
|
try {
|
|
2446
|
-
entries = await
|
|
2809
|
+
entries = await import_node_fs12.promises.readdir(dir, { withFileTypes: true });
|
|
2447
2810
|
} catch (err) {
|
|
2448
2811
|
if (err.code === "ENOENT") return;
|
|
2449
2812
|
throw err;
|
|
@@ -2454,7 +2817,7 @@ async function walkLocalDir(root) {
|
|
|
2454
2817
|
await walk2(full);
|
|
2455
2818
|
} else if (ent.isFile()) {
|
|
2456
2819
|
try {
|
|
2457
|
-
const buf = await
|
|
2820
|
+
const buf = await import_node_fs12.promises.readFile(full);
|
|
2458
2821
|
const rel = (0, import_node_path12.relative)(root, full).split(import_node_path12.sep).join("/");
|
|
2459
2822
|
out.set(rel, sha256Buffer(buf));
|
|
2460
2823
|
} catch {
|
|
@@ -2528,11 +2891,11 @@ async function ackSkillSync(cloud, agentImUserId, input, signal) {
|
|
|
2528
2891
|
`);
|
|
2529
2892
|
}
|
|
2530
2893
|
}
|
|
2531
|
-
var
|
|
2894
|
+
var import_node_fs12, import_node_path12, import_node_crypto3, skillSyncWarnedProfiles;
|
|
2532
2895
|
var init_skill_sync = __esm({
|
|
2533
2896
|
"src/daemon/skill-sync.ts"() {
|
|
2534
2897
|
"use strict";
|
|
2535
|
-
|
|
2898
|
+
import_node_fs12 = require("fs");
|
|
2536
2899
|
import_node_path12 = require("path");
|
|
2537
2900
|
import_node_crypto3 = require("crypto");
|
|
2538
2901
|
init_hermes();
|
|
@@ -2546,7 +2909,7 @@ var import_commander20 = require("commander");
|
|
|
2546
2909
|
|
|
2547
2910
|
// src/cli/commands/adapter.ts
|
|
2548
2911
|
var import_node_child_process4 = require("child_process");
|
|
2549
|
-
var
|
|
2912
|
+
var import_node_fs6 = require("fs");
|
|
2550
2913
|
var import_node_os4 = require("os");
|
|
2551
2914
|
var import_node_path6 = require("path");
|
|
2552
2915
|
var import_commander = require("commander");
|
|
@@ -3045,7 +3408,7 @@ init_openclaw();
|
|
|
3045
3408
|
|
|
3046
3409
|
// src/config.ts
|
|
3047
3410
|
var TOML = __toESM(require("@iarna/toml"), 1);
|
|
3048
|
-
var
|
|
3411
|
+
var import_node_fs4 = require("fs");
|
|
3049
3412
|
var import_node_os3 = require("os");
|
|
3050
3413
|
var import_node_path4 = require("path");
|
|
3051
3414
|
var import_zod5 = require("zod");
|
|
@@ -3088,15 +3451,15 @@ function resolvePaths(home) {
|
|
|
3088
3451
|
};
|
|
3089
3452
|
}
|
|
3090
3453
|
function configExists(paths = resolvePaths()) {
|
|
3091
|
-
return (0,
|
|
3454
|
+
return (0, import_node_fs4.existsSync)(paths.configFile);
|
|
3092
3455
|
}
|
|
3093
3456
|
function loadConfig(paths = resolvePaths()) {
|
|
3094
|
-
if (!(0,
|
|
3457
|
+
if (!(0, import_node_fs4.existsSync)(paths.configFile)) {
|
|
3095
3458
|
throw new Error(
|
|
3096
3459
|
`Config not found at ${paths.configFile}. Run \`prismer setup\` to create it.`
|
|
3097
3460
|
);
|
|
3098
3461
|
}
|
|
3099
|
-
const raw = (0,
|
|
3462
|
+
const raw = (0, import_node_fs4.readFileSync)(paths.configFile, "utf8");
|
|
3100
3463
|
const parsed = TOML.parse(raw);
|
|
3101
3464
|
const merged = {
|
|
3102
3465
|
...parsed,
|
|
@@ -3111,14 +3474,14 @@ function loadConfig(paths = resolvePaths()) {
|
|
|
3111
3474
|
return result.data;
|
|
3112
3475
|
}
|
|
3113
3476
|
function saveConfig(config, paths = resolvePaths()) {
|
|
3114
|
-
if (!(0,
|
|
3115
|
-
(0,
|
|
3477
|
+
if (!(0, import_node_fs4.existsSync)(paths.root)) {
|
|
3478
|
+
(0, import_node_fs4.mkdirSync)(paths.root, { recursive: true });
|
|
3116
3479
|
}
|
|
3117
|
-
if (!(0,
|
|
3118
|
-
(0,
|
|
3480
|
+
if (!(0, import_node_fs4.existsSync)((0, import_node_path4.dirname)(paths.configFile))) {
|
|
3481
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(paths.configFile), { recursive: true });
|
|
3119
3482
|
}
|
|
3120
3483
|
ConfigSchema.parse(config);
|
|
3121
|
-
(0,
|
|
3484
|
+
(0, import_node_fs4.writeFileSync)(paths.configFile, TOML.stringify(config), "utf8");
|
|
3122
3485
|
}
|
|
3123
3486
|
function deriveWsUrl(httpBase) {
|
|
3124
3487
|
const u = new URL(httpBase);
|
|
@@ -3381,15 +3744,15 @@ function runHooks(spec, opts) {
|
|
|
3381
3744
|
}
|
|
3382
3745
|
let backup;
|
|
3383
3746
|
if (planned.kind === "directory") {
|
|
3384
|
-
(0,
|
|
3747
|
+
(0, import_node_fs6.mkdirSync)(planned.path, { recursive: true });
|
|
3385
3748
|
const markerPath = (0, import_node_path6.join)(planned.path, "prismer.json");
|
|
3386
3749
|
backup = backupIfExists(markerPath);
|
|
3387
|
-
(0,
|
|
3750
|
+
(0, import_node_fs6.writeFileSync)(markerPath, JSON.stringify(prismerHookMarker(spec), null, 2) + "\n", "utf8");
|
|
3388
3751
|
} else {
|
|
3389
|
-
(0,
|
|
3752
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path6.dirname)(planned.path), { recursive: true });
|
|
3390
3753
|
backup = backupIfExists(planned.path);
|
|
3391
3754
|
const merged = mergeHookJson(planned.path, spec);
|
|
3392
|
-
(0,
|
|
3755
|
+
(0, import_node_fs6.writeFileSync)(planned.path, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
3393
3756
|
}
|
|
3394
3757
|
return {
|
|
3395
3758
|
ok: true,
|
|
@@ -3458,10 +3821,10 @@ function inspectAuthEnv(spec) {
|
|
|
3458
3821
|
case "hermes": {
|
|
3459
3822
|
const envFile = (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".hermes", ".env");
|
|
3460
3823
|
return {
|
|
3461
|
-
ok: (0,
|
|
3824
|
+
ok: (0, import_node_fs6.existsSync)(envFile) || Boolean(process.env.HERMES_API_KEY || process.env.API_SERVER_KEY),
|
|
3462
3825
|
present: [
|
|
3463
3826
|
...["HERMES_API_KEY", "API_SERVER_KEY"].filter((k) => Boolean(process.env[k])),
|
|
3464
|
-
...(0,
|
|
3827
|
+
...(0, import_node_fs6.existsSync)(envFile) ? [envFile] : []
|
|
3465
3828
|
],
|
|
3466
3829
|
hints: spec.authHints ?? []
|
|
3467
3830
|
};
|
|
@@ -3469,10 +3832,10 @@ function inspectAuthEnv(spec) {
|
|
|
3469
3832
|
case "openclaw": {
|
|
3470
3833
|
const cfgFile = (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".openclaw", "openclaw.json");
|
|
3471
3834
|
return {
|
|
3472
|
-
ok: (0,
|
|
3835
|
+
ok: (0, import_node_fs6.existsSync)(cfgFile) || Boolean(process.env.OPENCLAW_API_KEY),
|
|
3473
3836
|
present: [
|
|
3474
3837
|
...["OPENCLAW_API_KEY"].filter((k) => Boolean(process.env[k])),
|
|
3475
|
-
...(0,
|
|
3838
|
+
...(0, import_node_fs6.existsSync)(cfgFile) ? [cfgFile] : []
|
|
3476
3839
|
],
|
|
3477
3840
|
hints: spec.authHints ?? []
|
|
3478
3841
|
};
|
|
@@ -3489,14 +3852,14 @@ function inspectHook(spec) {
|
|
|
3489
3852
|
if (spec.name === "openclaw") {
|
|
3490
3853
|
const jsonPath = (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".openclaw", "hooks.json");
|
|
3491
3854
|
const markerPath = (0, import_node_path6.join)(target.path, "prismer.json");
|
|
3492
|
-
const jsonMarkerPresent = (0,
|
|
3493
|
-
const dirMarkerPresent = (0,
|
|
3855
|
+
const jsonMarkerPresent = (0, import_node_fs6.existsSync)(jsonPath) && fileContainsPrismerMarker(jsonPath);
|
|
3856
|
+
const dirMarkerPresent = (0, import_node_fs6.existsSync)(markerPath) && fileContainsPrismerMarker(markerPath);
|
|
3494
3857
|
return {
|
|
3495
3858
|
supported: true,
|
|
3496
3859
|
kind: "json-file-or-directory",
|
|
3497
3860
|
path: target.path,
|
|
3498
3861
|
jsonPath,
|
|
3499
|
-
exists: (0,
|
|
3862
|
+
exists: (0, import_node_fs6.existsSync)(target.path) || (0, import_node_fs6.existsSync)(jsonPath),
|
|
3500
3863
|
markerPath,
|
|
3501
3864
|
markerPresent: jsonMarkerPresent || dirMarkerPresent
|
|
3502
3865
|
};
|
|
@@ -3507,12 +3870,12 @@ function inspectHook(spec) {
|
|
|
3507
3870
|
supported: true,
|
|
3508
3871
|
kind: target.kind,
|
|
3509
3872
|
path: target.path,
|
|
3510
|
-
exists: (0,
|
|
3873
|
+
exists: (0, import_node_fs6.existsSync)(target.path),
|
|
3511
3874
|
markerPath,
|
|
3512
|
-
markerPresent: (0,
|
|
3875
|
+
markerPresent: (0, import_node_fs6.existsSync)(markerPath) && fileContainsPrismerMarker(markerPath)
|
|
3513
3876
|
};
|
|
3514
3877
|
}
|
|
3515
|
-
const exists = (0,
|
|
3878
|
+
const exists = (0, import_node_fs6.existsSync)(target.path);
|
|
3516
3879
|
if (!exists) {
|
|
3517
3880
|
return {
|
|
3518
3881
|
supported: true,
|
|
@@ -3548,14 +3911,14 @@ function prismerHookMarker(spec) {
|
|
|
3548
3911
|
}
|
|
3549
3912
|
function mergeHookJson(path9, spec) {
|
|
3550
3913
|
const marker = prismerHookMarker(spec);
|
|
3551
|
-
if (!(0,
|
|
3914
|
+
if (!(0, import_node_fs6.existsSync)(path9)) {
|
|
3552
3915
|
return {
|
|
3553
3916
|
prismer: marker
|
|
3554
3917
|
};
|
|
3555
3918
|
}
|
|
3556
3919
|
let parsed;
|
|
3557
3920
|
try {
|
|
3558
|
-
parsed = JSON.parse((0,
|
|
3921
|
+
parsed = JSON.parse((0, import_node_fs6.readFileSync)(path9, "utf8"));
|
|
3559
3922
|
} catch (err) {
|
|
3560
3923
|
throw new Error(`Cannot parse ${path9} as JSON: ${err.message}`);
|
|
3561
3924
|
}
|
|
@@ -3579,17 +3942,17 @@ function mergeHookJson(path9, spec) {
|
|
|
3579
3942
|
return next;
|
|
3580
3943
|
}
|
|
3581
3944
|
function backupIfExists(path9) {
|
|
3582
|
-
if (!(0,
|
|
3945
|
+
if (!(0, import_node_fs6.existsSync)(path9)) return null;
|
|
3583
3946
|
const suffix = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3584
3947
|
const backup = `${path9}.bak.${suffix}`;
|
|
3585
|
-
const stat = (0,
|
|
3948
|
+
const stat = (0, import_node_fs6.statSync)(path9);
|
|
3586
3949
|
if (stat.isDirectory()) return null;
|
|
3587
|
-
(0,
|
|
3950
|
+
(0, import_node_fs6.copyFileSync)(path9, backup);
|
|
3588
3951
|
return backup;
|
|
3589
3952
|
}
|
|
3590
3953
|
function fileContainsPrismerMarker(path9) {
|
|
3591
3954
|
try {
|
|
3592
|
-
return (0,
|
|
3955
|
+
return (0, import_node_fs6.readFileSync)(path9, "utf8").includes("prismer-daemon-runtime");
|
|
3593
3956
|
} catch {
|
|
3594
3957
|
return false;
|
|
3595
3958
|
}
|
|
@@ -3787,7 +4150,7 @@ function mapStatusToCode(status) {
|
|
|
3787
4150
|
|
|
3788
4151
|
// src/sync/store.ts
|
|
3789
4152
|
var import_better_sqlite32 = __toESM(require("better-sqlite3"), 1);
|
|
3790
|
-
var
|
|
4153
|
+
var import_node_fs7 = require("fs");
|
|
3791
4154
|
var import_node_path7 = require("path");
|
|
3792
4155
|
var MIGRATIONS = [
|
|
3793
4156
|
{
|
|
@@ -3914,14 +4277,44 @@ var MIGRATIONS = [
|
|
|
3914
4277
|
CREATE INDEX IF NOT EXISTS idx_asset_meta_seq
|
|
3915
4278
|
ON asset_metadata_index(workspace_id, asset_index_seq);
|
|
3916
4279
|
`
|
|
4280
|
+
},
|
|
4281
|
+
{
|
|
4282
|
+
version: 4,
|
|
4283
|
+
up: `
|
|
4284
|
+
-- Wave 4 / W3 \xA74.3 \u2014 pending dispatch replies for crash recovery.
|
|
4285
|
+
--
|
|
4286
|
+
-- The two-phase reply protocol writes a row here BEFORE calling
|
|
4287
|
+
-- POST /dispatch/:taskId/prepare so that a crash between prepare
|
|
4288
|
+
-- and commit leaves a recoverable trail. On daemon cold-start, any
|
|
4289
|
+
-- row with status='prepared' triggers an idempotent commit retry.
|
|
4290
|
+
--
|
|
4291
|
+
-- The server-side cutoff for orphaned 'prepared' rows is 1h
|
|
4292
|
+
-- (PREPARED_REPLY_ORPHAN_MS in dispatch-reply.service.ts); rows
|
|
4293
|
+
-- older than that will fail commit with DISPATCH_REPLY_INVALID_STATE
|
|
4294
|
+
-- and must be reported to the user as 'dispatch lost'.
|
|
4295
|
+
CREATE TABLE IF NOT EXISTS pending_dispatch_replies (
|
|
4296
|
+
idempotency_key TEXT PRIMARY KEY,
|
|
4297
|
+
task_id TEXT NOT NULL,
|
|
4298
|
+
reply_id TEXT,
|
|
4299
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
4300
|
+
payload_json TEXT NOT NULL,
|
|
4301
|
+
prepared_at INTEGER,
|
|
4302
|
+
created_at INTEGER NOT NULL,
|
|
4303
|
+
last_attempt_at INTEGER,
|
|
4304
|
+
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
4305
|
+
last_error TEXT
|
|
4306
|
+
);
|
|
4307
|
+
CREATE INDEX IF NOT EXISTS idx_pending_reply_task ON pending_dispatch_replies (task_id);
|
|
4308
|
+
CREATE INDEX IF NOT EXISTS idx_pending_reply_status ON pending_dispatch_replies (status, created_at);
|
|
4309
|
+
`
|
|
3917
4310
|
}
|
|
3918
4311
|
];
|
|
3919
4312
|
function runSql(db, sql) {
|
|
3920
4313
|
db["exec"](sql);
|
|
3921
4314
|
}
|
|
3922
4315
|
function openLocalDb(path9) {
|
|
3923
|
-
if (path9 !== ":memory:" && !(0,
|
|
3924
|
-
(0,
|
|
4316
|
+
if (path9 !== ":memory:" && !(0, import_node_fs7.existsSync)((0, import_node_path7.dirname)(path9))) {
|
|
4317
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path7.dirname)(path9), { recursive: true });
|
|
3925
4318
|
}
|
|
3926
4319
|
const db = new import_better_sqlite32.default(path9);
|
|
3927
4320
|
db.pragma("journal_mode = WAL");
|
|
@@ -4243,8 +4636,74 @@ function buildAgentCommand() {
|
|
|
4243
4636
|
}
|
|
4244
4637
|
getUI().ok("Skill install requested", `agent=${opts.agent} skill=${slugOrId}`);
|
|
4245
4638
|
}, { code: "agent_install_skill_failed" }));
|
|
4639
|
+
cmd.command("snapshot <imUserId>").description("Create an agent-level snapshot").option("--include-memory", "Include memory dump reference").option("--label <label>", "Snapshot label").option("--json", "Output JSON").action(runAction(async (imUserId, opts) => {
|
|
4640
|
+
const cloud = makeCloudClient();
|
|
4641
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agents/${encodeURIComponent(imUserId)}/snapshot`, {
|
|
4642
|
+
includeMemory: Boolean(opts.includeMemory),
|
|
4643
|
+
label: opts.label
|
|
4644
|
+
}, "agent_snapshot_failed");
|
|
4645
|
+
if (opts.json) {
|
|
4646
|
+
printJson(data);
|
|
4647
|
+
return;
|
|
4648
|
+
}
|
|
4649
|
+
const rec = data;
|
|
4650
|
+
getUI().ok("Snapshot created", String(rec.id ?? "<unknown>"));
|
|
4651
|
+
getUI().line(`agent=${String(rec.agentImUserId ?? imUserId)} memory=${rec.includeMemory ? "included" : "stripped"}`);
|
|
4652
|
+
}, { code: "agent_snapshot_failed" }));
|
|
4653
|
+
cmd.command("publish <imUserId>").description("Publish an agent as an Agent Pack").requiredOption("--slug <slug>", "Agent Pack slug").option("--version <version>", "Package version", "1.0.0").option("--license <license>", "Package license", "proprietary").option("--title <title>", "Display title metadata").option("--description <description>", "Description metadata").option("--json", "Output JSON").action(runAction(async (imUserId, opts) => {
|
|
4654
|
+
const cloud = makeCloudClient();
|
|
4655
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agents/${encodeURIComponent(imUserId)}/publish`, {
|
|
4656
|
+
slug: opts.slug,
|
|
4657
|
+
version: opts.version,
|
|
4658
|
+
license: opts.license,
|
|
4659
|
+
metadata: {
|
|
4660
|
+
...opts.title ? { title: opts.title } : {},
|
|
4661
|
+
...opts.description ? { description: opts.description } : {}
|
|
4662
|
+
},
|
|
4663
|
+
stripMemory: true
|
|
4664
|
+
}, "agent_publish_failed");
|
|
4665
|
+
if (opts.json) {
|
|
4666
|
+
printJson(data);
|
|
4667
|
+
return;
|
|
4668
|
+
}
|
|
4669
|
+
const rec = data;
|
|
4670
|
+
getUI().ok("Agent Pack published", `${String(rec.slug ?? opts.slug)}@${String(rec.version ?? opts.version ?? "1.0.0")}`);
|
|
4671
|
+
getUI().line(`package=${String(rec.id ?? "<unknown>")}`);
|
|
4672
|
+
}, { code: "agent_publish_failed" }));
|
|
4673
|
+
cmd.command("fork <packIdOrSlug>").description("Fork an Agent Pack into a workspace").requiredOption("--workspace-id <id>", "Target workspace id").option("--display-name <name>", "New agent display name").option("--json", "Output JSON").action(runAction(async (packIdOrSlug, opts) => {
|
|
4674
|
+
const cloud = makeCloudClient();
|
|
4675
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agent-packs/${encodeURIComponent(packIdOrSlug)}/fork`, {
|
|
4676
|
+
targetWorkspaceId: opts.workspaceId,
|
|
4677
|
+
displayName: opts.displayName
|
|
4678
|
+
}, "agent_fork_failed");
|
|
4679
|
+
if (opts.json) {
|
|
4680
|
+
printJson(data);
|
|
4681
|
+
return;
|
|
4682
|
+
}
|
|
4683
|
+
const rec = data;
|
|
4684
|
+
getUI().ok("Agent Pack forked", `newAgent=${String(rec.newImUserId ?? "<unknown>")}`);
|
|
4685
|
+
getUI().line(`did=${String(rec.newDid ?? "not issued")}`);
|
|
4686
|
+
}, { code: "agent_fork_failed" }));
|
|
4246
4687
|
return cmd;
|
|
4247
4688
|
}
|
|
4689
|
+
function makeCloudClient() {
|
|
4690
|
+
const cfg = loadConfig(resolvePaths());
|
|
4691
|
+
return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
4692
|
+
}
|
|
4693
|
+
async function requestEnvelope(cloud, method, path9, body, code) {
|
|
4694
|
+
const res = await cloud.request(
|
|
4695
|
+
method,
|
|
4696
|
+
path9,
|
|
4697
|
+
body === void 0 ? void 0 : { body }
|
|
4698
|
+
);
|
|
4699
|
+
if (!res.ok || res.data?.ok === false) {
|
|
4700
|
+
const message = res.error?.message ?? res.data?.error?.message ?? "request failed";
|
|
4701
|
+
exitWithError(`${path9} failed (${res.status === 0 ? "network error" : `HTTP ${res.status}`}): ${message}`, {
|
|
4702
|
+
code: res.error?.code ?? res.data?.error?.code ?? code
|
|
4703
|
+
});
|
|
4704
|
+
}
|
|
4705
|
+
return res.data && typeof res.data === "object" && "data" in res.data ? res.data.data : res.data;
|
|
4706
|
+
}
|
|
4248
4707
|
function readLocalAgents(localDb) {
|
|
4249
4708
|
const db = openLocalDb(localDb);
|
|
4250
4709
|
try {
|
|
@@ -4349,13 +4808,13 @@ function whichBinary2(bin) {
|
|
|
4349
4808
|
// src/cli/commands/asset.ts
|
|
4350
4809
|
var import_node_crypto2 = require("crypto");
|
|
4351
4810
|
var import_commander3 = require("commander");
|
|
4352
|
-
var
|
|
4811
|
+
var import_node_fs11 = require("fs");
|
|
4353
4812
|
var import_node_path11 = require("path");
|
|
4354
4813
|
|
|
4355
4814
|
// src/asset-cache.ts
|
|
4356
4815
|
var import_node_crypto = require("crypto");
|
|
4357
4816
|
var import_node_dns = require("dns");
|
|
4358
|
-
var
|
|
4817
|
+
var import_node_fs8 = require("fs");
|
|
4359
4818
|
var import_node_net = require("net");
|
|
4360
4819
|
var import_node_path8 = require("path");
|
|
4361
4820
|
var import_undici = require("undici");
|
|
@@ -4380,8 +4839,8 @@ var AssetCache = class {
|
|
|
4380
4839
|
this.cloud = opts.cloud;
|
|
4381
4840
|
this.cacheDir = opts.cacheDir;
|
|
4382
4841
|
this.maxBytes = opts.maxBytes ?? 5 * 1024 * 1024 * 1024;
|
|
4383
|
-
if (!(0,
|
|
4384
|
-
(0,
|
|
4842
|
+
if (!(0, import_node_fs8.existsSync)(this.cacheDir)) {
|
|
4843
|
+
(0, import_node_fs8.mkdirSync)(this.cacheDir, { recursive: true });
|
|
4385
4844
|
}
|
|
4386
4845
|
}
|
|
4387
4846
|
/** Local file path for a content hash. Files split by first 2 chars for fs scaling. */
|
|
@@ -4391,7 +4850,7 @@ var AssetCache = class {
|
|
|
4391
4850
|
get(hash) {
|
|
4392
4851
|
const row = this.db.prepare("SELECT * FROM cached_assets WHERE content_hash = ?").get(hash);
|
|
4393
4852
|
if (!row) return void 0;
|
|
4394
|
-
if (!(0,
|
|
4853
|
+
if (!(0, import_node_fs8.existsSync)(row.local_path)) {
|
|
4395
4854
|
this.db.prepare("DELETE FROM cached_assets WHERE content_hash = ?").run(hash);
|
|
4396
4855
|
return void 0;
|
|
4397
4856
|
}
|
|
@@ -4455,12 +4914,12 @@ var AssetCache = class {
|
|
|
4455
4914
|
throw new Error(`Asset hash mismatch for ${hash}: downloaded ${actualHash}`);
|
|
4456
4915
|
}
|
|
4457
4916
|
const localPath = this.pathFor(hash);
|
|
4458
|
-
if (!(0,
|
|
4459
|
-
(0,
|
|
4917
|
+
if (!(0, import_node_fs8.existsSync)((0, import_node_path8.dirname)(localPath))) {
|
|
4918
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path8.dirname)(localPath), { recursive: true });
|
|
4460
4919
|
}
|
|
4461
4920
|
const tmpPath = `${localPath}.${process.pid}.${Date.now()}.tmp`;
|
|
4462
|
-
(0,
|
|
4463
|
-
(0,
|
|
4921
|
+
(0, import_node_fs8.writeFileSync)(tmpPath, buf);
|
|
4922
|
+
(0, import_node_fs8.renameSync)(tmpPath, localPath);
|
|
4464
4923
|
const mime = res.headers.get("content-type");
|
|
4465
4924
|
const now = Date.now();
|
|
4466
4925
|
this.db.prepare(
|
|
@@ -4519,12 +4978,12 @@ var AssetCache = class {
|
|
|
4519
4978
|
return { cached: existing, finalUrl, durationMs: Date.now() - started };
|
|
4520
4979
|
}
|
|
4521
4980
|
const localPath = this.pathFor(hash);
|
|
4522
|
-
if (!(0,
|
|
4523
|
-
(0,
|
|
4981
|
+
if (!(0, import_node_fs8.existsSync)((0, import_node_path8.dirname)(localPath))) {
|
|
4982
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path8.dirname)(localPath), { recursive: true });
|
|
4524
4983
|
}
|
|
4525
4984
|
const tmpPath = `${localPath}.${process.pid}.${Date.now()}.tmp`;
|
|
4526
|
-
(0,
|
|
4527
|
-
(0,
|
|
4985
|
+
(0, import_node_fs8.writeFileSync)(tmpPath, body);
|
|
4986
|
+
(0, import_node_fs8.renameSync)(tmpPath, localPath);
|
|
4528
4987
|
const now = Date.now();
|
|
4529
4988
|
this.db.prepare(
|
|
4530
4989
|
`INSERT OR REPLACE INTO cached_assets
|
|
@@ -4552,7 +5011,7 @@ var AssetCache = class {
|
|
|
4552
5011
|
if (actualHash !== hash) {
|
|
4553
5012
|
throw new Error(`Asset hash mismatch for ${hash}: local file is ${actualHash}`);
|
|
4554
5013
|
}
|
|
4555
|
-
const size = (0,
|
|
5014
|
+
const size = (0, import_node_fs8.statSync)(localPath).size;
|
|
4556
5015
|
const now = Date.now();
|
|
4557
5016
|
this.db.prepare(
|
|
4558
5017
|
`INSERT OR REPLACE INTO cached_assets
|
|
@@ -4580,7 +5039,7 @@ var AssetCache = class {
|
|
|
4580
5039
|
for (const row of candidates) {
|
|
4581
5040
|
if (total <= this.maxBytes) break;
|
|
4582
5041
|
try {
|
|
4583
|
-
if ((0,
|
|
5042
|
+
if ((0, import_node_fs8.existsSync)(row.local_path)) (0, import_node_fs8.unlinkSync)(row.local_path);
|
|
4584
5043
|
} catch {
|
|
4585
5044
|
}
|
|
4586
5045
|
this.db.prepare("DELETE FROM cached_assets WHERE content_hash = ?").run(row.content_hash);
|
|
@@ -4595,7 +5054,7 @@ function sha256(buf) {
|
|
|
4595
5054
|
return (0, import_node_crypto.createHash)("sha256").update(buf).digest("hex");
|
|
4596
5055
|
}
|
|
4597
5056
|
function readFileBuffer(path9) {
|
|
4598
|
-
return (0,
|
|
5057
|
+
return (0, import_node_fs8.readFileSync)(path9);
|
|
4599
5058
|
}
|
|
4600
5059
|
var DEFAULT_URL_FETCH_MAX_BYTES = 5 * 1024 * 1024;
|
|
4601
5060
|
var DEFAULT_URL_FETCH_TIMEOUT_MS = 15e3;
|
|
@@ -4777,7 +5236,7 @@ async function fetchUrlWithGuards(rawUrl, opts) {
|
|
|
4777
5236
|
}
|
|
4778
5237
|
|
|
4779
5238
|
// src/daemon/asset/metadata-index.ts
|
|
4780
|
-
var
|
|
5239
|
+
var import_node_fs9 = require("fs");
|
|
4781
5240
|
var import_node_path9 = require("path");
|
|
4782
5241
|
var DEFAULT_LIMIT = 8;
|
|
4783
5242
|
var PULL_PAGE_SIZE = 500;
|
|
@@ -4806,16 +5265,16 @@ var AssetMetadataIndex = class {
|
|
|
4806
5265
|
this.db = opts.db;
|
|
4807
5266
|
this.cloud = opts.cloud;
|
|
4808
5267
|
this.workspaceId = opts.workspaceId;
|
|
4809
|
-
if (!(0,
|
|
4810
|
-
(0,
|
|
5268
|
+
if (!(0, import_node_fs9.existsSync)(opts.workspaceStateDir)) {
|
|
5269
|
+
(0, import_node_fs9.mkdirSync)(opts.workspaceStateDir, { recursive: true });
|
|
4811
5270
|
}
|
|
4812
5271
|
this.cursorPath = (0, import_node_path9.join)(opts.workspaceStateDir, "asset-metadata-cursor.json");
|
|
4813
5272
|
}
|
|
4814
5273
|
/** Persisted cursor for this workspace, or 0 on first run / corrupted file. */
|
|
4815
5274
|
readCursor() {
|
|
4816
|
-
if (!(0,
|
|
5275
|
+
if (!(0, import_node_fs9.existsSync)(this.cursorPath)) return 0;
|
|
4817
5276
|
try {
|
|
4818
|
-
const parsed = JSON.parse((0,
|
|
5277
|
+
const parsed = JSON.parse((0, import_node_fs9.readFileSync)(this.cursorPath, "utf8"));
|
|
4819
5278
|
if (parsed.workspaceId !== this.workspaceId) return 0;
|
|
4820
5279
|
return parsed.cursor;
|
|
4821
5280
|
} catch {
|
|
@@ -4828,7 +5287,7 @@ var AssetMetadataIndex = class {
|
|
|
4828
5287
|
cursor,
|
|
4829
5288
|
writtenAt: Date.now()
|
|
4830
5289
|
};
|
|
4831
|
-
(0,
|
|
5290
|
+
(0, import_node_fs9.writeFileSync)(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
4832
5291
|
}
|
|
4833
5292
|
/**
|
|
4834
5293
|
* Pull incremental asset metadata changes since the persisted cursor and
|
|
@@ -4945,7 +5404,7 @@ var AssetMetadataIndex = class {
|
|
|
4945
5404
|
};
|
|
4946
5405
|
|
|
4947
5406
|
// src/daemon/asset/mirror.ts
|
|
4948
|
-
var
|
|
5407
|
+
var import_node_fs10 = require("fs");
|
|
4949
5408
|
var import_node_path10 = require("path");
|
|
4950
5409
|
function rowToBinding(row) {
|
|
4951
5410
|
return {
|
|
@@ -4967,16 +5426,16 @@ var WorkspaceMirror = class {
|
|
|
4967
5426
|
this.db = opts.db;
|
|
4968
5427
|
this.cloud = opts.cloud;
|
|
4969
5428
|
this.workspaceId = opts.workspaceId;
|
|
4970
|
-
if (!(0,
|
|
4971
|
-
(0,
|
|
5429
|
+
if (!(0, import_node_fs10.existsSync)(opts.workspaceStateDir)) {
|
|
5430
|
+
(0, import_node_fs10.mkdirSync)(opts.workspaceStateDir, { recursive: true });
|
|
4972
5431
|
}
|
|
4973
5432
|
this.cursorPath = (0, import_node_path10.join)(opts.workspaceStateDir, "asset-cursor.json");
|
|
4974
5433
|
}
|
|
4975
5434
|
/** Persisted cursor for this workspace, or null on first run / corrupted file. */
|
|
4976
5435
|
readCursor() {
|
|
4977
|
-
if (!(0,
|
|
5436
|
+
if (!(0, import_node_fs10.existsSync)(this.cursorPath)) return null;
|
|
4978
5437
|
try {
|
|
4979
|
-
const parsed = JSON.parse((0,
|
|
5438
|
+
const parsed = JSON.parse((0, import_node_fs10.readFileSync)(this.cursorPath, "utf8"));
|
|
4980
5439
|
if (parsed.workspaceId !== this.workspaceId) return null;
|
|
4981
5440
|
return parsed.cursor;
|
|
4982
5441
|
} catch {
|
|
@@ -4989,7 +5448,7 @@ var WorkspaceMirror = class {
|
|
|
4989
5448
|
cursor,
|
|
4990
5449
|
writtenAt: Date.now()
|
|
4991
5450
|
};
|
|
4992
|
-
(0,
|
|
5451
|
+
(0, import_node_fs10.writeFileSync)(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
4993
5452
|
}
|
|
4994
5453
|
/**
|
|
4995
5454
|
* Pull workspace_file changes since the persisted cursor and upsert into
|
|
@@ -5233,8 +5692,8 @@ async function prefetchAssetBytes(opts) {
|
|
|
5233
5692
|
return { considered: rows.length, fetched, bytes, failed };
|
|
5234
5693
|
}
|
|
5235
5694
|
async function uploadAssetPath(inputPath, opts) {
|
|
5236
|
-
if (!(0,
|
|
5237
|
-
const stat = (0,
|
|
5695
|
+
if (!(0, import_node_fs11.existsSync)(inputPath)) exitWithError(`path not found: ${inputPath}`);
|
|
5696
|
+
const stat = (0, import_node_fs11.lstatSync)(inputPath);
|
|
5238
5697
|
if (stat.isSymbolicLink()) exitWithError(`refusing to upload symlink: ${inputPath}`);
|
|
5239
5698
|
if (stat.isFile()) {
|
|
5240
5699
|
return uploadAsset(inputPath, { ...opts, folderPath: normalizeFolderPath(opts.folderPath) });
|
|
@@ -5252,9 +5711,9 @@ async function uploadAssetPath(inputPath, opts) {
|
|
|
5252
5711
|
return { ok: true, data: { count: items.length, items } };
|
|
5253
5712
|
}
|
|
5254
5713
|
async function uploadAsset(file, opts) {
|
|
5255
|
-
if (!(0,
|
|
5714
|
+
if (!(0, import_node_fs11.existsSync)(file)) exitWithError(`file not found: ${file}`);
|
|
5256
5715
|
const cfg = loadConfig(resolvePaths());
|
|
5257
|
-
const bytes = (0,
|
|
5716
|
+
const bytes = (0, import_node_fs11.readFileSync)(file);
|
|
5258
5717
|
const contentHash = (0, import_node_crypto2.createHash)("sha256").update(bytes).digest("hex");
|
|
5259
5718
|
const metadata = parseMetadata(opts.metadata);
|
|
5260
5719
|
if (opts.taskId && metadata.taskId === void 0) metadata.taskId = opts.taskId;
|
|
@@ -5390,10 +5849,10 @@ async function putDirectAssetBytes(plan, bytes, mime) {
|
|
|
5390
5849
|
}
|
|
5391
5850
|
function listFilesRecursive(root) {
|
|
5392
5851
|
const out = [];
|
|
5393
|
-
for (const name of (0,
|
|
5852
|
+
for (const name of (0, import_node_fs11.readdirSync)(root)) {
|
|
5394
5853
|
if (name.startsWith(".")) continue;
|
|
5395
5854
|
const full = (0, import_node_path11.join)(root, name);
|
|
5396
|
-
const stat = (0,
|
|
5855
|
+
const stat = (0, import_node_fs11.lstatSync)(full);
|
|
5397
5856
|
if (stat.isSymbolicLink()) continue;
|
|
5398
5857
|
if (stat.isDirectory()) {
|
|
5399
5858
|
out.push(...listFilesRecursive(full));
|
|
@@ -5427,7 +5886,7 @@ async function downloadAsset(assetId, outPath) {
|
|
|
5427
5886
|
exitWithError(`asset download failed (${res.status}): ${errorMessage(body)}`);
|
|
5428
5887
|
}
|
|
5429
5888
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
5430
|
-
(0,
|
|
5889
|
+
(0, import_node_fs11.writeFileSync)(outPath, bytes);
|
|
5431
5890
|
}
|
|
5432
5891
|
function parseMetadata(raw) {
|
|
5433
5892
|
if (!raw) return {};
|
|
@@ -6074,16 +6533,16 @@ function parsePositiveInt3(value) {
|
|
|
6074
6533
|
// src/cli/commands/daemon.ts
|
|
6075
6534
|
var import_commander8 = require("commander");
|
|
6076
6535
|
var import_node_child_process7 = require("child_process");
|
|
6077
|
-
var
|
|
6536
|
+
var import_node_fs20 = require("fs");
|
|
6078
6537
|
var import_promises2 = require("timers/promises");
|
|
6079
6538
|
var import_node_path15 = require("path");
|
|
6080
6539
|
|
|
6081
6540
|
// src/daemon/runner.ts
|
|
6082
6541
|
var import_node_events3 = require("events");
|
|
6083
6542
|
var import_node_module2 = require("module");
|
|
6084
|
-
var
|
|
6543
|
+
var import_node_fs19 = require("fs");
|
|
6085
6544
|
var import_promises = require("fs/promises");
|
|
6086
|
-
var
|
|
6545
|
+
var import_node_os8 = require("os");
|
|
6087
6546
|
var import_node_path14 = require("path");
|
|
6088
6547
|
|
|
6089
6548
|
// src/adapters/registry.ts
|
|
@@ -6561,59 +7020,317 @@ function createLogger(tag) {
|
|
|
6561
7020
|
}
|
|
6562
7021
|
|
|
6563
7022
|
// src/daemon/dispatch.ts
|
|
6564
|
-
var
|
|
7023
|
+
var import_node_fs13 = require("fs");
|
|
6565
7024
|
var path2 = __toESM(require("path"), 1);
|
|
6566
7025
|
init_skill_sync();
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
var
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
var ASSET_INLINE_FULL_MAX_BYTES = 64 * 1024;
|
|
6577
|
-
var ASSET_INLINE_TRUNCATE_BYTES = 16 * 1024;
|
|
6578
|
-
async function handleDispatch(payload, requestId, deps) {
|
|
6579
|
-
const { taskId, agentImUserId } = payload;
|
|
6580
|
-
let resolvedHashes = [];
|
|
6581
|
-
let reply;
|
|
6582
|
-
const outboxBase = deps.paths?.runsDir ? path2.join(deps.paths.runsDir, taskId) : null;
|
|
6583
|
-
const outboxDir = outboxBase ? path2.join(outboxBase, "_outbox") : null;
|
|
6584
|
-
const workDir = outboxBase ? path2.join(outboxBase, "workdir") : null;
|
|
6585
|
-
if (outboxDir) {
|
|
6586
|
-
try {
|
|
6587
|
-
await import_node_fs12.promises.mkdir(outboxDir, { recursive: true });
|
|
6588
|
-
} catch (err) {
|
|
6589
|
-
process.stderr.write(
|
|
6590
|
-
`[daemon] outbox provision failed task=${taskId} dir=${outboxDir}: ${err.message}
|
|
6591
|
-
`
|
|
6592
|
-
);
|
|
6593
|
-
}
|
|
7026
|
+
|
|
7027
|
+
// src/daemon/task-heartbeat.ts
|
|
7028
|
+
var TaskHeartbeat = class {
|
|
7029
|
+
constructor(opts) {
|
|
7030
|
+
this.opts = opts;
|
|
7031
|
+
this.intervalMs = opts.intervalMs ?? 15e3;
|
|
7032
|
+
this.maxRetries = opts.maxRetries ?? 3;
|
|
7033
|
+
this.retryBackoffMs = opts.retryBackoffMs ?? 100;
|
|
7034
|
+
this.now = opts.now ?? Date.now;
|
|
6594
7035
|
}
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
7036
|
+
opts;
|
|
7037
|
+
timer;
|
|
7038
|
+
version = 0;
|
|
7039
|
+
taskId;
|
|
7040
|
+
getPhase;
|
|
7041
|
+
/** Tracks the last step time pushed via touchStep() — informational. */
|
|
7042
|
+
lastStepAt;
|
|
7043
|
+
intervalMs;
|
|
7044
|
+
maxRetries;
|
|
7045
|
+
retryBackoffMs;
|
|
7046
|
+
now;
|
|
7047
|
+
/**
|
|
7048
|
+
* Begin heartbeating for `taskId`. `getPhase` is invoked at every tick to
|
|
7049
|
+
* pick up the live phase string — adapters can mutate their phase state
|
|
7050
|
+
* freely and the heartbeat will surface it on the next 15s boundary.
|
|
7051
|
+
*
|
|
7052
|
+
* Calling start() twice on the same instance replaces the active task
|
|
7053
|
+
* (previous timer is cleared). Returns silently when already started for
|
|
7054
|
+
* the same taskId.
|
|
7055
|
+
*/
|
|
7056
|
+
start(taskId, getPhase) {
|
|
7057
|
+
if (this.taskId === taskId && this.timer) return;
|
|
7058
|
+
this.stop();
|
|
7059
|
+
this.taskId = taskId;
|
|
7060
|
+
this.getPhase = getPhase;
|
|
7061
|
+
this.timer = setInterval(() => {
|
|
7062
|
+
void this.push();
|
|
7063
|
+
}, this.intervalMs);
|
|
7064
|
+
this.timer.unref?.();
|
|
7065
|
+
}
|
|
7066
|
+
/**
|
|
7067
|
+
* Stop the heartbeat loop. Idempotent.
|
|
7068
|
+
*/
|
|
7069
|
+
stop() {
|
|
7070
|
+
if (this.timer) {
|
|
7071
|
+
clearInterval(this.timer);
|
|
7072
|
+
this.timer = void 0;
|
|
6603
7073
|
}
|
|
7074
|
+
this.taskId = void 0;
|
|
7075
|
+
this.getPhase = void 0;
|
|
6604
7076
|
}
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
7077
|
+
/**
|
|
7078
|
+
* Phase transition — push immediately AND update what the timer-driven
|
|
7079
|
+
* tick will report next time. Adapter calls this whenever the lifecycle
|
|
7080
|
+
* advances (thinking → tool_use → reasoning → responding → ...).
|
|
7081
|
+
*
|
|
7082
|
+
* No-op when no task is active.
|
|
7083
|
+
*/
|
|
7084
|
+
setPhase(phase) {
|
|
7085
|
+
if (!this.taskId) return;
|
|
7086
|
+
this.getPhase = () => phase;
|
|
7087
|
+
void this.push();
|
|
7088
|
+
}
|
|
7089
|
+
/**
|
|
7090
|
+
* Mark "I just made observable progress." Updates lastStepAt; next push
|
|
7091
|
+
* (timer-driven or setPhase-driven) carries it. Lightweight — no immediate
|
|
7092
|
+
* send. Adapters call this around tool calls / token boundaries so the
|
|
7093
|
+
* cloud reaper can distinguish "alive but slow" from "truly stuck".
|
|
7094
|
+
*/
|
|
7095
|
+
touchStep() {
|
|
7096
|
+
this.lastStepAt = this.now();
|
|
7097
|
+
}
|
|
7098
|
+
/**
|
|
7099
|
+
* Manually trigger one heartbeat send (does not affect the timer). Mainly
|
|
7100
|
+
* useful for tests that don't want to await a 15s tick. Returns the
|
|
7101
|
+
* heartbeatVersion that was sent, or undefined if no task is active.
|
|
7102
|
+
*/
|
|
7103
|
+
async forceTick() {
|
|
7104
|
+
if (!this.taskId) return void 0;
|
|
7105
|
+
return this.push();
|
|
7106
|
+
}
|
|
7107
|
+
/** Test helper: current heartbeatVersion (last value sent, may be 0 before first tick). */
|
|
7108
|
+
get currentVersion() {
|
|
7109
|
+
return this.version;
|
|
7110
|
+
}
|
|
7111
|
+
async push() {
|
|
7112
|
+
if (!this.taskId || !this.getPhase) return void 0;
|
|
7113
|
+
const taskId = this.taskId;
|
|
7114
|
+
const phase = this.getPhase();
|
|
7115
|
+
const heartbeatVersion = ++this.version;
|
|
7116
|
+
const payload = {
|
|
7117
|
+
taskId,
|
|
7118
|
+
heartbeatVersion,
|
|
7119
|
+
currentPhase: phase,
|
|
7120
|
+
...this.lastStepAt !== void 0 ? { lastStepAt: this.lastStepAt } : {}
|
|
7121
|
+
};
|
|
7122
|
+
const msg = envelope("task.heartbeat", payload);
|
|
7123
|
+
let lastErr;
|
|
7124
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
|
|
7125
|
+
try {
|
|
7126
|
+
if (this.opts.ws.isOpen && !this.opts.ws.isOpen()) {
|
|
7127
|
+
return heartbeatVersion;
|
|
7128
|
+
}
|
|
7129
|
+
this.opts.ws.send(msg);
|
|
7130
|
+
return heartbeatVersion;
|
|
7131
|
+
} catch (err) {
|
|
7132
|
+
lastErr = err instanceof Error ? err : new Error(String(err));
|
|
7133
|
+
if (attempt < this.maxRetries) {
|
|
7134
|
+
const backoff = this.retryBackoffMs * Math.pow(2, attempt);
|
|
7135
|
+
await sleep(backoff);
|
|
7136
|
+
}
|
|
7137
|
+
}
|
|
7138
|
+
}
|
|
7139
|
+
if (lastErr) {
|
|
7140
|
+
try {
|
|
7141
|
+
this.opts.onGiveUp?.(taskId, lastErr);
|
|
7142
|
+
} catch {
|
|
7143
|
+
}
|
|
6610
7144
|
}
|
|
7145
|
+
return heartbeatVersion;
|
|
6611
7146
|
}
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
7147
|
+
};
|
|
7148
|
+
function sleep(ms) {
|
|
7149
|
+
return new Promise((resolve3) => {
|
|
7150
|
+
const handle = setTimeout(resolve3, ms);
|
|
7151
|
+
handle.unref?.();
|
|
7152
|
+
});
|
|
7153
|
+
}
|
|
7154
|
+
|
|
7155
|
+
// src/daemon/step-recorder.ts
|
|
7156
|
+
var StepRecorder = class {
|
|
7157
|
+
constructor(opts) {
|
|
7158
|
+
this.opts = opts;
|
|
7159
|
+
this.reasoningThrottleMs = opts.reasoningThrottleMs ?? 500;
|
|
7160
|
+
this.now = opts.now ?? Date.now;
|
|
7161
|
+
}
|
|
7162
|
+
opts;
|
|
7163
|
+
seq = 0;
|
|
7164
|
+
reasoningBuf = { texts: [], firstAt: 0 };
|
|
7165
|
+
reasoningTimer;
|
|
7166
|
+
reasoningThrottleMs;
|
|
7167
|
+
now;
|
|
7168
|
+
/** Push a phase transition step. Immediate (no throttle). */
|
|
7169
|
+
recordPhaseChange(phase) {
|
|
7170
|
+
this.emit("phase_change", { phase });
|
|
7171
|
+
}
|
|
7172
|
+
/**
|
|
7173
|
+
* Push a tool-call step. Immediate.
|
|
7174
|
+
*
|
|
7175
|
+
* `toolCallId` is the daemon-side correlation id used to pair with the
|
|
7176
|
+
* subsequent recordToolResult call. Free-form; adapter decides.
|
|
7177
|
+
*/
|
|
7178
|
+
recordToolCall(toolName, input, toolCallId) {
|
|
7179
|
+
this.emit("tool_call", {
|
|
7180
|
+
toolName,
|
|
7181
|
+
inputSummary: summarize2(input),
|
|
7182
|
+
...toolCallId ? { toolCallId } : {}
|
|
7183
|
+
});
|
|
7184
|
+
}
|
|
7185
|
+
/** Push a tool-result step. Immediate. */
|
|
7186
|
+
recordToolResult(toolCallId, output) {
|
|
7187
|
+
this.emit("tool_result", {
|
|
7188
|
+
toolCallId,
|
|
7189
|
+
outputSummary: summarize2(output)
|
|
7190
|
+
});
|
|
7191
|
+
}
|
|
7192
|
+
/**
|
|
7193
|
+
* Buffer a reasoning chunk for batched dispatch. Multiple chunks within
|
|
7194
|
+
* `reasoningThrottleMs` are concatenated and emitted as a single
|
|
7195
|
+
* `reasoning_chunk` step when the timer fires.
|
|
7196
|
+
*/
|
|
7197
|
+
recordReasoningChunk(text) {
|
|
7198
|
+
if (!text) return;
|
|
7199
|
+
if (this.reasoningBuf.texts.length === 0) {
|
|
7200
|
+
this.reasoningBuf.firstAt = this.now();
|
|
7201
|
+
}
|
|
7202
|
+
this.reasoningBuf.texts.push(text);
|
|
7203
|
+
if (this.reasoningTimer) return;
|
|
7204
|
+
this.reasoningTimer = setTimeout(() => {
|
|
7205
|
+
this.flushReasoning();
|
|
7206
|
+
}, this.reasoningThrottleMs);
|
|
7207
|
+
this.reasoningTimer.unref?.();
|
|
7208
|
+
}
|
|
7209
|
+
/** Push an error step. Immediate. */
|
|
7210
|
+
recordError(message, payload) {
|
|
7211
|
+
this.emit("error", { message, ...payload ?? {} });
|
|
7212
|
+
}
|
|
7213
|
+
/**
|
|
7214
|
+
* Force any pending reasoning buffer out the door. Call on shutdown so
|
|
7215
|
+
* trailing tokens aren't lost.
|
|
7216
|
+
*/
|
|
7217
|
+
flush() {
|
|
7218
|
+
this.flushReasoning();
|
|
7219
|
+
}
|
|
7220
|
+
/** Test helper. */
|
|
7221
|
+
get currentSeq() {
|
|
7222
|
+
return this.seq;
|
|
7223
|
+
}
|
|
7224
|
+
flushReasoning() {
|
|
7225
|
+
if (this.reasoningTimer) {
|
|
7226
|
+
clearTimeout(this.reasoningTimer);
|
|
7227
|
+
this.reasoningTimer = void 0;
|
|
7228
|
+
}
|
|
7229
|
+
if (this.reasoningBuf.texts.length === 0) return;
|
|
7230
|
+
const text = this.reasoningBuf.texts.join("");
|
|
7231
|
+
const firstAt = this.reasoningBuf.firstAt;
|
|
7232
|
+
this.reasoningBuf = { texts: [], firstAt: 0 };
|
|
7233
|
+
this.emit("reasoning_chunk", { text }, firstAt);
|
|
7234
|
+
}
|
|
7235
|
+
emit(kind, payload, occurredAt) {
|
|
7236
|
+
const frame = {
|
|
7237
|
+
seq: ++this.seq,
|
|
7238
|
+
kind,
|
|
7239
|
+
payload,
|
|
7240
|
+
occurredAt: occurredAt ?? this.now()
|
|
7241
|
+
};
|
|
7242
|
+
const msg = envelope("task.step.append", {
|
|
7243
|
+
taskRunId: this.opts.taskRunId,
|
|
7244
|
+
step: frame
|
|
7245
|
+
});
|
|
7246
|
+
try {
|
|
7247
|
+
if (this.opts.ws.isOpen && !this.opts.ws.isOpen()) {
|
|
7248
|
+
try {
|
|
7249
|
+
this.opts.onSend?.(frame);
|
|
7250
|
+
} catch {
|
|
7251
|
+
}
|
|
7252
|
+
return;
|
|
7253
|
+
}
|
|
7254
|
+
this.opts.ws.send(msg);
|
|
7255
|
+
try {
|
|
7256
|
+
this.opts.onSend?.(frame);
|
|
7257
|
+
} catch {
|
|
7258
|
+
}
|
|
7259
|
+
} catch {
|
|
7260
|
+
}
|
|
7261
|
+
}
|
|
7262
|
+
};
|
|
7263
|
+
var MAX_SUMMARY_CHARS = 512;
|
|
7264
|
+
function summarize2(value) {
|
|
7265
|
+
let text;
|
|
7266
|
+
if (value == null) text = "";
|
|
7267
|
+
else if (typeof value === "string") text = value;
|
|
7268
|
+
else {
|
|
7269
|
+
try {
|
|
7270
|
+
text = JSON.stringify(value);
|
|
7271
|
+
} catch {
|
|
7272
|
+
text = String(value);
|
|
7273
|
+
}
|
|
7274
|
+
}
|
|
7275
|
+
if (text.length > MAX_SUMMARY_CHARS) {
|
|
7276
|
+
return `${text.slice(0, MAX_SUMMARY_CHARS)}\u2026(+${text.length - MAX_SUMMARY_CHARS} chars)`;
|
|
7277
|
+
}
|
|
7278
|
+
return text;
|
|
7279
|
+
}
|
|
7280
|
+
|
|
7281
|
+
// src/daemon/dispatch.ts
|
|
7282
|
+
var DEFAULT_CONTEXT_MAX = 8e3;
|
|
7283
|
+
var GOAL_CONTEXT_MAX = 4;
|
|
7284
|
+
var MEMORY_DIGEST_MAX_BYTES = 4e3;
|
|
7285
|
+
var DEFAULT_OPERATING_PRINCIPLES = [
|
|
7286
|
+
"- Decide and act; only request human approval for destructive operations or irreversible external effects.",
|
|
7287
|
+
"- Assignable work should become explicit tasks; do not leave concrete work as chat discussion only.",
|
|
7288
|
+
"- Use workspace asset tools for uploaded files: search/describe first, then read bounded ranges; do not claim file contents unless a tool call succeeded.",
|
|
7289
|
+
"- Keep users informed with concise status and concrete next steps."
|
|
7290
|
+
].join("\n");
|
|
7291
|
+
var ASSET_INLINE_FULL_MAX_BYTES = 64 * 1024;
|
|
7292
|
+
var ASSET_INLINE_TRUNCATE_BYTES = 16 * 1024;
|
|
7293
|
+
async function handleDispatch(payload, requestId, deps) {
|
|
7294
|
+
const { taskId, agentImUserId } = payload;
|
|
7295
|
+
let resolvedHashes = [];
|
|
7296
|
+
let reply;
|
|
7297
|
+
let heartbeatRef;
|
|
7298
|
+
let recorderRef;
|
|
7299
|
+
const outboxBase = deps.paths?.runsDir ? path2.join(deps.paths.runsDir, taskId) : null;
|
|
7300
|
+
const outboxDir = outboxBase ? path2.join(outboxBase, "_outbox") : null;
|
|
7301
|
+
const workDir = outboxBase ? path2.join(outboxBase, "workdir") : null;
|
|
7302
|
+
if (outboxDir) {
|
|
7303
|
+
try {
|
|
7304
|
+
await import_node_fs13.promises.mkdir(outboxDir, { recursive: true });
|
|
7305
|
+
} catch (err) {
|
|
7306
|
+
process.stderr.write(
|
|
7307
|
+
`[daemon] outbox provision failed task=${taskId} dir=${outboxDir}: ${err.message}
|
|
7308
|
+
`
|
|
7309
|
+
);
|
|
7310
|
+
}
|
|
7311
|
+
}
|
|
7312
|
+
if (workDir) {
|
|
7313
|
+
try {
|
|
7314
|
+
await import_node_fs13.promises.mkdir(workDir, { recursive: true });
|
|
7315
|
+
} catch (err) {
|
|
7316
|
+
process.stderr.write(
|
|
7317
|
+
`[daemon] workdir provision failed task=${taskId} dir=${workDir}: ${err.message}
|
|
7318
|
+
`
|
|
7319
|
+
);
|
|
7320
|
+
}
|
|
7321
|
+
}
|
|
7322
|
+
if (deps.outboxWatcher) {
|
|
7323
|
+
if (outboxDir) {
|
|
7324
|
+
deps.outboxWatcher.addActiveTask({ taskId, outboxDir });
|
|
7325
|
+
} else {
|
|
7326
|
+
deps.outboxWatcher.setActiveTask({ taskId });
|
|
7327
|
+
}
|
|
7328
|
+
}
|
|
7329
|
+
try {
|
|
7330
|
+
const profile = await resolveProfile(payload, deps.cloud);
|
|
7331
|
+
const adapter = deps.registry.get(profile.adapterName);
|
|
7332
|
+
if (!adapter) {
|
|
7333
|
+
reply = {
|
|
6617
7334
|
taskId,
|
|
6618
7335
|
ok: false,
|
|
6619
7336
|
error: { code: "adapter_unhealthy", message: `Adapter ${profile.adapterName} not registered` }
|
|
@@ -6660,7 +7377,13 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6660
7377
|
resolvedHashes.push(...r.resolvedHashes);
|
|
6661
7378
|
rewrittenContext = payload.context.map((e, i) => ({ ...e, content: r.texts[i] }));
|
|
6662
7379
|
}
|
|
6663
|
-
const assetResolution = await resolveAssetRefs(payload.assetRefs, deps.assetCache, taskId
|
|
7380
|
+
const assetResolution = await resolveAssetRefs(payload.assetRefs, deps.assetCache, taskId, {
|
|
7381
|
+
cloud: deps.cloud,
|
|
7382
|
+
enabled: deps.visionAux?.enabled !== false,
|
|
7383
|
+
cacheDir: deps.visionAux?.cacheDir ?? (deps.paths ? path2.join(deps.paths.cacheDir, "vision-cache") : void 0),
|
|
7384
|
+
timeoutMs: deps.visionAux?.timeoutMs,
|
|
7385
|
+
signal: deps.signal
|
|
7386
|
+
});
|
|
6664
7387
|
resolvedHashes.push(...assetResolution.pinnedHashes);
|
|
6665
7388
|
const basePrompt = composePrompt(
|
|
6666
7389
|
rewrittenPrompt.text,
|
|
@@ -6695,9 +7418,44 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6695
7418
|
}
|
|
6696
7419
|
const operatingPrinciples = resolveOperatingPrinciples(profile.config);
|
|
6697
7420
|
const composedSystemPrompt = [profileSystemPrompt, operatingPrinciples].filter((part) => typeof part === "string" && part.length > 0).join("\n\n");
|
|
7421
|
+
let activePhase = "thinking";
|
|
7422
|
+
const heartbeat = new TaskHeartbeat({
|
|
7423
|
+
ws: deps.ws,
|
|
7424
|
+
onGiveUp: (tid, err) => process.stderr.write(
|
|
7425
|
+
`[daemon] task.heartbeat give-up task=${tid}: ${err.message}
|
|
7426
|
+
`
|
|
7427
|
+
)
|
|
7428
|
+
});
|
|
7429
|
+
heartbeatRef = heartbeat;
|
|
7430
|
+
heartbeat.start(taskId, () => activePhase);
|
|
7431
|
+
const recorder = new StepRecorder({ ws: deps.ws, taskRunId: taskId });
|
|
7432
|
+
recorderRef = recorder;
|
|
7433
|
+
recorder.recordPhaseChange(activePhase);
|
|
6698
7434
|
const taskInput = {
|
|
6699
7435
|
taskId,
|
|
6700
7436
|
prompt: adapterPrompt,
|
|
7437
|
+
heartbeat: {
|
|
7438
|
+
setPhase: (phase) => {
|
|
7439
|
+
activePhase = phase;
|
|
7440
|
+
heartbeat.setPhase(phase);
|
|
7441
|
+
recorder.recordPhaseChange(phase);
|
|
7442
|
+
},
|
|
7443
|
+
touchStep: () => heartbeat.touchStep()
|
|
7444
|
+
},
|
|
7445
|
+
recorder: {
|
|
7446
|
+
recordPhaseChange: (phase) => {
|
|
7447
|
+
activePhase = phase;
|
|
7448
|
+
heartbeat.setPhase(phase);
|
|
7449
|
+
recorder.recordPhaseChange(phase);
|
|
7450
|
+
},
|
|
7451
|
+
recordToolCall: (toolName, input, toolCallId) => recorder.recordToolCall(toolName, input, toolCallId),
|
|
7452
|
+
recordToolResult: (toolCallId, output) => recorder.recordToolResult(toolCallId, output),
|
|
7453
|
+
recordReasoningChunk: (text) => recorder.recordReasoningChunk(text),
|
|
7454
|
+
recordError: (message, payload2) => recorder.recordError(message, payload2)
|
|
7455
|
+
},
|
|
7456
|
+
// 14b rev.3 §9 P3 — multimodal-aware adapters (Hermes, OpenClaw) pick
|
|
7457
|
+
// up the resolved bytes/URLs here; text-only adapters ignore the field.
|
|
7458
|
+
...assetResolution.resolvedRefs.length > 0 ? { assetRefs: assetResolution.resolvedRefs } : {},
|
|
6701
7459
|
metadata: {
|
|
6702
7460
|
...payload.metadata,
|
|
6703
7461
|
conversationId: payload.conversationId,
|
|
@@ -6760,6 +7518,9 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6760
7518
|
}
|
|
6761
7519
|
}
|
|
6762
7520
|
const reaperAborted = deps.signal?.aborted && result.error?.code === "task_cancelled";
|
|
7521
|
+
const approvalRequested = Boolean(
|
|
7522
|
+
result.metadata?.approvalRequested
|
|
7523
|
+
);
|
|
6763
7524
|
let collectedAssetIds = [];
|
|
6764
7525
|
if (deps.outboxWatcher && outboxDir) {
|
|
6765
7526
|
try {
|
|
@@ -6771,14 +7532,23 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6771
7532
|
collectedAssetIds = deps.outboxWatcher.flushPending(taskId);
|
|
6772
7533
|
}
|
|
6773
7534
|
const reaperLimitMs = typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? payload.timeoutMs : 5 * 6e4;
|
|
7535
|
+
const finalError = approvalRequested ? {
|
|
7536
|
+
code: "awaiting_human_approval",
|
|
7537
|
+
message: "Agent requested human approval and is suspended pending decision. Cloud will redispatch with `approval.decided` once the human responds."
|
|
7538
|
+
} : reaperAborted ? {
|
|
7539
|
+
code: "daemon_task_timeout",
|
|
7540
|
+
message: `Daemon-side reaper aborted after ${reaperLimitMs}ms inactivity (no progress events from adapter)`
|
|
7541
|
+
} : result.error;
|
|
6774
7542
|
reply = {
|
|
6775
7543
|
taskId,
|
|
7544
|
+
// When approval is pending we keep ok=false (the run did not produce a
|
|
7545
|
+
// completion-style output) but the cloud-side handler treats the
|
|
7546
|
+
// `awaiting_human_approval` code as non-terminal — task stays alive,
|
|
7547
|
+
// no red failure pill, and the next dispatch (after the human decides)
|
|
7548
|
+
// continues normally.
|
|
6776
7549
|
ok: result.ok,
|
|
6777
7550
|
output: result.output,
|
|
6778
|
-
error:
|
|
6779
|
-
code: "daemon_task_timeout",
|
|
6780
|
-
message: `Daemon-side reaper aborted after ${reaperLimitMs}ms inactivity (no progress events from adapter)`
|
|
6781
|
-
} : result.error,
|
|
7551
|
+
error: finalError,
|
|
6782
7552
|
...collectedAssetIds.length > 0 ? { assetIds: collectedAssetIds } : {},
|
|
6783
7553
|
metrics: result.metrics,
|
|
6784
7554
|
...assetResolution.observability.length > 0 || urlObservations.length > 0 ? { assetObservability: [...assetResolution.observability, ...urlObservations] } : {}
|
|
@@ -6800,6 +7570,18 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6800
7570
|
}
|
|
6801
7571
|
};
|
|
6802
7572
|
} finally {
|
|
7573
|
+
try {
|
|
7574
|
+
recorderRef?.flush();
|
|
7575
|
+
} catch (err) {
|
|
7576
|
+
process.stderr.write(`[daemon] step recorder flush failed task=${taskId}: ${err.message}
|
|
7577
|
+
`);
|
|
7578
|
+
}
|
|
7579
|
+
try {
|
|
7580
|
+
heartbeatRef?.stop();
|
|
7581
|
+
} catch (err) {
|
|
7582
|
+
process.stderr.write(`[daemon] heartbeat stop failed task=${taskId}: ${err.message}
|
|
7583
|
+
`);
|
|
7584
|
+
}
|
|
6803
7585
|
for (const hash of new Set(resolvedHashes)) {
|
|
6804
7586
|
try {
|
|
6805
7587
|
deps.assetCache.unpin(hash);
|
|
@@ -6876,6 +7658,10 @@ function isTextLikeMime(mime) {
|
|
|
6876
7658
|
if (m.endsWith("+json") || m.endsWith("+xml") || m.endsWith("+csv")) return true;
|
|
6877
7659
|
return false;
|
|
6878
7660
|
}
|
|
7661
|
+
function isImageMime(mime) {
|
|
7662
|
+
if (!mime) return false;
|
|
7663
|
+
return mime.toLowerCase().split(";")[0].trim().startsWith("image/");
|
|
7664
|
+
}
|
|
6879
7665
|
var HASH_REF_RE = /(?:^|\s)#([^\s#]+)/g;
|
|
6880
7666
|
var HEX_COLOR_RE = /^[0-9a-fA-F]{3,8}$/;
|
|
6881
7667
|
var FILE_EXT_RE = /\.[a-zA-Z0-9]{1,10}$/;
|
|
@@ -6954,8 +7740,34 @@ async function resolveHashRefs(prompt, assetIndex, cloud) {
|
|
|
6954
7740
|
}
|
|
6955
7741
|
return { text: result, resolutions };
|
|
6956
7742
|
}
|
|
6957
|
-
|
|
6958
|
-
|
|
7743
|
+
var ASSET_BASE64_INLINE_MAX_BYTES = 10 * 1024 * 1024;
|
|
7744
|
+
async function probeCdnReachable(cdnUrl, timeoutMs = 1500) {
|
|
7745
|
+
try {
|
|
7746
|
+
const u = new URL(cdnUrl);
|
|
7747
|
+
if (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname.startsWith("192.168.")) {
|
|
7748
|
+
return false;
|
|
7749
|
+
}
|
|
7750
|
+
} catch {
|
|
7751
|
+
return false;
|
|
7752
|
+
}
|
|
7753
|
+
try {
|
|
7754
|
+
const r = await fetch(cdnUrl, {
|
|
7755
|
+
method: "HEAD",
|
|
7756
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
7757
|
+
});
|
|
7758
|
+
if (r.ok) return true;
|
|
7759
|
+
return false;
|
|
7760
|
+
} catch {
|
|
7761
|
+
return false;
|
|
7762
|
+
}
|
|
7763
|
+
}
|
|
7764
|
+
async function resolveAssetRefs(refs, cache, taskId, visionAux) {
|
|
7765
|
+
const out = {
|
|
7766
|
+
promptBlocks: [],
|
|
7767
|
+
observability: [],
|
|
7768
|
+
pinnedHashes: [],
|
|
7769
|
+
resolvedRefs: []
|
|
7770
|
+
};
|
|
6959
7771
|
if (!refs || refs.length === 0) return out;
|
|
6960
7772
|
for (const ref of refs) {
|
|
6961
7773
|
let cached;
|
|
@@ -6984,7 +7796,7 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
6984
7796
|
const inlineCandidate = isTextLikeMime(effectiveMime);
|
|
6985
7797
|
if (inlineCandidate) {
|
|
6986
7798
|
try {
|
|
6987
|
-
const buf = (0,
|
|
7799
|
+
const buf = (0, import_node_fs13.readFileSync)(cached.localPath);
|
|
6988
7800
|
let strategy;
|
|
6989
7801
|
let body;
|
|
6990
7802
|
if (buf.byteLength <= ASSET_INLINE_FULL_MAX_BYTES) {
|
|
@@ -7020,7 +7832,42 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
7020
7832
|
continue;
|
|
7021
7833
|
}
|
|
7022
7834
|
}
|
|
7023
|
-
|
|
7835
|
+
let reachable = "unknown";
|
|
7836
|
+
let base64;
|
|
7837
|
+
if (ref.cdnUrl) {
|
|
7838
|
+
const ok2 = await probeCdnReachable(ref.cdnUrl);
|
|
7839
|
+
if (ok2) {
|
|
7840
|
+
reachable = "cdn";
|
|
7841
|
+
}
|
|
7842
|
+
}
|
|
7843
|
+
if (reachable !== "cdn") {
|
|
7844
|
+
try {
|
|
7845
|
+
const buf = (0, import_node_fs13.readFileSync)(cached.localPath);
|
|
7846
|
+
if (buf.byteLength <= ASSET_BASE64_INLINE_MAX_BYTES) {
|
|
7847
|
+
base64 = buf.toString("base64");
|
|
7848
|
+
reachable = "base64";
|
|
7849
|
+
} else {
|
|
7850
|
+
reachable = "unknown";
|
|
7851
|
+
}
|
|
7852
|
+
} catch (err) {
|
|
7853
|
+
const errMsg = `base64 fallback read failed: ${err.message}`;
|
|
7854
|
+
logAssetResolveFailure(taskId, ref, errMsg, effectiveMime);
|
|
7855
|
+
}
|
|
7856
|
+
}
|
|
7857
|
+
const resolvedRef = {
|
|
7858
|
+
...ref,
|
|
7859
|
+
mime: effectiveMime,
|
|
7860
|
+
localPath: cached.localPath,
|
|
7861
|
+
base64,
|
|
7862
|
+
reachable
|
|
7863
|
+
};
|
|
7864
|
+
out.resolvedRefs.push(resolvedRef);
|
|
7865
|
+
if (isImageMime(effectiveMime) && visionAux?.enabled) {
|
|
7866
|
+
const block = await describeImageAttachment(ref, effectiveMime, resolvedRef, visionAux, taskId);
|
|
7867
|
+
out.promptBlocks.push(block);
|
|
7868
|
+
} else {
|
|
7869
|
+
out.promptBlocks.push(formatAttachmentReminderBlock(ref, effectiveMime));
|
|
7870
|
+
}
|
|
7024
7871
|
out.observability.push({
|
|
7025
7872
|
assetId: ref.assetId,
|
|
7026
7873
|
contentHash: ref.contentHash,
|
|
@@ -7031,6 +7878,110 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
7031
7878
|
}
|
|
7032
7879
|
return out;
|
|
7033
7880
|
}
|
|
7881
|
+
async function describeImageAttachment(ref, mime, resolved, opts, taskId) {
|
|
7882
|
+
const cached = await readVisionAuxLocalCache(opts.cacheDir, ref.contentHash, mime);
|
|
7883
|
+
if (cached) return formatVisionAuxBlock(ref, mime, cached.description, true);
|
|
7884
|
+
const source = buildVisionAuxSource(resolved, mime);
|
|
7885
|
+
if (!source) {
|
|
7886
|
+
return formatVisionAuxUnavailableBlock(ref, mime, "no reachable URL or inline bytes");
|
|
7887
|
+
}
|
|
7888
|
+
try {
|
|
7889
|
+
const description = await callVisionAux(ref, mime, source, opts);
|
|
7890
|
+
await writeVisionAuxLocalCache(opts.cacheDir, ref.contentHash, mime, description);
|
|
7891
|
+
return formatVisionAuxBlock(ref, mime, description.description, false);
|
|
7892
|
+
} catch (err) {
|
|
7893
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7894
|
+
process.stderr.write(
|
|
7895
|
+
`[daemon] vision-aux failed task=${taskId ?? "unknown"} assetId=${ref.assetId} hash=${ref.contentHash}: ${message}
|
|
7896
|
+
`
|
|
7897
|
+
);
|
|
7898
|
+
return formatVisionAuxUnavailableBlock(ref, mime, message);
|
|
7899
|
+
}
|
|
7900
|
+
}
|
|
7901
|
+
function buildVisionAuxSource(resolved, mime) {
|
|
7902
|
+
if (resolved.reachable === "cdn" && resolved.cdnUrl) return { kind: "url", url: resolved.cdnUrl };
|
|
7903
|
+
if (resolved.base64 && mime) return { kind: "data_url", url: `data:${mime};base64,${resolved.base64}` };
|
|
7904
|
+
return null;
|
|
7905
|
+
}
|
|
7906
|
+
async function callVisionAux(ref, mime, source, opts) {
|
|
7907
|
+
const secret = process.env.VISION_AUX_INTERNAL_SECRET || process.env.INTERNAL_API_SECRET || process.env.PRISMER_INTERNAL_SECRET;
|
|
7908
|
+
const headers = secret ? { "x-prismer-internal-secret": secret } : void 0;
|
|
7909
|
+
const res = await opts.cloud.request(
|
|
7910
|
+
"POST",
|
|
7911
|
+
"/api/internal/vision-aux/describe",
|
|
7912
|
+
{
|
|
7913
|
+
body: {
|
|
7914
|
+
assetId: ref.assetId,
|
|
7915
|
+
contentHash: ref.contentHash,
|
|
7916
|
+
mime: mime ?? ref.mime ?? "image/unknown",
|
|
7917
|
+
source
|
|
7918
|
+
},
|
|
7919
|
+
headers,
|
|
7920
|
+
timeoutMs: opts.timeoutMs ?? 12e3,
|
|
7921
|
+
signal: opts.signal
|
|
7922
|
+
}
|
|
7923
|
+
);
|
|
7924
|
+
const env = res.data;
|
|
7925
|
+
if (!res.ok || env?.ok === false || !env?.data?.description) {
|
|
7926
|
+
const message = res.error?.message || (typeof env?.error === "string" ? env.error : env?.error?.message) || env?.message || `HTTP ${res.status}`;
|
|
7927
|
+
throw new Error(message);
|
|
7928
|
+
}
|
|
7929
|
+
const now = /* @__PURE__ */ new Date();
|
|
7930
|
+
const ttlSec = Number.isFinite(env.data.cacheTtlSec) ? Number(env.data.cacheTtlSec) : 300;
|
|
7931
|
+
return {
|
|
7932
|
+
description: env.data.description,
|
|
7933
|
+
modelUsed: env.data.modelUsed || "unknown",
|
|
7934
|
+
provider: env.data.provider || "vision-aux",
|
|
7935
|
+
generatedAt: now.toISOString(),
|
|
7936
|
+
expiresAt: new Date(now.getTime() + Math.max(1, ttlSec) * 1e3).toISOString(),
|
|
7937
|
+
mime: mime ?? ref.mime ?? "image/unknown"
|
|
7938
|
+
};
|
|
7939
|
+
}
|
|
7940
|
+
async function readVisionAuxLocalCache(cacheDir, contentHash, mime) {
|
|
7941
|
+
if (!cacheDir) return null;
|
|
7942
|
+
const file = visionAuxCachePath(cacheDir, contentHash);
|
|
7943
|
+
try {
|
|
7944
|
+
const raw = await import_node_fs13.promises.readFile(file, "utf8");
|
|
7945
|
+
const parsed = parseVisionAuxCache(raw);
|
|
7946
|
+
if (!parsed) {
|
|
7947
|
+
await import_node_fs13.promises.unlink(file).catch(() => void 0);
|
|
7948
|
+
return null;
|
|
7949
|
+
}
|
|
7950
|
+
if (parsed.mime !== mime) return null;
|
|
7951
|
+
const expiresAt = Date.parse(parsed.expiresAt);
|
|
7952
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) return null;
|
|
7953
|
+
return parsed;
|
|
7954
|
+
} catch (err) {
|
|
7955
|
+
if (err.code !== "ENOENT") {
|
|
7956
|
+
await import_node_fs13.promises.unlink(file).catch(() => void 0);
|
|
7957
|
+
}
|
|
7958
|
+
return null;
|
|
7959
|
+
}
|
|
7960
|
+
}
|
|
7961
|
+
async function writeVisionAuxLocalCache(cacheDir, contentHash, mime, description) {
|
|
7962
|
+
if (!cacheDir || !mime) return;
|
|
7963
|
+
const file = visionAuxCachePath(cacheDir, contentHash);
|
|
7964
|
+
await import_node_fs13.promises.mkdir(path2.dirname(file), { recursive: true });
|
|
7965
|
+
await import_node_fs13.promises.writeFile(file, JSON.stringify({ ...description, mime }, null, 2), "utf8");
|
|
7966
|
+
}
|
|
7967
|
+
function visionAuxCachePath(cacheDir, contentHash) {
|
|
7968
|
+
const safeHash = /^[a-f0-9]{32,128}$/i.test(contentHash) ? contentHash : createSafeCacheKey(contentHash);
|
|
7969
|
+
return path2.join(cacheDir, `${safeHash}.json`);
|
|
7970
|
+
}
|
|
7971
|
+
function createSafeCacheKey(value) {
|
|
7972
|
+
return Buffer.from(value).toString("base64url").slice(0, 128) || "unknown";
|
|
7973
|
+
}
|
|
7974
|
+
function parseVisionAuxCache(raw) {
|
|
7975
|
+
try {
|
|
7976
|
+
const parsed = JSON.parse(raw);
|
|
7977
|
+
if (typeof parsed.description === "string" && typeof parsed.modelUsed === "string" && typeof parsed.provider === "string" && typeof parsed.generatedAt === "string" && typeof parsed.expiresAt === "string" && typeof parsed.mime === "string") {
|
|
7978
|
+
return parsed;
|
|
7979
|
+
}
|
|
7980
|
+
} catch {
|
|
7981
|
+
return null;
|
|
7982
|
+
}
|
|
7983
|
+
return null;
|
|
7984
|
+
}
|
|
7034
7985
|
function logAssetResolveFailure(taskId, ref, error, mime = ref.mime) {
|
|
7035
7986
|
const message = error.replace(/\s+/g, " ").slice(0, 500);
|
|
7036
7987
|
process.stderr.write(
|
|
@@ -7045,8 +7996,19 @@ function formatInlineAssetBlock(ref, mime, body, strategy) {
|
|
|
7045
7996
|
${body}
|
|
7046
7997
|
---`;
|
|
7047
7998
|
}
|
|
7048
|
-
function
|
|
7049
|
-
|
|
7999
|
+
function formatAttachmentReminderBlock(ref, mime) {
|
|
8000
|
+
const name = ref.filename ? ` name=${ref.filename}` : "";
|
|
8001
|
+
return `[Attached file] id=${ref.assetId} mime=${mime ?? "unknown"}${name} \u2014 the user uploaded this file; multimodal adapters receive the bytes directly.`;
|
|
8002
|
+
}
|
|
8003
|
+
function formatVisionAuxBlock(ref, mime, description, cached) {
|
|
8004
|
+
const name = ref.filename ? `: ${ref.filename}` : "";
|
|
8005
|
+
const cacheLabel = cached ? " local-cache" : "";
|
|
8006
|
+
return `[Image attachment${name}] id=${ref.assetId} mime=${mime ?? "unknown"}${cacheLabel}
|
|
8007
|
+
${description.trim()}`;
|
|
8008
|
+
}
|
|
8009
|
+
function formatVisionAuxUnavailableBlock(ref, mime, reason) {
|
|
8010
|
+
const name = ref.filename ? `: ${ref.filename}` : "";
|
|
8011
|
+
return `[Image attachment${name}; description unavailable due to vision-aux error] id=${ref.assetId} mime=${mime ?? "unknown"} reason=${reason.slice(0, 180)}`;
|
|
7050
8012
|
}
|
|
7051
8013
|
function formatErrorAssetBlock(ref, mime, error) {
|
|
7052
8014
|
return `[Attached file] id=${ref.assetId} mime=${mime ?? "unknown"} ERROR: ${error} \u2014 this file failed to load on the daemon side. Tell the user the specific error and ask them to re-upload or paste the content directly; do not guess at the cause.`;
|
|
@@ -7386,8 +8348,9 @@ function updatedTime(task) {
|
|
|
7386
8348
|
// src/daemon/local-server.ts
|
|
7387
8349
|
var import_node_http = require("http");
|
|
7388
8350
|
var import_node_crypto4 = require("crypto");
|
|
7389
|
-
var
|
|
8351
|
+
var import_node_fs14 = require("fs");
|
|
7390
8352
|
var path3 = __toESM(require("path"), 1);
|
|
8353
|
+
var import_node_os5 = require("os");
|
|
7391
8354
|
|
|
7392
8355
|
// src/daemon/message-dispatch.ts
|
|
7393
8356
|
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
@@ -7598,6 +8561,11 @@ var LocalServer = class {
|
|
|
7598
8561
|
void this.handleSnapshot(req, res);
|
|
7599
8562
|
return;
|
|
7600
8563
|
}
|
|
8564
|
+
const agentDumpMatch = /^\/v1\/agents\/([^/]+)\/dump-state$/.exec(url);
|
|
8565
|
+
if (req.method === "POST" && agentDumpMatch) {
|
|
8566
|
+
void this.handleAgentDumpState(req, res, decodeURIComponent(agentDumpMatch[1]));
|
|
8567
|
+
return;
|
|
8568
|
+
}
|
|
7601
8569
|
if (req.method === "POST" && url === "/local/asset/write") {
|
|
7602
8570
|
void this.handleAssetWrite(req, res);
|
|
7603
8571
|
return;
|
|
@@ -7648,7 +8616,7 @@ var LocalServer = class {
|
|
|
7648
8616
|
async handleSnapshot(req, res) {
|
|
7649
8617
|
const root = this.opts.snapshotRoot ?? "/workspace";
|
|
7650
8618
|
try {
|
|
7651
|
-
const st = await
|
|
8619
|
+
const st = await import_node_fs14.promises.stat(root);
|
|
7652
8620
|
if (!st.isDirectory()) {
|
|
7653
8621
|
respond(res, 400, { error: "snapshot_root_not_directory", rootPath: root });
|
|
7654
8622
|
return;
|
|
@@ -7675,6 +8643,54 @@ var LocalServer = class {
|
|
|
7675
8643
|
}
|
|
7676
8644
|
void req;
|
|
7677
8645
|
}
|
|
8646
|
+
/**
|
|
8647
|
+
* POST /v1/agents/:agentId/dump-state — agent-scoped daemon manifest.
|
|
8648
|
+
*
|
|
8649
|
+
* This is narrower than `/v1/snapshot`: it walks only the current agent's
|
|
8650
|
+
* profile/work dirs so cloud can attach the result to IMAgentSnapshot without
|
|
8651
|
+
* capturing unrelated agents hosted in the same daemon/container.
|
|
8652
|
+
*/
|
|
8653
|
+
async handleAgentDumpState(req, res, agentId) {
|
|
8654
|
+
const state = this.opts.getState();
|
|
8655
|
+
const agent = state.hostedAgents.find((item) => item.imUserId === agentId);
|
|
8656
|
+
if (!agent) {
|
|
8657
|
+
respond(res, 404, { error: "agent_not_hosted", agentId });
|
|
8658
|
+
return;
|
|
8659
|
+
}
|
|
8660
|
+
const roots = getAgentStateRoots(agent, this.opts.snapshotRoot ?? "/workspace");
|
|
8661
|
+
try {
|
|
8662
|
+
const manifests = await Promise.all(
|
|
8663
|
+
roots.map(async (root) => {
|
|
8664
|
+
const stat = await statOptional(root.rootPath);
|
|
8665
|
+
if (!stat) return { ...root, exists: false, files: [] };
|
|
8666
|
+
if (!stat.isDirectory()) return { ...root, exists: true, error: "not_directory", files: [] };
|
|
8667
|
+
return { ...root, exists: true, files: await walkAndDigest(root.rootPath, root.rootPath) };
|
|
8668
|
+
})
|
|
8669
|
+
);
|
|
8670
|
+
const files = manifests.flatMap(
|
|
8671
|
+
(manifest) => manifest.files.map((file) => ({
|
|
8672
|
+
...file,
|
|
8673
|
+
path: `${manifest.kind}/${file.path}`,
|
|
8674
|
+
rootKind: manifest.kind,
|
|
8675
|
+
rootPath: manifest.rootPath
|
|
8676
|
+
}))
|
|
8677
|
+
);
|
|
8678
|
+
respond(res, 200, {
|
|
8679
|
+
agentId,
|
|
8680
|
+
adapterName: agent.adapterName,
|
|
8681
|
+
dumpedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8682
|
+
roots: manifests,
|
|
8683
|
+
files
|
|
8684
|
+
});
|
|
8685
|
+
} catch (err) {
|
|
8686
|
+
respond(res, 500, {
|
|
8687
|
+
error: "agent_dump_state_failed",
|
|
8688
|
+
agentId,
|
|
8689
|
+
message: err instanceof Error ? err.message : String(err)
|
|
8690
|
+
});
|
|
8691
|
+
}
|
|
8692
|
+
void req;
|
|
8693
|
+
}
|
|
7678
8694
|
/**
|
|
7679
8695
|
* POST /local/asset/write — agent-gen adapter RPC.
|
|
7680
8696
|
*
|
|
@@ -7946,11 +8962,44 @@ function validateAgentDispatchRequest(body) {
|
|
|
7946
8962
|
if (payload.attachments != null && !Array.isArray(payload.attachments)) return "attachments must be an array";
|
|
7947
8963
|
return null;
|
|
7948
8964
|
}
|
|
8965
|
+
function getAgentStateRoots(agent, snapshotRoot) {
|
|
8966
|
+
const profileName = sanitizeProfileName(agent.name || agent.imUserId);
|
|
8967
|
+
const roots = [
|
|
8968
|
+
{
|
|
8969
|
+
kind: "workspace-agent",
|
|
8970
|
+
rootPath: path3.join(snapshotRoot, "agents", agent.imUserId)
|
|
8971
|
+
}
|
|
8972
|
+
];
|
|
8973
|
+
if (agent.adapterName === "hermes" || agent.adapterName === "claude-code") {
|
|
8974
|
+
roots.unshift({
|
|
8975
|
+
kind: "hermes-profile",
|
|
8976
|
+
rootPath: path3.join(process.env.HERMES_HOME || path3.join((0, import_node_os5.homedir)(), ".hermes"), "profiles", profileName)
|
|
8977
|
+
});
|
|
8978
|
+
}
|
|
8979
|
+
if (agent.adapterName === "openclaw") {
|
|
8980
|
+
roots.unshift({
|
|
8981
|
+
kind: "openclaw-profile",
|
|
8982
|
+
rootPath: path3.join(process.env.OPENCLAW_HOME || path3.join((0, import_node_os5.homedir)(), ".openclaw"), "profiles", profileName)
|
|
8983
|
+
});
|
|
8984
|
+
}
|
|
8985
|
+
return roots;
|
|
8986
|
+
}
|
|
8987
|
+
function sanitizeProfileName(value) {
|
|
8988
|
+
return value.replace(/[\\/]/g, "_").trim() || "default";
|
|
8989
|
+
}
|
|
8990
|
+
async function statOptional(rootPath) {
|
|
8991
|
+
try {
|
|
8992
|
+
return await import_node_fs14.promises.stat(rootPath);
|
|
8993
|
+
} catch (err) {
|
|
8994
|
+
if (err.code === "ENOENT") return null;
|
|
8995
|
+
throw err;
|
|
8996
|
+
}
|
|
8997
|
+
}
|
|
7949
8998
|
async function walkAndDigest(root, current) {
|
|
7950
8999
|
const out = [];
|
|
7951
9000
|
let entries;
|
|
7952
9001
|
try {
|
|
7953
|
-
entries = await
|
|
9002
|
+
entries = await import_node_fs14.promises.readdir(current);
|
|
7954
9003
|
} catch (err) {
|
|
7955
9004
|
if (err.code === "ENOENT") return out;
|
|
7956
9005
|
throw err;
|
|
@@ -7961,7 +9010,7 @@ async function walkAndDigest(root, current) {
|
|
|
7961
9010
|
const rel = path3.relative(root, full);
|
|
7962
9011
|
let st;
|
|
7963
9012
|
try {
|
|
7964
|
-
st = await
|
|
9013
|
+
st = await import_node_fs14.promises.stat(full);
|
|
7965
9014
|
} catch {
|
|
7966
9015
|
continue;
|
|
7967
9016
|
}
|
|
@@ -7972,7 +9021,7 @@ async function walkAndDigest(root, current) {
|
|
|
7972
9021
|
continue;
|
|
7973
9022
|
}
|
|
7974
9023
|
if (!st.isFile()) continue;
|
|
7975
|
-
const buf = await
|
|
9024
|
+
const buf = await import_node_fs14.promises.readFile(full);
|
|
7976
9025
|
const sha2563 = (0, import_node_crypto4.createHash)("sha256").update(buf).digest("hex");
|
|
7977
9026
|
out.push({
|
|
7978
9027
|
path: rel,
|
|
@@ -9523,7 +10572,7 @@ function parsePrismerUri(uri) {
|
|
|
9523
10572
|
}
|
|
9524
10573
|
|
|
9525
10574
|
// src/daemon/asset/rpc.ts
|
|
9526
|
-
var
|
|
10575
|
+
var import_node_fs15 = require("fs");
|
|
9527
10576
|
var ASSET_PATH_PREFIX = "/local/asset/";
|
|
9528
10577
|
var MAX_READ_BYTES = 256 * 1024;
|
|
9529
10578
|
var MAX_SEARCH_BYTES = 1024 * 1024;
|
|
@@ -9698,16 +10747,16 @@ function isTextLike(row) {
|
|
|
9698
10747
|
);
|
|
9699
10748
|
}
|
|
9700
10749
|
function readFileRange(path9, offset, length) {
|
|
9701
|
-
const sizeBytes = (0,
|
|
10750
|
+
const sizeBytes = (0, import_node_fs15.statSync)(path9).size;
|
|
9702
10751
|
if (offset >= sizeBytes) return { buffer: Buffer.alloc(0), bytesRead: 0, sizeBytes };
|
|
9703
|
-
const fd = (0,
|
|
10752
|
+
const fd = (0, import_node_fs15.openSync)(path9, "r");
|
|
9704
10753
|
try {
|
|
9705
10754
|
const target = Math.min(length, sizeBytes - offset);
|
|
9706
10755
|
const buffer = Buffer.alloc(target);
|
|
9707
|
-
const bytesRead = (0,
|
|
10756
|
+
const bytesRead = (0, import_node_fs15.readSync)(fd, buffer, 0, target, offset);
|
|
9708
10757
|
return { buffer: buffer.subarray(0, bytesRead), bytesRead, sizeBytes };
|
|
9709
10758
|
} finally {
|
|
9710
|
-
(0,
|
|
10759
|
+
(0, import_node_fs15.closeSync)(fd);
|
|
9711
10760
|
}
|
|
9712
10761
|
}
|
|
9713
10762
|
function respond3(res, status, body) {
|
|
@@ -9732,8 +10781,8 @@ async function readJson3(req) {
|
|
|
9732
10781
|
}
|
|
9733
10782
|
|
|
9734
10783
|
// src/daemon/asset/origin/drop-folder.ts
|
|
9735
|
-
var
|
|
9736
|
-
var
|
|
10784
|
+
var import_node_fs16 = require("fs");
|
|
10785
|
+
var import_node_os6 = require("os");
|
|
9737
10786
|
var path6 = __toESM(require("path"), 1);
|
|
9738
10787
|
var DropFolderAdapter = class {
|
|
9739
10788
|
kind = "drop-folder";
|
|
@@ -9741,7 +10790,7 @@ var DropFolderAdapter = class {
|
|
|
9741
10790
|
rootDir;
|
|
9742
10791
|
constructor(opts = {}) {
|
|
9743
10792
|
this.workspaceDir = opts.workspaceDir;
|
|
9744
|
-
this.rootDir = opts.rootDir ?? path6.join(opts.homeDir ?? (0,
|
|
10793
|
+
this.rootDir = opts.rootDir ?? path6.join(opts.homeDir ?? (0, import_node_os6.homedir)(), ".prismer");
|
|
9745
10794
|
}
|
|
9746
10795
|
/**
|
|
9747
10796
|
* Long-lived async iterable wrapping `scanOnce` on a 1s polling interval.
|
|
@@ -9751,7 +10800,7 @@ var DropFolderAdapter = class {
|
|
|
9751
10800
|
while (true) {
|
|
9752
10801
|
const batch = await this.scanOnce(workspaceId);
|
|
9753
10802
|
for (const obs of batch) yield obs;
|
|
9754
|
-
await
|
|
10803
|
+
await sleep2(1e3);
|
|
9755
10804
|
}
|
|
9756
10805
|
}
|
|
9757
10806
|
/**
|
|
@@ -9765,7 +10814,7 @@ var DropFolderAdapter = class {
|
|
|
9765
10814
|
const drop = this.dropDir(workspaceId);
|
|
9766
10815
|
let entries = [];
|
|
9767
10816
|
try {
|
|
9768
|
-
entries = await
|
|
10817
|
+
entries = await import_node_fs16.promises.readdir(drop);
|
|
9769
10818
|
} catch (err) {
|
|
9770
10819
|
if (err.code === "ENOENT") return [];
|
|
9771
10820
|
throw err;
|
|
@@ -9774,12 +10823,12 @@ var DropFolderAdapter = class {
|
|
|
9774
10823
|
for (const name of entries) {
|
|
9775
10824
|
if (name.startsWith(".")) continue;
|
|
9776
10825
|
const fullPath = path6.join(drop, name);
|
|
9777
|
-
const stat = await
|
|
10826
|
+
const stat = await import_node_fs16.promises.lstat(fullPath).catch(() => null);
|
|
9778
10827
|
if (!stat) continue;
|
|
9779
10828
|
if (stat.isSymbolicLink()) continue;
|
|
9780
10829
|
if (stat.isDirectory()) {
|
|
9781
10830
|
for (const nested of await walk(fullPath)) {
|
|
9782
|
-
const nstat = await
|
|
10831
|
+
const nstat = await import_node_fs16.promises.lstat(nested).catch(() => null);
|
|
9783
10832
|
if (!nstat || !nstat.isFile()) continue;
|
|
9784
10833
|
out.push(makeObservation(workspaceId, nested, drop, nstat.size, Math.floor(nstat.mtimeMs)));
|
|
9785
10834
|
}
|
|
@@ -9804,7 +10853,7 @@ var DropFolderAdapter = class {
|
|
|
9804
10853
|
}
|
|
9805
10854
|
async fetch(obs) {
|
|
9806
10855
|
const detail = obs.detail;
|
|
9807
|
-
const bytes = await
|
|
10856
|
+
const bytes = await import_node_fs16.promises.readFile(detail.path);
|
|
9808
10857
|
return {
|
|
9809
10858
|
bytes,
|
|
9810
10859
|
mime: mimeFromExtension(detail.path),
|
|
@@ -9826,8 +10875,8 @@ var DropFolderAdapter = class {
|
|
|
9826
10875
|
const rel = typeof detail.watchDir === "string" ? path6.relative(detail.watchDir, detail.path) : path6.basename(detail.path);
|
|
9827
10876
|
const safeRel = rel && !rel.startsWith("..") && !path6.isAbsolute(rel) ? rel : path6.basename(detail.path);
|
|
9828
10877
|
const destPath = path6.join(destDir, safeRel);
|
|
9829
|
-
await
|
|
9830
|
-
await
|
|
10878
|
+
await import_node_fs16.promises.mkdir(path6.dirname(destPath), { recursive: true });
|
|
10879
|
+
await import_node_fs16.promises.rename(detail.path, destPath);
|
|
9831
10880
|
} catch (err) {
|
|
9832
10881
|
if (err.code === "ENOENT") return;
|
|
9833
10882
|
throw err;
|
|
@@ -9855,11 +10904,11 @@ function makeObservation(workspaceId, filePath, watchDir, size, mtime) {
|
|
|
9855
10904
|
}
|
|
9856
10905
|
async function walk(root) {
|
|
9857
10906
|
const out = [];
|
|
9858
|
-
const entries = await
|
|
10907
|
+
const entries = await import_node_fs16.promises.readdir(root).catch(() => []);
|
|
9859
10908
|
for (const name of entries) {
|
|
9860
10909
|
if (name.startsWith(".")) continue;
|
|
9861
10910
|
const full = path6.join(root, name);
|
|
9862
|
-
const stat = await
|
|
10911
|
+
const stat = await import_node_fs16.promises.lstat(full).catch(() => null);
|
|
9863
10912
|
if (!stat) continue;
|
|
9864
10913
|
if (stat.isSymbolicLink()) continue;
|
|
9865
10914
|
if (stat.isDirectory()) {
|
|
@@ -9900,7 +10949,7 @@ function mimeFromExtension(p) {
|
|
|
9900
10949
|
const ext = path6.extname(p).toLowerCase();
|
|
9901
10950
|
return MIME_BY_EXT[ext] ?? null;
|
|
9902
10951
|
}
|
|
9903
|
-
function
|
|
10952
|
+
function sleep2(ms) {
|
|
9904
10953
|
return new Promise((r) => setTimeout(r, ms));
|
|
9905
10954
|
}
|
|
9906
10955
|
|
|
@@ -10455,7 +11504,7 @@ function stringValue(obj, key) {
|
|
|
10455
11504
|
}
|
|
10456
11505
|
|
|
10457
11506
|
// src/daemon/outbox-watcher.ts
|
|
10458
|
-
var
|
|
11507
|
+
var import_node_fs17 = require("fs");
|
|
10459
11508
|
var path8 = __toESM(require("path"), 1);
|
|
10460
11509
|
|
|
10461
11510
|
// src/daemon/asset/agent-output-policy.ts
|
|
@@ -10611,9 +11660,120 @@ function extensionOf(filename) {
|
|
|
10611
11660
|
return idx >= 0 ? name.slice(idx) : "";
|
|
10612
11661
|
}
|
|
10613
11662
|
|
|
11663
|
+
// src/daemon/asset/magic-bytes.ts
|
|
11664
|
+
var PDF_SIG = Buffer.from("%PDF-", "ascii");
|
|
11665
|
+
var PNG_SIG = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
11666
|
+
var JPEG_SIG = Buffer.from([255, 216, 255]);
|
|
11667
|
+
var GIF87 = Buffer.from("GIF87a", "ascii");
|
|
11668
|
+
var GIF89 = Buffer.from("GIF89a", "ascii");
|
|
11669
|
+
var ZIP_LOCAL = Buffer.from([80, 75, 3, 4]);
|
|
11670
|
+
var ZIP_EMPTY = Buffer.from([80, 75, 5, 6]);
|
|
11671
|
+
var ZIP_SPANNED = Buffer.from([80, 75, 7, 8]);
|
|
11672
|
+
function detectMagicBytes(bytes) {
|
|
11673
|
+
const head = bytes.subarray(0, Math.min(bytes.length, 4096));
|
|
11674
|
+
if (head.length === 0) return { detected: null, mime: null };
|
|
11675
|
+
if (head.length >= PDF_SIG.length && head.subarray(0, PDF_SIG.length).equals(PDF_SIG)) {
|
|
11676
|
+
return { detected: "pdf", mime: "application/pdf" };
|
|
11677
|
+
}
|
|
11678
|
+
if (head.length >= PNG_SIG.length && head.subarray(0, PNG_SIG.length).equals(PNG_SIG)) {
|
|
11679
|
+
return { detected: "png", mime: "image/png" };
|
|
11680
|
+
}
|
|
11681
|
+
if (head.length >= JPEG_SIG.length && head.subarray(0, JPEG_SIG.length).equals(JPEG_SIG)) {
|
|
11682
|
+
return { detected: "jpeg", mime: "image/jpeg" };
|
|
11683
|
+
}
|
|
11684
|
+
if (head.length >= GIF87.length && head.subarray(0, GIF87.length).equals(GIF87) || head.length >= GIF89.length && head.subarray(0, GIF89.length).equals(GIF89)) {
|
|
11685
|
+
return { detected: "gif", mime: "image/gif" };
|
|
11686
|
+
}
|
|
11687
|
+
if (head.length >= 4 && (head.subarray(0, 4).equals(ZIP_LOCAL) || head.subarray(0, 4).equals(ZIP_EMPTY) || head.subarray(0, 4).equals(ZIP_SPANNED))) {
|
|
11688
|
+
return { detected: "zip", mime: "application/zip" };
|
|
11689
|
+
}
|
|
11690
|
+
const text = head.toString("utf8");
|
|
11691
|
+
const trimmed = text.trimStart();
|
|
11692
|
+
if (/^<\?xml[^>]*>\s*<svg[\s>]/i.test(trimmed) || /^<svg[\s>]/i.test(trimmed)) {
|
|
11693
|
+
return { detected: "svg", mime: "image/svg+xml" };
|
|
11694
|
+
}
|
|
11695
|
+
if (/^<(!doctype html|html[\s>])/i.test(trimmed)) {
|
|
11696
|
+
return { detected: "html", mime: "text/html" };
|
|
11697
|
+
}
|
|
11698
|
+
if (/^[{[]/.test(trimmed)) {
|
|
11699
|
+
return { detected: "json", mime: "application/json" };
|
|
11700
|
+
}
|
|
11701
|
+
if (looksLikePrintableText(head)) {
|
|
11702
|
+
return { detected: "markdown-or-text", mime: "text/plain" };
|
|
11703
|
+
}
|
|
11704
|
+
return { detected: null, mime: null };
|
|
11705
|
+
}
|
|
11706
|
+
var ZIP_DERIVED_MIMES = /* @__PURE__ */ new Set([
|
|
11707
|
+
"application/zip",
|
|
11708
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
11709
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
11710
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
11711
|
+
"application/vnd.oasis.opendocument.text",
|
|
11712
|
+
"application/vnd.oasis.opendocument.spreadsheet",
|
|
11713
|
+
"application/vnd.oasis.opendocument.presentation",
|
|
11714
|
+
"application/epub+zip",
|
|
11715
|
+
"application/java-archive"
|
|
11716
|
+
]);
|
|
11717
|
+
function mimeMismatchReason(detected, declared) {
|
|
11718
|
+
if (detected.detected === null) return null;
|
|
11719
|
+
const dec = (declared ?? "").toLowerCase().split(";")[0]?.trim() ?? "";
|
|
11720
|
+
if (!dec || dec === "application/octet-stream") return null;
|
|
11721
|
+
switch (detected.detected) {
|
|
11722
|
+
case "pdf":
|
|
11723
|
+
return dec === "application/pdf" ? null : `declared ${dec} but content is application/pdf`;
|
|
11724
|
+
case "png":
|
|
11725
|
+
return dec === "image/png" ? null : `declared ${dec} but content is image/png`;
|
|
11726
|
+
case "jpeg":
|
|
11727
|
+
return dec === "image/jpeg" || dec === "image/jpg" ? null : `declared ${dec} but content is image/jpeg`;
|
|
11728
|
+
case "gif":
|
|
11729
|
+
return dec === "image/gif" ? null : `declared ${dec} but content is image/gif`;
|
|
11730
|
+
case "zip":
|
|
11731
|
+
if (ZIP_DERIVED_MIMES.has(dec)) return null;
|
|
11732
|
+
return `declared ${dec} but content is a ZIP-container (application/zip family)`;
|
|
11733
|
+
case "svg":
|
|
11734
|
+
if (dec === "image/svg+xml" || dec.startsWith("text/")) return null;
|
|
11735
|
+
return `declared ${dec} but content is image/svg+xml`;
|
|
11736
|
+
case "html":
|
|
11737
|
+
if (dec === "text/html" || dec === "application/xhtml+xml" || dec.startsWith("text/")) return null;
|
|
11738
|
+
if (dec === "application/pdf" || dec.startsWith("image/") || dec === "application/zip") {
|
|
11739
|
+
return `declared ${dec} but content is text/html`;
|
|
11740
|
+
}
|
|
11741
|
+
return null;
|
|
11742
|
+
case "json":
|
|
11743
|
+
if (dec === "application/json" || dec.startsWith("text/")) return null;
|
|
11744
|
+
if (dec === "application/pdf" || dec.startsWith("image/") || dec === "application/zip") {
|
|
11745
|
+
return `declared ${dec} but content is application/json`;
|
|
11746
|
+
}
|
|
11747
|
+
return null;
|
|
11748
|
+
case "markdown-or-text":
|
|
11749
|
+
if (dec === "application/pdf" || dec.startsWith("image/") || dec === "application/zip") {
|
|
11750
|
+
return `declared ${dec} but content is text (not a real ${dec} payload)`;
|
|
11751
|
+
}
|
|
11752
|
+
return null;
|
|
11753
|
+
default:
|
|
11754
|
+
return null;
|
|
11755
|
+
}
|
|
11756
|
+
}
|
|
11757
|
+
function looksLikePrintableText(buf) {
|
|
11758
|
+
if (buf.length === 0) return false;
|
|
11759
|
+
let printable = 0;
|
|
11760
|
+
let total = 0;
|
|
11761
|
+
for (let i = 0; i < buf.length; i++) {
|
|
11762
|
+
const b = buf[i];
|
|
11763
|
+
if (b === 0) return false;
|
|
11764
|
+
total++;
|
|
11765
|
+
if (b >= 32 && b <= 126 || b === 9 || b === 10 || b === 13 || // utf-8 lead bytes
|
|
11766
|
+
b >= 128) {
|
|
11767
|
+
printable++;
|
|
11768
|
+
}
|
|
11769
|
+
}
|
|
11770
|
+
return total > 0 && printable / total >= 0.95;
|
|
11771
|
+
}
|
|
11772
|
+
|
|
10614
11773
|
// src/daemon/outbox-watcher.ts
|
|
10615
11774
|
var DEFAULT_INTERVAL_MS = 2e3;
|
|
10616
11775
|
var RESERVED_SUBDIR = "_uploaded";
|
|
11776
|
+
var REJECTED_SUBDIR = "_rejected";
|
|
10617
11777
|
var OutboxWatcher = class {
|
|
10618
11778
|
constructor(opts) {
|
|
10619
11779
|
this.opts = opts;
|
|
@@ -10778,7 +11938,7 @@ var OutboxWatcher = class {
|
|
|
10778
11938
|
async scanDir(dir, kind, task) {
|
|
10779
11939
|
let entries = [];
|
|
10780
11940
|
try {
|
|
10781
|
-
entries = await
|
|
11941
|
+
entries = await import_node_fs17.promises.readdir(dir);
|
|
10782
11942
|
} catch (err) {
|
|
10783
11943
|
const code = err.code;
|
|
10784
11944
|
if (code === "ENOENT") return;
|
|
@@ -10788,10 +11948,11 @@ var OutboxWatcher = class {
|
|
|
10788
11948
|
for (const name of entries) {
|
|
10789
11949
|
if (name.startsWith(".")) continue;
|
|
10790
11950
|
if (name === RESERVED_SUBDIR) continue;
|
|
11951
|
+
if (name === REJECTED_SUBDIR) continue;
|
|
10791
11952
|
const full = path8.join(dir, name);
|
|
10792
11953
|
let st;
|
|
10793
11954
|
try {
|
|
10794
|
-
st = await
|
|
11955
|
+
st = await import_node_fs17.promises.stat(full);
|
|
10795
11956
|
} catch {
|
|
10796
11957
|
continue;
|
|
10797
11958
|
}
|
|
@@ -10802,10 +11963,77 @@ var OutboxWatcher = class {
|
|
|
10802
11963
|
await this.upload(full, st, kind, task);
|
|
10803
11964
|
this.uploaded.add(key);
|
|
10804
11965
|
} catch (err) {
|
|
10805
|
-
|
|
11966
|
+
const msg = err.message;
|
|
11967
|
+
this.log("warn", `upload ${full} failed: ${msg}`);
|
|
11968
|
+
if (/\bMIME_MISMATCH\b/i.test(msg) || /\bmime mismatch\b/i.test(msg)) {
|
|
11969
|
+
await this.quarantine(dir, full).catch(
|
|
11970
|
+
(moveErr) => this.log("warn", `quarantine ${full} failed: ${moveErr.message}`)
|
|
11971
|
+
);
|
|
11972
|
+
this.uploaded.add(key);
|
|
11973
|
+
await this.reportMimeMismatchToCloud(task, full, msg).catch(
|
|
11974
|
+
(reportErr) => this.log("warn", `report MIME_MISMATCH failed: ${reportErr.message}`)
|
|
11975
|
+
);
|
|
11976
|
+
}
|
|
10806
11977
|
}
|
|
10807
11978
|
}
|
|
10808
11979
|
}
|
|
11980
|
+
/**
|
|
11981
|
+
* F2B — report an OUTBOX_MIME_MISMATCH task event to cloud so the task log
|
|
11982
|
+
* gains an auditable record of the rejection. Without this, the failure is
|
|
11983
|
+
* silent (file quarantined locally, agent unaware) and the next dispatch
|
|
11984
|
+
* loops on the same bad strategy.
|
|
11985
|
+
*
|
|
11986
|
+
* Best-effort: every catch site swallows errors. We never want this
|
|
11987
|
+
* upstream call to block local quarantine or trigger a retry loop, because
|
|
11988
|
+
* the reject decision is already final by the time we get here.
|
|
11989
|
+
*
|
|
11990
|
+
* Endpoint contract: POST /api/im/tasks/:id/event
|
|
11991
|
+
* { code: 'OUTBOX_MIME_MISMATCH', payload: { file, daemonError }, message }
|
|
11992
|
+
* Only callable when `task?.taskId` is known — pre-dispatch outbox files
|
|
11993
|
+
* (no active task) are quarantined silently as before.
|
|
11994
|
+
*/
|
|
11995
|
+
async reportMimeMismatchToCloud(task, filePath, daemonErrorMessage) {
|
|
11996
|
+
if (!task?.taskId) return;
|
|
11997
|
+
const fileName = path8.basename(filePath);
|
|
11998
|
+
const reasonMatch = daemonErrorMessage.match(/MIME_MISMATCH[^:]*:\s*(.+)$/i);
|
|
11999
|
+
const reason = reasonMatch?.[1] ? reasonMatch[1].trim() : daemonErrorMessage;
|
|
12000
|
+
const body = {
|
|
12001
|
+
code: "OUTBOX_MIME_MISMATCH",
|
|
12002
|
+
payload: {
|
|
12003
|
+
file: fileName,
|
|
12004
|
+
reason,
|
|
12005
|
+
daemonError: daemonErrorMessage
|
|
12006
|
+
},
|
|
12007
|
+
message: `Your output file ${fileName} was rejected because its bytes do not match the claimed format. Use a real library (e.g. reportlab for PDF, openpyxl for XLSX) instead of renaming an extension.`
|
|
12008
|
+
};
|
|
12009
|
+
const res = await this.opts.cloud.request("POST", `/api/im/tasks/${encodeURIComponent(task.taskId)}/event`, {
|
|
12010
|
+
body
|
|
12011
|
+
});
|
|
12012
|
+
if (!res.ok) {
|
|
12013
|
+
throw new Error(`cloud rejected event: ${res.status} ${res.error?.code ?? ""} ${res.error?.message ?? ""}`);
|
|
12014
|
+
}
|
|
12015
|
+
}
|
|
12016
|
+
/**
|
|
12017
|
+
* Move a file under `<dir>/_rejected/` so it stops being a scan candidate
|
|
12018
|
+
* (the loop skips the reserved subdir) and operators can find the
|
|
12019
|
+
* offending artifact. Filename collision is rare per task; on collision
|
|
12020
|
+
* we append a millisecond suffix to keep the original observable.
|
|
12021
|
+
*/
|
|
12022
|
+
async quarantine(dir, full) {
|
|
12023
|
+
const rejectedDir = path8.join(dir, REJECTED_SUBDIR);
|
|
12024
|
+
await import_node_fs17.promises.mkdir(rejectedDir, { recursive: true });
|
|
12025
|
+
const base = path8.basename(full);
|
|
12026
|
+
let dest = path8.join(rejectedDir, base);
|
|
12027
|
+
try {
|
|
12028
|
+
await import_node_fs17.promises.access(dest);
|
|
12029
|
+
const ext = path8.extname(base);
|
|
12030
|
+
const stem = base.slice(0, base.length - ext.length);
|
|
12031
|
+
dest = path8.join(rejectedDir, `${stem}.${Date.now()}${ext}`);
|
|
12032
|
+
} catch {
|
|
12033
|
+
}
|
|
12034
|
+
await import_node_fs17.promises.rename(full, dest);
|
|
12035
|
+
this.log("warn", `quarantined ${full} \u2192 ${dest}`);
|
|
12036
|
+
}
|
|
10809
12037
|
async upload(filePath, st, kind, task) {
|
|
10810
12038
|
const wsId = this.opts.workspaceId();
|
|
10811
12039
|
if (!wsId) {
|
|
@@ -10816,13 +12044,18 @@ var OutboxWatcher = class {
|
|
|
10816
12044
|
this.log("warn", `skip ${filePath}: no active taskId (no dispatch/handoff received yet)`);
|
|
10817
12045
|
return;
|
|
10818
12046
|
}
|
|
10819
|
-
const bytes = await
|
|
12047
|
+
const bytes = await import_node_fs17.promises.readFile(filePath);
|
|
10820
12048
|
const fileName = path8.basename(filePath);
|
|
10821
12049
|
const inferredMime = inferAgentOutputMime(fileName);
|
|
10822
12050
|
const policy = validateAgentOutputAsset({ filename: fileName, mime: inferredMime, sizeBytes: bytes.length });
|
|
10823
12051
|
if (!policy.ok) {
|
|
10824
12052
|
throw new Error(`agent output rejected: ${policy.reason}`);
|
|
10825
12053
|
}
|
|
12054
|
+
const detection = detectMagicBytes(bytes);
|
|
12055
|
+
const reason = mimeMismatchReason(detection, policy.mime);
|
|
12056
|
+
if (reason) {
|
|
12057
|
+
throw new Error(`MIME_MISMATCH for ${fileName}: ${reason}`);
|
|
12058
|
+
}
|
|
10826
12059
|
const metadata = {
|
|
10827
12060
|
taskId: task.taskId,
|
|
10828
12061
|
...task.adapter ? { adapter: task.adapter } : {}
|
|
@@ -10904,7 +12137,7 @@ var ServicePool = class {
|
|
|
10904
12137
|
|
|
10905
12138
|
// src/daemon/shell-executor.ts
|
|
10906
12139
|
var import_node_child_process6 = require("child_process");
|
|
10907
|
-
var
|
|
12140
|
+
var import_node_fs18 = require("fs");
|
|
10908
12141
|
var import_node_path13 = require("path");
|
|
10909
12142
|
var DEFAULT_OUTPUT_LIMIT = 256 * 1024;
|
|
10910
12143
|
var DEFAULT_TIMEOUT = 6e4;
|
|
@@ -10941,7 +12174,7 @@ async function executeShellDispatch(payload, deps) {
|
|
|
10941
12174
|
const command = readCommand(payload, execution);
|
|
10942
12175
|
if (!command.trim()) return fail2(payload.taskId, "shell_command_required", "Shell command is required");
|
|
10943
12176
|
const cwd = resolveCwd(execution.cwd, deps.config.defaultCwd);
|
|
10944
|
-
if (!(0,
|
|
12177
|
+
if (!(0, import_node_fs18.existsSync)(cwd)) return fail2(payload.taskId, "shell_cwd_missing", `cwd does not exist: ${cwd}`);
|
|
10945
12178
|
const timeoutMs = Math.min(
|
|
10946
12179
|
typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? payload.timeoutMs : deps.config.maxTimeoutMs,
|
|
10947
12180
|
deps.config.maxTimeoutMs
|
|
@@ -11176,6 +12409,350 @@ var WsClient = class extends import_node_events2.EventEmitter {
|
|
|
11176
12409
|
}
|
|
11177
12410
|
};
|
|
11178
12411
|
|
|
12412
|
+
// src/daemon/pending-reply-cache.ts
|
|
12413
|
+
function fromRaw(r) {
|
|
12414
|
+
return {
|
|
12415
|
+
idempotencyKey: r.idempotency_key,
|
|
12416
|
+
taskId: r.task_id,
|
|
12417
|
+
replyId: r.reply_id,
|
|
12418
|
+
status: r.status,
|
|
12419
|
+
payloadJson: r.payload_json,
|
|
12420
|
+
createdAt: r.created_at,
|
|
12421
|
+
preparedAt: r.prepared_at,
|
|
12422
|
+
lastAttemptAt: r.last_attempt_at,
|
|
12423
|
+
attemptCount: r.attempt_count,
|
|
12424
|
+
lastError: r.last_error
|
|
12425
|
+
};
|
|
12426
|
+
}
|
|
12427
|
+
function createPendingReplyCache(db) {
|
|
12428
|
+
const insert = db.prepare(
|
|
12429
|
+
`INSERT OR REPLACE INTO pending_dispatch_replies
|
|
12430
|
+
(idempotency_key, task_id, reply_id, status, payload_json,
|
|
12431
|
+
prepared_at, created_at, last_attempt_at, attempt_count, last_error)
|
|
12432
|
+
VALUES (?, ?, NULL, 'pending', ?, NULL, ?, NULL, 0, NULL)`
|
|
12433
|
+
);
|
|
12434
|
+
const markPreparedStmt = db.prepare(
|
|
12435
|
+
`UPDATE pending_dispatch_replies
|
|
12436
|
+
SET reply_id = ?, status = 'prepared', prepared_at = ?,
|
|
12437
|
+
last_attempt_at = ?, attempt_count = attempt_count + 1,
|
|
12438
|
+
last_error = NULL
|
|
12439
|
+
WHERE idempotency_key = ?`
|
|
12440
|
+
);
|
|
12441
|
+
const markAttemptStmt = db.prepare(
|
|
12442
|
+
`UPDATE pending_dispatch_replies
|
|
12443
|
+
SET last_attempt_at = ?, attempt_count = attempt_count + 1,
|
|
12444
|
+
last_error = ?
|
|
12445
|
+
WHERE idempotency_key = ?`
|
|
12446
|
+
);
|
|
12447
|
+
const clearStmt = db.prepare(
|
|
12448
|
+
`DELETE FROM pending_dispatch_replies WHERE idempotency_key = ?`
|
|
12449
|
+
);
|
|
12450
|
+
const listRecoveryStmt = db.prepare(
|
|
12451
|
+
`SELECT * FROM pending_dispatch_replies
|
|
12452
|
+
WHERE status IN ('pending', 'prepared')
|
|
12453
|
+
ORDER BY created_at ASC`
|
|
12454
|
+
);
|
|
12455
|
+
const countStmt = db.prepare(
|
|
12456
|
+
`SELECT status, COUNT(*) as cnt FROM pending_dispatch_replies
|
|
12457
|
+
WHERE status IN ('pending','prepared') GROUP BY status`
|
|
12458
|
+
);
|
|
12459
|
+
const findStmt = db.prepare(
|
|
12460
|
+
`SELECT * FROM pending_dispatch_replies WHERE idempotency_key = ?`
|
|
12461
|
+
);
|
|
12462
|
+
return {
|
|
12463
|
+
savePending(idempotencyKey, taskId, payloadJson, now = Date.now()) {
|
|
12464
|
+
insert.run(idempotencyKey, taskId, payloadJson, now);
|
|
12465
|
+
},
|
|
12466
|
+
markPrepared(idempotencyKey, replyId, now = Date.now()) {
|
|
12467
|
+
markPreparedStmt.run(replyId, now, now, idempotencyKey);
|
|
12468
|
+
},
|
|
12469
|
+
markAttempt(idempotencyKey, now = Date.now(), error) {
|
|
12470
|
+
markAttemptStmt.run(now, error ?? null, idempotencyKey);
|
|
12471
|
+
},
|
|
12472
|
+
clearPending(idempotencyKey) {
|
|
12473
|
+
clearStmt.run(idempotencyKey);
|
|
12474
|
+
},
|
|
12475
|
+
listForRecovery() {
|
|
12476
|
+
const rows = listRecoveryStmt.all();
|
|
12477
|
+
return rows.map(fromRaw);
|
|
12478
|
+
},
|
|
12479
|
+
countByStatus() {
|
|
12480
|
+
const rows = countStmt.all();
|
|
12481
|
+
const result = { pending: 0, prepared: 0 };
|
|
12482
|
+
for (const r of rows) {
|
|
12483
|
+
if (r.status === "pending") result.pending = r.cnt;
|
|
12484
|
+
else if (r.status === "prepared") result.prepared = r.cnt;
|
|
12485
|
+
}
|
|
12486
|
+
return result;
|
|
12487
|
+
},
|
|
12488
|
+
findByIdempotencyKey(idempotencyKey) {
|
|
12489
|
+
const row = findStmt.get(idempotencyKey);
|
|
12490
|
+
return row ? fromRaw(row) : null;
|
|
12491
|
+
}
|
|
12492
|
+
};
|
|
12493
|
+
}
|
|
12494
|
+
function generateIdempotencyKey() {
|
|
12495
|
+
const c = globalThis.crypto;
|
|
12496
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
12497
|
+
const bytes = new Uint8Array(16);
|
|
12498
|
+
const cryptoNode = globalThis.crypto;
|
|
12499
|
+
if (cryptoNode?.getRandomValues) {
|
|
12500
|
+
cryptoNode.getRandomValues(bytes);
|
|
12501
|
+
} else {
|
|
12502
|
+
for (let i = 0; i < 16; i += 1) bytes[i] = Math.floor(Math.random() * 256);
|
|
12503
|
+
}
|
|
12504
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
12505
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
12506
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
12507
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
12508
|
+
}
|
|
12509
|
+
|
|
12510
|
+
// src/daemon/dispatch-reply-transport.ts
|
|
12511
|
+
var DispatchReplyAbortedError = class extends Error {
|
|
12512
|
+
constructor(replyId) {
|
|
12513
|
+
super(`dispatch reply ${replyId} aborted by server reaper (likely >1h since prepare)`);
|
|
12514
|
+
this.replyId = replyId;
|
|
12515
|
+
this.name = "DispatchReplyAbortedError";
|
|
12516
|
+
}
|
|
12517
|
+
replyId;
|
|
12518
|
+
code = "DISPATCH_REPLY_INVALID_STATE";
|
|
12519
|
+
};
|
|
12520
|
+
function defaultLog2(line) {
|
|
12521
|
+
process.stderr.write(`${line}
|
|
12522
|
+
`);
|
|
12523
|
+
}
|
|
12524
|
+
async function sendDispatchReplyTwoPhase(args) {
|
|
12525
|
+
const { taskId, payload, cloud, cache } = args;
|
|
12526
|
+
const log8 = args.log ?? defaultLog2;
|
|
12527
|
+
const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
|
|
12528
|
+
const payloadJson = JSON.stringify({ ...payload, _taskId: taskId, _idempotencyKey: idempotencyKey });
|
|
12529
|
+
cache.savePending(idempotencyKey, taskId, payloadJson);
|
|
12530
|
+
const prepareRes = await cloud.request("POST", `/api/im/dispatch/${encodeURIComponent(taskId)}/prepare`, {
|
|
12531
|
+
body: { ...payload, idempotencyKey },
|
|
12532
|
+
timeoutMs: 3e4
|
|
12533
|
+
});
|
|
12534
|
+
if (!prepareRes.ok || !prepareRes.data) {
|
|
12535
|
+
const errCode = prepareRes.error?.code ?? "unknown";
|
|
12536
|
+
const errMsg = prepareRes.error?.message ?? `prepare failed (status=${prepareRes.status})`;
|
|
12537
|
+
cache.markAttempt(idempotencyKey, Date.now(), `prepare: ${errCode}: ${errMsg}`);
|
|
12538
|
+
throw new Error(`dispatch prepare failed: ${errCode}: ${errMsg}`);
|
|
12539
|
+
}
|
|
12540
|
+
const envelope2 = prepareRes.data;
|
|
12541
|
+
const inner = "data" in envelope2 && envelope2.data ? envelope2.data : envelope2;
|
|
12542
|
+
if (!inner || typeof inner.replyId !== "string") {
|
|
12543
|
+
cache.markAttempt(idempotencyKey, Date.now(), "prepare: missing replyId in response");
|
|
12544
|
+
throw new Error("dispatch prepare returned no replyId");
|
|
12545
|
+
}
|
|
12546
|
+
if ("ok" in envelope2 && envelope2.ok === false) {
|
|
12547
|
+
const errCode = envelope2.error?.code ?? "prepare_failed";
|
|
12548
|
+
const errMsg = envelope2.error?.message ?? "prepare ok=false";
|
|
12549
|
+
cache.markAttempt(idempotencyKey, Date.now(), `prepare: ${errCode}: ${errMsg}`);
|
|
12550
|
+
throw new Error(`dispatch prepare failed: ${errCode}: ${errMsg}`);
|
|
12551
|
+
}
|
|
12552
|
+
cache.markPrepared(idempotencyKey, inner.replyId);
|
|
12553
|
+
log8(
|
|
12554
|
+
`[daemon] dispatch.prepare task=${taskId} replyId=${inner.replyId} status=${inner.status} idem=${idempotencyKey}`
|
|
12555
|
+
);
|
|
12556
|
+
if (inner.status === "committed") {
|
|
12557
|
+
cache.clearPending(idempotencyKey);
|
|
12558
|
+
return {
|
|
12559
|
+
taskId,
|
|
12560
|
+
replyId: inner.replyId,
|
|
12561
|
+
status: "committed",
|
|
12562
|
+
alreadyCommitted: true
|
|
12563
|
+
};
|
|
12564
|
+
}
|
|
12565
|
+
try {
|
|
12566
|
+
const result = await commitDispatchReply({ taskId, replyId: inner.replyId, cloud, log: log8 });
|
|
12567
|
+
cache.clearPending(idempotencyKey);
|
|
12568
|
+
return { taskId, replyId: inner.replyId, ...result };
|
|
12569
|
+
} catch (err) {
|
|
12570
|
+
if (err instanceof DispatchReplyAbortedError) {
|
|
12571
|
+
cache.clearPending(idempotencyKey);
|
|
12572
|
+
log8(`[daemon] dispatch.commit aborted task=${taskId} replyId=${inner.replyId} \u2014 dispatch lost`);
|
|
12573
|
+
return {
|
|
12574
|
+
taskId,
|
|
12575
|
+
replyId: inner.replyId,
|
|
12576
|
+
status: "aborted",
|
|
12577
|
+
alreadyCommitted: false
|
|
12578
|
+
};
|
|
12579
|
+
}
|
|
12580
|
+
cache.markAttempt(idempotencyKey, Date.now(), `commit: ${err.message}`);
|
|
12581
|
+
throw err;
|
|
12582
|
+
}
|
|
12583
|
+
}
|
|
12584
|
+
async function commitDispatchReply(args) {
|
|
12585
|
+
const log8 = args.log ?? defaultLog2;
|
|
12586
|
+
const res = await args.cloud.request("POST", `/api/im/dispatch/${encodeURIComponent(args.taskId)}/commit`, {
|
|
12587
|
+
body: { replyId: args.replyId },
|
|
12588
|
+
timeoutMs: 3e4
|
|
12589
|
+
});
|
|
12590
|
+
if (!res.ok || !res.data) {
|
|
12591
|
+
const code = res.error?.code ?? "unknown";
|
|
12592
|
+
const msg = res.error?.message ?? `commit failed (status=${res.status})`;
|
|
12593
|
+
if (code === "DISPATCH_REPLY_INVALID_STATE") {
|
|
12594
|
+
throw new DispatchReplyAbortedError(args.replyId);
|
|
12595
|
+
}
|
|
12596
|
+
throw new Error(`dispatch commit failed: ${code}: ${msg}`);
|
|
12597
|
+
}
|
|
12598
|
+
const env = res.data;
|
|
12599
|
+
const inner = "data" in env && env.data ? env.data : env;
|
|
12600
|
+
if ("ok" in env && env.ok === false) {
|
|
12601
|
+
const code = env.error?.code ?? "commit_failed";
|
|
12602
|
+
if (code === "DISPATCH_REPLY_INVALID_STATE") {
|
|
12603
|
+
throw new DispatchReplyAbortedError(args.replyId);
|
|
12604
|
+
}
|
|
12605
|
+
throw new Error(`dispatch commit failed: ${code}: ${env.error?.message ?? "commit ok=false"}`);
|
|
12606
|
+
}
|
|
12607
|
+
log8(
|
|
12608
|
+
`[daemon] dispatch.commit task=${args.taskId} replyId=${args.replyId} alreadyCommitted=${inner.alreadyCommitted ?? false}`
|
|
12609
|
+
);
|
|
12610
|
+
return {
|
|
12611
|
+
status: "committed",
|
|
12612
|
+
alreadyCommitted: Boolean(inner.alreadyCommitted),
|
|
12613
|
+
...inner.messageId ? { messageId: inner.messageId } : {}
|
|
12614
|
+
};
|
|
12615
|
+
}
|
|
12616
|
+
async function recoverPendingReplies(deps) {
|
|
12617
|
+
const log8 = deps.log ?? defaultLog2;
|
|
12618
|
+
const rows = deps.cache.listForRecovery();
|
|
12619
|
+
if (rows.length === 0) {
|
|
12620
|
+
return { attempted: 0, committed: 0, aborted: 0, failed: 0 };
|
|
12621
|
+
}
|
|
12622
|
+
log8(`[daemon] dispatch.recovery: ${rows.length} pending row(s) to replay`);
|
|
12623
|
+
let committed = 0;
|
|
12624
|
+
let aborted = 0;
|
|
12625
|
+
let failed = 0;
|
|
12626
|
+
for (const row of rows) {
|
|
12627
|
+
try {
|
|
12628
|
+
const result = await replayRow(row, deps);
|
|
12629
|
+
if (result.status === "committed") committed += 1;
|
|
12630
|
+
else if (result.status === "aborted") aborted += 1;
|
|
12631
|
+
} catch (err) {
|
|
12632
|
+
failed += 1;
|
|
12633
|
+
log8(
|
|
12634
|
+
`[daemon] dispatch.recovery FAIL idem=${row.idempotencyKey} task=${row.taskId}: ${err.message}`
|
|
12635
|
+
);
|
|
12636
|
+
}
|
|
12637
|
+
}
|
|
12638
|
+
log8(
|
|
12639
|
+
`[daemon] dispatch.recovery done attempted=${rows.length} committed=${committed} aborted=${aborted} failed=${failed}`
|
|
12640
|
+
);
|
|
12641
|
+
return { attempted: rows.length, committed, aborted, failed };
|
|
12642
|
+
}
|
|
12643
|
+
async function replayRow(row, deps) {
|
|
12644
|
+
const log8 = deps.log ?? defaultLog2;
|
|
12645
|
+
if (row.status === "prepared" && row.replyId) {
|
|
12646
|
+
try {
|
|
12647
|
+
const result2 = await commitDispatchReply({
|
|
12648
|
+
taskId: row.taskId,
|
|
12649
|
+
replyId: row.replyId,
|
|
12650
|
+
cloud: deps.cloud,
|
|
12651
|
+
log: log8
|
|
12652
|
+
});
|
|
12653
|
+
deps.cache.clearPending(row.idempotencyKey);
|
|
12654
|
+
return { status: result2.status };
|
|
12655
|
+
} catch (err) {
|
|
12656
|
+
if (err instanceof DispatchReplyAbortedError) {
|
|
12657
|
+
deps.cache.clearPending(row.idempotencyKey);
|
|
12658
|
+
return { status: "aborted" };
|
|
12659
|
+
}
|
|
12660
|
+
throw err;
|
|
12661
|
+
}
|
|
12662
|
+
}
|
|
12663
|
+
let parsed;
|
|
12664
|
+
try {
|
|
12665
|
+
parsed = JSON.parse(row.payloadJson);
|
|
12666
|
+
} catch (err) {
|
|
12667
|
+
deps.cache.clearPending(row.idempotencyKey);
|
|
12668
|
+
throw new Error(`pending row ${row.idempotencyKey} has unparseable payload: ${err.message}`);
|
|
12669
|
+
}
|
|
12670
|
+
const { _taskId, _idempotencyKey, ...payload } = parsed;
|
|
12671
|
+
const result = await sendDispatchReplyTwoPhase({
|
|
12672
|
+
taskId: row.taskId,
|
|
12673
|
+
payload,
|
|
12674
|
+
cloud: deps.cloud,
|
|
12675
|
+
cache: deps.cache,
|
|
12676
|
+
idempotencyKey: row.idempotencyKey,
|
|
12677
|
+
log: deps.log
|
|
12678
|
+
});
|
|
12679
|
+
return { status: result.status };
|
|
12680
|
+
}
|
|
12681
|
+
|
|
12682
|
+
// src/daemon/transport-probe.ts
|
|
12683
|
+
var import_node_os7 = require("os");
|
|
12684
|
+
function isPrivateAddress(addr) {
|
|
12685
|
+
if (!addr) return true;
|
|
12686
|
+
const h = String(addr).trim().toLowerCase();
|
|
12687
|
+
if (!h) return true;
|
|
12688
|
+
if (h === "localhost" || h === "::1") return true;
|
|
12689
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
|
|
12690
|
+
if (m) {
|
|
12691
|
+
const a = Number(m[1]);
|
|
12692
|
+
const b = Number(m[2]);
|
|
12693
|
+
if (a === 10) return true;
|
|
12694
|
+
if (a === 127) return true;
|
|
12695
|
+
if (a === 169 && b === 254) return true;
|
|
12696
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
12697
|
+
if (a === 192 && b === 168) return true;
|
|
12698
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
12699
|
+
return false;
|
|
12700
|
+
}
|
|
12701
|
+
if (h.endsWith(".local")) return true;
|
|
12702
|
+
if (h.endsWith(".localhost")) return true;
|
|
12703
|
+
return false;
|
|
12704
|
+
}
|
|
12705
|
+
function buildTransportProbe(gatewayUrl) {
|
|
12706
|
+
if (!gatewayUrl) {
|
|
12707
|
+
return { transport: "ws", gatewayUrl: null, gatewayIsPrivate: true };
|
|
12708
|
+
}
|
|
12709
|
+
let host = null;
|
|
12710
|
+
try {
|
|
12711
|
+
host = new URL(gatewayUrl).hostname;
|
|
12712
|
+
} catch {
|
|
12713
|
+
host = null;
|
|
12714
|
+
}
|
|
12715
|
+
const gatewayIsPrivate = isPrivateAddress(host);
|
|
12716
|
+
return {
|
|
12717
|
+
transport: gatewayIsPrivate ? "ws" : "both",
|
|
12718
|
+
gatewayUrl,
|
|
12719
|
+
gatewayIsPrivate
|
|
12720
|
+
};
|
|
12721
|
+
}
|
|
12722
|
+
function pickLocalIPv4() {
|
|
12723
|
+
const nics = (0, import_node_os7.networkInterfaces)();
|
|
12724
|
+
let firstPrivate = null;
|
|
12725
|
+
for (const list of Object.values(nics)) {
|
|
12726
|
+
if (!list) continue;
|
|
12727
|
+
for (const ni of list) {
|
|
12728
|
+
if (ni.internal || ni.family !== "IPv4") continue;
|
|
12729
|
+
if (!isPrivateAddress(ni.address)) return ni.address;
|
|
12730
|
+
if (!firstPrivate) firstPrivate = ni.address;
|
|
12731
|
+
}
|
|
12732
|
+
}
|
|
12733
|
+
return firstPrivate;
|
|
12734
|
+
}
|
|
12735
|
+
async function reportTransportProbe(cloud, daemonId, probe) {
|
|
12736
|
+
const body = {
|
|
12737
|
+
daemonId,
|
|
12738
|
+
transport: probe.transport,
|
|
12739
|
+
gatewayUrl: probe.gatewayUrl,
|
|
12740
|
+
gatewayIsPrivate: probe.gatewayIsPrivate
|
|
12741
|
+
};
|
|
12742
|
+
const res = await cloud.request("POST", "/api/im/runtime/transport-report", {
|
|
12743
|
+
body,
|
|
12744
|
+
timeoutMs: 5e3
|
|
12745
|
+
});
|
|
12746
|
+
if (!res.ok) {
|
|
12747
|
+
return {
|
|
12748
|
+
ok: false,
|
|
12749
|
+
status: res.status,
|
|
12750
|
+
error: res.error?.message ?? `transport-report failed (${res.status})`
|
|
12751
|
+
};
|
|
12752
|
+
}
|
|
12753
|
+
return { ok: true, status: res.status };
|
|
12754
|
+
}
|
|
12755
|
+
|
|
11179
12756
|
// src/daemon/runner.ts
|
|
11180
12757
|
var import_meta3 = {};
|
|
11181
12758
|
var DEFAULT_LOCAL_PORT = 3210;
|
|
@@ -11225,6 +12802,14 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11225
12802
|
// F16 (2026-05-20) — periodic skill resync timer (default 10min).
|
|
11226
12803
|
skillResyncTimer;
|
|
11227
12804
|
skillSyncInFlight = false;
|
|
12805
|
+
// Wave-4 E7 — local cache of in-flight two-phase dispatch replies. Survives
|
|
12806
|
+
// daemon crash so cold-start can resume `prepared` rows via idempotent
|
|
12807
|
+
// commit (server enforces (taskId, idempotencyKey) UNIQUE).
|
|
12808
|
+
pendingReplyCache;
|
|
12809
|
+
// One-shot guard so we only recover on the first post-authenticated tick,
|
|
12810
|
+
// not every reconnect (server-side commits are idempotent but the log noise
|
|
12811
|
+
// and DB pressure of re-running on every redeclare is wasteful).
|
|
12812
|
+
pendingReplyRecoveryDone = false;
|
|
11228
12813
|
async start() {
|
|
11229
12814
|
if (this.state !== "idle") throw new Error(`Runner already in state ${this.state}`);
|
|
11230
12815
|
this.state = "starting";
|
|
@@ -11239,6 +12824,7 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11239
12824
|
process.env.PRISMER_API_KEY = this.config.api_key;
|
|
11240
12825
|
this.db = openLocalDb(this.paths.localDb);
|
|
11241
12826
|
this.cloud = new CloudClient({ baseUrl: this.config.cloud_api_base, apiKey: this.config.api_key });
|
|
12827
|
+
this.pendingReplyCache = createPendingReplyCache(this.db);
|
|
11242
12828
|
this.assetCache = new AssetCache({
|
|
11243
12829
|
db: this.db,
|
|
11244
12830
|
cloud: this.cloud,
|
|
@@ -11565,19 +13151,19 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11565
13151
|
sampleCgroupResources() {
|
|
11566
13152
|
const baseline = { cpu: { usagePct: 0 }, mem: { usedBytes: 0, limitBytes: 0 } };
|
|
11567
13153
|
try {
|
|
11568
|
-
if (!(0,
|
|
13154
|
+
if (!(0, import_node_fs19.existsSync)("/sys/fs/cgroup/memory.current")) {
|
|
11569
13155
|
return baseline;
|
|
11570
13156
|
}
|
|
11571
|
-
const memUsedRaw = (0,
|
|
13157
|
+
const memUsedRaw = (0, import_node_fs19.readFileSync)("/sys/fs/cgroup/memory.current", "utf-8").trim();
|
|
11572
13158
|
const memUsed = Number.parseInt(memUsedRaw, 10);
|
|
11573
13159
|
let memLimit = 0;
|
|
11574
|
-
if ((0,
|
|
11575
|
-
const limitRaw = (0,
|
|
13160
|
+
if ((0, import_node_fs19.existsSync)("/sys/fs/cgroup/memory.max")) {
|
|
13161
|
+
const limitRaw = (0, import_node_fs19.readFileSync)("/sys/fs/cgroup/memory.max", "utf-8").trim();
|
|
11576
13162
|
memLimit = limitRaw === "max" ? 0 : Number.parseInt(limitRaw, 10) || 0;
|
|
11577
13163
|
}
|
|
11578
13164
|
let cpuUsagePct = 0;
|
|
11579
|
-
if ((0,
|
|
11580
|
-
const statLines = (0,
|
|
13165
|
+
if ((0, import_node_fs19.existsSync)("/sys/fs/cgroup/cpu.stat")) {
|
|
13166
|
+
const statLines = (0, import_node_fs19.readFileSync)("/sys/fs/cgroup/cpu.stat", "utf-8").split("\n");
|
|
11581
13167
|
const usecLine = statLines.find((line) => line.startsWith("usage_usec "));
|
|
11582
13168
|
if (usecLine) {
|
|
11583
13169
|
const usec = Number.parseInt(usecLine.split(" ")[1] ?? "0", 10);
|
|
@@ -11755,10 +13341,10 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11755
13341
|
const rawJson = process.env.PRISMER_HOSTED_AGENT_JSON;
|
|
11756
13342
|
let raw;
|
|
11757
13343
|
if (rawFile) {
|
|
11758
|
-
if (!(0,
|
|
13344
|
+
if (!(0, import_node_fs19.existsSync)(rawFile)) {
|
|
11759
13345
|
throw new Error(`PRISMER_HOSTED_AGENT_FILE not found: ${rawFile}`);
|
|
11760
13346
|
}
|
|
11761
|
-
raw = (0,
|
|
13347
|
+
raw = (0, import_node_fs19.readFileSync)(rawFile, "utf8");
|
|
11762
13348
|
} else if (rawJson) {
|
|
11763
13349
|
raw = rawJson;
|
|
11764
13350
|
}
|
|
@@ -11845,6 +13431,19 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11845
13431
|
process.stdout.write(`[daemon] ws authenticated, sending agent.host.declare (${this.hostedAgents.size} agents)
|
|
11846
13432
|
`);
|
|
11847
13433
|
this.sendDeclare();
|
|
13434
|
+
if (!this.pendingReplyRecoveryDone) {
|
|
13435
|
+
this.pendingReplyRecoveryDone = true;
|
|
13436
|
+
void recoverPendingReplies({ cloud: this.cloud, cache: this.pendingReplyCache }).then((summary) => {
|
|
13437
|
+
if (summary.attempted === 0) return;
|
|
13438
|
+
process.stdout.write(
|
|
13439
|
+
`[daemon] dispatch.recovery summary attempted=${summary.attempted} committed=${summary.committed} aborted=${summary.aborted} failed=${summary.failed}
|
|
13440
|
+
`
|
|
13441
|
+
);
|
|
13442
|
+
}).catch((err) => {
|
|
13443
|
+
process.stderr.write(`[daemon] dispatch.recovery threw: ${err.message}
|
|
13444
|
+
`);
|
|
13445
|
+
});
|
|
13446
|
+
}
|
|
11848
13447
|
return;
|
|
11849
13448
|
}
|
|
11850
13449
|
this.handleIncoming(msg);
|
|
@@ -11854,7 +13453,7 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11854
13453
|
const payload = {
|
|
11855
13454
|
daemonId: this.config.daemon_id,
|
|
11856
13455
|
daemonVersion: this.opts.daemonVersion ?? "0.0.0",
|
|
11857
|
-
platform: (0,
|
|
13456
|
+
platform: (0, import_node_os8.platform)() === "win32" ? "win32" : (0, import_node_os8.platform)() === "linux" ? "linux" : "darwin",
|
|
11858
13457
|
agents: Array.from(this.hostedAgents.values()).map((a) => ({
|
|
11859
13458
|
imUserId: a.imUserId,
|
|
11860
13459
|
name: a.name,
|
|
@@ -11891,12 +13490,90 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11891
13490
|
case "asset.changed":
|
|
11892
13491
|
void this.onAssetChanged(msg.payload);
|
|
11893
13492
|
return;
|
|
13493
|
+
// v2.0 §4.8.1 (Wave 4-E4) — webhook reverse-channel RPC. Cloud's
|
|
13494
|
+
// dispatchExternalMention pushes an AgentDispatchRequest plus an
|
|
13495
|
+
// embedded `_rpcId`; the daemon runs the same handler as the
|
|
13496
|
+
// HTTP /dispatch path and echoes the reply back over WS with the
|
|
13497
|
+
// identical `_rpcId` so cloud's WsRpcService can settle the
|
|
13498
|
+
// pending promise.
|
|
13499
|
+
case "webhook.dispatch.request":
|
|
13500
|
+
void this.onWebhookDispatch(msg.payload);
|
|
13501
|
+
return;
|
|
11894
13502
|
default:
|
|
11895
13503
|
this.emit("unknown-message", msg);
|
|
11896
13504
|
}
|
|
11897
13505
|
}
|
|
13506
|
+
/**
|
|
13507
|
+
* v2.0 §4.8.1 — webhook dispatch over WS reverse channel.
|
|
13508
|
+
*
|
|
13509
|
+
* Acks via `webhook.dispatch.reply { _rpcId, ok, acceptedAt }` immediately
|
|
13510
|
+
* after `handleAgentMessageDispatch()` returns its synchronous response
|
|
13511
|
+
* — the actual reply (the agent's reply text/attachments) still flows
|
|
13512
|
+
* through `postMessageDispatchReply()` which posts to
|
|
13513
|
+
* `/api/im/dispatch/reply` after the adapter finishes. That mirrors the
|
|
13514
|
+
* HTTP /dispatch contract: ack first, asynchronous final reply via the
|
|
13515
|
+
* dispatch-reply REST endpoint.
|
|
13516
|
+
*
|
|
13517
|
+
* The `_rpcId` field is the only addition versus the HTTP path; we strip
|
|
13518
|
+
* it before passing the payload to the dispatch handler so existing
|
|
13519
|
+
* validation logic doesn't trip on an unknown field.
|
|
13520
|
+
*/
|
|
13521
|
+
async onWebhookDispatch(payload) {
|
|
13522
|
+
const rpcId = payload._rpcId;
|
|
13523
|
+
if (!rpcId) {
|
|
13524
|
+
process.stderr.write(`[daemon] webhook.dispatch.request without _rpcId \u2014 dropped
|
|
13525
|
+
`);
|
|
13526
|
+
return;
|
|
13527
|
+
}
|
|
13528
|
+
const { _rpcId: _, ...request } = payload;
|
|
13529
|
+
try {
|
|
13530
|
+
const handle = handleAgentMessageDispatch(request, {
|
|
13531
|
+
findAgent: (agentImUserId) => this.findMessageDispatchAgent(agentImUserId),
|
|
13532
|
+
postReply: (replyPayload) => this.postMessageDispatchReply(replyPayload),
|
|
13533
|
+
onError: (err, req) => {
|
|
13534
|
+
process.stderr.write(
|
|
13535
|
+
`[daemon] webhook.dispatch.request failed message=${req.messageId} agent=${req.mentionedAgentImUserId}: ${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
13536
|
+
`
|
|
13537
|
+
);
|
|
13538
|
+
}
|
|
13539
|
+
});
|
|
13540
|
+
this.ws.send({
|
|
13541
|
+
type: "webhook.dispatch.reply",
|
|
13542
|
+
payload: {
|
|
13543
|
+
_rpcId: rpcId,
|
|
13544
|
+
ok: handle.response.ok,
|
|
13545
|
+
acceptedAt: handle.response.acceptedAt,
|
|
13546
|
+
error: handle.response.error
|
|
13547
|
+
}
|
|
13548
|
+
});
|
|
13549
|
+
void handle.done.catch((err) => {
|
|
13550
|
+
process.stderr.write(
|
|
13551
|
+
`[daemon] webhook.dispatch final reply failed message=${request.messageId}: ${err instanceof Error ? err.message : String(err)}
|
|
13552
|
+
`
|
|
13553
|
+
);
|
|
13554
|
+
});
|
|
13555
|
+
} catch (err) {
|
|
13556
|
+
this.ws.send({
|
|
13557
|
+
type: "webhook.dispatch.reply",
|
|
13558
|
+
payload: {
|
|
13559
|
+
_rpcId: rpcId,
|
|
13560
|
+
ok: false,
|
|
13561
|
+
error: {
|
|
13562
|
+
code: "daemon_dispatch_failed",
|
|
13563
|
+
message: err instanceof Error ? err.message : String(err)
|
|
13564
|
+
}
|
|
13565
|
+
}
|
|
13566
|
+
});
|
|
13567
|
+
}
|
|
13568
|
+
}
|
|
11898
13569
|
async onHostAcked(payload) {
|
|
11899
13570
|
this.workspaceId = payload.workspaceId;
|
|
13571
|
+
void this.reportTransport().catch((err) => {
|
|
13572
|
+
process.stderr.write(
|
|
13573
|
+
`[daemon] transport-probe report failed: ${err.message}
|
|
13574
|
+
`
|
|
13575
|
+
);
|
|
13576
|
+
});
|
|
11900
13577
|
if (this.memoryWiring && this.workspaceId) {
|
|
11901
13578
|
syncMemoryFromCloud(this.memoryWiring, this.cloud, [this.workspaceId]).catch(
|
|
11902
13579
|
(err) => log7.error("Initial memory sync failed", err.message)
|
|
@@ -12078,15 +13755,69 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
12078
13755
|
};
|
|
12079
13756
|
}
|
|
12080
13757
|
async postMessageDispatchReply(payload) {
|
|
12081
|
-
const
|
|
12082
|
-
|
|
12083
|
-
|
|
12084
|
-
|
|
12085
|
-
|
|
12086
|
-
|
|
12087
|
-
|
|
13758
|
+
const taskId = `external:${payload.replyToMessageId}`;
|
|
13759
|
+
try {
|
|
13760
|
+
const result = await sendDispatchReplyTwoPhase({
|
|
13761
|
+
taskId,
|
|
13762
|
+
payload: {
|
|
13763
|
+
conversationId: payload.conversationId,
|
|
13764
|
+
replyToken: payload.replyToken,
|
|
13765
|
+
replyToMessageId: payload.replyToMessageId,
|
|
13766
|
+
agentImUserId: payload.agentImUserId,
|
|
13767
|
+
status: payload.status,
|
|
13768
|
+
...payload.replyText !== void 0 ? { replyText: payload.replyText } : {},
|
|
13769
|
+
...payload.attachments ? { attachments: payload.attachments } : {},
|
|
13770
|
+
assetIds: payload.attachments?.map((a) => a.assetId).filter((id) => typeof id === "string") ?? [],
|
|
13771
|
+
completedAt: payload.completedAt,
|
|
13772
|
+
...payload.error ? { error: payload.error } : {}
|
|
13773
|
+
},
|
|
13774
|
+
cloud: this.cloud,
|
|
13775
|
+
cache: this.pendingReplyCache
|
|
13776
|
+
});
|
|
13777
|
+
if (result.status === "aborted") {
|
|
13778
|
+
throw new Error(`dispatch reply aborted by server reaper (replyId=${result.replyId})`);
|
|
13779
|
+
}
|
|
13780
|
+
} catch (err) {
|
|
13781
|
+
throw new Error(
|
|
13782
|
+
err.message?.length ? err.message : `dispatch reply failed`
|
|
13783
|
+
);
|
|
12088
13784
|
}
|
|
12089
13785
|
}
|
|
13786
|
+
/**
|
|
13787
|
+
* v2.0 §4.8.1 (Wave 4-E4) — submit the transport-probe report. Called
|
|
13788
|
+
* from `onHostAcked` so the daemon's container row exists in cloud
|
|
13789
|
+
* before we POST to /runtime/transport-report.
|
|
13790
|
+
*
|
|
13791
|
+
* The gatewayUrl reported here is `http://<local-ipv4>:<localPort>`
|
|
13792
|
+
* (typically `http://192.168.x.x:3210`). For a daemon without a local
|
|
13793
|
+
* HTTP server (startLocalServer=false in tests) we still post the
|
|
13794
|
+
* probe with `gatewayUrl=null` so cloud knows transport='ws' is the
|
|
13795
|
+
* only path.
|
|
13796
|
+
*/
|
|
13797
|
+
async reportTransport() {
|
|
13798
|
+
const hasLocalServer = this.opts.startLocalServer !== false;
|
|
13799
|
+
let gatewayUrl = null;
|
|
13800
|
+
if (hasLocalServer) {
|
|
13801
|
+
const localIp = pickLocalIPv4();
|
|
13802
|
+
if (localIp) {
|
|
13803
|
+
const port = this.opts.localPort ?? DEFAULT_LOCAL_PORT;
|
|
13804
|
+
gatewayUrl = `http://${localIp}:${port}`;
|
|
13805
|
+
}
|
|
13806
|
+
}
|
|
13807
|
+
const probe = buildTransportProbe(gatewayUrl);
|
|
13808
|
+
const result = await reportTransportProbe(this.cloud, this.config.daemon_id, probe);
|
|
13809
|
+
if (!result.ok) {
|
|
13810
|
+
process.stderr.write(
|
|
13811
|
+
`[daemon] transport-probe report rejected status=${result.status} error=${result.error ?? "-"}
|
|
13812
|
+
`
|
|
13813
|
+
);
|
|
13814
|
+
return;
|
|
13815
|
+
}
|
|
13816
|
+
process.stdout.write(
|
|
13817
|
+
`[daemon] transport-probe reported transport=${probe.transport} gateway=${probe.gatewayUrl ?? "null"} private=${probe.gatewayIsPrivate}
|
|
13818
|
+
`
|
|
13819
|
+
);
|
|
13820
|
+
}
|
|
12090
13821
|
onAgentChanged(payload) {
|
|
12091
13822
|
const a = this.hostedAgents.get(payload.agentImUserId);
|
|
12092
13823
|
if (!a) return;
|
|
@@ -12528,9 +14259,9 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
12528
14259
|
function resolveUserAssetWorkspaceDir(workspaceId) {
|
|
12529
14260
|
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
12530
14261
|
if (override) return (0, import_node_path14.join)(override, workspaceId);
|
|
12531
|
-
const home = (0,
|
|
14262
|
+
const home = (0, import_node_os8.homedir)();
|
|
12532
14263
|
const desktop = (0, import_node_path14.join)(home, "Desktop");
|
|
12533
|
-
const base = (0,
|
|
14264
|
+
const base = (0, import_node_fs19.existsSync)(desktop) ? desktop : home;
|
|
12534
14265
|
return (0, import_node_path14.join)(base, "Prismer Assets", workspaceId);
|
|
12535
14266
|
}
|
|
12536
14267
|
function readTargetDaemonId(payload) {
|
|
@@ -12679,9 +14410,9 @@ function buildDaemonCommand() {
|
|
|
12679
14410
|
if (existingPid && pidAlive2(existingPid)) {
|
|
12680
14411
|
exitWithError(`Daemon already running (pid ${existingPid}). Use \`prismer daemon stop\` first.`);
|
|
12681
14412
|
}
|
|
12682
|
-
if (!(0,
|
|
14413
|
+
if (!(0, import_node_fs20.existsSync)(paths.logsDir)) (0, import_node_fs20.mkdirSync)(paths.logsDir, { recursive: true });
|
|
12683
14414
|
const logFile = (0, import_node_path15.join)(paths.logsDir, "daemon.log");
|
|
12684
|
-
const fd = (0,
|
|
14415
|
+
const fd = (0, import_node_fs20.openSync)(logFile, "a");
|
|
12685
14416
|
const args = [process.argv[1], "daemon", "run"];
|
|
12686
14417
|
if (opts.port) args.push("--port", String(opts.port));
|
|
12687
14418
|
if (opts.localServer === false) args.push("--no-local-server");
|
|
@@ -12765,7 +14496,7 @@ function buildDaemonCommand() {
|
|
|
12765
14496
|
cmd.command("logs").description("Show daemon logs").option("--tail <n>", "Number of lines to show", (v) => Number.parseInt(v, 10), 80).option("--follow", "Follow log output").option("--json", "Accept --json for global flag compatibility (raw log bytes are streamed)").action(async (opts) => {
|
|
12766
14497
|
const paths = resolvePaths();
|
|
12767
14498
|
const logFile = (0, import_node_path15.join)(paths.logsDir, "daemon.log");
|
|
12768
|
-
if (!(0,
|
|
14499
|
+
if (!(0, import_node_fs20.existsSync)(logFile)) {
|
|
12769
14500
|
exitWithError(`No daemon log found at ${logFile}. Start the daemon with \`prismer daemon start\`.`);
|
|
12770
14501
|
}
|
|
12771
14502
|
const lines = Math.max(1, opts.tail);
|
|
@@ -12806,13 +14537,13 @@ async function tailFromEnd(path9, lines) {
|
|
|
12806
14537
|
}
|
|
12807
14538
|
}
|
|
12808
14539
|
async function followFile(path9) {
|
|
12809
|
-
let offset = (0,
|
|
14540
|
+
let offset = (0, import_node_fs20.statSync)(path9).size;
|
|
12810
14541
|
for (; ; ) {
|
|
12811
14542
|
await (0, import_promises2.setTimeout)(1e3);
|
|
12812
|
-
const size = (0,
|
|
14543
|
+
const size = (0, import_node_fs20.statSync)(path9).size;
|
|
12813
14544
|
if (size < offset) offset = 0;
|
|
12814
14545
|
if (size === offset) continue;
|
|
12815
|
-
const stream = (0,
|
|
14546
|
+
const stream = (0, import_node_fs20.createReadStream)(path9, { start: offset, end: size - 1, encoding: "utf8" });
|
|
12816
14547
|
for await (const chunk of stream) process.stdout.write(chunk);
|
|
12817
14548
|
offset = size;
|
|
12818
14549
|
}
|
|
@@ -12820,8 +14551,8 @@ async function followFile(path9) {
|
|
|
12820
14551
|
|
|
12821
14552
|
// src/cli/commands/events.ts
|
|
12822
14553
|
var import_commander9 = require("commander");
|
|
12823
|
-
var
|
|
12824
|
-
var
|
|
14554
|
+
var import_node_fs21 = require("fs");
|
|
14555
|
+
var import_node_os9 = require("os");
|
|
12825
14556
|
var import_node_path16 = require("path");
|
|
12826
14557
|
var import_node_readline = require("readline");
|
|
12827
14558
|
init_util();
|
|
@@ -12829,7 +14560,7 @@ var DEFAULT_LIMIT2 = 50;
|
|
|
12829
14560
|
function buildEventsCommand() {
|
|
12830
14561
|
return addEventOptions(new import_commander9.Command("events").description("Read local PARA events from ~/.prismer/para/events.jsonl")).action(async (opts) => {
|
|
12831
14562
|
const file = eventsPath();
|
|
12832
|
-
if (!(0,
|
|
14563
|
+
if (!(0, import_node_fs21.existsSync)(file)) {
|
|
12833
14564
|
printJson(unavailable(file));
|
|
12834
14565
|
process.exitCode = 1;
|
|
12835
14566
|
return;
|
|
@@ -12846,7 +14577,7 @@ function buildEventsCommand() {
|
|
|
12846
14577
|
function buildEventsStatsCommand() {
|
|
12847
14578
|
return addEventOptions(new import_commander9.Command("events:stats").description("Summarize local PARA events from ~/.prismer/para/events.jsonl")).action(async (opts) => {
|
|
12848
14579
|
const file = eventsPath();
|
|
12849
|
-
if (!(0,
|
|
14580
|
+
if (!(0, import_node_fs21.existsSync)(file)) {
|
|
12850
14581
|
printJson(unavailable(file));
|
|
12851
14582
|
process.exitCode = 1;
|
|
12852
14583
|
return;
|
|
@@ -12855,7 +14586,7 @@ function buildEventsStatsCommand() {
|
|
|
12855
14586
|
const events = await readEvents(file, filters);
|
|
12856
14587
|
printJson({
|
|
12857
14588
|
ok: true,
|
|
12858
|
-
data: { stats:
|
|
14589
|
+
data: { stats: summarize3(events), count: events.length, limit: filters.limit },
|
|
12859
14590
|
checks: { file, exists: true, filters: cleanFilters(filters) }
|
|
12860
14591
|
});
|
|
12861
14592
|
});
|
|
@@ -12864,7 +14595,7 @@ function addEventOptions(cmd) {
|
|
|
12864
14595
|
return cmd.option("--limit <n>", "Max events to return/read", parsePositiveInt4, DEFAULT_LIMIT2).option("--agent-id <id>", "Filter by agent id").option("--session-id <id>", "Filter by session id").option("--family <name>", "Filter by event family").option("--type <name>", "Filter by event type").option("--json", "Output JSON (default)");
|
|
12865
14596
|
}
|
|
12866
14597
|
function eventsPath() {
|
|
12867
|
-
return (0, import_node_path16.join)(process.env.PRISMER_HOME ?? (0, import_node_path16.join)((0,
|
|
14598
|
+
return (0, import_node_path16.join)(process.env.PRISMER_HOME ?? (0, import_node_path16.join)((0, import_node_os9.homedir)(), ".prismer"), "para", "events.jsonl");
|
|
12868
14599
|
}
|
|
12869
14600
|
function unavailable(file) {
|
|
12870
14601
|
return {
|
|
@@ -12880,7 +14611,7 @@ function unavailable(file) {
|
|
|
12880
14611
|
async function readEvents(file, filters) {
|
|
12881
14612
|
const out = [];
|
|
12882
14613
|
const rl = (0, import_node_readline.createInterface)({
|
|
12883
|
-
input: (0,
|
|
14614
|
+
input: (0, import_node_fs21.createReadStream)(file, { encoding: "utf8" }),
|
|
12884
14615
|
crlfDelay: Infinity
|
|
12885
14616
|
});
|
|
12886
14617
|
for await (const line of rl) {
|
|
@@ -12912,7 +14643,7 @@ function field(event, names) {
|
|
|
12912
14643
|
}
|
|
12913
14644
|
return void 0;
|
|
12914
14645
|
}
|
|
12915
|
-
function
|
|
14646
|
+
function summarize3(events) {
|
|
12916
14647
|
const byFamily = {};
|
|
12917
14648
|
const byType = {};
|
|
12918
14649
|
const byAgentId = {};
|
|
@@ -12950,7 +14681,7 @@ function parsePositiveInt4(v) {
|
|
|
12950
14681
|
// src/cli/commands/memory.ts
|
|
12951
14682
|
var import_better_sqlite35 = __toESM(require("better-sqlite3"), 1);
|
|
12952
14683
|
var import_commander10 = require("commander");
|
|
12953
|
-
var
|
|
14684
|
+
var import_node_fs22 = require("fs");
|
|
12954
14685
|
init_util();
|
|
12955
14686
|
var LOCAL_BASE = process.env.PRISMER_DAEMON_URL ?? "http://127.0.0.1:3210";
|
|
12956
14687
|
function buildMemoryCommand() {
|
|
@@ -13102,7 +14833,7 @@ function readCacheSnapshot(limit) {
|
|
|
13102
14833
|
const paths = resolvePaths();
|
|
13103
14834
|
const empty = {
|
|
13104
14835
|
dbPath: paths.localDb,
|
|
13105
|
-
dbExists: (0,
|
|
14836
|
+
dbExists: (0, import_node_fs22.existsSync)(paths.localDb),
|
|
13106
14837
|
tables: {
|
|
13107
14838
|
cached_assets: { exists: false, count: 0, sizeBytes: 0 },
|
|
13108
14839
|
workspace_files_mirror: { exists: false, count: 0 }
|
|
@@ -13225,16 +14956,16 @@ var import_commander11 = require("commander");
|
|
|
13225
14956
|
|
|
13226
14957
|
// src/pair.ts
|
|
13227
14958
|
var import_node_crypto11 = require("crypto");
|
|
13228
|
-
var
|
|
14959
|
+
var import_node_os11 = require("os");
|
|
13229
14960
|
var import_promises3 = require("timers/promises");
|
|
13230
14961
|
var import_qrcode = __toESM(require("qrcode"), 1);
|
|
13231
14962
|
|
|
13232
14963
|
// src/daemon-id.ts
|
|
13233
14964
|
var import_node_crypto10 = require("crypto");
|
|
13234
|
-
var
|
|
14965
|
+
var import_node_os10 = require("os");
|
|
13235
14966
|
var PREFIX = "daemon-";
|
|
13236
14967
|
function sanitizeHostname() {
|
|
13237
|
-
return (0,
|
|
14968
|
+
return (0, import_node_os10.hostname)().replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "unknown";
|
|
13238
14969
|
}
|
|
13239
14970
|
function newDaemonId(opts = {}) {
|
|
13240
14971
|
const host = opts.hostnameOverride ?? sanitizeHostname();
|
|
@@ -13273,7 +15004,7 @@ async function pair(opts) {
|
|
|
13273
15004
|
"/api/im/pair/offer",
|
|
13274
15005
|
{
|
|
13275
15006
|
auth: false,
|
|
13276
|
-
body: { devicePub, deviceName: opts.deviceName ?? (0,
|
|
15007
|
+
body: { devicePub, deviceName: opts.deviceName ?? (0, import_node_os11.hostname)() }
|
|
13277
15008
|
}
|
|
13278
15009
|
);
|
|
13279
15010
|
if (!offerRes.ok) {
|
|
@@ -13394,8 +15125,8 @@ function buildPairCommand() {
|
|
|
13394
15125
|
|
|
13395
15126
|
// src/cli/commands/profile.ts
|
|
13396
15127
|
var import_commander12 = require("commander");
|
|
13397
|
-
var
|
|
13398
|
-
var
|
|
15128
|
+
var import_node_fs23 = require("fs");
|
|
15129
|
+
var import_node_os12 = require("os");
|
|
13399
15130
|
var import_node_path17 = require("path");
|
|
13400
15131
|
var import_node_child_process8 = require("child_process");
|
|
13401
15132
|
|
|
@@ -13520,12 +15251,12 @@ function buildProfileCommand() {
|
|
|
13520
15251
|
const profile = await cloud.get(
|
|
13521
15252
|
`/api/im/agent_profiles/${encodeURIComponent(profileId)}`
|
|
13522
15253
|
);
|
|
13523
|
-
const tmpFile = (0, import_node_path17.join)((0,
|
|
13524
|
-
(0,
|
|
15254
|
+
const tmpFile = (0, import_node_path17.join)((0, import_node_os12.tmpdir)(), `prismer-profile-${profileId}.json`);
|
|
15255
|
+
(0, import_node_fs23.writeFileSync)(tmpFile, JSON.stringify(profile.config, null, 2), "utf8");
|
|
13525
15256
|
const editor = process.env.EDITOR || "vi";
|
|
13526
15257
|
const ed = (0, import_node_child_process8.spawnSync)(editor, [tmpFile], { stdio: "inherit" });
|
|
13527
15258
|
if (ed.status !== 0) exitWithError(`editor exited ${ed.status}`, { code: "editor_failed" });
|
|
13528
|
-
const newConfig = JSON.parse((0,
|
|
15259
|
+
const newConfig = JSON.parse((0, import_node_fs23.readFileSync)(tmpFile, "utf8"));
|
|
13529
15260
|
const res = await cloud.request("PATCH", `/api/im/agent_profiles/${encodeURIComponent(profileId)}`, {
|
|
13530
15261
|
body: { config: newConfig, version: profile.version }
|
|
13531
15262
|
});
|
|
@@ -13547,8 +15278,8 @@ function mkCloud4() {
|
|
|
13547
15278
|
function readJsonArg(arg) {
|
|
13548
15279
|
if (arg.startsWith("@")) {
|
|
13549
15280
|
const path9 = arg.slice(1);
|
|
13550
|
-
if (!(0,
|
|
13551
|
-
return JSON.parse((0,
|
|
15281
|
+
if (!(0, import_node_fs23.existsSync)(path9)) throw new Error(`File not found: ${path9}`);
|
|
15282
|
+
return JSON.parse((0, import_node_fs23.readFileSync)(path9, "utf8"));
|
|
13552
15283
|
}
|
|
13553
15284
|
return JSON.parse(arg);
|
|
13554
15285
|
}
|
|
@@ -13562,8 +15293,8 @@ async function resolveDefaultWorkspaceId(cloud) {
|
|
|
13562
15293
|
// src/cli/commands/reset.ts
|
|
13563
15294
|
var import_commander13 = require("commander");
|
|
13564
15295
|
var import_node_child_process9 = require("child_process");
|
|
13565
|
-
var
|
|
13566
|
-
var
|
|
15296
|
+
var import_node_fs24 = require("fs");
|
|
15297
|
+
var import_node_os13 = require("os");
|
|
13567
15298
|
var import_node_path18 = require("path");
|
|
13568
15299
|
var import_promises4 = require("timers/promises");
|
|
13569
15300
|
init_util();
|
|
@@ -13581,15 +15312,15 @@ function buildResetCommand() {
|
|
|
13581
15312
|
const hermesGatewayPids = findHermesGatewayPids2();
|
|
13582
15313
|
const plan = {
|
|
13583
15314
|
root: paths.root,
|
|
13584
|
-
exists: (0,
|
|
15315
|
+
exists: (0, import_node_fs24.existsSync)(paths.root),
|
|
13585
15316
|
runningPid,
|
|
13586
15317
|
mode: opts.wipe ? "wipe" : "archive",
|
|
13587
15318
|
archivePath,
|
|
13588
15319
|
assetSyncRoot,
|
|
13589
|
-
assetSyncExists: (0,
|
|
15320
|
+
assetSyncExists: (0, import_node_fs24.existsSync)(assetSyncRoot),
|
|
13590
15321
|
assetSyncArchivePath,
|
|
13591
15322
|
hermesHome,
|
|
13592
|
-
hermesExists: (0,
|
|
15323
|
+
hermesExists: (0, import_node_fs24.existsSync)(hermesHome),
|
|
13593
15324
|
hermesArchivePath,
|
|
13594
15325
|
hermesGatewayPids,
|
|
13595
15326
|
willStopDaemon: runningPid !== null,
|
|
@@ -13613,40 +15344,40 @@ function buildResetCommand() {
|
|
|
13613
15344
|
for (const gatewayPid of hermesGatewayPids) {
|
|
13614
15345
|
await stopProcess(gatewayPid, opts.timeout ?? 5e3, "Hermes gateway");
|
|
13615
15346
|
}
|
|
13616
|
-
if ((0,
|
|
15347
|
+
if ((0, import_node_fs24.existsSync)(paths.root)) {
|
|
13617
15348
|
if (opts.wipe) {
|
|
13618
|
-
(0,
|
|
15349
|
+
(0, import_node_fs24.rmSync)(paths.root, { recursive: true, force: true });
|
|
13619
15350
|
} else {
|
|
13620
15351
|
if (!archivePath) throw new Error("internal: archivePath missing");
|
|
13621
|
-
if (!(0,
|
|
13622
|
-
(0,
|
|
15352
|
+
if (!(0, import_node_fs24.existsSync)((0, import_node_path18.dirname)(archivePath))) {
|
|
15353
|
+
(0, import_node_fs24.mkdirSync)((0, import_node_path18.dirname)(archivePath), { recursive: true });
|
|
13623
15354
|
}
|
|
13624
|
-
(0,
|
|
15355
|
+
(0, import_node_fs24.renameSync)(paths.root, archivePath);
|
|
13625
15356
|
}
|
|
13626
15357
|
}
|
|
13627
|
-
if ((0,
|
|
15358
|
+
if ((0, import_node_fs24.existsSync)(assetSyncRoot)) {
|
|
13628
15359
|
if (opts.wipe) {
|
|
13629
|
-
(0,
|
|
15360
|
+
(0, import_node_fs24.rmSync)(assetSyncRoot, { recursive: true, force: true });
|
|
13630
15361
|
} else {
|
|
13631
15362
|
if (!assetSyncArchivePath) throw new Error("internal: assetSyncArchivePath missing");
|
|
13632
|
-
if (!(0,
|
|
13633
|
-
(0,
|
|
15363
|
+
if (!(0, import_node_fs24.existsSync)((0, import_node_path18.dirname)(assetSyncArchivePath))) {
|
|
15364
|
+
(0, import_node_fs24.mkdirSync)((0, import_node_path18.dirname)(assetSyncArchivePath), { recursive: true });
|
|
13634
15365
|
}
|
|
13635
|
-
(0,
|
|
15366
|
+
(0, import_node_fs24.renameSync)(assetSyncRoot, assetSyncArchivePath);
|
|
13636
15367
|
}
|
|
13637
15368
|
}
|
|
13638
|
-
if ((0,
|
|
15369
|
+
if ((0, import_node_fs24.existsSync)(hermesHome)) {
|
|
13639
15370
|
if (opts.wipe) {
|
|
13640
|
-
(0,
|
|
15371
|
+
(0, import_node_fs24.rmSync)(hermesHome, { recursive: true, force: true });
|
|
13641
15372
|
} else {
|
|
13642
15373
|
if (!hermesArchivePath) throw new Error("internal: hermesArchivePath missing");
|
|
13643
|
-
if (!(0,
|
|
13644
|
-
(0,
|
|
15374
|
+
if (!(0, import_node_fs24.existsSync)((0, import_node_path18.dirname)(hermesArchivePath))) {
|
|
15375
|
+
(0, import_node_fs24.mkdirSync)((0, import_node_path18.dirname)(hermesArchivePath), { recursive: true });
|
|
13645
15376
|
}
|
|
13646
|
-
(0,
|
|
15377
|
+
(0, import_node_fs24.renameSync)(hermesHome, hermesArchivePath);
|
|
13647
15378
|
}
|
|
13648
15379
|
}
|
|
13649
|
-
(0,
|
|
15380
|
+
(0, import_node_fs24.mkdirSync)(paths.root, { recursive: true });
|
|
13650
15381
|
if (opts.json) {
|
|
13651
15382
|
printJson({ ok: true, reset: true, plan });
|
|
13652
15383
|
return;
|
|
@@ -13715,13 +15446,13 @@ function timestamp() {
|
|
|
13715
15446
|
function resolveAssetSyncRoot() {
|
|
13716
15447
|
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
13717
15448
|
if (override) return override;
|
|
13718
|
-
const home = (0,
|
|
15449
|
+
const home = (0, import_node_os13.homedir)();
|
|
13719
15450
|
const desktop = (0, import_node_path18.join)(home, "Desktop");
|
|
13720
|
-
const base = (0,
|
|
15451
|
+
const base = (0, import_node_fs24.existsSync)(desktop) ? desktop : home;
|
|
13721
15452
|
return (0, import_node_path18.join)(base, "Prismer Assets");
|
|
13722
15453
|
}
|
|
13723
15454
|
function resolveHermesHome() {
|
|
13724
|
-
return process.env.HERMES_HOME || (0, import_node_path18.join)((0,
|
|
15455
|
+
return process.env.HERMES_HOME || (0, import_node_path18.join)((0, import_node_os13.homedir)(), ".hermes");
|
|
13725
15456
|
}
|
|
13726
15457
|
function findHermesGatewayPids2() {
|
|
13727
15458
|
try {
|
|
@@ -13951,10 +15682,10 @@ async function safeParseResponse(res) {
|
|
|
13951
15682
|
|
|
13952
15683
|
// src/cli/commands/setup.ts
|
|
13953
15684
|
var import_commander15 = require("commander");
|
|
13954
|
-
var
|
|
15685
|
+
var import_node_os14 = require("os");
|
|
13955
15686
|
var import_node_child_process10 = require("child_process");
|
|
13956
15687
|
var import_node_crypto12 = require("crypto");
|
|
13957
|
-
var
|
|
15688
|
+
var import_node_fs25 = require("fs");
|
|
13958
15689
|
var import_node_http2 = require("http");
|
|
13959
15690
|
init_util();
|
|
13960
15691
|
init_ui();
|
|
@@ -13996,7 +15727,7 @@ function buildSetupCommand() {
|
|
|
13996
15727
|
}
|
|
13997
15728
|
const result = await pair({
|
|
13998
15729
|
cloudBaseUrl,
|
|
13999
|
-
deviceName: opts.deviceName ?? (0,
|
|
15730
|
+
deviceName: opts.deviceName ?? (0, import_node_os14.hostname)(),
|
|
14000
15731
|
force: opts.force,
|
|
14001
15732
|
paths,
|
|
14002
15733
|
asUserEmail: opts.asUser
|
|
@@ -14013,13 +15744,13 @@ function buildSetupCommand() {
|
|
|
14013
15744
|
apiKey = await mintDaemonApiKey({
|
|
14014
15745
|
cloudBaseUrl,
|
|
14015
15746
|
token: authToken,
|
|
14016
|
-
deviceName: opts.deviceName ?? (0,
|
|
15747
|
+
deviceName: opts.deviceName ?? (0, import_node_os14.hostname)()
|
|
14017
15748
|
});
|
|
14018
15749
|
}
|
|
14019
15750
|
if (!apiKey && !authToken && !opts.check && opts.browser !== false) {
|
|
14020
15751
|
apiKey = await runBrowserSetup({
|
|
14021
15752
|
cloudBaseUrl,
|
|
14022
|
-
deviceName: opts.deviceName ?? (0,
|
|
15753
|
+
deviceName: opts.deviceName ?? (0, import_node_os14.hostname)(),
|
|
14023
15754
|
json: Boolean(opts.json)
|
|
14024
15755
|
});
|
|
14025
15756
|
}
|
|
@@ -14205,9 +15936,9 @@ function shouldArchiveLocalDb(previous, next) {
|
|
|
14205
15936
|
return previous.api_key !== next.api_key || previous.cloud_api_base !== next.cloud_api_base || previous.daemon_id !== next.daemon_id;
|
|
14206
15937
|
}
|
|
14207
15938
|
function archiveLocalDb(localDbPath) {
|
|
14208
|
-
if (!(0,
|
|
15939
|
+
if (!(0, import_node_fs25.existsSync)(localDbPath)) return;
|
|
14209
15940
|
const archived = `${localDbPath}.${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
|
|
14210
|
-
(0,
|
|
15941
|
+
(0, import_node_fs25.renameSync)(localDbPath, archived);
|
|
14211
15942
|
}
|
|
14212
15943
|
async function mintDaemonApiKey(input) {
|
|
14213
15944
|
const label = `Daemon: ${input.deviceName} ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
|
|
@@ -14276,7 +16007,7 @@ function buildSkillCommand() {
|
|
|
14276
16007
|
const cloud = mkCloud6();
|
|
14277
16008
|
const path9 = opts.agent ? `/api/im/agents/${encodeURIComponent(opts.agent)}/skills` : `/api/im/skills/${encodeURIComponent(slugOrId)}/install`;
|
|
14278
16009
|
const body = opts.agent ? { skillId: slugOrId, workspaceId: opts.workspaceId, version: opts.version } : { workspaceId: opts.workspaceId, version: opts.version };
|
|
14279
|
-
const data = await
|
|
16010
|
+
const data = await requestEnvelope2(cloud, "POST", path9, body, "skill_install_failed");
|
|
14280
16011
|
if (opts.json) {
|
|
14281
16012
|
printJson(data);
|
|
14282
16013
|
return;
|
|
@@ -14287,7 +16018,7 @@ function buildSkillCommand() {
|
|
|
14287
16018
|
const cloud = mkCloud6();
|
|
14288
16019
|
const path9 = opts.agent ? `/api/im/agents/${encodeURIComponent(opts.agent)}/skills` : `/api/im/skills/${encodeURIComponent(slugOrId)}/install`;
|
|
14289
16020
|
const body = opts.agent ? { skillId: slugOrId, workspaceId: opts.workspaceId } : void 0;
|
|
14290
|
-
const data = await
|
|
16021
|
+
const data = await requestEnvelope2(cloud, "DELETE", path9, body, "skill_uninstall_failed");
|
|
14291
16022
|
if (opts.json) {
|
|
14292
16023
|
printJson(data);
|
|
14293
16024
|
return;
|
|
@@ -14340,7 +16071,7 @@ function mkCloud6() {
|
|
|
14340
16071
|
const cfg = loadConfig(resolvePaths());
|
|
14341
16072
|
return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
14342
16073
|
}
|
|
14343
|
-
async function
|
|
16074
|
+
async function requestEnvelope2(cloud, method, path9, body, code) {
|
|
14344
16075
|
const res = await cloud.request(method, path9, {
|
|
14345
16076
|
body
|
|
14346
16077
|
});
|
|
@@ -14459,7 +16190,7 @@ function printSkill(skill, includeContent) {
|
|
|
14459
16190
|
|
|
14460
16191
|
// src/cli/commands/status.ts
|
|
14461
16192
|
var import_commander17 = require("commander");
|
|
14462
|
-
var
|
|
16193
|
+
var import_node_os15 = require("os");
|
|
14463
16194
|
init_util();
|
|
14464
16195
|
init_ui();
|
|
14465
16196
|
function buildStatusCommand() {
|
|
@@ -14490,6 +16221,7 @@ function buildStatusCommand() {
|
|
|
14490
16221
|
let me = null;
|
|
14491
16222
|
let devices = null;
|
|
14492
16223
|
let agents = null;
|
|
16224
|
+
let diagnose = null;
|
|
14493
16225
|
try {
|
|
14494
16226
|
const meRes = await cloud.request("GET", "/api/im/me", { timeoutMs: 3e3 });
|
|
14495
16227
|
cloudOk = meRes.ok;
|
|
@@ -14502,19 +16234,33 @@ function buildStatusCommand() {
|
|
|
14502
16234
|
if (Array.isArray(wsList) && wsList.length > 0) {
|
|
14503
16235
|
const wsId = wsList[0]?.id;
|
|
14504
16236
|
if (wsId) {
|
|
14505
|
-
const
|
|
14506
|
-
if (
|
|
14507
|
-
const
|
|
14508
|
-
|
|
14509
|
-
|
|
14510
|
-
|
|
14511
|
-
|
|
14512
|
-
|
|
14513
|
-
agents = agBody?.data;
|
|
16237
|
+
const rtRes = await cloud.request("GET", `/api/im/workspaces/${encodeURIComponent(wsId)}/runtime`, { timeoutMs: 3e3 });
|
|
16238
|
+
if (rtRes.ok) {
|
|
16239
|
+
const rtBody = rtRes.data;
|
|
16240
|
+
const snapshot = rtBody?.data;
|
|
16241
|
+
if (snapshot && Array.isArray(snapshot.devices)) {
|
|
16242
|
+
devices = snapshot.devices;
|
|
16243
|
+
agents = snapshot.devices.flatMap((d) => d.agents ?? []);
|
|
16244
|
+
}
|
|
14514
16245
|
}
|
|
14515
16246
|
}
|
|
14516
16247
|
}
|
|
14517
16248
|
}
|
|
16249
|
+
const daemonId = cfg.daemon_id;
|
|
16250
|
+
if (daemonId) {
|
|
16251
|
+
try {
|
|
16252
|
+
const diagRes = await cloud.request(
|
|
16253
|
+
"GET",
|
|
16254
|
+
`/api/im/runtime/diagnose?daemonId=${encodeURIComponent(daemonId)}`,
|
|
16255
|
+
{ timeoutMs: 3e3 }
|
|
16256
|
+
);
|
|
16257
|
+
if (diagRes.ok) {
|
|
16258
|
+
const body = diagRes.data;
|
|
16259
|
+
if (body?.data && typeof body.data === "object") diagnose = body.data;
|
|
16260
|
+
}
|
|
16261
|
+
} catch {
|
|
16262
|
+
}
|
|
16263
|
+
}
|
|
14518
16264
|
}
|
|
14519
16265
|
} catch {
|
|
14520
16266
|
cloudOk = false;
|
|
@@ -14531,7 +16277,8 @@ function buildStatusCommand() {
|
|
|
14531
16277
|
info: daemonStatus.info ?? {}
|
|
14532
16278
|
},
|
|
14533
16279
|
cloud: { base: cfg.cloud_api_base, reachable: cloudOk, me, devices, agents },
|
|
14534
|
-
local
|
|
16280
|
+
local,
|
|
16281
|
+
diagnose
|
|
14535
16282
|
};
|
|
14536
16283
|
if (opts.json) {
|
|
14537
16284
|
printJson(report);
|
|
@@ -14540,6 +16287,60 @@ function buildStatusCommand() {
|
|
|
14540
16287
|
printPretty(report);
|
|
14541
16288
|
});
|
|
14542
16289
|
}
|
|
16290
|
+
function computeDeviceIcon(lastSeenAt, now) {
|
|
16291
|
+
if (!lastSeenAt) return "\u25CB";
|
|
16292
|
+
const age = now.getTime() - Date.parse(lastSeenAt);
|
|
16293
|
+
if (!Number.isFinite(age) || age < 0) return "\u25CB";
|
|
16294
|
+
if (age < 5 * 6e4) return "\u25CF";
|
|
16295
|
+
if (age < 60 * 6e4) return "\u25D0";
|
|
16296
|
+
return "\u25CB";
|
|
16297
|
+
}
|
|
16298
|
+
function computeAgentIcon(status, lastHeartbeat, now) {
|
|
16299
|
+
const s = (status || "").toLowerCase();
|
|
16300
|
+
if (s === "error" || s === "failed") return "\u25C6";
|
|
16301
|
+
if (!lastHeartbeat) return s === "offline" ? "\u25CB" : "\u25D0";
|
|
16302
|
+
const age = now.getTime() - Date.parse(lastHeartbeat);
|
|
16303
|
+
if (!Number.isFinite(age) || age < 0 || age >= 60 * 6e4) return "\u25CB";
|
|
16304
|
+
if (age >= 5 * 6e4) return "\u25D0";
|
|
16305
|
+
return "\u25CF";
|
|
16306
|
+
}
|
|
16307
|
+
function pad(value, width) {
|
|
16308
|
+
if (value.length >= width) return value.slice(0, width);
|
|
16309
|
+
return value + " ".repeat(width - value.length);
|
|
16310
|
+
}
|
|
16311
|
+
function formatDeviceBindings(devices, now = /* @__PURE__ */ new Date()) {
|
|
16312
|
+
const lines = [];
|
|
16313
|
+
if (devices.length === 0) {
|
|
16314
|
+
lines.push(" No paired devices.");
|
|
16315
|
+
return lines;
|
|
16316
|
+
}
|
|
16317
|
+
const totalAgents = devices.reduce((s, d) => s + (d.agents?.length ?? 0), 0);
|
|
16318
|
+
const devWord = devices.length === 1 ? "device" : "devices";
|
|
16319
|
+
const agWord = totalAgents === 1 ? "agent" : "agents";
|
|
16320
|
+
lines.push(` Device & Agent Bindings (${devices.length} ${devWord} \xB7 ${totalAgents} ${agWord})`);
|
|
16321
|
+
lines.push("");
|
|
16322
|
+
for (let di = 0; di < devices.length; di++) {
|
|
16323
|
+
const d = devices[di];
|
|
16324
|
+
const dIcon = computeDeviceIcon(d.lastSeenAt, now);
|
|
16325
|
+
const dName = pad(d.name || d.deviceId, 40);
|
|
16326
|
+
const lastSeen = d.lastSeenAt ? formatRelative(d.lastSeenAt, now) : "never seen";
|
|
16327
|
+
lines.push(` ${dIcon} ${dName} ${lastSeen}`);
|
|
16328
|
+
const agents = d.agents ?? [];
|
|
16329
|
+
for (let ai = 0; ai < agents.length; ai++) {
|
|
16330
|
+
const a = agents[ai];
|
|
16331
|
+
const isLast = ai === agents.length - 1;
|
|
16332
|
+
const branch = isLast ? "\u2514\u2500" : "\u251C\u2500";
|
|
16333
|
+
const aIcon = computeAgentIcon(a.status, a.lastHeartbeat, now);
|
|
16334
|
+
const aName = pad(a.name || a.id, 24);
|
|
16335
|
+
const aStatus = pad(a.status || "?", 8);
|
|
16336
|
+
const aVersion = pad(a.version ? `v${a.version}` : "\u2014", 10);
|
|
16337
|
+
const tail = a.currentTaskId ? `task=${a.currentTaskId}` : a.lastHeartbeat ? `\u2764 ${formatRelative(a.lastHeartbeat, now)}` : "";
|
|
16338
|
+
lines.push(` ${branch} ${aIcon} ${aName} ${aStatus} ${aVersion} ${tail}`.trimEnd());
|
|
16339
|
+
}
|
|
16340
|
+
if (di < devices.length - 1) lines.push("");
|
|
16341
|
+
}
|
|
16342
|
+
return lines;
|
|
16343
|
+
}
|
|
14543
16344
|
async function readDaemonStatus() {
|
|
14544
16345
|
try {
|
|
14545
16346
|
const res = await fetch("http://127.0.0.1:3210/healthz", {
|
|
@@ -14583,7 +16384,7 @@ function printPretty(report) {
|
|
|
14583
16384
|
ui.blank();
|
|
14584
16385
|
ok("Config", report.paths.config);
|
|
14585
16386
|
if (report.daemon.running) {
|
|
14586
|
-
const daemonName = report.cloud.me && typeof report.cloud.me === "object" ? report.cloud.me.user?.username ?? (0,
|
|
16387
|
+
const daemonName = report.cloud.me && typeof report.cloud.me === "object" ? report.cloud.me.user?.username ?? (0, import_node_os15.hostname)() : (0, import_node_os15.hostname)();
|
|
14587
16388
|
const ws = report.daemon.wsConnected ? "connected" : "pending";
|
|
14588
16389
|
ok("Daemon", `${daemonName} pid=${report.daemon.pid} ws=${ws}`);
|
|
14589
16390
|
if (report.daemon.info) {
|
|
@@ -14612,16 +16413,7 @@ function printPretty(report) {
|
|
|
14612
16413
|
if (Array.isArray(report.cloud.devices) && report.cloud.devices.length > 0) {
|
|
14613
16414
|
const devs = report.cloud.devices;
|
|
14614
16415
|
ui.blank();
|
|
14615
|
-
|
|
14616
|
-
for (const d of devs) {
|
|
14617
|
-
const statusIcon = d.daemonStatus === "connected" ? "\u25CF" : "\u25CB";
|
|
14618
|
-
const kind = d.runtimeKind === "docker" ? "Local" : "K8s";
|
|
14619
|
-
const declared = d.hostedAgentSummary?.declared ?? 0;
|
|
14620
|
-
ui.line(` ${statusIcon} ${d.podName?.replace(/^daemon:/, "")?.slice(0, 28) ?? "?"} kind=${kind} agents=${declared}`);
|
|
14621
|
-
}
|
|
14622
|
-
}
|
|
14623
|
-
if (Array.isArray(report.cloud.agents) && report.cloud.agents.length > 0) {
|
|
14624
|
-
ui.line(` Hosted agents: ${report.cloud.agents.length}`);
|
|
16416
|
+
for (const ln of formatDeviceBindings(devs)) ui.line(ln);
|
|
14625
16417
|
}
|
|
14626
16418
|
if (report.local) {
|
|
14627
16419
|
ui.blank();
|
|
@@ -14631,6 +16423,58 @@ function printPretty(report) {
|
|
|
14631
16423
|
} else {
|
|
14632
16424
|
warn("Local DB", "unavailable");
|
|
14633
16425
|
}
|
|
16426
|
+
if (report.diagnose) {
|
|
16427
|
+
ui.blank();
|
|
16428
|
+
ui.header("Cloud dispatch reachability");
|
|
16429
|
+
ui.blank();
|
|
16430
|
+
printDiagnose(report.diagnose, report.daemon.pid ?? null);
|
|
16431
|
+
}
|
|
16432
|
+
}
|
|
16433
|
+
function printDiagnose(d, pid) {
|
|
16434
|
+
const ui = getUI();
|
|
16435
|
+
const heartbeatLine = d.lastHeartbeatAt ? `last heartbeat ${formatRelative(d.lastHeartbeatAt)}` : "no heartbeat seen";
|
|
16436
|
+
if (d.wsAlive) {
|
|
16437
|
+
ok("Daemon", `running${pid ? ` (pid ${pid})` : ""}`);
|
|
16438
|
+
ok("Cloud WS", `connected (${heartbeatLine})`);
|
|
16439
|
+
} else {
|
|
16440
|
+
warn("Daemon", `running${pid ? ` (pid ${pid})` : ""} \u2014 not visible to cloud WS`);
|
|
16441
|
+
fail("Cloud WS", `disconnected (${heartbeatLine})`);
|
|
16442
|
+
}
|
|
16443
|
+
if (d.gatewayUrl) {
|
|
16444
|
+
if (d.gatewayIsPrivate) {
|
|
16445
|
+
warn("HTTP gateway", `${d.gatewayUrl} (private IP \u2014 only reachable on same network)`);
|
|
16446
|
+
} else if (d.httpReachable) {
|
|
16447
|
+
ok(
|
|
16448
|
+
"HTTP gateway",
|
|
16449
|
+
`${d.gatewayUrl} (reachable${d.httpLatencyMs != null ? `, ${d.httpLatencyMs}ms` : ""})`
|
|
16450
|
+
);
|
|
16451
|
+
} else {
|
|
16452
|
+
fail("HTTP gateway", `${d.gatewayUrl} (unreachable${d.httpError ? ` \u2014 ${d.httpError}` : ""})`);
|
|
16453
|
+
}
|
|
16454
|
+
} else {
|
|
16455
|
+
ui.line(" HTTP gateway: not declared (WS-only daemon)");
|
|
16456
|
+
}
|
|
16457
|
+
const activeAgents = /* @__PURE__ */ (() => {
|
|
16458
|
+
return 0;
|
|
16459
|
+
})();
|
|
16460
|
+
if (activeAgents > 0) {
|
|
16461
|
+
ok("Active agents", String(activeAgents));
|
|
16462
|
+
}
|
|
16463
|
+
const recColour = d.recommendedTransport === "ws" ? ok : d.recommendedTransport === "http" ? warn : fail;
|
|
16464
|
+
recColour("Recommended transport", d.recommendedTransport);
|
|
16465
|
+
if (d.lastProbeAt) {
|
|
16466
|
+
ui.line(` Last transport probe: ${formatRelative(d.lastProbeAt)}`);
|
|
16467
|
+
}
|
|
16468
|
+
}
|
|
16469
|
+
function formatRelative(iso, now = /* @__PURE__ */ new Date()) {
|
|
16470
|
+
const ts = Date.parse(iso);
|
|
16471
|
+
if (!Number.isFinite(ts)) return "unknown";
|
|
16472
|
+
const diff = Math.max(0, now.getTime() - ts);
|
|
16473
|
+
if (diff < 3e4) return "just now";
|
|
16474
|
+
if (diff < 6e4) return `${Math.round(diff / 1e3)}s ago`;
|
|
16475
|
+
if (diff < 36e5) return `${Math.round(diff / 6e4)}m ago`;
|
|
16476
|
+
if (diff < 864e5) return `${Math.round(diff / 36e5)}h ago`;
|
|
16477
|
+
return `${Math.round(diff / 864e5)}d ago`;
|
|
14634
16478
|
}
|
|
14635
16479
|
|
|
14636
16480
|
// src/cli/commands/task.ts
|
|
@@ -14993,7 +16837,7 @@ async function readResponseError(res) {
|
|
|
14993
16837
|
|
|
14994
16838
|
// src/cli/index.ts
|
|
14995
16839
|
init_ui();
|
|
14996
|
-
var VERSION = "2.0.
|
|
16840
|
+
var VERSION = "2.0.5";
|
|
14997
16841
|
function buildProgram() {
|
|
14998
16842
|
const program = new import_commander20.Command("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
|
|
14999
16843
|
program.addCommand(buildBannerCommand());
|