@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.js
CHANGED
|
@@ -154,6 +154,42 @@ import { fileURLToPath } from "url";
|
|
|
154
154
|
import Database from "better-sqlite3";
|
|
155
155
|
import * as YAML from "yaml";
|
|
156
156
|
import { z as z3 } from "zod";
|
|
157
|
+
function readPlainRecord(value) {
|
|
158
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
159
|
+
}
|
|
160
|
+
function readNonEmptyString(value) {
|
|
161
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
162
|
+
}
|
|
163
|
+
function sanitizeRoleSkillSlug(value) {
|
|
164
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "template";
|
|
165
|
+
}
|
|
166
|
+
function renderRoleTemplateSkill(skillName, agents, roleTemplate) {
|
|
167
|
+
const mcpServers = Array.isArray(roleTemplate?.mcpServers) ? roleTemplate.mcpServers : [];
|
|
168
|
+
const tools = /* @__PURE__ */ new Set();
|
|
169
|
+
for (const server of mcpServers) {
|
|
170
|
+
const rec = readPlainRecord(server);
|
|
171
|
+
const allowlist = Array.isArray(rec?.toolsAllowlist) ? rec.toolsAllowlist : [];
|
|
172
|
+
for (const tool of allowlist) {
|
|
173
|
+
if (typeof tool === "string" && tool.trim()) tools.add(tool.trim());
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const allowedTools = [...tools].join(" ");
|
|
177
|
+
const roleTemplateSlug = JSON.stringify(readNonEmptyString(roleTemplate?.slug) ?? null);
|
|
178
|
+
return `---
|
|
179
|
+
name: ${skillName}
|
|
180
|
+
description: Role-template operating playbook projected from Prismer RoleTemplate.
|
|
181
|
+
${allowedTools ? `allowed-tools: ${allowedTools}
|
|
182
|
+
` : ""}metadata:
|
|
183
|
+
prismer:
|
|
184
|
+
source: role-template
|
|
185
|
+
roleTemplateSlug: ${roleTemplateSlug}
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
# Role Template Playbook
|
|
189
|
+
|
|
190
|
+
${agents.trim()}
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
157
193
|
function parsePrismerGoals(value) {
|
|
158
194
|
if (!Array.isArray(value)) return [];
|
|
159
195
|
return value.map((entry) => {
|
|
@@ -563,13 +599,14 @@ function failure(status, body) {
|
|
|
563
599
|
error: { code: "adapter_dispatch_failed", message: `Hermes ${status}: ${body || "<no body>"}` }
|
|
564
600
|
};
|
|
565
601
|
}
|
|
566
|
-
async function consumeSse(body, task) {
|
|
602
|
+
async function consumeSse(body, task, state) {
|
|
567
603
|
const reader = body.getReader();
|
|
568
604
|
const decoder = new TextDecoder();
|
|
569
605
|
let buf = "";
|
|
570
606
|
let deltas = "";
|
|
571
607
|
let finalOutput;
|
|
572
608
|
let lastProgress = 0;
|
|
609
|
+
let approvalRequested = false;
|
|
573
610
|
for (; ; ) {
|
|
574
611
|
const readStart = Date.now();
|
|
575
612
|
const { value, done } = await reader.read();
|
|
@@ -618,6 +655,10 @@ async function consumeSse(body, task) {
|
|
|
618
655
|
const next = Math.min(0.99, lastProgress + 0.05);
|
|
619
656
|
lastProgress = next;
|
|
620
657
|
const toolName = typeof payload.tool === "string" ? payload.tool : typeof payload.name === "string" ? payload.name : void 0;
|
|
658
|
+
if (eventName === "tool.started" && toolName === APPROVAL_TOOL_NAME) {
|
|
659
|
+
approvalRequested = true;
|
|
660
|
+
if (state) state.approvalRequested = true;
|
|
661
|
+
}
|
|
621
662
|
const previewStr = typeof payload.preview === "string" ? payload.preview : payload.preview != null ? JSON.stringify(payload.preview).slice(0, 200) : void 0;
|
|
622
663
|
task.onProgress?.({
|
|
623
664
|
progress: next,
|
|
@@ -647,7 +688,53 @@ async function consumeSse(body, task) {
|
|
|
647
688
|
}
|
|
648
689
|
}
|
|
649
690
|
}
|
|
650
|
-
return
|
|
691
|
+
return {
|
|
692
|
+
output: finalOutput && finalOutput.length > 0 ? finalOutput : deltas,
|
|
693
|
+
approvalRequested
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
async function consumeChatCompletionsSse(body, task) {
|
|
697
|
+
const reader = body.getReader();
|
|
698
|
+
const decoder = new TextDecoder();
|
|
699
|
+
let buf = "";
|
|
700
|
+
let output = "";
|
|
701
|
+
let lastProgress = 0;
|
|
702
|
+
for (; ; ) {
|
|
703
|
+
const { value, done } = await reader.read();
|
|
704
|
+
if (done) break;
|
|
705
|
+
buf += decoder.decode(value, { stream: true });
|
|
706
|
+
const events = buf.split("\n\n");
|
|
707
|
+
buf = events.pop() ?? "";
|
|
708
|
+
for (const ev of events) {
|
|
709
|
+
const dataLine = ev.match(/^data: (.+)$/m);
|
|
710
|
+
if (!dataLine) continue;
|
|
711
|
+
const raw = dataLine[1].trim();
|
|
712
|
+
if (raw === "[DONE]") continue;
|
|
713
|
+
let payload;
|
|
714
|
+
try {
|
|
715
|
+
payload = JSON.parse(raw);
|
|
716
|
+
} catch (err) {
|
|
717
|
+
process.stderr.write(
|
|
718
|
+
`[hermes-adapter] chat-completions SSE parse skipped: ${err.message}
|
|
719
|
+
`
|
|
720
|
+
);
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
const delta = payload.choices?.[0]?.delta?.content;
|
|
724
|
+
if (typeof delta === "string") {
|
|
725
|
+
output += delta;
|
|
726
|
+
} else if (Array.isArray(delta)) {
|
|
727
|
+
for (const part of delta) {
|
|
728
|
+
if (part?.type === "text" && typeof part.text === "string") output += part.text;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (output.length > 0 && lastProgress < 0.99) {
|
|
732
|
+
lastProgress = Math.min(0.99, lastProgress + 0.05);
|
|
733
|
+
task.onProgress?.({ progress: lastProgress, message: "streaming" });
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return output;
|
|
651
738
|
}
|
|
652
739
|
async function checkHealth(baseUrl, apiKey) {
|
|
653
740
|
try {
|
|
@@ -728,13 +815,14 @@ function pidAlive(pid) {
|
|
|
728
815
|
function escapeRegExp(value) {
|
|
729
816
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
730
817
|
}
|
|
731
|
-
var PRISMER_IM_SKILL_NAME, PRISMER_IM_SKILL_CONTENT, HermesProfileConfigSchema, hermesAdapter, HermesService;
|
|
818
|
+
var PRISMER_IM_SKILL_NAME, PRISMER_ROLE_SKILL_PREFIX, PRISMER_IM_SKILL_CONTENT, HermesProfileConfigSchema, hermesAdapter, HermesService, APPROVAL_TOOL_NAME;
|
|
732
819
|
var init_hermes = __esm({
|
|
733
820
|
"src/adapters/hermes/index.ts"() {
|
|
734
821
|
"use strict";
|
|
735
822
|
init_contract();
|
|
736
823
|
init_skill_loader2();
|
|
737
824
|
PRISMER_IM_SKILL_NAME = "prismer-im-collab";
|
|
825
|
+
PRISMER_ROLE_SKILL_PREFIX = "prismer-role";
|
|
738
826
|
PRISMER_IM_SKILL_CONTENT = `---
|
|
739
827
|
name: prismer-im-collab
|
|
740
828
|
description: Coordinate reliably in Prismer conversations, use workspace assets through bounded MCP tools, and keep task work on the board.
|
|
@@ -1047,6 +1135,7 @@ No @ needed; the human is the only other party.
|
|
|
1047
1135
|
const startedAt = Date.now();
|
|
1048
1136
|
let runId;
|
|
1049
1137
|
const sysPrompt = typeof task.metadata?.systemPrompt === "string" ? task.metadata.systemPrompt : void 0;
|
|
1138
|
+
this.installRoleTemplateSkill();
|
|
1050
1139
|
const skillPrompt = await this.skillLoader.loadSystemPromptFragment().catch((err) => {
|
|
1051
1140
|
process.stderr.write(
|
|
1052
1141
|
`[hermes-adapter] failed to read skills for ${this.profileName}: ${err.message}
|
|
@@ -1054,7 +1143,21 @@ No @ needed; the human is the only other party.
|
|
|
1054
1143
|
);
|
|
1055
1144
|
return void 0;
|
|
1056
1145
|
});
|
|
1057
|
-
const
|
|
1146
|
+
const outboxDir = typeof task.metadata?.prismerOutboxDir === "string" ? task.metadata.prismerOutboxDir : void 0;
|
|
1147
|
+
const outboxDirective = outboxDir ? [
|
|
1148
|
+
"[Outbox directive \u2014 MANDATORY, overrides any prior outbox path]",
|
|
1149
|
+
"",
|
|
1150
|
+
"For THIS turn ONLY, your active outbox is:",
|
|
1151
|
+
` ${outboxDir}`,
|
|
1152
|
+
"",
|
|
1153
|
+
"Any user-deliverable file (PDF, image, CSV, archive, doc) MUST be",
|
|
1154
|
+
"written to that EXACT absolute path. Do NOT reuse the outbox path",
|
|
1155
|
+
"from any previous turn \u2014 those directories no longer accept new",
|
|
1156
|
+
"uploads. Do NOT copy files into the previous outbox; copy them",
|
|
1157
|
+
"into the path above. Files outside this directory will not become",
|
|
1158
|
+
'chat attachments and the user will see "no files were attached".'
|
|
1159
|
+
].join("\n") : "";
|
|
1160
|
+
const instructions = [outboxDirective, sysPrompt, skillPrompt].filter(Boolean).join("\n\n");
|
|
1058
1161
|
if (sysPrompt) {
|
|
1059
1162
|
try {
|
|
1060
1163
|
const profileDir = getHermesProfileDir(this.profileName);
|
|
@@ -1078,8 +1181,15 @@ No @ needed; the human is the only other party.
|
|
|
1078
1181
|
`
|
|
1079
1182
|
);
|
|
1080
1183
|
}
|
|
1184
|
+
const sseState = { approvalRequested: false };
|
|
1081
1185
|
try {
|
|
1082
1186
|
const nativeBridgePatch = await this.prepareNativeBridgePatch(task);
|
|
1187
|
+
const imageRefs = (task.assetRefs ?? []).filter(
|
|
1188
|
+
(r) => r.mime?.startsWith("image/")
|
|
1189
|
+
);
|
|
1190
|
+
if (imageRefs.length > 0) {
|
|
1191
|
+
return await this.dispatchMultimodalChat(task, instructions, imageRefs, startedAt);
|
|
1192
|
+
}
|
|
1083
1193
|
const createRes = await fetch(`${this.baseUrl}/v1/runs`, {
|
|
1084
1194
|
method: "POST",
|
|
1085
1195
|
headers: {
|
|
@@ -1130,10 +1240,10 @@ No @ needed; the human is the only other party.
|
|
|
1130
1240
|
}
|
|
1131
1241
|
};
|
|
1132
1242
|
}
|
|
1133
|
-
const
|
|
1243
|
+
const sseResult = await consumeSse(eventsRes.body, task, sseState);
|
|
1134
1244
|
return {
|
|
1135
1245
|
ok: true,
|
|
1136
|
-
output,
|
|
1246
|
+
output: sseResult.output,
|
|
1137
1247
|
metrics: { durationMs: Date.now() - startedAt },
|
|
1138
1248
|
metadata: {
|
|
1139
1249
|
hermes: this.bridgeSnapshot("dispatched", {
|
|
@@ -1141,7 +1251,13 @@ No @ needed; the human is the only other party.
|
|
|
1141
1251
|
runId,
|
|
1142
1252
|
baseUrl: this.baseUrl,
|
|
1143
1253
|
model: this.config.model
|
|
1144
|
-
})
|
|
1254
|
+
}),
|
|
1255
|
+
// Surfaced to dispatch.ts so a subsequent reaper kill on this
|
|
1256
|
+
// task is reclassified as `awaiting_human_approval` rather than
|
|
1257
|
+
// `daemon_task_timeout`. Lives under a stable key (not under
|
|
1258
|
+
// `hermes.*`) so non-hermes adapters can adopt the same signal
|
|
1259
|
+
// later without bleeding hermes-specific metadata shape.
|
|
1260
|
+
...sseResult.approvalRequested ? { approvalRequested: true } : {}
|
|
1145
1261
|
}
|
|
1146
1262
|
};
|
|
1147
1263
|
} catch (err) {
|
|
@@ -1151,6 +1267,7 @@ No @ needed; the human is the only other party.
|
|
|
1151
1267
|
);
|
|
1152
1268
|
const categorized = categorizeDispatchError(err, task.signal);
|
|
1153
1269
|
const bridgeKind = categorized.error?.code === "task_cancelled" ? "cancelled" : "failed";
|
|
1270
|
+
const approvalSignal = sseState.approvalRequested ? { approvalRequested: true } : {};
|
|
1154
1271
|
return {
|
|
1155
1272
|
...categorized,
|
|
1156
1273
|
metadata: {
|
|
@@ -1159,13 +1276,87 @@ No @ needed; the human is the only other party.
|
|
|
1159
1276
|
baseUrl: this.baseUrl,
|
|
1160
1277
|
model: this.config.model,
|
|
1161
1278
|
...bridgeKind === "failed" ? { error: err.message } : {}
|
|
1162
|
-
})
|
|
1279
|
+
}),
|
|
1280
|
+
...approvalSignal
|
|
1163
1281
|
}
|
|
1164
1282
|
};
|
|
1165
1283
|
} finally {
|
|
1166
1284
|
if (runId && this.currentRunId === runId) this.currentRunId = void 0;
|
|
1167
1285
|
}
|
|
1168
1286
|
}
|
|
1287
|
+
/**
|
|
1288
|
+
* 14b rev.3 §3.0.4 — Hermes multimodal path.
|
|
1289
|
+
*
|
|
1290
|
+
* Builds an OpenAI-shape chat/completions body where the user message
|
|
1291
|
+
* `content` is a multipart array (`{type:'text'}` + N × `{type:'image_url'}`).
|
|
1292
|
+
* Hermes `_normalize_multimodal_content` (api_server.py:170) collapses the
|
|
1293
|
+
* blocks into its native multimodal representation before routing into
|
|
1294
|
+
* the agent pipeline.
|
|
1295
|
+
*
|
|
1296
|
+
* `image_url.url` is the cdn URL when the daemon's reachability probe said
|
|
1297
|
+
* the upstream LLM can see it (D35 path A); otherwise it's a `data:` URI
|
|
1298
|
+
* carrying the base64 bytes the daemon pre-fetched (D35 path B fallback).
|
|
1299
|
+
* The adapter doesn't decide — `resolveAssetRefs` does, and the adapter
|
|
1300
|
+
* just shuttles `cdnUrl ?? data:base64` into the wire.
|
|
1301
|
+
*/
|
|
1302
|
+
async dispatchMultimodalChat(task, instructions, imageRefs, startedAt) {
|
|
1303
|
+
const userContent = [{ type: "text", text: task.prompt }];
|
|
1304
|
+
for (const r of imageRefs) {
|
|
1305
|
+
const url = r.reachable === "cdn" && r.cdnUrl ? r.cdnUrl : r.base64 ? `data:${r.mime ?? "image/png"};base64,${r.base64}` : r.cdnUrl ?? "";
|
|
1306
|
+
if (!url) {
|
|
1307
|
+
userContent.push({
|
|
1308
|
+
type: "text",
|
|
1309
|
+
text: `[image attachment id=${r.assetId} mime=${r.mime ?? "unknown"} unavailable: cdn not reachable and bytes exceeded inline cap]`
|
|
1310
|
+
});
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
userContent.push({ type: "image_url", image_url: { url, detail: "auto" } });
|
|
1314
|
+
}
|
|
1315
|
+
const messages = [];
|
|
1316
|
+
if (instructions) messages.push({ role: "system", content: instructions });
|
|
1317
|
+
messages.push({ role: "user", content: userContent });
|
|
1318
|
+
const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
1319
|
+
method: "POST",
|
|
1320
|
+
headers: {
|
|
1321
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
1322
|
+
"Content-Type": "application/json"
|
|
1323
|
+
},
|
|
1324
|
+
body: JSON.stringify({
|
|
1325
|
+
model: this.config.model,
|
|
1326
|
+
messages,
|
|
1327
|
+
stream: true
|
|
1328
|
+
}),
|
|
1329
|
+
signal: task.signal
|
|
1330
|
+
});
|
|
1331
|
+
if (!res.ok) {
|
|
1332
|
+
const body = await res.text().catch(() => "");
|
|
1333
|
+
return failure(res.status, body);
|
|
1334
|
+
}
|
|
1335
|
+
if (!res.body) {
|
|
1336
|
+
return {
|
|
1337
|
+
ok: false,
|
|
1338
|
+
error: {
|
|
1339
|
+
code: "adapter_dispatch_failed",
|
|
1340
|
+
message: "Hermes /v1/chat/completions returned no body"
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
const output = await consumeChatCompletionsSse(res.body, task);
|
|
1345
|
+
return {
|
|
1346
|
+
ok: true,
|
|
1347
|
+
output,
|
|
1348
|
+
metrics: { durationMs: Date.now() - startedAt },
|
|
1349
|
+
metadata: {
|
|
1350
|
+
hermes: this.bridgeSnapshot("dispatched", {
|
|
1351
|
+
endpoint: "/v1/chat/completions",
|
|
1352
|
+
multimodal: true,
|
|
1353
|
+
imageRefs: imageRefs.length,
|
|
1354
|
+
baseUrl: this.baseUrl,
|
|
1355
|
+
model: this.config.model
|
|
1356
|
+
})
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1169
1360
|
async shutdown() {
|
|
1170
1361
|
if (this.currentRunId) await this.stopRun(this.currentRunId);
|
|
1171
1362
|
}
|
|
@@ -1187,6 +1378,25 @@ No @ needed; the human is the only other party.
|
|
|
1187
1378
|
]);
|
|
1188
1379
|
return { ...kanbanPatch, ...goalsPatch };
|
|
1189
1380
|
}
|
|
1381
|
+
installRoleTemplateSkill() {
|
|
1382
|
+
const roleTemplate = readPlainRecord(this.config.roleTemplate);
|
|
1383
|
+
const hermesConfig = readPlainRecord(roleTemplate?.hermesConfig);
|
|
1384
|
+
const agents = readNonEmptyString(hermesConfig?.agents);
|
|
1385
|
+
if (!agents) return;
|
|
1386
|
+
const slug = sanitizeRoleSkillSlug(readNonEmptyString(roleTemplate?.slug) ?? "template");
|
|
1387
|
+
const skillName = `${PRISMER_ROLE_SKILL_PREFIX}-${slug}`;
|
|
1388
|
+
const content = renderRoleTemplateSkill(skillName, agents, roleTemplate);
|
|
1389
|
+
try {
|
|
1390
|
+
const skillDir = join2(this.skillLoader.getSkillsRoot(), skillName);
|
|
1391
|
+
mkdirSync(skillDir, { recursive: true });
|
|
1392
|
+
writeFileSync(join2(skillDir, "SKILL.md"), content, "utf8");
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
process.stderr.write(
|
|
1395
|
+
`[hermes-adapter] failed to install role-template skill ${skillName} for ${this.profileName}: ${err.message}
|
|
1396
|
+
`
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1190
1400
|
async prepareNativeKanbanPatch(task) {
|
|
1191
1401
|
const sourceKind = typeof task.metadata?.sourceKind === "string" ? task.metadata.sourceKind : null;
|
|
1192
1402
|
const parentTaskId = typeof task.metadata?.parentTaskId === "string" ? task.metadata.parentTaskId : null;
|
|
@@ -1295,6 +1505,7 @@ No @ needed; the human is the only other party.
|
|
|
1295
1505
|
};
|
|
1296
1506
|
}
|
|
1297
1507
|
};
|
|
1508
|
+
APPROVAL_TOOL_NAME = "prismer.approval.request_human_approval";
|
|
1298
1509
|
}
|
|
1299
1510
|
});
|
|
1300
1511
|
|
|
@@ -1327,8 +1538,61 @@ var init_skill_loader3 = __esm({
|
|
|
1327
1538
|
|
|
1328
1539
|
// src/adapters/openclaw/index.ts
|
|
1329
1540
|
import { z as z4 } from "zod";
|
|
1541
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1330
1542
|
import { homedir as homedir2 } from "os";
|
|
1331
1543
|
import { join as join3 } from "path";
|
|
1544
|
+
function readPlainRecord2(value) {
|
|
1545
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1546
|
+
}
|
|
1547
|
+
function readNonEmptyString2(value) {
|
|
1548
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1549
|
+
}
|
|
1550
|
+
function sanitizeRoleSkillSlug2(value) {
|
|
1551
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "template";
|
|
1552
|
+
}
|
|
1553
|
+
function renderRoleTemplateSkill2(skillName, agents, roleTemplate) {
|
|
1554
|
+
const mcpServers = Array.isArray(roleTemplate?.mcpServers) ? roleTemplate.mcpServers : [];
|
|
1555
|
+
const tools = /* @__PURE__ */ new Set();
|
|
1556
|
+
for (const server of mcpServers) {
|
|
1557
|
+
const rec = readPlainRecord2(server);
|
|
1558
|
+
const allowlist = Array.isArray(rec?.toolsAllowlist) ? rec.toolsAllowlist : [];
|
|
1559
|
+
for (const tool of allowlist) {
|
|
1560
|
+
if (typeof tool === "string" && tool.trim()) tools.add(tool.trim());
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
const allowedTools = [...tools].join(" ");
|
|
1564
|
+
const roleTemplateSlug = JSON.stringify(readNonEmptyString2(roleTemplate?.slug) ?? null);
|
|
1565
|
+
return `---
|
|
1566
|
+
name: ${skillName}
|
|
1567
|
+
description: Role-template operating playbook projected from Prismer RoleTemplate.
|
|
1568
|
+
${allowedTools ? `allowed-tools: ${allowedTools}
|
|
1569
|
+
` : ""}metadata:
|
|
1570
|
+
prismer:
|
|
1571
|
+
source: role-template
|
|
1572
|
+
roleTemplateSlug: ${roleTemplateSlug}
|
|
1573
|
+
---
|
|
1574
|
+
|
|
1575
|
+
# Role Template Playbook
|
|
1576
|
+
|
|
1577
|
+
${agents.trim()}
|
|
1578
|
+
`;
|
|
1579
|
+
}
|
|
1580
|
+
function pickResponsesSource(ref) {
|
|
1581
|
+
if (ref.reachable === "cdn" && ref.cdnUrl) {
|
|
1582
|
+
return { type: "url", url: ref.cdnUrl };
|
|
1583
|
+
}
|
|
1584
|
+
if (ref.base64) {
|
|
1585
|
+
return {
|
|
1586
|
+
type: "base64",
|
|
1587
|
+
data: ref.base64,
|
|
1588
|
+
media_type: ref.mime ?? "application/octet-stream"
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
if (ref.cdnUrl) {
|
|
1592
|
+
return { type: "url", url: ref.cdnUrl };
|
|
1593
|
+
}
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1332
1596
|
function getOpenClawSkillsDir(profile) {
|
|
1333
1597
|
const config = OpenClawProfileConfigSchema.parse(profile.config);
|
|
1334
1598
|
return resolveOpenClawSkillsDir(profile, config);
|
|
@@ -1349,12 +1613,13 @@ async function checkHealth2(baseUrl, apiKey) {
|
|
|
1349
1613
|
return false;
|
|
1350
1614
|
}
|
|
1351
1615
|
}
|
|
1352
|
-
var OpenClawProfileConfigSchema, openclawAdapter, OpenClawService;
|
|
1616
|
+
var PRISMER_ROLE_SKILL_PREFIX2, OpenClawProfileConfigSchema, openclawAdapter, OpenClawService;
|
|
1353
1617
|
var init_openclaw = __esm({
|
|
1354
1618
|
"src/adapters/openclaw/index.ts"() {
|
|
1355
1619
|
"use strict";
|
|
1356
1620
|
init_contract();
|
|
1357
1621
|
init_skill_loader3();
|
|
1622
|
+
PRISMER_ROLE_SKILL_PREFIX2 = "prismer-role";
|
|
1358
1623
|
OpenClawProfileConfigSchema = z4.object({
|
|
1359
1624
|
/** OpenClaw chat-completions HTTP port. */
|
|
1360
1625
|
port: z4.number().int().min(1).max(65535).default(18789),
|
|
@@ -1399,23 +1664,25 @@ var init_openclaw = __esm({
|
|
|
1399
1664
|
);
|
|
1400
1665
|
}
|
|
1401
1666
|
const skillsDir = resolveOpenClawSkillsDir(profile, config);
|
|
1402
|
-
return new OpenClawService(baseUrl, config.apiKey, config.model, new OpenClawSkillLoader(skillsDir));
|
|
1667
|
+
return new OpenClawService(baseUrl, config.apiKey, config.model, config, new OpenClawSkillLoader(skillsDir));
|
|
1403
1668
|
},
|
|
1404
1669
|
async health() {
|
|
1405
1670
|
return { available: true };
|
|
1406
1671
|
}
|
|
1407
1672
|
};
|
|
1408
1673
|
OpenClawService = class {
|
|
1409
|
-
constructor(baseUrl, apiKey, model, skillLoader) {
|
|
1674
|
+
constructor(baseUrl, apiKey, model, config, skillLoader) {
|
|
1410
1675
|
this.baseUrl = baseUrl;
|
|
1411
1676
|
this.apiKey = apiKey;
|
|
1412
1677
|
this.model = model;
|
|
1678
|
+
this.config = config;
|
|
1413
1679
|
this.skillLoader = skillLoader;
|
|
1414
1680
|
this.id = baseUrl;
|
|
1415
1681
|
}
|
|
1416
1682
|
baseUrl;
|
|
1417
1683
|
apiKey;
|
|
1418
1684
|
model;
|
|
1685
|
+
config;
|
|
1419
1686
|
skillLoader;
|
|
1420
1687
|
id;
|
|
1421
1688
|
async healthy() {
|
|
@@ -1424,17 +1691,47 @@ var init_openclaw = __esm({
|
|
|
1424
1691
|
async dispatch(task) {
|
|
1425
1692
|
const startedAt = Date.now();
|
|
1426
1693
|
const systemPrompt = typeof task.metadata?.systemPrompt === "string" ? task.metadata.systemPrompt : void 0;
|
|
1694
|
+
this.installRoleTemplateSkill();
|
|
1427
1695
|
const skillPrompt = await this.skillLoader.loadSystemPromptFragment().catch((err) => {
|
|
1428
1696
|
process.stderr.write(`[openclaw-adapter] failed to read skills: ${err.message}
|
|
1429
1697
|
`);
|
|
1430
1698
|
return void 0;
|
|
1431
1699
|
});
|
|
1432
1700
|
const systemContent = [systemPrompt, skillPrompt].filter(Boolean).join("\n\n");
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1435
|
-
|
|
1701
|
+
const refs = task.assetRefs ?? [];
|
|
1702
|
+
const imageRefs = refs.filter((r) => r.mime?.startsWith("image/"));
|
|
1703
|
+
const fileRefs = refs.filter((r) => r.mime && !r.mime.startsWith("image/"));
|
|
1704
|
+
const inputContent = [
|
|
1705
|
+
{ type: "input_text", text: task.prompt }
|
|
1706
|
+
];
|
|
1707
|
+
for (const r of imageRefs) {
|
|
1708
|
+
const source = pickResponsesSource(r);
|
|
1709
|
+
if (!source) {
|
|
1710
|
+
inputContent.push({
|
|
1711
|
+
type: "input_text",
|
|
1712
|
+
text: `[image attachment id=${r.assetId} unavailable: cdn unreachable and bytes exceeded inline cap]`
|
|
1713
|
+
});
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1716
|
+
inputContent.push({ type: "input_image", source });
|
|
1717
|
+
}
|
|
1718
|
+
for (const r of fileRefs) {
|
|
1719
|
+
const source = pickResponsesSource(r);
|
|
1720
|
+
if (!source) {
|
|
1721
|
+
inputContent.push({
|
|
1722
|
+
type: "input_text",
|
|
1723
|
+
text: `[file attachment id=${r.assetId} mime=${r.mime ?? "unknown"} unavailable]`
|
|
1724
|
+
});
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
if (r.filename && source.type === "base64") {
|
|
1728
|
+
source.filename = r.filename;
|
|
1729
|
+
}
|
|
1730
|
+
inputContent.push({ type: "input_file", source });
|
|
1731
|
+
}
|
|
1732
|
+
const input = [{ type: "message", role: "user", content: inputContent }];
|
|
1436
1733
|
try {
|
|
1437
|
-
const res = await fetch(`${this.baseUrl}/v1/
|
|
1734
|
+
const res = await fetch(`${this.baseUrl}/v1/responses`, {
|
|
1438
1735
|
method: "POST",
|
|
1439
1736
|
headers: {
|
|
1440
1737
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -1442,7 +1739,11 @@ var init_openclaw = __esm({
|
|
|
1442
1739
|
},
|
|
1443
1740
|
body: JSON.stringify({
|
|
1444
1741
|
model: this.model,
|
|
1445
|
-
|
|
1742
|
+
input,
|
|
1743
|
+
// OpenClaw `/v1/responses` accepts top-level `instructions` for
|
|
1744
|
+
// the system prompt — keeping it out of the user message
|
|
1745
|
+
// preserves cleanish channel-context separation.
|
|
1746
|
+
...systemContent ? { instructions: systemContent } : {},
|
|
1446
1747
|
stream: false
|
|
1447
1748
|
}),
|
|
1448
1749
|
signal: task.signal
|
|
@@ -1458,7 +1759,21 @@ var init_openclaw = __esm({
|
|
|
1458
1759
|
};
|
|
1459
1760
|
}
|
|
1460
1761
|
const json = await res.json();
|
|
1461
|
-
let output =
|
|
1762
|
+
let output = "";
|
|
1763
|
+
if (Array.isArray(json.output)) {
|
|
1764
|
+
for (const item of json.output) {
|
|
1765
|
+
if (item?.type === "message" && Array.isArray(item.content)) {
|
|
1766
|
+
for (const part of item.content) {
|
|
1767
|
+
if (part?.type === "output_text" && typeof part.text === "string") {
|
|
1768
|
+
output += part.text;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
if (!output) {
|
|
1775
|
+
output = json.choices?.[0]?.message?.content ?? "";
|
|
1776
|
+
}
|
|
1462
1777
|
const ERR_PREFIX_RE = /^Cannot read properties of undefined \(reading '[^']+'\)\s*\n+/;
|
|
1463
1778
|
if (ERR_PREFIX_RE.test(output)) {
|
|
1464
1779
|
process.stderr.write(
|
|
@@ -1476,6 +1791,25 @@ var init_openclaw = __esm({
|
|
|
1476
1791
|
return categorizeDispatchError(err, task.signal);
|
|
1477
1792
|
}
|
|
1478
1793
|
}
|
|
1794
|
+
installRoleTemplateSkill() {
|
|
1795
|
+
const roleTemplate = readPlainRecord2(this.config.roleTemplate);
|
|
1796
|
+
const openclawConfig = readPlainRecord2(roleTemplate?.openclawConfig);
|
|
1797
|
+
const agents = readNonEmptyString2(openclawConfig?.agents);
|
|
1798
|
+
if (!agents) return;
|
|
1799
|
+
const slug = sanitizeRoleSkillSlug2(readNonEmptyString2(roleTemplate?.slug) ?? "template");
|
|
1800
|
+
const skillName = `${PRISMER_ROLE_SKILL_PREFIX2}-${slug}`;
|
|
1801
|
+
const content = renderRoleTemplateSkill2(skillName, agents, roleTemplate);
|
|
1802
|
+
try {
|
|
1803
|
+
const skillDir = join3(this.skillLoader.getSkillsRoot(), skillName);
|
|
1804
|
+
mkdirSync2(skillDir, { recursive: true });
|
|
1805
|
+
writeFileSync2(join3(skillDir, "SKILL.md"), content, "utf8");
|
|
1806
|
+
} catch (err) {
|
|
1807
|
+
process.stderr.write(
|
|
1808
|
+
`[openclaw-adapter] failed to install role-template skill ${skillName}: ${err.message}
|
|
1809
|
+
`
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1479
1813
|
};
|
|
1480
1814
|
}
|
|
1481
1815
|
});
|
|
@@ -1911,7 +2245,7 @@ var require_package = __commonJS({
|
|
|
1911
2245
|
"package.json"(exports, module) {
|
|
1912
2246
|
module.exports = {
|
|
1913
2247
|
name: "@prismer/runtime",
|
|
1914
|
-
version: "2.0.
|
|
2248
|
+
version: "2.0.5",
|
|
1915
2249
|
description: "Prismer Cloud daemon runtime \u2014 TS-only adapter host for hosted IM agents",
|
|
1916
2250
|
type: "module",
|
|
1917
2251
|
main: "dist/index.js",
|
|
@@ -1945,6 +2279,7 @@ var require_package = __commonJS({
|
|
|
1945
2279
|
},
|
|
1946
2280
|
dependencies: {
|
|
1947
2281
|
"@iarna/toml": "^2.2.5",
|
|
2282
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
1948
2283
|
"better-sqlite3": "^11.8.0",
|
|
1949
2284
|
commander: "^12.1.0",
|
|
1950
2285
|
qrcode: "^1.5.4",
|
|
@@ -2006,7 +2341,7 @@ __export(util_exports, {
|
|
|
2006
2341
|
warn: () => warn,
|
|
2007
2342
|
writePidFile: () => writePidFile
|
|
2008
2343
|
});
|
|
2009
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync, writeFileSync as
|
|
2344
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
2010
2345
|
import { join as join5 } from "path";
|
|
2011
2346
|
function color(kind, text) {
|
|
2012
2347
|
if (process.env.NO_COLOR === "1" || process.env.NO_COLOR === "true") return text;
|
|
@@ -2104,7 +2439,7 @@ function pidFilePath(paths) {
|
|
|
2104
2439
|
return join5(paths.root, "daemon.pid");
|
|
2105
2440
|
}
|
|
2106
2441
|
function writePidFile(paths, pid) {
|
|
2107
|
-
|
|
2442
|
+
writeFileSync4(pidFilePath(paths), `${pid}
|
|
2108
2443
|
`, "utf8");
|
|
2109
2444
|
}
|
|
2110
2445
|
function readPidFile(paths) {
|
|
@@ -2203,11 +2538,39 @@ async function syncAllAgentSkills(profiles, cloud, options = {}) {
|
|
|
2203
2538
|
}
|
|
2204
2539
|
async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, signal) {
|
|
2205
2540
|
if (!agentImUserId) return { synced: 0, skipped: 0, unchanged: 0 };
|
|
2206
|
-
|
|
2541
|
+
let data = await cloud.get(
|
|
2207
2542
|
`/api/im/skills/installed?agentId=${encodeURIComponent(agentImUserId)}`,
|
|
2208
2543
|
{ signal }
|
|
2209
2544
|
);
|
|
2210
|
-
|
|
2545
|
+
let entries = normalizeInstalledSkills(data);
|
|
2546
|
+
if (entries.length === 0 && (profile.adapterName === "hermes" || profile.adapterName === "openclaw")) {
|
|
2547
|
+
try {
|
|
2548
|
+
const backfill = await cloud.request(
|
|
2549
|
+
"POST",
|
|
2550
|
+
`/api/im/agents/${encodeURIComponent(agentImUserId)}/skills/install-builtins`,
|
|
2551
|
+
{ body: {}, signal }
|
|
2552
|
+
);
|
|
2553
|
+
if (backfill.ok && backfill.data && typeof backfill.data === "object" && "data" in backfill.data) {
|
|
2554
|
+
const installed = backfill.data.data?.installed ?? 0;
|
|
2555
|
+
if (installed > 0) {
|
|
2556
|
+
process.stdout.write(
|
|
2557
|
+
`[daemon] skill sync: backfilled ${installed} built-in skills for agent ${agentImUserId.slice(-8)}
|
|
2558
|
+
`
|
|
2559
|
+
);
|
|
2560
|
+
data = await cloud.get(
|
|
2561
|
+
`/api/im/skills/installed?agentId=${encodeURIComponent(agentImUserId)}`,
|
|
2562
|
+
{ signal }
|
|
2563
|
+
);
|
|
2564
|
+
entries = normalizeInstalledSkills(data);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
} catch (err) {
|
|
2568
|
+
process.stderr.write(
|
|
2569
|
+
`[daemon] skill sync: backfill skipped for ${agentImUserId.slice(-8)}: ${err.message}
|
|
2570
|
+
`
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2211
2574
|
const skillsRoot = resolveSkillsRoot(profile);
|
|
2212
2575
|
if (!skillsRoot) return { synced: 0, skipped: 0, unchanged: 0 };
|
|
2213
2576
|
let synced = 0;
|
|
@@ -2526,10 +2889,10 @@ import { spawnSync as spawnSync2 } from "child_process";
|
|
|
2526
2889
|
import {
|
|
2527
2890
|
copyFileSync,
|
|
2528
2891
|
existsSync as existsSync5,
|
|
2529
|
-
mkdirSync as
|
|
2892
|
+
mkdirSync as mkdirSync4,
|
|
2530
2893
|
readFileSync as readFileSync5,
|
|
2531
2894
|
statSync,
|
|
2532
|
-
writeFileSync as
|
|
2895
|
+
writeFileSync as writeFileSync5
|
|
2533
2896
|
} from "fs";
|
|
2534
2897
|
import { homedir as homedir4 } from "os";
|
|
2535
2898
|
import { dirname as dirname4, join as join6 } from "path";
|
|
@@ -3029,7 +3392,7 @@ init_openclaw();
|
|
|
3029
3392
|
|
|
3030
3393
|
// src/config.ts
|
|
3031
3394
|
import * as TOML from "@iarna/toml";
|
|
3032
|
-
import { existsSync as existsSync2, mkdirSync as
|
|
3395
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
3033
3396
|
import { homedir as homedir3 } from "os";
|
|
3034
3397
|
import { dirname as dirname2, join as join4 } from "path";
|
|
3035
3398
|
import { z as z5 } from "zod";
|
|
@@ -3096,13 +3459,13 @@ function loadConfig(paths = resolvePaths()) {
|
|
|
3096
3459
|
}
|
|
3097
3460
|
function saveConfig(config, paths = resolvePaths()) {
|
|
3098
3461
|
if (!existsSync2(paths.root)) {
|
|
3099
|
-
|
|
3462
|
+
mkdirSync3(paths.root, { recursive: true });
|
|
3100
3463
|
}
|
|
3101
3464
|
if (!existsSync2(dirname2(paths.configFile))) {
|
|
3102
|
-
|
|
3465
|
+
mkdirSync3(dirname2(paths.configFile), { recursive: true });
|
|
3103
3466
|
}
|
|
3104
3467
|
ConfigSchema.parse(config);
|
|
3105
|
-
|
|
3468
|
+
writeFileSync3(paths.configFile, TOML.stringify(config), "utf8");
|
|
3106
3469
|
}
|
|
3107
3470
|
function deriveWsUrl(httpBase) {
|
|
3108
3471
|
const u = new URL(httpBase);
|
|
@@ -3365,15 +3728,15 @@ function runHooks(spec, opts) {
|
|
|
3365
3728
|
}
|
|
3366
3729
|
let backup;
|
|
3367
3730
|
if (planned.kind === "directory") {
|
|
3368
|
-
|
|
3731
|
+
mkdirSync4(planned.path, { recursive: true });
|
|
3369
3732
|
const markerPath = join6(planned.path, "prismer.json");
|
|
3370
3733
|
backup = backupIfExists(markerPath);
|
|
3371
|
-
|
|
3734
|
+
writeFileSync5(markerPath, JSON.stringify(prismerHookMarker(spec), null, 2) + "\n", "utf8");
|
|
3372
3735
|
} else {
|
|
3373
|
-
|
|
3736
|
+
mkdirSync4(dirname4(planned.path), { recursive: true });
|
|
3374
3737
|
backup = backupIfExists(planned.path);
|
|
3375
3738
|
const merged = mergeHookJson(planned.path, spec);
|
|
3376
|
-
|
|
3739
|
+
writeFileSync5(planned.path, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
3377
3740
|
}
|
|
3378
3741
|
return {
|
|
3379
3742
|
ok: true,
|
|
@@ -3771,7 +4134,7 @@ function mapStatusToCode(status) {
|
|
|
3771
4134
|
|
|
3772
4135
|
// src/sync/store.ts
|
|
3773
4136
|
import Database2 from "better-sqlite3";
|
|
3774
|
-
import { existsSync as existsSync6, mkdirSync as
|
|
4137
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "fs";
|
|
3775
4138
|
import { dirname as dirname5 } from "path";
|
|
3776
4139
|
var MIGRATIONS = [
|
|
3777
4140
|
{
|
|
@@ -3898,6 +4261,36 @@ var MIGRATIONS = [
|
|
|
3898
4261
|
CREATE INDEX IF NOT EXISTS idx_asset_meta_seq
|
|
3899
4262
|
ON asset_metadata_index(workspace_id, asset_index_seq);
|
|
3900
4263
|
`
|
|
4264
|
+
},
|
|
4265
|
+
{
|
|
4266
|
+
version: 4,
|
|
4267
|
+
up: `
|
|
4268
|
+
-- Wave 4 / W3 \xA74.3 \u2014 pending dispatch replies for crash recovery.
|
|
4269
|
+
--
|
|
4270
|
+
-- The two-phase reply protocol writes a row here BEFORE calling
|
|
4271
|
+
-- POST /dispatch/:taskId/prepare so that a crash between prepare
|
|
4272
|
+
-- and commit leaves a recoverable trail. On daemon cold-start, any
|
|
4273
|
+
-- row with status='prepared' triggers an idempotent commit retry.
|
|
4274
|
+
--
|
|
4275
|
+
-- The server-side cutoff for orphaned 'prepared' rows is 1h
|
|
4276
|
+
-- (PREPARED_REPLY_ORPHAN_MS in dispatch-reply.service.ts); rows
|
|
4277
|
+
-- older than that will fail commit with DISPATCH_REPLY_INVALID_STATE
|
|
4278
|
+
-- and must be reported to the user as 'dispatch lost'.
|
|
4279
|
+
CREATE TABLE IF NOT EXISTS pending_dispatch_replies (
|
|
4280
|
+
idempotency_key TEXT PRIMARY KEY,
|
|
4281
|
+
task_id TEXT NOT NULL,
|
|
4282
|
+
reply_id TEXT,
|
|
4283
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
4284
|
+
payload_json TEXT NOT NULL,
|
|
4285
|
+
prepared_at INTEGER,
|
|
4286
|
+
created_at INTEGER NOT NULL,
|
|
4287
|
+
last_attempt_at INTEGER,
|
|
4288
|
+
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
4289
|
+
last_error TEXT
|
|
4290
|
+
);
|
|
4291
|
+
CREATE INDEX IF NOT EXISTS idx_pending_reply_task ON pending_dispatch_replies (task_id);
|
|
4292
|
+
CREATE INDEX IF NOT EXISTS idx_pending_reply_status ON pending_dispatch_replies (status, created_at);
|
|
4293
|
+
`
|
|
3901
4294
|
}
|
|
3902
4295
|
];
|
|
3903
4296
|
function runSql(db, sql) {
|
|
@@ -3905,7 +4298,7 @@ function runSql(db, sql) {
|
|
|
3905
4298
|
}
|
|
3906
4299
|
function openLocalDb(path9) {
|
|
3907
4300
|
if (path9 !== ":memory:" && !existsSync6(dirname5(path9))) {
|
|
3908
|
-
|
|
4301
|
+
mkdirSync5(dirname5(path9), { recursive: true });
|
|
3909
4302
|
}
|
|
3910
4303
|
const db = new Database2(path9);
|
|
3911
4304
|
db.pragma("journal_mode = WAL");
|
|
@@ -4227,8 +4620,74 @@ function buildAgentCommand() {
|
|
|
4227
4620
|
}
|
|
4228
4621
|
getUI().ok("Skill install requested", `agent=${opts.agent} skill=${slugOrId}`);
|
|
4229
4622
|
}, { code: "agent_install_skill_failed" }));
|
|
4623
|
+
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) => {
|
|
4624
|
+
const cloud = makeCloudClient();
|
|
4625
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agents/${encodeURIComponent(imUserId)}/snapshot`, {
|
|
4626
|
+
includeMemory: Boolean(opts.includeMemory),
|
|
4627
|
+
label: opts.label
|
|
4628
|
+
}, "agent_snapshot_failed");
|
|
4629
|
+
if (opts.json) {
|
|
4630
|
+
printJson(data);
|
|
4631
|
+
return;
|
|
4632
|
+
}
|
|
4633
|
+
const rec = data;
|
|
4634
|
+
getUI().ok("Snapshot created", String(rec.id ?? "<unknown>"));
|
|
4635
|
+
getUI().line(`agent=${String(rec.agentImUserId ?? imUserId)} memory=${rec.includeMemory ? "included" : "stripped"}`);
|
|
4636
|
+
}, { code: "agent_snapshot_failed" }));
|
|
4637
|
+
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) => {
|
|
4638
|
+
const cloud = makeCloudClient();
|
|
4639
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agents/${encodeURIComponent(imUserId)}/publish`, {
|
|
4640
|
+
slug: opts.slug,
|
|
4641
|
+
version: opts.version,
|
|
4642
|
+
license: opts.license,
|
|
4643
|
+
metadata: {
|
|
4644
|
+
...opts.title ? { title: opts.title } : {},
|
|
4645
|
+
...opts.description ? { description: opts.description } : {}
|
|
4646
|
+
},
|
|
4647
|
+
stripMemory: true
|
|
4648
|
+
}, "agent_publish_failed");
|
|
4649
|
+
if (opts.json) {
|
|
4650
|
+
printJson(data);
|
|
4651
|
+
return;
|
|
4652
|
+
}
|
|
4653
|
+
const rec = data;
|
|
4654
|
+
getUI().ok("Agent Pack published", `${String(rec.slug ?? opts.slug)}@${String(rec.version ?? opts.version ?? "1.0.0")}`);
|
|
4655
|
+
getUI().line(`package=${String(rec.id ?? "<unknown>")}`);
|
|
4656
|
+
}, { code: "agent_publish_failed" }));
|
|
4657
|
+
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) => {
|
|
4658
|
+
const cloud = makeCloudClient();
|
|
4659
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agent-packs/${encodeURIComponent(packIdOrSlug)}/fork`, {
|
|
4660
|
+
targetWorkspaceId: opts.workspaceId,
|
|
4661
|
+
displayName: opts.displayName
|
|
4662
|
+
}, "agent_fork_failed");
|
|
4663
|
+
if (opts.json) {
|
|
4664
|
+
printJson(data);
|
|
4665
|
+
return;
|
|
4666
|
+
}
|
|
4667
|
+
const rec = data;
|
|
4668
|
+
getUI().ok("Agent Pack forked", `newAgent=${String(rec.newImUserId ?? "<unknown>")}`);
|
|
4669
|
+
getUI().line(`did=${String(rec.newDid ?? "not issued")}`);
|
|
4670
|
+
}, { code: "agent_fork_failed" }));
|
|
4230
4671
|
return cmd;
|
|
4231
4672
|
}
|
|
4673
|
+
function makeCloudClient() {
|
|
4674
|
+
const cfg = loadConfig(resolvePaths());
|
|
4675
|
+
return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
4676
|
+
}
|
|
4677
|
+
async function requestEnvelope(cloud, method, path9, body, code) {
|
|
4678
|
+
const res = await cloud.request(
|
|
4679
|
+
method,
|
|
4680
|
+
path9,
|
|
4681
|
+
body === void 0 ? void 0 : { body }
|
|
4682
|
+
);
|
|
4683
|
+
if (!res.ok || res.data?.ok === false) {
|
|
4684
|
+
const message = res.error?.message ?? res.data?.error?.message ?? "request failed";
|
|
4685
|
+
exitWithError(`${path9} failed (${res.status === 0 ? "network error" : `HTTP ${res.status}`}): ${message}`, {
|
|
4686
|
+
code: res.error?.code ?? res.data?.error?.code ?? code
|
|
4687
|
+
});
|
|
4688
|
+
}
|
|
4689
|
+
return res.data && typeof res.data === "object" && "data" in res.data ? res.data.data : res.data;
|
|
4690
|
+
}
|
|
4232
4691
|
function readLocalAgents(localDb) {
|
|
4233
4692
|
const db = openLocalDb(localDb);
|
|
4234
4693
|
try {
|
|
@@ -4333,13 +4792,13 @@ function whichBinary2(bin) {
|
|
|
4333
4792
|
// src/cli/commands/asset.ts
|
|
4334
4793
|
import { createHash as createHash2 } from "crypto";
|
|
4335
4794
|
import { Command as Command3 } from "commander";
|
|
4336
|
-
import { existsSync as existsSync10, lstatSync, readdirSync, readFileSync as readFileSync9, writeFileSync as
|
|
4795
|
+
import { existsSync as existsSync10, lstatSync, readdirSync, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
4337
4796
|
import { basename, dirname as dirname7, join as join10, relative } from "path";
|
|
4338
4797
|
|
|
4339
4798
|
// src/asset-cache.ts
|
|
4340
4799
|
import { createHash } from "crypto";
|
|
4341
4800
|
import { promises as dnsp } from "dns";
|
|
4342
|
-
import { existsSync as existsSync7, mkdirSync as
|
|
4801
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync6, renameSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
4343
4802
|
import { isIP, isIPv4, isIPv6 } from "net";
|
|
4344
4803
|
import { dirname as dirname6, join as join7 } from "path";
|
|
4345
4804
|
import { Agent as UndiciAgent } from "undici";
|
|
@@ -4365,7 +4824,7 @@ var AssetCache = class {
|
|
|
4365
4824
|
this.cacheDir = opts.cacheDir;
|
|
4366
4825
|
this.maxBytes = opts.maxBytes ?? 5 * 1024 * 1024 * 1024;
|
|
4367
4826
|
if (!existsSync7(this.cacheDir)) {
|
|
4368
|
-
|
|
4827
|
+
mkdirSync6(this.cacheDir, { recursive: true });
|
|
4369
4828
|
}
|
|
4370
4829
|
}
|
|
4371
4830
|
/** Local file path for a content hash. Files split by first 2 chars for fs scaling. */
|
|
@@ -4440,10 +4899,10 @@ var AssetCache = class {
|
|
|
4440
4899
|
}
|
|
4441
4900
|
const localPath = this.pathFor(hash);
|
|
4442
4901
|
if (!existsSync7(dirname6(localPath))) {
|
|
4443
|
-
|
|
4902
|
+
mkdirSync6(dirname6(localPath), { recursive: true });
|
|
4444
4903
|
}
|
|
4445
4904
|
const tmpPath = `${localPath}.${process.pid}.${Date.now()}.tmp`;
|
|
4446
|
-
|
|
4905
|
+
writeFileSync6(tmpPath, buf);
|
|
4447
4906
|
renameSync(tmpPath, localPath);
|
|
4448
4907
|
const mime = res.headers.get("content-type");
|
|
4449
4908
|
const now = Date.now();
|
|
@@ -4504,10 +4963,10 @@ var AssetCache = class {
|
|
|
4504
4963
|
}
|
|
4505
4964
|
const localPath = this.pathFor(hash);
|
|
4506
4965
|
if (!existsSync7(dirname6(localPath))) {
|
|
4507
|
-
|
|
4966
|
+
mkdirSync6(dirname6(localPath), { recursive: true });
|
|
4508
4967
|
}
|
|
4509
4968
|
const tmpPath = `${localPath}.${process.pid}.${Date.now()}.tmp`;
|
|
4510
|
-
|
|
4969
|
+
writeFileSync6(tmpPath, body);
|
|
4511
4970
|
renameSync(tmpPath, localPath);
|
|
4512
4971
|
const now = Date.now();
|
|
4513
4972
|
this.db.prepare(
|
|
@@ -4761,7 +5220,7 @@ async function fetchUrlWithGuards(rawUrl, opts) {
|
|
|
4761
5220
|
}
|
|
4762
5221
|
|
|
4763
5222
|
// src/daemon/asset/metadata-index.ts
|
|
4764
|
-
import { existsSync as existsSync8, mkdirSync as
|
|
5223
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
4765
5224
|
import { join as join8 } from "path";
|
|
4766
5225
|
var DEFAULT_LIMIT = 8;
|
|
4767
5226
|
var PULL_PAGE_SIZE = 500;
|
|
@@ -4791,7 +5250,7 @@ var AssetMetadataIndex = class {
|
|
|
4791
5250
|
this.cloud = opts.cloud;
|
|
4792
5251
|
this.workspaceId = opts.workspaceId;
|
|
4793
5252
|
if (!existsSync8(opts.workspaceStateDir)) {
|
|
4794
|
-
|
|
5253
|
+
mkdirSync7(opts.workspaceStateDir, { recursive: true });
|
|
4795
5254
|
}
|
|
4796
5255
|
this.cursorPath = join8(opts.workspaceStateDir, "asset-metadata-cursor.json");
|
|
4797
5256
|
}
|
|
@@ -4812,7 +5271,7 @@ var AssetMetadataIndex = class {
|
|
|
4812
5271
|
cursor,
|
|
4813
5272
|
writtenAt: Date.now()
|
|
4814
5273
|
};
|
|
4815
|
-
|
|
5274
|
+
writeFileSync7(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
4816
5275
|
}
|
|
4817
5276
|
/**
|
|
4818
5277
|
* Pull incremental asset metadata changes since the persisted cursor and
|
|
@@ -4929,7 +5388,7 @@ var AssetMetadataIndex = class {
|
|
|
4929
5388
|
};
|
|
4930
5389
|
|
|
4931
5390
|
// src/daemon/asset/mirror.ts
|
|
4932
|
-
import { existsSync as existsSync9, mkdirSync as
|
|
5391
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
4933
5392
|
import { join as join9 } from "path";
|
|
4934
5393
|
function rowToBinding(row) {
|
|
4935
5394
|
return {
|
|
@@ -4952,7 +5411,7 @@ var WorkspaceMirror = class {
|
|
|
4952
5411
|
this.cloud = opts.cloud;
|
|
4953
5412
|
this.workspaceId = opts.workspaceId;
|
|
4954
5413
|
if (!existsSync9(opts.workspaceStateDir)) {
|
|
4955
|
-
|
|
5414
|
+
mkdirSync8(opts.workspaceStateDir, { recursive: true });
|
|
4956
5415
|
}
|
|
4957
5416
|
this.cursorPath = join9(opts.workspaceStateDir, "asset-cursor.json");
|
|
4958
5417
|
}
|
|
@@ -4973,7 +5432,7 @@ var WorkspaceMirror = class {
|
|
|
4973
5432
|
cursor,
|
|
4974
5433
|
writtenAt: Date.now()
|
|
4975
5434
|
};
|
|
4976
|
-
|
|
5435
|
+
writeFileSync8(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
4977
5436
|
}
|
|
4978
5437
|
/**
|
|
4979
5438
|
* Pull workspace_file changes since the persisted cursor and upsert into
|
|
@@ -5411,7 +5870,7 @@ async function downloadAsset(assetId, outPath) {
|
|
|
5411
5870
|
exitWithError(`asset download failed (${res.status}): ${errorMessage(body)}`);
|
|
5412
5871
|
}
|
|
5413
5872
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
5414
|
-
|
|
5873
|
+
writeFileSync9(outPath, bytes);
|
|
5415
5874
|
}
|
|
5416
5875
|
function parseMetadata(raw) {
|
|
5417
5876
|
if (!raw) return {};
|
|
@@ -6058,8 +6517,8 @@ function parsePositiveInt3(value) {
|
|
|
6058
6517
|
// src/cli/commands/daemon.ts
|
|
6059
6518
|
import { Command as Command8 } from "commander";
|
|
6060
6519
|
import { spawn as spawn5 } from "child_process";
|
|
6061
|
-
import { createReadStream, existsSync as existsSync14, mkdirSync as
|
|
6062
|
-
import { setTimeout as
|
|
6520
|
+
import { createReadStream, existsSync as existsSync14, mkdirSync as mkdirSync10, openSync as openSync2, statSync as statSync4 } from "fs";
|
|
6521
|
+
import { setTimeout as sleep3 } from "timers/promises";
|
|
6063
6522
|
import { join as join18 } from "path";
|
|
6064
6523
|
|
|
6065
6524
|
// src/daemon/runner.ts
|
|
@@ -6067,7 +6526,7 @@ import { EventEmitter as EventEmitter3 } from "events";
|
|
|
6067
6526
|
import { createRequire as createRequire2 } from "module";
|
|
6068
6527
|
import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
|
|
6069
6528
|
import { mkdir } from "fs/promises";
|
|
6070
|
-
import { homedir as
|
|
6529
|
+
import { homedir as homedir8, platform } from "os";
|
|
6071
6530
|
import { join as join17 } from "path";
|
|
6072
6531
|
|
|
6073
6532
|
// src/adapters/registry.ts
|
|
@@ -6548,6 +7007,262 @@ function createLogger(tag) {
|
|
|
6548
7007
|
import { readFileSync as readFileSync10, promises as fsp3 } from "fs";
|
|
6549
7008
|
import * as path2 from "path";
|
|
6550
7009
|
init_skill_sync();
|
|
7010
|
+
|
|
7011
|
+
// src/daemon/task-heartbeat.ts
|
|
7012
|
+
var TaskHeartbeat = class {
|
|
7013
|
+
constructor(opts) {
|
|
7014
|
+
this.opts = opts;
|
|
7015
|
+
this.intervalMs = opts.intervalMs ?? 15e3;
|
|
7016
|
+
this.maxRetries = opts.maxRetries ?? 3;
|
|
7017
|
+
this.retryBackoffMs = opts.retryBackoffMs ?? 100;
|
|
7018
|
+
this.now = opts.now ?? Date.now;
|
|
7019
|
+
}
|
|
7020
|
+
opts;
|
|
7021
|
+
timer;
|
|
7022
|
+
version = 0;
|
|
7023
|
+
taskId;
|
|
7024
|
+
getPhase;
|
|
7025
|
+
/** Tracks the last step time pushed via touchStep() — informational. */
|
|
7026
|
+
lastStepAt;
|
|
7027
|
+
intervalMs;
|
|
7028
|
+
maxRetries;
|
|
7029
|
+
retryBackoffMs;
|
|
7030
|
+
now;
|
|
7031
|
+
/**
|
|
7032
|
+
* Begin heartbeating for `taskId`. `getPhase` is invoked at every tick to
|
|
7033
|
+
* pick up the live phase string — adapters can mutate their phase state
|
|
7034
|
+
* freely and the heartbeat will surface it on the next 15s boundary.
|
|
7035
|
+
*
|
|
7036
|
+
* Calling start() twice on the same instance replaces the active task
|
|
7037
|
+
* (previous timer is cleared). Returns silently when already started for
|
|
7038
|
+
* the same taskId.
|
|
7039
|
+
*/
|
|
7040
|
+
start(taskId, getPhase) {
|
|
7041
|
+
if (this.taskId === taskId && this.timer) return;
|
|
7042
|
+
this.stop();
|
|
7043
|
+
this.taskId = taskId;
|
|
7044
|
+
this.getPhase = getPhase;
|
|
7045
|
+
this.timer = setInterval(() => {
|
|
7046
|
+
void this.push();
|
|
7047
|
+
}, this.intervalMs);
|
|
7048
|
+
this.timer.unref?.();
|
|
7049
|
+
}
|
|
7050
|
+
/**
|
|
7051
|
+
* Stop the heartbeat loop. Idempotent.
|
|
7052
|
+
*/
|
|
7053
|
+
stop() {
|
|
7054
|
+
if (this.timer) {
|
|
7055
|
+
clearInterval(this.timer);
|
|
7056
|
+
this.timer = void 0;
|
|
7057
|
+
}
|
|
7058
|
+
this.taskId = void 0;
|
|
7059
|
+
this.getPhase = void 0;
|
|
7060
|
+
}
|
|
7061
|
+
/**
|
|
7062
|
+
* Phase transition — push immediately AND update what the timer-driven
|
|
7063
|
+
* tick will report next time. Adapter calls this whenever the lifecycle
|
|
7064
|
+
* advances (thinking → tool_use → reasoning → responding → ...).
|
|
7065
|
+
*
|
|
7066
|
+
* No-op when no task is active.
|
|
7067
|
+
*/
|
|
7068
|
+
setPhase(phase) {
|
|
7069
|
+
if (!this.taskId) return;
|
|
7070
|
+
this.getPhase = () => phase;
|
|
7071
|
+
void this.push();
|
|
7072
|
+
}
|
|
7073
|
+
/**
|
|
7074
|
+
* Mark "I just made observable progress." Updates lastStepAt; next push
|
|
7075
|
+
* (timer-driven or setPhase-driven) carries it. Lightweight — no immediate
|
|
7076
|
+
* send. Adapters call this around tool calls / token boundaries so the
|
|
7077
|
+
* cloud reaper can distinguish "alive but slow" from "truly stuck".
|
|
7078
|
+
*/
|
|
7079
|
+
touchStep() {
|
|
7080
|
+
this.lastStepAt = this.now();
|
|
7081
|
+
}
|
|
7082
|
+
/**
|
|
7083
|
+
* Manually trigger one heartbeat send (does not affect the timer). Mainly
|
|
7084
|
+
* useful for tests that don't want to await a 15s tick. Returns the
|
|
7085
|
+
* heartbeatVersion that was sent, or undefined if no task is active.
|
|
7086
|
+
*/
|
|
7087
|
+
async forceTick() {
|
|
7088
|
+
if (!this.taskId) return void 0;
|
|
7089
|
+
return this.push();
|
|
7090
|
+
}
|
|
7091
|
+
/** Test helper: current heartbeatVersion (last value sent, may be 0 before first tick). */
|
|
7092
|
+
get currentVersion() {
|
|
7093
|
+
return this.version;
|
|
7094
|
+
}
|
|
7095
|
+
async push() {
|
|
7096
|
+
if (!this.taskId || !this.getPhase) return void 0;
|
|
7097
|
+
const taskId = this.taskId;
|
|
7098
|
+
const phase = this.getPhase();
|
|
7099
|
+
const heartbeatVersion = ++this.version;
|
|
7100
|
+
const payload = {
|
|
7101
|
+
taskId,
|
|
7102
|
+
heartbeatVersion,
|
|
7103
|
+
currentPhase: phase,
|
|
7104
|
+
...this.lastStepAt !== void 0 ? { lastStepAt: this.lastStepAt } : {}
|
|
7105
|
+
};
|
|
7106
|
+
const msg = envelope("task.heartbeat", payload);
|
|
7107
|
+
let lastErr;
|
|
7108
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
|
|
7109
|
+
try {
|
|
7110
|
+
if (this.opts.ws.isOpen && !this.opts.ws.isOpen()) {
|
|
7111
|
+
return heartbeatVersion;
|
|
7112
|
+
}
|
|
7113
|
+
this.opts.ws.send(msg);
|
|
7114
|
+
return heartbeatVersion;
|
|
7115
|
+
} catch (err) {
|
|
7116
|
+
lastErr = err instanceof Error ? err : new Error(String(err));
|
|
7117
|
+
if (attempt < this.maxRetries) {
|
|
7118
|
+
const backoff = this.retryBackoffMs * Math.pow(2, attempt);
|
|
7119
|
+
await sleep(backoff);
|
|
7120
|
+
}
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7123
|
+
if (lastErr) {
|
|
7124
|
+
try {
|
|
7125
|
+
this.opts.onGiveUp?.(taskId, lastErr);
|
|
7126
|
+
} catch {
|
|
7127
|
+
}
|
|
7128
|
+
}
|
|
7129
|
+
return heartbeatVersion;
|
|
7130
|
+
}
|
|
7131
|
+
};
|
|
7132
|
+
function sleep(ms) {
|
|
7133
|
+
return new Promise((resolve3) => {
|
|
7134
|
+
const handle = setTimeout(resolve3, ms);
|
|
7135
|
+
handle.unref?.();
|
|
7136
|
+
});
|
|
7137
|
+
}
|
|
7138
|
+
|
|
7139
|
+
// src/daemon/step-recorder.ts
|
|
7140
|
+
var StepRecorder = class {
|
|
7141
|
+
constructor(opts) {
|
|
7142
|
+
this.opts = opts;
|
|
7143
|
+
this.reasoningThrottleMs = opts.reasoningThrottleMs ?? 500;
|
|
7144
|
+
this.now = opts.now ?? Date.now;
|
|
7145
|
+
}
|
|
7146
|
+
opts;
|
|
7147
|
+
seq = 0;
|
|
7148
|
+
reasoningBuf = { texts: [], firstAt: 0 };
|
|
7149
|
+
reasoningTimer;
|
|
7150
|
+
reasoningThrottleMs;
|
|
7151
|
+
now;
|
|
7152
|
+
/** Push a phase transition step. Immediate (no throttle). */
|
|
7153
|
+
recordPhaseChange(phase) {
|
|
7154
|
+
this.emit("phase_change", { phase });
|
|
7155
|
+
}
|
|
7156
|
+
/**
|
|
7157
|
+
* Push a tool-call step. Immediate.
|
|
7158
|
+
*
|
|
7159
|
+
* `toolCallId` is the daemon-side correlation id used to pair with the
|
|
7160
|
+
* subsequent recordToolResult call. Free-form; adapter decides.
|
|
7161
|
+
*/
|
|
7162
|
+
recordToolCall(toolName, input, toolCallId) {
|
|
7163
|
+
this.emit("tool_call", {
|
|
7164
|
+
toolName,
|
|
7165
|
+
inputSummary: summarize2(input),
|
|
7166
|
+
...toolCallId ? { toolCallId } : {}
|
|
7167
|
+
});
|
|
7168
|
+
}
|
|
7169
|
+
/** Push a tool-result step. Immediate. */
|
|
7170
|
+
recordToolResult(toolCallId, output) {
|
|
7171
|
+
this.emit("tool_result", {
|
|
7172
|
+
toolCallId,
|
|
7173
|
+
outputSummary: summarize2(output)
|
|
7174
|
+
});
|
|
7175
|
+
}
|
|
7176
|
+
/**
|
|
7177
|
+
* Buffer a reasoning chunk for batched dispatch. Multiple chunks within
|
|
7178
|
+
* `reasoningThrottleMs` are concatenated and emitted as a single
|
|
7179
|
+
* `reasoning_chunk` step when the timer fires.
|
|
7180
|
+
*/
|
|
7181
|
+
recordReasoningChunk(text) {
|
|
7182
|
+
if (!text) return;
|
|
7183
|
+
if (this.reasoningBuf.texts.length === 0) {
|
|
7184
|
+
this.reasoningBuf.firstAt = this.now();
|
|
7185
|
+
}
|
|
7186
|
+
this.reasoningBuf.texts.push(text);
|
|
7187
|
+
if (this.reasoningTimer) return;
|
|
7188
|
+
this.reasoningTimer = setTimeout(() => {
|
|
7189
|
+
this.flushReasoning();
|
|
7190
|
+
}, this.reasoningThrottleMs);
|
|
7191
|
+
this.reasoningTimer.unref?.();
|
|
7192
|
+
}
|
|
7193
|
+
/** Push an error step. Immediate. */
|
|
7194
|
+
recordError(message, payload) {
|
|
7195
|
+
this.emit("error", { message, ...payload ?? {} });
|
|
7196
|
+
}
|
|
7197
|
+
/**
|
|
7198
|
+
* Force any pending reasoning buffer out the door. Call on shutdown so
|
|
7199
|
+
* trailing tokens aren't lost.
|
|
7200
|
+
*/
|
|
7201
|
+
flush() {
|
|
7202
|
+
this.flushReasoning();
|
|
7203
|
+
}
|
|
7204
|
+
/** Test helper. */
|
|
7205
|
+
get currentSeq() {
|
|
7206
|
+
return this.seq;
|
|
7207
|
+
}
|
|
7208
|
+
flushReasoning() {
|
|
7209
|
+
if (this.reasoningTimer) {
|
|
7210
|
+
clearTimeout(this.reasoningTimer);
|
|
7211
|
+
this.reasoningTimer = void 0;
|
|
7212
|
+
}
|
|
7213
|
+
if (this.reasoningBuf.texts.length === 0) return;
|
|
7214
|
+
const text = this.reasoningBuf.texts.join("");
|
|
7215
|
+
const firstAt = this.reasoningBuf.firstAt;
|
|
7216
|
+
this.reasoningBuf = { texts: [], firstAt: 0 };
|
|
7217
|
+
this.emit("reasoning_chunk", { text }, firstAt);
|
|
7218
|
+
}
|
|
7219
|
+
emit(kind, payload, occurredAt) {
|
|
7220
|
+
const frame = {
|
|
7221
|
+
seq: ++this.seq,
|
|
7222
|
+
kind,
|
|
7223
|
+
payload,
|
|
7224
|
+
occurredAt: occurredAt ?? this.now()
|
|
7225
|
+
};
|
|
7226
|
+
const msg = envelope("task.step.append", {
|
|
7227
|
+
taskRunId: this.opts.taskRunId,
|
|
7228
|
+
step: frame
|
|
7229
|
+
});
|
|
7230
|
+
try {
|
|
7231
|
+
if (this.opts.ws.isOpen && !this.opts.ws.isOpen()) {
|
|
7232
|
+
try {
|
|
7233
|
+
this.opts.onSend?.(frame);
|
|
7234
|
+
} catch {
|
|
7235
|
+
}
|
|
7236
|
+
return;
|
|
7237
|
+
}
|
|
7238
|
+
this.opts.ws.send(msg);
|
|
7239
|
+
try {
|
|
7240
|
+
this.opts.onSend?.(frame);
|
|
7241
|
+
} catch {
|
|
7242
|
+
}
|
|
7243
|
+
} catch {
|
|
7244
|
+
}
|
|
7245
|
+
}
|
|
7246
|
+
};
|
|
7247
|
+
var MAX_SUMMARY_CHARS = 512;
|
|
7248
|
+
function summarize2(value) {
|
|
7249
|
+
let text;
|
|
7250
|
+
if (value == null) text = "";
|
|
7251
|
+
else if (typeof value === "string") text = value;
|
|
7252
|
+
else {
|
|
7253
|
+
try {
|
|
7254
|
+
text = JSON.stringify(value);
|
|
7255
|
+
} catch {
|
|
7256
|
+
text = String(value);
|
|
7257
|
+
}
|
|
7258
|
+
}
|
|
7259
|
+
if (text.length > MAX_SUMMARY_CHARS) {
|
|
7260
|
+
return `${text.slice(0, MAX_SUMMARY_CHARS)}\u2026(+${text.length - MAX_SUMMARY_CHARS} chars)`;
|
|
7261
|
+
}
|
|
7262
|
+
return text;
|
|
7263
|
+
}
|
|
7264
|
+
|
|
7265
|
+
// src/daemon/dispatch.ts
|
|
6551
7266
|
var DEFAULT_CONTEXT_MAX = 8e3;
|
|
6552
7267
|
var GOAL_CONTEXT_MAX = 4;
|
|
6553
7268
|
var MEMORY_DIGEST_MAX_BYTES = 4e3;
|
|
@@ -6563,6 +7278,8 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6563
7278
|
const { taskId, agentImUserId } = payload;
|
|
6564
7279
|
let resolvedHashes = [];
|
|
6565
7280
|
let reply;
|
|
7281
|
+
let heartbeatRef;
|
|
7282
|
+
let recorderRef;
|
|
6566
7283
|
const outboxBase = deps.paths?.runsDir ? path2.join(deps.paths.runsDir, taskId) : null;
|
|
6567
7284
|
const outboxDir = outboxBase ? path2.join(outboxBase, "_outbox") : null;
|
|
6568
7285
|
const workDir = outboxBase ? path2.join(outboxBase, "workdir") : null;
|
|
@@ -6644,7 +7361,13 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6644
7361
|
resolvedHashes.push(...r.resolvedHashes);
|
|
6645
7362
|
rewrittenContext = payload.context.map((e, i) => ({ ...e, content: r.texts[i] }));
|
|
6646
7363
|
}
|
|
6647
|
-
const assetResolution = await resolveAssetRefs(payload.assetRefs, deps.assetCache, taskId
|
|
7364
|
+
const assetResolution = await resolveAssetRefs(payload.assetRefs, deps.assetCache, taskId, {
|
|
7365
|
+
cloud: deps.cloud,
|
|
7366
|
+
enabled: deps.visionAux?.enabled !== false,
|
|
7367
|
+
cacheDir: deps.visionAux?.cacheDir ?? (deps.paths ? path2.join(deps.paths.cacheDir, "vision-cache") : void 0),
|
|
7368
|
+
timeoutMs: deps.visionAux?.timeoutMs,
|
|
7369
|
+
signal: deps.signal
|
|
7370
|
+
});
|
|
6648
7371
|
resolvedHashes.push(...assetResolution.pinnedHashes);
|
|
6649
7372
|
const basePrompt = composePrompt(
|
|
6650
7373
|
rewrittenPrompt.text,
|
|
@@ -6679,9 +7402,44 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6679
7402
|
}
|
|
6680
7403
|
const operatingPrinciples = resolveOperatingPrinciples(profile.config);
|
|
6681
7404
|
const composedSystemPrompt = [profileSystemPrompt, operatingPrinciples].filter((part) => typeof part === "string" && part.length > 0).join("\n\n");
|
|
7405
|
+
let activePhase = "thinking";
|
|
7406
|
+
const heartbeat = new TaskHeartbeat({
|
|
7407
|
+
ws: deps.ws,
|
|
7408
|
+
onGiveUp: (tid, err) => process.stderr.write(
|
|
7409
|
+
`[daemon] task.heartbeat give-up task=${tid}: ${err.message}
|
|
7410
|
+
`
|
|
7411
|
+
)
|
|
7412
|
+
});
|
|
7413
|
+
heartbeatRef = heartbeat;
|
|
7414
|
+
heartbeat.start(taskId, () => activePhase);
|
|
7415
|
+
const recorder = new StepRecorder({ ws: deps.ws, taskRunId: taskId });
|
|
7416
|
+
recorderRef = recorder;
|
|
7417
|
+
recorder.recordPhaseChange(activePhase);
|
|
6682
7418
|
const taskInput = {
|
|
6683
7419
|
taskId,
|
|
6684
7420
|
prompt: adapterPrompt,
|
|
7421
|
+
heartbeat: {
|
|
7422
|
+
setPhase: (phase) => {
|
|
7423
|
+
activePhase = phase;
|
|
7424
|
+
heartbeat.setPhase(phase);
|
|
7425
|
+
recorder.recordPhaseChange(phase);
|
|
7426
|
+
},
|
|
7427
|
+
touchStep: () => heartbeat.touchStep()
|
|
7428
|
+
},
|
|
7429
|
+
recorder: {
|
|
7430
|
+
recordPhaseChange: (phase) => {
|
|
7431
|
+
activePhase = phase;
|
|
7432
|
+
heartbeat.setPhase(phase);
|
|
7433
|
+
recorder.recordPhaseChange(phase);
|
|
7434
|
+
},
|
|
7435
|
+
recordToolCall: (toolName, input, toolCallId) => recorder.recordToolCall(toolName, input, toolCallId),
|
|
7436
|
+
recordToolResult: (toolCallId, output) => recorder.recordToolResult(toolCallId, output),
|
|
7437
|
+
recordReasoningChunk: (text) => recorder.recordReasoningChunk(text),
|
|
7438
|
+
recordError: (message, payload2) => recorder.recordError(message, payload2)
|
|
7439
|
+
},
|
|
7440
|
+
// 14b rev.3 §9 P3 — multimodal-aware adapters (Hermes, OpenClaw) pick
|
|
7441
|
+
// up the resolved bytes/URLs here; text-only adapters ignore the field.
|
|
7442
|
+
...assetResolution.resolvedRefs.length > 0 ? { assetRefs: assetResolution.resolvedRefs } : {},
|
|
6685
7443
|
metadata: {
|
|
6686
7444
|
...payload.metadata,
|
|
6687
7445
|
conversationId: payload.conversationId,
|
|
@@ -6744,6 +7502,9 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6744
7502
|
}
|
|
6745
7503
|
}
|
|
6746
7504
|
const reaperAborted = deps.signal?.aborted && result.error?.code === "task_cancelled";
|
|
7505
|
+
const approvalRequested = Boolean(
|
|
7506
|
+
result.metadata?.approvalRequested
|
|
7507
|
+
);
|
|
6747
7508
|
let collectedAssetIds = [];
|
|
6748
7509
|
if (deps.outboxWatcher && outboxDir) {
|
|
6749
7510
|
try {
|
|
@@ -6755,14 +7516,23 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6755
7516
|
collectedAssetIds = deps.outboxWatcher.flushPending(taskId);
|
|
6756
7517
|
}
|
|
6757
7518
|
const reaperLimitMs = typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? payload.timeoutMs : 5 * 6e4;
|
|
7519
|
+
const finalError = approvalRequested ? {
|
|
7520
|
+
code: "awaiting_human_approval",
|
|
7521
|
+
message: "Agent requested human approval and is suspended pending decision. Cloud will redispatch with `approval.decided` once the human responds."
|
|
7522
|
+
} : reaperAborted ? {
|
|
7523
|
+
code: "daemon_task_timeout",
|
|
7524
|
+
message: `Daemon-side reaper aborted after ${reaperLimitMs}ms inactivity (no progress events from adapter)`
|
|
7525
|
+
} : result.error;
|
|
6758
7526
|
reply = {
|
|
6759
7527
|
taskId,
|
|
7528
|
+
// When approval is pending we keep ok=false (the run did not produce a
|
|
7529
|
+
// completion-style output) but the cloud-side handler treats the
|
|
7530
|
+
// `awaiting_human_approval` code as non-terminal — task stays alive,
|
|
7531
|
+
// no red failure pill, and the next dispatch (after the human decides)
|
|
7532
|
+
// continues normally.
|
|
6760
7533
|
ok: result.ok,
|
|
6761
7534
|
output: result.output,
|
|
6762
|
-
error:
|
|
6763
|
-
code: "daemon_task_timeout",
|
|
6764
|
-
message: `Daemon-side reaper aborted after ${reaperLimitMs}ms inactivity (no progress events from adapter)`
|
|
6765
|
-
} : result.error,
|
|
7535
|
+
error: finalError,
|
|
6766
7536
|
...collectedAssetIds.length > 0 ? { assetIds: collectedAssetIds } : {},
|
|
6767
7537
|
metrics: result.metrics,
|
|
6768
7538
|
...assetResolution.observability.length > 0 || urlObservations.length > 0 ? { assetObservability: [...assetResolution.observability, ...urlObservations] } : {}
|
|
@@ -6784,6 +7554,18 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6784
7554
|
}
|
|
6785
7555
|
};
|
|
6786
7556
|
} finally {
|
|
7557
|
+
try {
|
|
7558
|
+
recorderRef?.flush();
|
|
7559
|
+
} catch (err) {
|
|
7560
|
+
process.stderr.write(`[daemon] step recorder flush failed task=${taskId}: ${err.message}
|
|
7561
|
+
`);
|
|
7562
|
+
}
|
|
7563
|
+
try {
|
|
7564
|
+
heartbeatRef?.stop();
|
|
7565
|
+
} catch (err) {
|
|
7566
|
+
process.stderr.write(`[daemon] heartbeat stop failed task=${taskId}: ${err.message}
|
|
7567
|
+
`);
|
|
7568
|
+
}
|
|
6787
7569
|
for (const hash of new Set(resolvedHashes)) {
|
|
6788
7570
|
try {
|
|
6789
7571
|
deps.assetCache.unpin(hash);
|
|
@@ -6860,6 +7642,10 @@ function isTextLikeMime(mime) {
|
|
|
6860
7642
|
if (m.endsWith("+json") || m.endsWith("+xml") || m.endsWith("+csv")) return true;
|
|
6861
7643
|
return false;
|
|
6862
7644
|
}
|
|
7645
|
+
function isImageMime(mime) {
|
|
7646
|
+
if (!mime) return false;
|
|
7647
|
+
return mime.toLowerCase().split(";")[0].trim().startsWith("image/");
|
|
7648
|
+
}
|
|
6863
7649
|
var HASH_REF_RE = /(?:^|\s)#([^\s#]+)/g;
|
|
6864
7650
|
var HEX_COLOR_RE = /^[0-9a-fA-F]{3,8}$/;
|
|
6865
7651
|
var FILE_EXT_RE = /\.[a-zA-Z0-9]{1,10}$/;
|
|
@@ -6938,8 +7724,34 @@ async function resolveHashRefs(prompt, assetIndex, cloud) {
|
|
|
6938
7724
|
}
|
|
6939
7725
|
return { text: result, resolutions };
|
|
6940
7726
|
}
|
|
6941
|
-
|
|
6942
|
-
|
|
7727
|
+
var ASSET_BASE64_INLINE_MAX_BYTES = 10 * 1024 * 1024;
|
|
7728
|
+
async function probeCdnReachable(cdnUrl, timeoutMs = 1500) {
|
|
7729
|
+
try {
|
|
7730
|
+
const u = new URL(cdnUrl);
|
|
7731
|
+
if (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname.startsWith("192.168.")) {
|
|
7732
|
+
return false;
|
|
7733
|
+
}
|
|
7734
|
+
} catch {
|
|
7735
|
+
return false;
|
|
7736
|
+
}
|
|
7737
|
+
try {
|
|
7738
|
+
const r = await fetch(cdnUrl, {
|
|
7739
|
+
method: "HEAD",
|
|
7740
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
7741
|
+
});
|
|
7742
|
+
if (r.ok) return true;
|
|
7743
|
+
return false;
|
|
7744
|
+
} catch {
|
|
7745
|
+
return false;
|
|
7746
|
+
}
|
|
7747
|
+
}
|
|
7748
|
+
async function resolveAssetRefs(refs, cache, taskId, visionAux) {
|
|
7749
|
+
const out = {
|
|
7750
|
+
promptBlocks: [],
|
|
7751
|
+
observability: [],
|
|
7752
|
+
pinnedHashes: [],
|
|
7753
|
+
resolvedRefs: []
|
|
7754
|
+
};
|
|
6943
7755
|
if (!refs || refs.length === 0) return out;
|
|
6944
7756
|
for (const ref of refs) {
|
|
6945
7757
|
let cached;
|
|
@@ -7004,7 +7816,42 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
7004
7816
|
continue;
|
|
7005
7817
|
}
|
|
7006
7818
|
}
|
|
7007
|
-
|
|
7819
|
+
let reachable = "unknown";
|
|
7820
|
+
let base64;
|
|
7821
|
+
if (ref.cdnUrl) {
|
|
7822
|
+
const ok2 = await probeCdnReachable(ref.cdnUrl);
|
|
7823
|
+
if (ok2) {
|
|
7824
|
+
reachable = "cdn";
|
|
7825
|
+
}
|
|
7826
|
+
}
|
|
7827
|
+
if (reachable !== "cdn") {
|
|
7828
|
+
try {
|
|
7829
|
+
const buf = readFileSync10(cached.localPath);
|
|
7830
|
+
if (buf.byteLength <= ASSET_BASE64_INLINE_MAX_BYTES) {
|
|
7831
|
+
base64 = buf.toString("base64");
|
|
7832
|
+
reachable = "base64";
|
|
7833
|
+
} else {
|
|
7834
|
+
reachable = "unknown";
|
|
7835
|
+
}
|
|
7836
|
+
} catch (err) {
|
|
7837
|
+
const errMsg = `base64 fallback read failed: ${err.message}`;
|
|
7838
|
+
logAssetResolveFailure(taskId, ref, errMsg, effectiveMime);
|
|
7839
|
+
}
|
|
7840
|
+
}
|
|
7841
|
+
const resolvedRef = {
|
|
7842
|
+
...ref,
|
|
7843
|
+
mime: effectiveMime,
|
|
7844
|
+
localPath: cached.localPath,
|
|
7845
|
+
base64,
|
|
7846
|
+
reachable
|
|
7847
|
+
};
|
|
7848
|
+
out.resolvedRefs.push(resolvedRef);
|
|
7849
|
+
if (isImageMime(effectiveMime) && visionAux?.enabled) {
|
|
7850
|
+
const block = await describeImageAttachment(ref, effectiveMime, resolvedRef, visionAux, taskId);
|
|
7851
|
+
out.promptBlocks.push(block);
|
|
7852
|
+
} else {
|
|
7853
|
+
out.promptBlocks.push(formatAttachmentReminderBlock(ref, effectiveMime));
|
|
7854
|
+
}
|
|
7008
7855
|
out.observability.push({
|
|
7009
7856
|
assetId: ref.assetId,
|
|
7010
7857
|
contentHash: ref.contentHash,
|
|
@@ -7015,6 +7862,110 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
7015
7862
|
}
|
|
7016
7863
|
return out;
|
|
7017
7864
|
}
|
|
7865
|
+
async function describeImageAttachment(ref, mime, resolved, opts, taskId) {
|
|
7866
|
+
const cached = await readVisionAuxLocalCache(opts.cacheDir, ref.contentHash, mime);
|
|
7867
|
+
if (cached) return formatVisionAuxBlock(ref, mime, cached.description, true);
|
|
7868
|
+
const source = buildVisionAuxSource(resolved, mime);
|
|
7869
|
+
if (!source) {
|
|
7870
|
+
return formatVisionAuxUnavailableBlock(ref, mime, "no reachable URL or inline bytes");
|
|
7871
|
+
}
|
|
7872
|
+
try {
|
|
7873
|
+
const description = await callVisionAux(ref, mime, source, opts);
|
|
7874
|
+
await writeVisionAuxLocalCache(opts.cacheDir, ref.contentHash, mime, description);
|
|
7875
|
+
return formatVisionAuxBlock(ref, mime, description.description, false);
|
|
7876
|
+
} catch (err) {
|
|
7877
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7878
|
+
process.stderr.write(
|
|
7879
|
+
`[daemon] vision-aux failed task=${taskId ?? "unknown"} assetId=${ref.assetId} hash=${ref.contentHash}: ${message}
|
|
7880
|
+
`
|
|
7881
|
+
);
|
|
7882
|
+
return formatVisionAuxUnavailableBlock(ref, mime, message);
|
|
7883
|
+
}
|
|
7884
|
+
}
|
|
7885
|
+
function buildVisionAuxSource(resolved, mime) {
|
|
7886
|
+
if (resolved.reachable === "cdn" && resolved.cdnUrl) return { kind: "url", url: resolved.cdnUrl };
|
|
7887
|
+
if (resolved.base64 && mime) return { kind: "data_url", url: `data:${mime};base64,${resolved.base64}` };
|
|
7888
|
+
return null;
|
|
7889
|
+
}
|
|
7890
|
+
async function callVisionAux(ref, mime, source, opts) {
|
|
7891
|
+
const secret = process.env.VISION_AUX_INTERNAL_SECRET || process.env.INTERNAL_API_SECRET || process.env.PRISMER_INTERNAL_SECRET;
|
|
7892
|
+
const headers = secret ? { "x-prismer-internal-secret": secret } : void 0;
|
|
7893
|
+
const res = await opts.cloud.request(
|
|
7894
|
+
"POST",
|
|
7895
|
+
"/api/internal/vision-aux/describe",
|
|
7896
|
+
{
|
|
7897
|
+
body: {
|
|
7898
|
+
assetId: ref.assetId,
|
|
7899
|
+
contentHash: ref.contentHash,
|
|
7900
|
+
mime: mime ?? ref.mime ?? "image/unknown",
|
|
7901
|
+
source
|
|
7902
|
+
},
|
|
7903
|
+
headers,
|
|
7904
|
+
timeoutMs: opts.timeoutMs ?? 12e3,
|
|
7905
|
+
signal: opts.signal
|
|
7906
|
+
}
|
|
7907
|
+
);
|
|
7908
|
+
const env = res.data;
|
|
7909
|
+
if (!res.ok || env?.ok === false || !env?.data?.description) {
|
|
7910
|
+
const message = res.error?.message || (typeof env?.error === "string" ? env.error : env?.error?.message) || env?.message || `HTTP ${res.status}`;
|
|
7911
|
+
throw new Error(message);
|
|
7912
|
+
}
|
|
7913
|
+
const now = /* @__PURE__ */ new Date();
|
|
7914
|
+
const ttlSec = Number.isFinite(env.data.cacheTtlSec) ? Number(env.data.cacheTtlSec) : 300;
|
|
7915
|
+
return {
|
|
7916
|
+
description: env.data.description,
|
|
7917
|
+
modelUsed: env.data.modelUsed || "unknown",
|
|
7918
|
+
provider: env.data.provider || "vision-aux",
|
|
7919
|
+
generatedAt: now.toISOString(),
|
|
7920
|
+
expiresAt: new Date(now.getTime() + Math.max(1, ttlSec) * 1e3).toISOString(),
|
|
7921
|
+
mime: mime ?? ref.mime ?? "image/unknown"
|
|
7922
|
+
};
|
|
7923
|
+
}
|
|
7924
|
+
async function readVisionAuxLocalCache(cacheDir, contentHash, mime) {
|
|
7925
|
+
if (!cacheDir) return null;
|
|
7926
|
+
const file = visionAuxCachePath(cacheDir, contentHash);
|
|
7927
|
+
try {
|
|
7928
|
+
const raw = await fsp3.readFile(file, "utf8");
|
|
7929
|
+
const parsed = parseVisionAuxCache(raw);
|
|
7930
|
+
if (!parsed) {
|
|
7931
|
+
await fsp3.unlink(file).catch(() => void 0);
|
|
7932
|
+
return null;
|
|
7933
|
+
}
|
|
7934
|
+
if (parsed.mime !== mime) return null;
|
|
7935
|
+
const expiresAt = Date.parse(parsed.expiresAt);
|
|
7936
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) return null;
|
|
7937
|
+
return parsed;
|
|
7938
|
+
} catch (err) {
|
|
7939
|
+
if (err.code !== "ENOENT") {
|
|
7940
|
+
await fsp3.unlink(file).catch(() => void 0);
|
|
7941
|
+
}
|
|
7942
|
+
return null;
|
|
7943
|
+
}
|
|
7944
|
+
}
|
|
7945
|
+
async function writeVisionAuxLocalCache(cacheDir, contentHash, mime, description) {
|
|
7946
|
+
if (!cacheDir || !mime) return;
|
|
7947
|
+
const file = visionAuxCachePath(cacheDir, contentHash);
|
|
7948
|
+
await fsp3.mkdir(path2.dirname(file), { recursive: true });
|
|
7949
|
+
await fsp3.writeFile(file, JSON.stringify({ ...description, mime }, null, 2), "utf8");
|
|
7950
|
+
}
|
|
7951
|
+
function visionAuxCachePath(cacheDir, contentHash) {
|
|
7952
|
+
const safeHash = /^[a-f0-9]{32,128}$/i.test(contentHash) ? contentHash : createSafeCacheKey(contentHash);
|
|
7953
|
+
return path2.join(cacheDir, `${safeHash}.json`);
|
|
7954
|
+
}
|
|
7955
|
+
function createSafeCacheKey(value) {
|
|
7956
|
+
return Buffer.from(value).toString("base64url").slice(0, 128) || "unknown";
|
|
7957
|
+
}
|
|
7958
|
+
function parseVisionAuxCache(raw) {
|
|
7959
|
+
try {
|
|
7960
|
+
const parsed = JSON.parse(raw);
|
|
7961
|
+
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") {
|
|
7962
|
+
return parsed;
|
|
7963
|
+
}
|
|
7964
|
+
} catch {
|
|
7965
|
+
return null;
|
|
7966
|
+
}
|
|
7967
|
+
return null;
|
|
7968
|
+
}
|
|
7018
7969
|
function logAssetResolveFailure(taskId, ref, error, mime = ref.mime) {
|
|
7019
7970
|
const message = error.replace(/\s+/g, " ").slice(0, 500);
|
|
7020
7971
|
process.stderr.write(
|
|
@@ -7029,8 +7980,19 @@ function formatInlineAssetBlock(ref, mime, body, strategy) {
|
|
|
7029
7980
|
${body}
|
|
7030
7981
|
---`;
|
|
7031
7982
|
}
|
|
7032
|
-
function
|
|
7033
|
-
|
|
7983
|
+
function formatAttachmentReminderBlock(ref, mime) {
|
|
7984
|
+
const name = ref.filename ? ` name=${ref.filename}` : "";
|
|
7985
|
+
return `[Attached file] id=${ref.assetId} mime=${mime ?? "unknown"}${name} \u2014 the user uploaded this file; multimodal adapters receive the bytes directly.`;
|
|
7986
|
+
}
|
|
7987
|
+
function formatVisionAuxBlock(ref, mime, description, cached) {
|
|
7988
|
+
const name = ref.filename ? `: ${ref.filename}` : "";
|
|
7989
|
+
const cacheLabel = cached ? " local-cache" : "";
|
|
7990
|
+
return `[Image attachment${name}] id=${ref.assetId} mime=${mime ?? "unknown"}${cacheLabel}
|
|
7991
|
+
${description.trim()}`;
|
|
7992
|
+
}
|
|
7993
|
+
function formatVisionAuxUnavailableBlock(ref, mime, reason) {
|
|
7994
|
+
const name = ref.filename ? `: ${ref.filename}` : "";
|
|
7995
|
+
return `[Image attachment${name}; description unavailable due to vision-aux error] id=${ref.assetId} mime=${mime ?? "unknown"} reason=${reason.slice(0, 180)}`;
|
|
7034
7996
|
}
|
|
7035
7997
|
function formatErrorAssetBlock(ref, mime, error) {
|
|
7036
7998
|
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.`;
|
|
@@ -7372,6 +8334,7 @@ import { createServer } from "http";
|
|
|
7372
8334
|
import { randomUUID, createHash as createHash4, createHmac, timingSafeEqual as cryptoTimingSafeEqual } from "crypto";
|
|
7373
8335
|
import { promises as fs2 } from "fs";
|
|
7374
8336
|
import * as path3 from "path";
|
|
8337
|
+
import { homedir as homedir5 } from "os";
|
|
7375
8338
|
|
|
7376
8339
|
// src/daemon/message-dispatch.ts
|
|
7377
8340
|
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
@@ -7582,6 +8545,11 @@ var LocalServer = class {
|
|
|
7582
8545
|
void this.handleSnapshot(req, res);
|
|
7583
8546
|
return;
|
|
7584
8547
|
}
|
|
8548
|
+
const agentDumpMatch = /^\/v1\/agents\/([^/]+)\/dump-state$/.exec(url);
|
|
8549
|
+
if (req.method === "POST" && agentDumpMatch) {
|
|
8550
|
+
void this.handleAgentDumpState(req, res, decodeURIComponent(agentDumpMatch[1]));
|
|
8551
|
+
return;
|
|
8552
|
+
}
|
|
7585
8553
|
if (req.method === "POST" && url === "/local/asset/write") {
|
|
7586
8554
|
void this.handleAssetWrite(req, res);
|
|
7587
8555
|
return;
|
|
@@ -7646,14 +8614,62 @@ var LocalServer = class {
|
|
|
7646
8614
|
error: "snapshot_failed",
|
|
7647
8615
|
message: err instanceof Error ? err.message : String(err)
|
|
7648
8616
|
});
|
|
7649
|
-
return;
|
|
7650
|
-
}
|
|
7651
|
-
try {
|
|
7652
|
-
const files = await walkAndDigest(root, root);
|
|
7653
|
-
respond(res, 200, { rootPath: root, files });
|
|
8617
|
+
return;
|
|
8618
|
+
}
|
|
8619
|
+
try {
|
|
8620
|
+
const files = await walkAndDigest(root, root);
|
|
8621
|
+
respond(res, 200, { rootPath: root, files });
|
|
8622
|
+
} catch (err) {
|
|
8623
|
+
respond(res, 500, {
|
|
8624
|
+
error: "snapshot_failed",
|
|
8625
|
+
message: err instanceof Error ? err.message : String(err)
|
|
8626
|
+
});
|
|
8627
|
+
}
|
|
8628
|
+
void req;
|
|
8629
|
+
}
|
|
8630
|
+
/**
|
|
8631
|
+
* POST /v1/agents/:agentId/dump-state — agent-scoped daemon manifest.
|
|
8632
|
+
*
|
|
8633
|
+
* This is narrower than `/v1/snapshot`: it walks only the current agent's
|
|
8634
|
+
* profile/work dirs so cloud can attach the result to IMAgentSnapshot without
|
|
8635
|
+
* capturing unrelated agents hosted in the same daemon/container.
|
|
8636
|
+
*/
|
|
8637
|
+
async handleAgentDumpState(req, res, agentId) {
|
|
8638
|
+
const state = this.opts.getState();
|
|
8639
|
+
const agent = state.hostedAgents.find((item) => item.imUserId === agentId);
|
|
8640
|
+
if (!agent) {
|
|
8641
|
+
respond(res, 404, { error: "agent_not_hosted", agentId });
|
|
8642
|
+
return;
|
|
8643
|
+
}
|
|
8644
|
+
const roots = getAgentStateRoots(agent, this.opts.snapshotRoot ?? "/workspace");
|
|
8645
|
+
try {
|
|
8646
|
+
const manifests = await Promise.all(
|
|
8647
|
+
roots.map(async (root) => {
|
|
8648
|
+
const stat = await statOptional(root.rootPath);
|
|
8649
|
+
if (!stat) return { ...root, exists: false, files: [] };
|
|
8650
|
+
if (!stat.isDirectory()) return { ...root, exists: true, error: "not_directory", files: [] };
|
|
8651
|
+
return { ...root, exists: true, files: await walkAndDigest(root.rootPath, root.rootPath) };
|
|
8652
|
+
})
|
|
8653
|
+
);
|
|
8654
|
+
const files = manifests.flatMap(
|
|
8655
|
+
(manifest) => manifest.files.map((file) => ({
|
|
8656
|
+
...file,
|
|
8657
|
+
path: `${manifest.kind}/${file.path}`,
|
|
8658
|
+
rootKind: manifest.kind,
|
|
8659
|
+
rootPath: manifest.rootPath
|
|
8660
|
+
}))
|
|
8661
|
+
);
|
|
8662
|
+
respond(res, 200, {
|
|
8663
|
+
agentId,
|
|
8664
|
+
adapterName: agent.adapterName,
|
|
8665
|
+
dumpedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8666
|
+
roots: manifests,
|
|
8667
|
+
files
|
|
8668
|
+
});
|
|
7654
8669
|
} catch (err) {
|
|
7655
8670
|
respond(res, 500, {
|
|
7656
|
-
error: "
|
|
8671
|
+
error: "agent_dump_state_failed",
|
|
8672
|
+
agentId,
|
|
7657
8673
|
message: err instanceof Error ? err.message : String(err)
|
|
7658
8674
|
});
|
|
7659
8675
|
}
|
|
@@ -7930,6 +8946,39 @@ function validateAgentDispatchRequest(body) {
|
|
|
7930
8946
|
if (payload.attachments != null && !Array.isArray(payload.attachments)) return "attachments must be an array";
|
|
7931
8947
|
return null;
|
|
7932
8948
|
}
|
|
8949
|
+
function getAgentStateRoots(agent, snapshotRoot) {
|
|
8950
|
+
const profileName = sanitizeProfileName(agent.name || agent.imUserId);
|
|
8951
|
+
const roots = [
|
|
8952
|
+
{
|
|
8953
|
+
kind: "workspace-agent",
|
|
8954
|
+
rootPath: path3.join(snapshotRoot, "agents", agent.imUserId)
|
|
8955
|
+
}
|
|
8956
|
+
];
|
|
8957
|
+
if (agent.adapterName === "hermes" || agent.adapterName === "claude-code") {
|
|
8958
|
+
roots.unshift({
|
|
8959
|
+
kind: "hermes-profile",
|
|
8960
|
+
rootPath: path3.join(process.env.HERMES_HOME || path3.join(homedir5(), ".hermes"), "profiles", profileName)
|
|
8961
|
+
});
|
|
8962
|
+
}
|
|
8963
|
+
if (agent.adapterName === "openclaw") {
|
|
8964
|
+
roots.unshift({
|
|
8965
|
+
kind: "openclaw-profile",
|
|
8966
|
+
rootPath: path3.join(process.env.OPENCLAW_HOME || path3.join(homedir5(), ".openclaw"), "profiles", profileName)
|
|
8967
|
+
});
|
|
8968
|
+
}
|
|
8969
|
+
return roots;
|
|
8970
|
+
}
|
|
8971
|
+
function sanitizeProfileName(value) {
|
|
8972
|
+
return value.replace(/[\\/]/g, "_").trim() || "default";
|
|
8973
|
+
}
|
|
8974
|
+
async function statOptional(rootPath) {
|
|
8975
|
+
try {
|
|
8976
|
+
return await fs2.stat(rootPath);
|
|
8977
|
+
} catch (err) {
|
|
8978
|
+
if (err.code === "ENOENT") return null;
|
|
8979
|
+
throw err;
|
|
8980
|
+
}
|
|
8981
|
+
}
|
|
7933
8982
|
async function walkAndDigest(root, current) {
|
|
7934
8983
|
const out = [];
|
|
7935
8984
|
let entries;
|
|
@@ -9717,7 +10766,7 @@ async function readJson3(req) {
|
|
|
9717
10766
|
|
|
9718
10767
|
// src/daemon/asset/origin/drop-folder.ts
|
|
9719
10768
|
import { promises as fs4 } from "fs";
|
|
9720
|
-
import { homedir as
|
|
10769
|
+
import { homedir as homedir7 } from "os";
|
|
9721
10770
|
import * as path6 from "path";
|
|
9722
10771
|
var DropFolderAdapter = class {
|
|
9723
10772
|
kind = "drop-folder";
|
|
@@ -9725,7 +10774,7 @@ var DropFolderAdapter = class {
|
|
|
9725
10774
|
rootDir;
|
|
9726
10775
|
constructor(opts = {}) {
|
|
9727
10776
|
this.workspaceDir = opts.workspaceDir;
|
|
9728
|
-
this.rootDir = opts.rootDir ?? path6.join(opts.homeDir ??
|
|
10777
|
+
this.rootDir = opts.rootDir ?? path6.join(opts.homeDir ?? homedir7(), ".prismer");
|
|
9729
10778
|
}
|
|
9730
10779
|
/**
|
|
9731
10780
|
* Long-lived async iterable wrapping `scanOnce` on a 1s polling interval.
|
|
@@ -9735,7 +10784,7 @@ var DropFolderAdapter = class {
|
|
|
9735
10784
|
while (true) {
|
|
9736
10785
|
const batch = await this.scanOnce(workspaceId);
|
|
9737
10786
|
for (const obs of batch) yield obs;
|
|
9738
|
-
await
|
|
10787
|
+
await sleep2(1e3);
|
|
9739
10788
|
}
|
|
9740
10789
|
}
|
|
9741
10790
|
/**
|
|
@@ -9884,7 +10933,7 @@ function mimeFromExtension(p) {
|
|
|
9884
10933
|
const ext = path6.extname(p).toLowerCase();
|
|
9885
10934
|
return MIME_BY_EXT[ext] ?? null;
|
|
9886
10935
|
}
|
|
9887
|
-
function
|
|
10936
|
+
function sleep2(ms) {
|
|
9888
10937
|
return new Promise((r) => setTimeout(r, ms));
|
|
9889
10938
|
}
|
|
9890
10939
|
|
|
@@ -10595,9 +11644,120 @@ function extensionOf(filename) {
|
|
|
10595
11644
|
return idx >= 0 ? name.slice(idx) : "";
|
|
10596
11645
|
}
|
|
10597
11646
|
|
|
11647
|
+
// src/daemon/asset/magic-bytes.ts
|
|
11648
|
+
var PDF_SIG = Buffer.from("%PDF-", "ascii");
|
|
11649
|
+
var PNG_SIG = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
11650
|
+
var JPEG_SIG = Buffer.from([255, 216, 255]);
|
|
11651
|
+
var GIF87 = Buffer.from("GIF87a", "ascii");
|
|
11652
|
+
var GIF89 = Buffer.from("GIF89a", "ascii");
|
|
11653
|
+
var ZIP_LOCAL = Buffer.from([80, 75, 3, 4]);
|
|
11654
|
+
var ZIP_EMPTY = Buffer.from([80, 75, 5, 6]);
|
|
11655
|
+
var ZIP_SPANNED = Buffer.from([80, 75, 7, 8]);
|
|
11656
|
+
function detectMagicBytes(bytes) {
|
|
11657
|
+
const head = bytes.subarray(0, Math.min(bytes.length, 4096));
|
|
11658
|
+
if (head.length === 0) return { detected: null, mime: null };
|
|
11659
|
+
if (head.length >= PDF_SIG.length && head.subarray(0, PDF_SIG.length).equals(PDF_SIG)) {
|
|
11660
|
+
return { detected: "pdf", mime: "application/pdf" };
|
|
11661
|
+
}
|
|
11662
|
+
if (head.length >= PNG_SIG.length && head.subarray(0, PNG_SIG.length).equals(PNG_SIG)) {
|
|
11663
|
+
return { detected: "png", mime: "image/png" };
|
|
11664
|
+
}
|
|
11665
|
+
if (head.length >= JPEG_SIG.length && head.subarray(0, JPEG_SIG.length).equals(JPEG_SIG)) {
|
|
11666
|
+
return { detected: "jpeg", mime: "image/jpeg" };
|
|
11667
|
+
}
|
|
11668
|
+
if (head.length >= GIF87.length && head.subarray(0, GIF87.length).equals(GIF87) || head.length >= GIF89.length && head.subarray(0, GIF89.length).equals(GIF89)) {
|
|
11669
|
+
return { detected: "gif", mime: "image/gif" };
|
|
11670
|
+
}
|
|
11671
|
+
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))) {
|
|
11672
|
+
return { detected: "zip", mime: "application/zip" };
|
|
11673
|
+
}
|
|
11674
|
+
const text = head.toString("utf8");
|
|
11675
|
+
const trimmed = text.trimStart();
|
|
11676
|
+
if (/^<\?xml[^>]*>\s*<svg[\s>]/i.test(trimmed) || /^<svg[\s>]/i.test(trimmed)) {
|
|
11677
|
+
return { detected: "svg", mime: "image/svg+xml" };
|
|
11678
|
+
}
|
|
11679
|
+
if (/^<(!doctype html|html[\s>])/i.test(trimmed)) {
|
|
11680
|
+
return { detected: "html", mime: "text/html" };
|
|
11681
|
+
}
|
|
11682
|
+
if (/^[{[]/.test(trimmed)) {
|
|
11683
|
+
return { detected: "json", mime: "application/json" };
|
|
11684
|
+
}
|
|
11685
|
+
if (looksLikePrintableText(head)) {
|
|
11686
|
+
return { detected: "markdown-or-text", mime: "text/plain" };
|
|
11687
|
+
}
|
|
11688
|
+
return { detected: null, mime: null };
|
|
11689
|
+
}
|
|
11690
|
+
var ZIP_DERIVED_MIMES = /* @__PURE__ */ new Set([
|
|
11691
|
+
"application/zip",
|
|
11692
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
11693
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
11694
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
11695
|
+
"application/vnd.oasis.opendocument.text",
|
|
11696
|
+
"application/vnd.oasis.opendocument.spreadsheet",
|
|
11697
|
+
"application/vnd.oasis.opendocument.presentation",
|
|
11698
|
+
"application/epub+zip",
|
|
11699
|
+
"application/java-archive"
|
|
11700
|
+
]);
|
|
11701
|
+
function mimeMismatchReason(detected, declared) {
|
|
11702
|
+
if (detected.detected === null) return null;
|
|
11703
|
+
const dec = (declared ?? "").toLowerCase().split(";")[0]?.trim() ?? "";
|
|
11704
|
+
if (!dec || dec === "application/octet-stream") return null;
|
|
11705
|
+
switch (detected.detected) {
|
|
11706
|
+
case "pdf":
|
|
11707
|
+
return dec === "application/pdf" ? null : `declared ${dec} but content is application/pdf`;
|
|
11708
|
+
case "png":
|
|
11709
|
+
return dec === "image/png" ? null : `declared ${dec} but content is image/png`;
|
|
11710
|
+
case "jpeg":
|
|
11711
|
+
return dec === "image/jpeg" || dec === "image/jpg" ? null : `declared ${dec} but content is image/jpeg`;
|
|
11712
|
+
case "gif":
|
|
11713
|
+
return dec === "image/gif" ? null : `declared ${dec} but content is image/gif`;
|
|
11714
|
+
case "zip":
|
|
11715
|
+
if (ZIP_DERIVED_MIMES.has(dec)) return null;
|
|
11716
|
+
return `declared ${dec} but content is a ZIP-container (application/zip family)`;
|
|
11717
|
+
case "svg":
|
|
11718
|
+
if (dec === "image/svg+xml" || dec.startsWith("text/")) return null;
|
|
11719
|
+
return `declared ${dec} but content is image/svg+xml`;
|
|
11720
|
+
case "html":
|
|
11721
|
+
if (dec === "text/html" || dec === "application/xhtml+xml" || dec.startsWith("text/")) return null;
|
|
11722
|
+
if (dec === "application/pdf" || dec.startsWith("image/") || dec === "application/zip") {
|
|
11723
|
+
return `declared ${dec} but content is text/html`;
|
|
11724
|
+
}
|
|
11725
|
+
return null;
|
|
11726
|
+
case "json":
|
|
11727
|
+
if (dec === "application/json" || dec.startsWith("text/")) return null;
|
|
11728
|
+
if (dec === "application/pdf" || dec.startsWith("image/") || dec === "application/zip") {
|
|
11729
|
+
return `declared ${dec} but content is application/json`;
|
|
11730
|
+
}
|
|
11731
|
+
return null;
|
|
11732
|
+
case "markdown-or-text":
|
|
11733
|
+
if (dec === "application/pdf" || dec.startsWith("image/") || dec === "application/zip") {
|
|
11734
|
+
return `declared ${dec} but content is text (not a real ${dec} payload)`;
|
|
11735
|
+
}
|
|
11736
|
+
return null;
|
|
11737
|
+
default:
|
|
11738
|
+
return null;
|
|
11739
|
+
}
|
|
11740
|
+
}
|
|
11741
|
+
function looksLikePrintableText(buf) {
|
|
11742
|
+
if (buf.length === 0) return false;
|
|
11743
|
+
let printable = 0;
|
|
11744
|
+
let total = 0;
|
|
11745
|
+
for (let i = 0; i < buf.length; i++) {
|
|
11746
|
+
const b = buf[i];
|
|
11747
|
+
if (b === 0) return false;
|
|
11748
|
+
total++;
|
|
11749
|
+
if (b >= 32 && b <= 126 || b === 9 || b === 10 || b === 13 || // utf-8 lead bytes
|
|
11750
|
+
b >= 128) {
|
|
11751
|
+
printable++;
|
|
11752
|
+
}
|
|
11753
|
+
}
|
|
11754
|
+
return total > 0 && printable / total >= 0.95;
|
|
11755
|
+
}
|
|
11756
|
+
|
|
10598
11757
|
// src/daemon/outbox-watcher.ts
|
|
10599
11758
|
var DEFAULT_INTERVAL_MS = 2e3;
|
|
10600
11759
|
var RESERVED_SUBDIR = "_uploaded";
|
|
11760
|
+
var REJECTED_SUBDIR = "_rejected";
|
|
10601
11761
|
var OutboxWatcher = class {
|
|
10602
11762
|
constructor(opts) {
|
|
10603
11763
|
this.opts = opts;
|
|
@@ -10772,6 +11932,7 @@ var OutboxWatcher = class {
|
|
|
10772
11932
|
for (const name of entries) {
|
|
10773
11933
|
if (name.startsWith(".")) continue;
|
|
10774
11934
|
if (name === RESERVED_SUBDIR) continue;
|
|
11935
|
+
if (name === REJECTED_SUBDIR) continue;
|
|
10775
11936
|
const full = path8.join(dir, name);
|
|
10776
11937
|
let st;
|
|
10777
11938
|
try {
|
|
@@ -10786,10 +11947,77 @@ var OutboxWatcher = class {
|
|
|
10786
11947
|
await this.upload(full, st, kind, task);
|
|
10787
11948
|
this.uploaded.add(key);
|
|
10788
11949
|
} catch (err) {
|
|
10789
|
-
|
|
11950
|
+
const msg = err.message;
|
|
11951
|
+
this.log("warn", `upload ${full} failed: ${msg}`);
|
|
11952
|
+
if (/\bMIME_MISMATCH\b/i.test(msg) || /\bmime mismatch\b/i.test(msg)) {
|
|
11953
|
+
await this.quarantine(dir, full).catch(
|
|
11954
|
+
(moveErr) => this.log("warn", `quarantine ${full} failed: ${moveErr.message}`)
|
|
11955
|
+
);
|
|
11956
|
+
this.uploaded.add(key);
|
|
11957
|
+
await this.reportMimeMismatchToCloud(task, full, msg).catch(
|
|
11958
|
+
(reportErr) => this.log("warn", `report MIME_MISMATCH failed: ${reportErr.message}`)
|
|
11959
|
+
);
|
|
11960
|
+
}
|
|
10790
11961
|
}
|
|
10791
11962
|
}
|
|
10792
11963
|
}
|
|
11964
|
+
/**
|
|
11965
|
+
* F2B — report an OUTBOX_MIME_MISMATCH task event to cloud so the task log
|
|
11966
|
+
* gains an auditable record of the rejection. Without this, the failure is
|
|
11967
|
+
* silent (file quarantined locally, agent unaware) and the next dispatch
|
|
11968
|
+
* loops on the same bad strategy.
|
|
11969
|
+
*
|
|
11970
|
+
* Best-effort: every catch site swallows errors. We never want this
|
|
11971
|
+
* upstream call to block local quarantine or trigger a retry loop, because
|
|
11972
|
+
* the reject decision is already final by the time we get here.
|
|
11973
|
+
*
|
|
11974
|
+
* Endpoint contract: POST /api/im/tasks/:id/event
|
|
11975
|
+
* { code: 'OUTBOX_MIME_MISMATCH', payload: { file, daemonError }, message }
|
|
11976
|
+
* Only callable when `task?.taskId` is known — pre-dispatch outbox files
|
|
11977
|
+
* (no active task) are quarantined silently as before.
|
|
11978
|
+
*/
|
|
11979
|
+
async reportMimeMismatchToCloud(task, filePath, daemonErrorMessage) {
|
|
11980
|
+
if (!task?.taskId) return;
|
|
11981
|
+
const fileName = path8.basename(filePath);
|
|
11982
|
+
const reasonMatch = daemonErrorMessage.match(/MIME_MISMATCH[^:]*:\s*(.+)$/i);
|
|
11983
|
+
const reason = reasonMatch?.[1] ? reasonMatch[1].trim() : daemonErrorMessage;
|
|
11984
|
+
const body = {
|
|
11985
|
+
code: "OUTBOX_MIME_MISMATCH",
|
|
11986
|
+
payload: {
|
|
11987
|
+
file: fileName,
|
|
11988
|
+
reason,
|
|
11989
|
+
daemonError: daemonErrorMessage
|
|
11990
|
+
},
|
|
11991
|
+
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.`
|
|
11992
|
+
};
|
|
11993
|
+
const res = await this.opts.cloud.request("POST", `/api/im/tasks/${encodeURIComponent(task.taskId)}/event`, {
|
|
11994
|
+
body
|
|
11995
|
+
});
|
|
11996
|
+
if (!res.ok) {
|
|
11997
|
+
throw new Error(`cloud rejected event: ${res.status} ${res.error?.code ?? ""} ${res.error?.message ?? ""}`);
|
|
11998
|
+
}
|
|
11999
|
+
}
|
|
12000
|
+
/**
|
|
12001
|
+
* Move a file under `<dir>/_rejected/` so it stops being a scan candidate
|
|
12002
|
+
* (the loop skips the reserved subdir) and operators can find the
|
|
12003
|
+
* offending artifact. Filename collision is rare per task; on collision
|
|
12004
|
+
* we append a millisecond suffix to keep the original observable.
|
|
12005
|
+
*/
|
|
12006
|
+
async quarantine(dir, full) {
|
|
12007
|
+
const rejectedDir = path8.join(dir, REJECTED_SUBDIR);
|
|
12008
|
+
await fs5.mkdir(rejectedDir, { recursive: true });
|
|
12009
|
+
const base = path8.basename(full);
|
|
12010
|
+
let dest = path8.join(rejectedDir, base);
|
|
12011
|
+
try {
|
|
12012
|
+
await fs5.access(dest);
|
|
12013
|
+
const ext = path8.extname(base);
|
|
12014
|
+
const stem = base.slice(0, base.length - ext.length);
|
|
12015
|
+
dest = path8.join(rejectedDir, `${stem}.${Date.now()}${ext}`);
|
|
12016
|
+
} catch {
|
|
12017
|
+
}
|
|
12018
|
+
await fs5.rename(full, dest);
|
|
12019
|
+
this.log("warn", `quarantined ${full} \u2192 ${dest}`);
|
|
12020
|
+
}
|
|
10793
12021
|
async upload(filePath, st, kind, task) {
|
|
10794
12022
|
const wsId = this.opts.workspaceId();
|
|
10795
12023
|
if (!wsId) {
|
|
@@ -10807,6 +12035,11 @@ var OutboxWatcher = class {
|
|
|
10807
12035
|
if (!policy.ok) {
|
|
10808
12036
|
throw new Error(`agent output rejected: ${policy.reason}`);
|
|
10809
12037
|
}
|
|
12038
|
+
const detection = detectMagicBytes(bytes);
|
|
12039
|
+
const reason = mimeMismatchReason(detection, policy.mime);
|
|
12040
|
+
if (reason) {
|
|
12041
|
+
throw new Error(`MIME_MISMATCH for ${fileName}: ${reason}`);
|
|
12042
|
+
}
|
|
10810
12043
|
const metadata = {
|
|
10811
12044
|
taskId: task.taskId,
|
|
10812
12045
|
...task.adapter ? { adapter: task.adapter } : {}
|
|
@@ -11160,6 +12393,350 @@ var WsClient = class extends EventEmitter2 {
|
|
|
11160
12393
|
}
|
|
11161
12394
|
};
|
|
11162
12395
|
|
|
12396
|
+
// src/daemon/pending-reply-cache.ts
|
|
12397
|
+
function fromRaw(r) {
|
|
12398
|
+
return {
|
|
12399
|
+
idempotencyKey: r.idempotency_key,
|
|
12400
|
+
taskId: r.task_id,
|
|
12401
|
+
replyId: r.reply_id,
|
|
12402
|
+
status: r.status,
|
|
12403
|
+
payloadJson: r.payload_json,
|
|
12404
|
+
createdAt: r.created_at,
|
|
12405
|
+
preparedAt: r.prepared_at,
|
|
12406
|
+
lastAttemptAt: r.last_attempt_at,
|
|
12407
|
+
attemptCount: r.attempt_count,
|
|
12408
|
+
lastError: r.last_error
|
|
12409
|
+
};
|
|
12410
|
+
}
|
|
12411
|
+
function createPendingReplyCache(db) {
|
|
12412
|
+
const insert = db.prepare(
|
|
12413
|
+
`INSERT OR REPLACE INTO pending_dispatch_replies
|
|
12414
|
+
(idempotency_key, task_id, reply_id, status, payload_json,
|
|
12415
|
+
prepared_at, created_at, last_attempt_at, attempt_count, last_error)
|
|
12416
|
+
VALUES (?, ?, NULL, 'pending', ?, NULL, ?, NULL, 0, NULL)`
|
|
12417
|
+
);
|
|
12418
|
+
const markPreparedStmt = db.prepare(
|
|
12419
|
+
`UPDATE pending_dispatch_replies
|
|
12420
|
+
SET reply_id = ?, status = 'prepared', prepared_at = ?,
|
|
12421
|
+
last_attempt_at = ?, attempt_count = attempt_count + 1,
|
|
12422
|
+
last_error = NULL
|
|
12423
|
+
WHERE idempotency_key = ?`
|
|
12424
|
+
);
|
|
12425
|
+
const markAttemptStmt = db.prepare(
|
|
12426
|
+
`UPDATE pending_dispatch_replies
|
|
12427
|
+
SET last_attempt_at = ?, attempt_count = attempt_count + 1,
|
|
12428
|
+
last_error = ?
|
|
12429
|
+
WHERE idempotency_key = ?`
|
|
12430
|
+
);
|
|
12431
|
+
const clearStmt = db.prepare(
|
|
12432
|
+
`DELETE FROM pending_dispatch_replies WHERE idempotency_key = ?`
|
|
12433
|
+
);
|
|
12434
|
+
const listRecoveryStmt = db.prepare(
|
|
12435
|
+
`SELECT * FROM pending_dispatch_replies
|
|
12436
|
+
WHERE status IN ('pending', 'prepared')
|
|
12437
|
+
ORDER BY created_at ASC`
|
|
12438
|
+
);
|
|
12439
|
+
const countStmt = db.prepare(
|
|
12440
|
+
`SELECT status, COUNT(*) as cnt FROM pending_dispatch_replies
|
|
12441
|
+
WHERE status IN ('pending','prepared') GROUP BY status`
|
|
12442
|
+
);
|
|
12443
|
+
const findStmt = db.prepare(
|
|
12444
|
+
`SELECT * FROM pending_dispatch_replies WHERE idempotency_key = ?`
|
|
12445
|
+
);
|
|
12446
|
+
return {
|
|
12447
|
+
savePending(idempotencyKey, taskId, payloadJson, now = Date.now()) {
|
|
12448
|
+
insert.run(idempotencyKey, taskId, payloadJson, now);
|
|
12449
|
+
},
|
|
12450
|
+
markPrepared(idempotencyKey, replyId, now = Date.now()) {
|
|
12451
|
+
markPreparedStmt.run(replyId, now, now, idempotencyKey);
|
|
12452
|
+
},
|
|
12453
|
+
markAttempt(idempotencyKey, now = Date.now(), error) {
|
|
12454
|
+
markAttemptStmt.run(now, error ?? null, idempotencyKey);
|
|
12455
|
+
},
|
|
12456
|
+
clearPending(idempotencyKey) {
|
|
12457
|
+
clearStmt.run(idempotencyKey);
|
|
12458
|
+
},
|
|
12459
|
+
listForRecovery() {
|
|
12460
|
+
const rows = listRecoveryStmt.all();
|
|
12461
|
+
return rows.map(fromRaw);
|
|
12462
|
+
},
|
|
12463
|
+
countByStatus() {
|
|
12464
|
+
const rows = countStmt.all();
|
|
12465
|
+
const result = { pending: 0, prepared: 0 };
|
|
12466
|
+
for (const r of rows) {
|
|
12467
|
+
if (r.status === "pending") result.pending = r.cnt;
|
|
12468
|
+
else if (r.status === "prepared") result.prepared = r.cnt;
|
|
12469
|
+
}
|
|
12470
|
+
return result;
|
|
12471
|
+
},
|
|
12472
|
+
findByIdempotencyKey(idempotencyKey) {
|
|
12473
|
+
const row = findStmt.get(idempotencyKey);
|
|
12474
|
+
return row ? fromRaw(row) : null;
|
|
12475
|
+
}
|
|
12476
|
+
};
|
|
12477
|
+
}
|
|
12478
|
+
function generateIdempotencyKey() {
|
|
12479
|
+
const c = globalThis.crypto;
|
|
12480
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
12481
|
+
const bytes = new Uint8Array(16);
|
|
12482
|
+
const cryptoNode = globalThis.crypto;
|
|
12483
|
+
if (cryptoNode?.getRandomValues) {
|
|
12484
|
+
cryptoNode.getRandomValues(bytes);
|
|
12485
|
+
} else {
|
|
12486
|
+
for (let i = 0; i < 16; i += 1) bytes[i] = Math.floor(Math.random() * 256);
|
|
12487
|
+
}
|
|
12488
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
12489
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
12490
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
12491
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
12492
|
+
}
|
|
12493
|
+
|
|
12494
|
+
// src/daemon/dispatch-reply-transport.ts
|
|
12495
|
+
var DispatchReplyAbortedError = class extends Error {
|
|
12496
|
+
constructor(replyId) {
|
|
12497
|
+
super(`dispatch reply ${replyId} aborted by server reaper (likely >1h since prepare)`);
|
|
12498
|
+
this.replyId = replyId;
|
|
12499
|
+
this.name = "DispatchReplyAbortedError";
|
|
12500
|
+
}
|
|
12501
|
+
replyId;
|
|
12502
|
+
code = "DISPATCH_REPLY_INVALID_STATE";
|
|
12503
|
+
};
|
|
12504
|
+
function defaultLog2(line) {
|
|
12505
|
+
process.stderr.write(`${line}
|
|
12506
|
+
`);
|
|
12507
|
+
}
|
|
12508
|
+
async function sendDispatchReplyTwoPhase(args) {
|
|
12509
|
+
const { taskId, payload, cloud, cache } = args;
|
|
12510
|
+
const log8 = args.log ?? defaultLog2;
|
|
12511
|
+
const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
|
|
12512
|
+
const payloadJson = JSON.stringify({ ...payload, _taskId: taskId, _idempotencyKey: idempotencyKey });
|
|
12513
|
+
cache.savePending(idempotencyKey, taskId, payloadJson);
|
|
12514
|
+
const prepareRes = await cloud.request("POST", `/api/im/dispatch/${encodeURIComponent(taskId)}/prepare`, {
|
|
12515
|
+
body: { ...payload, idempotencyKey },
|
|
12516
|
+
timeoutMs: 3e4
|
|
12517
|
+
});
|
|
12518
|
+
if (!prepareRes.ok || !prepareRes.data) {
|
|
12519
|
+
const errCode = prepareRes.error?.code ?? "unknown";
|
|
12520
|
+
const errMsg = prepareRes.error?.message ?? `prepare failed (status=${prepareRes.status})`;
|
|
12521
|
+
cache.markAttempt(idempotencyKey, Date.now(), `prepare: ${errCode}: ${errMsg}`);
|
|
12522
|
+
throw new Error(`dispatch prepare failed: ${errCode}: ${errMsg}`);
|
|
12523
|
+
}
|
|
12524
|
+
const envelope2 = prepareRes.data;
|
|
12525
|
+
const inner = "data" in envelope2 && envelope2.data ? envelope2.data : envelope2;
|
|
12526
|
+
if (!inner || typeof inner.replyId !== "string") {
|
|
12527
|
+
cache.markAttempt(idempotencyKey, Date.now(), "prepare: missing replyId in response");
|
|
12528
|
+
throw new Error("dispatch prepare returned no replyId");
|
|
12529
|
+
}
|
|
12530
|
+
if ("ok" in envelope2 && envelope2.ok === false) {
|
|
12531
|
+
const errCode = envelope2.error?.code ?? "prepare_failed";
|
|
12532
|
+
const errMsg = envelope2.error?.message ?? "prepare ok=false";
|
|
12533
|
+
cache.markAttempt(idempotencyKey, Date.now(), `prepare: ${errCode}: ${errMsg}`);
|
|
12534
|
+
throw new Error(`dispatch prepare failed: ${errCode}: ${errMsg}`);
|
|
12535
|
+
}
|
|
12536
|
+
cache.markPrepared(idempotencyKey, inner.replyId);
|
|
12537
|
+
log8(
|
|
12538
|
+
`[daemon] dispatch.prepare task=${taskId} replyId=${inner.replyId} status=${inner.status} idem=${idempotencyKey}`
|
|
12539
|
+
);
|
|
12540
|
+
if (inner.status === "committed") {
|
|
12541
|
+
cache.clearPending(idempotencyKey);
|
|
12542
|
+
return {
|
|
12543
|
+
taskId,
|
|
12544
|
+
replyId: inner.replyId,
|
|
12545
|
+
status: "committed",
|
|
12546
|
+
alreadyCommitted: true
|
|
12547
|
+
};
|
|
12548
|
+
}
|
|
12549
|
+
try {
|
|
12550
|
+
const result = await commitDispatchReply({ taskId, replyId: inner.replyId, cloud, log: log8 });
|
|
12551
|
+
cache.clearPending(idempotencyKey);
|
|
12552
|
+
return { taskId, replyId: inner.replyId, ...result };
|
|
12553
|
+
} catch (err) {
|
|
12554
|
+
if (err instanceof DispatchReplyAbortedError) {
|
|
12555
|
+
cache.clearPending(idempotencyKey);
|
|
12556
|
+
log8(`[daemon] dispatch.commit aborted task=${taskId} replyId=${inner.replyId} \u2014 dispatch lost`);
|
|
12557
|
+
return {
|
|
12558
|
+
taskId,
|
|
12559
|
+
replyId: inner.replyId,
|
|
12560
|
+
status: "aborted",
|
|
12561
|
+
alreadyCommitted: false
|
|
12562
|
+
};
|
|
12563
|
+
}
|
|
12564
|
+
cache.markAttempt(idempotencyKey, Date.now(), `commit: ${err.message}`);
|
|
12565
|
+
throw err;
|
|
12566
|
+
}
|
|
12567
|
+
}
|
|
12568
|
+
async function commitDispatchReply(args) {
|
|
12569
|
+
const log8 = args.log ?? defaultLog2;
|
|
12570
|
+
const res = await args.cloud.request("POST", `/api/im/dispatch/${encodeURIComponent(args.taskId)}/commit`, {
|
|
12571
|
+
body: { replyId: args.replyId },
|
|
12572
|
+
timeoutMs: 3e4
|
|
12573
|
+
});
|
|
12574
|
+
if (!res.ok || !res.data) {
|
|
12575
|
+
const code = res.error?.code ?? "unknown";
|
|
12576
|
+
const msg = res.error?.message ?? `commit failed (status=${res.status})`;
|
|
12577
|
+
if (code === "DISPATCH_REPLY_INVALID_STATE") {
|
|
12578
|
+
throw new DispatchReplyAbortedError(args.replyId);
|
|
12579
|
+
}
|
|
12580
|
+
throw new Error(`dispatch commit failed: ${code}: ${msg}`);
|
|
12581
|
+
}
|
|
12582
|
+
const env = res.data;
|
|
12583
|
+
const inner = "data" in env && env.data ? env.data : env;
|
|
12584
|
+
if ("ok" in env && env.ok === false) {
|
|
12585
|
+
const code = env.error?.code ?? "commit_failed";
|
|
12586
|
+
if (code === "DISPATCH_REPLY_INVALID_STATE") {
|
|
12587
|
+
throw new DispatchReplyAbortedError(args.replyId);
|
|
12588
|
+
}
|
|
12589
|
+
throw new Error(`dispatch commit failed: ${code}: ${env.error?.message ?? "commit ok=false"}`);
|
|
12590
|
+
}
|
|
12591
|
+
log8(
|
|
12592
|
+
`[daemon] dispatch.commit task=${args.taskId} replyId=${args.replyId} alreadyCommitted=${inner.alreadyCommitted ?? false}`
|
|
12593
|
+
);
|
|
12594
|
+
return {
|
|
12595
|
+
status: "committed",
|
|
12596
|
+
alreadyCommitted: Boolean(inner.alreadyCommitted),
|
|
12597
|
+
...inner.messageId ? { messageId: inner.messageId } : {}
|
|
12598
|
+
};
|
|
12599
|
+
}
|
|
12600
|
+
async function recoverPendingReplies(deps) {
|
|
12601
|
+
const log8 = deps.log ?? defaultLog2;
|
|
12602
|
+
const rows = deps.cache.listForRecovery();
|
|
12603
|
+
if (rows.length === 0) {
|
|
12604
|
+
return { attempted: 0, committed: 0, aborted: 0, failed: 0 };
|
|
12605
|
+
}
|
|
12606
|
+
log8(`[daemon] dispatch.recovery: ${rows.length} pending row(s) to replay`);
|
|
12607
|
+
let committed = 0;
|
|
12608
|
+
let aborted = 0;
|
|
12609
|
+
let failed = 0;
|
|
12610
|
+
for (const row of rows) {
|
|
12611
|
+
try {
|
|
12612
|
+
const result = await replayRow(row, deps);
|
|
12613
|
+
if (result.status === "committed") committed += 1;
|
|
12614
|
+
else if (result.status === "aborted") aborted += 1;
|
|
12615
|
+
} catch (err) {
|
|
12616
|
+
failed += 1;
|
|
12617
|
+
log8(
|
|
12618
|
+
`[daemon] dispatch.recovery FAIL idem=${row.idempotencyKey} task=${row.taskId}: ${err.message}`
|
|
12619
|
+
);
|
|
12620
|
+
}
|
|
12621
|
+
}
|
|
12622
|
+
log8(
|
|
12623
|
+
`[daemon] dispatch.recovery done attempted=${rows.length} committed=${committed} aborted=${aborted} failed=${failed}`
|
|
12624
|
+
);
|
|
12625
|
+
return { attempted: rows.length, committed, aborted, failed };
|
|
12626
|
+
}
|
|
12627
|
+
async function replayRow(row, deps) {
|
|
12628
|
+
const log8 = deps.log ?? defaultLog2;
|
|
12629
|
+
if (row.status === "prepared" && row.replyId) {
|
|
12630
|
+
try {
|
|
12631
|
+
const result2 = await commitDispatchReply({
|
|
12632
|
+
taskId: row.taskId,
|
|
12633
|
+
replyId: row.replyId,
|
|
12634
|
+
cloud: deps.cloud,
|
|
12635
|
+
log: log8
|
|
12636
|
+
});
|
|
12637
|
+
deps.cache.clearPending(row.idempotencyKey);
|
|
12638
|
+
return { status: result2.status };
|
|
12639
|
+
} catch (err) {
|
|
12640
|
+
if (err instanceof DispatchReplyAbortedError) {
|
|
12641
|
+
deps.cache.clearPending(row.idempotencyKey);
|
|
12642
|
+
return { status: "aborted" };
|
|
12643
|
+
}
|
|
12644
|
+
throw err;
|
|
12645
|
+
}
|
|
12646
|
+
}
|
|
12647
|
+
let parsed;
|
|
12648
|
+
try {
|
|
12649
|
+
parsed = JSON.parse(row.payloadJson);
|
|
12650
|
+
} catch (err) {
|
|
12651
|
+
deps.cache.clearPending(row.idempotencyKey);
|
|
12652
|
+
throw new Error(`pending row ${row.idempotencyKey} has unparseable payload: ${err.message}`);
|
|
12653
|
+
}
|
|
12654
|
+
const { _taskId, _idempotencyKey, ...payload } = parsed;
|
|
12655
|
+
const result = await sendDispatchReplyTwoPhase({
|
|
12656
|
+
taskId: row.taskId,
|
|
12657
|
+
payload,
|
|
12658
|
+
cloud: deps.cloud,
|
|
12659
|
+
cache: deps.cache,
|
|
12660
|
+
idempotencyKey: row.idempotencyKey,
|
|
12661
|
+
log: deps.log
|
|
12662
|
+
});
|
|
12663
|
+
return { status: result.status };
|
|
12664
|
+
}
|
|
12665
|
+
|
|
12666
|
+
// src/daemon/transport-probe.ts
|
|
12667
|
+
import { networkInterfaces } from "os";
|
|
12668
|
+
function isPrivateAddress(addr) {
|
|
12669
|
+
if (!addr) return true;
|
|
12670
|
+
const h = String(addr).trim().toLowerCase();
|
|
12671
|
+
if (!h) return true;
|
|
12672
|
+
if (h === "localhost" || h === "::1") return true;
|
|
12673
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
|
|
12674
|
+
if (m) {
|
|
12675
|
+
const a = Number(m[1]);
|
|
12676
|
+
const b = Number(m[2]);
|
|
12677
|
+
if (a === 10) return true;
|
|
12678
|
+
if (a === 127) return true;
|
|
12679
|
+
if (a === 169 && b === 254) return true;
|
|
12680
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
12681
|
+
if (a === 192 && b === 168) return true;
|
|
12682
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
12683
|
+
return false;
|
|
12684
|
+
}
|
|
12685
|
+
if (h.endsWith(".local")) return true;
|
|
12686
|
+
if (h.endsWith(".localhost")) return true;
|
|
12687
|
+
return false;
|
|
12688
|
+
}
|
|
12689
|
+
function buildTransportProbe(gatewayUrl) {
|
|
12690
|
+
if (!gatewayUrl) {
|
|
12691
|
+
return { transport: "ws", gatewayUrl: null, gatewayIsPrivate: true };
|
|
12692
|
+
}
|
|
12693
|
+
let host = null;
|
|
12694
|
+
try {
|
|
12695
|
+
host = new URL(gatewayUrl).hostname;
|
|
12696
|
+
} catch {
|
|
12697
|
+
host = null;
|
|
12698
|
+
}
|
|
12699
|
+
const gatewayIsPrivate = isPrivateAddress(host);
|
|
12700
|
+
return {
|
|
12701
|
+
transport: gatewayIsPrivate ? "ws" : "both",
|
|
12702
|
+
gatewayUrl,
|
|
12703
|
+
gatewayIsPrivate
|
|
12704
|
+
};
|
|
12705
|
+
}
|
|
12706
|
+
function pickLocalIPv4() {
|
|
12707
|
+
const nics = networkInterfaces();
|
|
12708
|
+
let firstPrivate = null;
|
|
12709
|
+
for (const list of Object.values(nics)) {
|
|
12710
|
+
if (!list) continue;
|
|
12711
|
+
for (const ni of list) {
|
|
12712
|
+
if (ni.internal || ni.family !== "IPv4") continue;
|
|
12713
|
+
if (!isPrivateAddress(ni.address)) return ni.address;
|
|
12714
|
+
if (!firstPrivate) firstPrivate = ni.address;
|
|
12715
|
+
}
|
|
12716
|
+
}
|
|
12717
|
+
return firstPrivate;
|
|
12718
|
+
}
|
|
12719
|
+
async function reportTransportProbe(cloud, daemonId, probe) {
|
|
12720
|
+
const body = {
|
|
12721
|
+
daemonId,
|
|
12722
|
+
transport: probe.transport,
|
|
12723
|
+
gatewayUrl: probe.gatewayUrl,
|
|
12724
|
+
gatewayIsPrivate: probe.gatewayIsPrivate
|
|
12725
|
+
};
|
|
12726
|
+
const res = await cloud.request("POST", "/api/im/runtime/transport-report", {
|
|
12727
|
+
body,
|
|
12728
|
+
timeoutMs: 5e3
|
|
12729
|
+
});
|
|
12730
|
+
if (!res.ok) {
|
|
12731
|
+
return {
|
|
12732
|
+
ok: false,
|
|
12733
|
+
status: res.status,
|
|
12734
|
+
error: res.error?.message ?? `transport-report failed (${res.status})`
|
|
12735
|
+
};
|
|
12736
|
+
}
|
|
12737
|
+
return { ok: true, status: res.status };
|
|
12738
|
+
}
|
|
12739
|
+
|
|
11163
12740
|
// src/daemon/runner.ts
|
|
11164
12741
|
var DEFAULT_LOCAL_PORT = 3210;
|
|
11165
12742
|
var log7 = createLogger("Daemon");
|
|
@@ -11208,6 +12785,14 @@ var Runner = class extends EventEmitter3 {
|
|
|
11208
12785
|
// F16 (2026-05-20) — periodic skill resync timer (default 10min).
|
|
11209
12786
|
skillResyncTimer;
|
|
11210
12787
|
skillSyncInFlight = false;
|
|
12788
|
+
// Wave-4 E7 — local cache of in-flight two-phase dispatch replies. Survives
|
|
12789
|
+
// daemon crash so cold-start can resume `prepared` rows via idempotent
|
|
12790
|
+
// commit (server enforces (taskId, idempotencyKey) UNIQUE).
|
|
12791
|
+
pendingReplyCache;
|
|
12792
|
+
// One-shot guard so we only recover on the first post-authenticated tick,
|
|
12793
|
+
// not every reconnect (server-side commits are idempotent but the log noise
|
|
12794
|
+
// and DB pressure of re-running on every redeclare is wasteful).
|
|
12795
|
+
pendingReplyRecoveryDone = false;
|
|
11211
12796
|
async start() {
|
|
11212
12797
|
if (this.state !== "idle") throw new Error(`Runner already in state ${this.state}`);
|
|
11213
12798
|
this.state = "starting";
|
|
@@ -11222,6 +12807,7 @@ var Runner = class extends EventEmitter3 {
|
|
|
11222
12807
|
process.env.PRISMER_API_KEY = this.config.api_key;
|
|
11223
12808
|
this.db = openLocalDb(this.paths.localDb);
|
|
11224
12809
|
this.cloud = new CloudClient({ baseUrl: this.config.cloud_api_base, apiKey: this.config.api_key });
|
|
12810
|
+
this.pendingReplyCache = createPendingReplyCache(this.db);
|
|
11225
12811
|
this.assetCache = new AssetCache({
|
|
11226
12812
|
db: this.db,
|
|
11227
12813
|
cloud: this.cloud,
|
|
@@ -11828,6 +13414,19 @@ var Runner = class extends EventEmitter3 {
|
|
|
11828
13414
|
process.stdout.write(`[daemon] ws authenticated, sending agent.host.declare (${this.hostedAgents.size} agents)
|
|
11829
13415
|
`);
|
|
11830
13416
|
this.sendDeclare();
|
|
13417
|
+
if (!this.pendingReplyRecoveryDone) {
|
|
13418
|
+
this.pendingReplyRecoveryDone = true;
|
|
13419
|
+
void recoverPendingReplies({ cloud: this.cloud, cache: this.pendingReplyCache }).then((summary) => {
|
|
13420
|
+
if (summary.attempted === 0) return;
|
|
13421
|
+
process.stdout.write(
|
|
13422
|
+
`[daemon] dispatch.recovery summary attempted=${summary.attempted} committed=${summary.committed} aborted=${summary.aborted} failed=${summary.failed}
|
|
13423
|
+
`
|
|
13424
|
+
);
|
|
13425
|
+
}).catch((err) => {
|
|
13426
|
+
process.stderr.write(`[daemon] dispatch.recovery threw: ${err.message}
|
|
13427
|
+
`);
|
|
13428
|
+
});
|
|
13429
|
+
}
|
|
11831
13430
|
return;
|
|
11832
13431
|
}
|
|
11833
13432
|
this.handleIncoming(msg);
|
|
@@ -11874,12 +13473,90 @@ var Runner = class extends EventEmitter3 {
|
|
|
11874
13473
|
case "asset.changed":
|
|
11875
13474
|
void this.onAssetChanged(msg.payload);
|
|
11876
13475
|
return;
|
|
13476
|
+
// v2.0 §4.8.1 (Wave 4-E4) — webhook reverse-channel RPC. Cloud's
|
|
13477
|
+
// dispatchExternalMention pushes an AgentDispatchRequest plus an
|
|
13478
|
+
// embedded `_rpcId`; the daemon runs the same handler as the
|
|
13479
|
+
// HTTP /dispatch path and echoes the reply back over WS with the
|
|
13480
|
+
// identical `_rpcId` so cloud's WsRpcService can settle the
|
|
13481
|
+
// pending promise.
|
|
13482
|
+
case "webhook.dispatch.request":
|
|
13483
|
+
void this.onWebhookDispatch(msg.payload);
|
|
13484
|
+
return;
|
|
11877
13485
|
default:
|
|
11878
13486
|
this.emit("unknown-message", msg);
|
|
11879
13487
|
}
|
|
11880
13488
|
}
|
|
13489
|
+
/**
|
|
13490
|
+
* v2.0 §4.8.1 — webhook dispatch over WS reverse channel.
|
|
13491
|
+
*
|
|
13492
|
+
* Acks via `webhook.dispatch.reply { _rpcId, ok, acceptedAt }` immediately
|
|
13493
|
+
* after `handleAgentMessageDispatch()` returns its synchronous response
|
|
13494
|
+
* — the actual reply (the agent's reply text/attachments) still flows
|
|
13495
|
+
* through `postMessageDispatchReply()` which posts to
|
|
13496
|
+
* `/api/im/dispatch/reply` after the adapter finishes. That mirrors the
|
|
13497
|
+
* HTTP /dispatch contract: ack first, asynchronous final reply via the
|
|
13498
|
+
* dispatch-reply REST endpoint.
|
|
13499
|
+
*
|
|
13500
|
+
* The `_rpcId` field is the only addition versus the HTTP path; we strip
|
|
13501
|
+
* it before passing the payload to the dispatch handler so existing
|
|
13502
|
+
* validation logic doesn't trip on an unknown field.
|
|
13503
|
+
*/
|
|
13504
|
+
async onWebhookDispatch(payload) {
|
|
13505
|
+
const rpcId = payload._rpcId;
|
|
13506
|
+
if (!rpcId) {
|
|
13507
|
+
process.stderr.write(`[daemon] webhook.dispatch.request without _rpcId \u2014 dropped
|
|
13508
|
+
`);
|
|
13509
|
+
return;
|
|
13510
|
+
}
|
|
13511
|
+
const { _rpcId: _, ...request } = payload;
|
|
13512
|
+
try {
|
|
13513
|
+
const handle = handleAgentMessageDispatch(request, {
|
|
13514
|
+
findAgent: (agentImUserId) => this.findMessageDispatchAgent(agentImUserId),
|
|
13515
|
+
postReply: (replyPayload) => this.postMessageDispatchReply(replyPayload),
|
|
13516
|
+
onError: (err, req) => {
|
|
13517
|
+
process.stderr.write(
|
|
13518
|
+
`[daemon] webhook.dispatch.request failed message=${req.messageId} agent=${req.mentionedAgentImUserId}: ${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
13519
|
+
`
|
|
13520
|
+
);
|
|
13521
|
+
}
|
|
13522
|
+
});
|
|
13523
|
+
this.ws.send({
|
|
13524
|
+
type: "webhook.dispatch.reply",
|
|
13525
|
+
payload: {
|
|
13526
|
+
_rpcId: rpcId,
|
|
13527
|
+
ok: handle.response.ok,
|
|
13528
|
+
acceptedAt: handle.response.acceptedAt,
|
|
13529
|
+
error: handle.response.error
|
|
13530
|
+
}
|
|
13531
|
+
});
|
|
13532
|
+
void handle.done.catch((err) => {
|
|
13533
|
+
process.stderr.write(
|
|
13534
|
+
`[daemon] webhook.dispatch final reply failed message=${request.messageId}: ${err instanceof Error ? err.message : String(err)}
|
|
13535
|
+
`
|
|
13536
|
+
);
|
|
13537
|
+
});
|
|
13538
|
+
} catch (err) {
|
|
13539
|
+
this.ws.send({
|
|
13540
|
+
type: "webhook.dispatch.reply",
|
|
13541
|
+
payload: {
|
|
13542
|
+
_rpcId: rpcId,
|
|
13543
|
+
ok: false,
|
|
13544
|
+
error: {
|
|
13545
|
+
code: "daemon_dispatch_failed",
|
|
13546
|
+
message: err instanceof Error ? err.message : String(err)
|
|
13547
|
+
}
|
|
13548
|
+
}
|
|
13549
|
+
});
|
|
13550
|
+
}
|
|
13551
|
+
}
|
|
11881
13552
|
async onHostAcked(payload) {
|
|
11882
13553
|
this.workspaceId = payload.workspaceId;
|
|
13554
|
+
void this.reportTransport().catch((err) => {
|
|
13555
|
+
process.stderr.write(
|
|
13556
|
+
`[daemon] transport-probe report failed: ${err.message}
|
|
13557
|
+
`
|
|
13558
|
+
);
|
|
13559
|
+
});
|
|
11883
13560
|
if (this.memoryWiring && this.workspaceId) {
|
|
11884
13561
|
syncMemoryFromCloud(this.memoryWiring, this.cloud, [this.workspaceId]).catch(
|
|
11885
13562
|
(err) => log7.error("Initial memory sync failed", err.message)
|
|
@@ -12061,14 +13738,68 @@ var Runner = class extends EventEmitter3 {
|
|
|
12061
13738
|
};
|
|
12062
13739
|
}
|
|
12063
13740
|
async postMessageDispatchReply(payload) {
|
|
12064
|
-
const
|
|
12065
|
-
|
|
12066
|
-
|
|
12067
|
-
|
|
12068
|
-
|
|
12069
|
-
|
|
12070
|
-
|
|
13741
|
+
const taskId = `external:${payload.replyToMessageId}`;
|
|
13742
|
+
try {
|
|
13743
|
+
const result = await sendDispatchReplyTwoPhase({
|
|
13744
|
+
taskId,
|
|
13745
|
+
payload: {
|
|
13746
|
+
conversationId: payload.conversationId,
|
|
13747
|
+
replyToken: payload.replyToken,
|
|
13748
|
+
replyToMessageId: payload.replyToMessageId,
|
|
13749
|
+
agentImUserId: payload.agentImUserId,
|
|
13750
|
+
status: payload.status,
|
|
13751
|
+
...payload.replyText !== void 0 ? { replyText: payload.replyText } : {},
|
|
13752
|
+
...payload.attachments ? { attachments: payload.attachments } : {},
|
|
13753
|
+
assetIds: payload.attachments?.map((a) => a.assetId).filter((id) => typeof id === "string") ?? [],
|
|
13754
|
+
completedAt: payload.completedAt,
|
|
13755
|
+
...payload.error ? { error: payload.error } : {}
|
|
13756
|
+
},
|
|
13757
|
+
cloud: this.cloud,
|
|
13758
|
+
cache: this.pendingReplyCache
|
|
13759
|
+
});
|
|
13760
|
+
if (result.status === "aborted") {
|
|
13761
|
+
throw new Error(`dispatch reply aborted by server reaper (replyId=${result.replyId})`);
|
|
13762
|
+
}
|
|
13763
|
+
} catch (err) {
|
|
13764
|
+
throw new Error(
|
|
13765
|
+
err.message?.length ? err.message : `dispatch reply failed`
|
|
13766
|
+
);
|
|
13767
|
+
}
|
|
13768
|
+
}
|
|
13769
|
+
/**
|
|
13770
|
+
* v2.0 §4.8.1 (Wave 4-E4) — submit the transport-probe report. Called
|
|
13771
|
+
* from `onHostAcked` so the daemon's container row exists in cloud
|
|
13772
|
+
* before we POST to /runtime/transport-report.
|
|
13773
|
+
*
|
|
13774
|
+
* The gatewayUrl reported here is `http://<local-ipv4>:<localPort>`
|
|
13775
|
+
* (typically `http://192.168.x.x:3210`). For a daemon without a local
|
|
13776
|
+
* HTTP server (startLocalServer=false in tests) we still post the
|
|
13777
|
+
* probe with `gatewayUrl=null` so cloud knows transport='ws' is the
|
|
13778
|
+
* only path.
|
|
13779
|
+
*/
|
|
13780
|
+
async reportTransport() {
|
|
13781
|
+
const hasLocalServer = this.opts.startLocalServer !== false;
|
|
13782
|
+
let gatewayUrl = null;
|
|
13783
|
+
if (hasLocalServer) {
|
|
13784
|
+
const localIp = pickLocalIPv4();
|
|
13785
|
+
if (localIp) {
|
|
13786
|
+
const port = this.opts.localPort ?? DEFAULT_LOCAL_PORT;
|
|
13787
|
+
gatewayUrl = `http://${localIp}:${port}`;
|
|
13788
|
+
}
|
|
12071
13789
|
}
|
|
13790
|
+
const probe = buildTransportProbe(gatewayUrl);
|
|
13791
|
+
const result = await reportTransportProbe(this.cloud, this.config.daemon_id, probe);
|
|
13792
|
+
if (!result.ok) {
|
|
13793
|
+
process.stderr.write(
|
|
13794
|
+
`[daemon] transport-probe report rejected status=${result.status} error=${result.error ?? "-"}
|
|
13795
|
+
`
|
|
13796
|
+
);
|
|
13797
|
+
return;
|
|
13798
|
+
}
|
|
13799
|
+
process.stdout.write(
|
|
13800
|
+
`[daemon] transport-probe reported transport=${probe.transport} gateway=${probe.gatewayUrl ?? "null"} private=${probe.gatewayIsPrivate}
|
|
13801
|
+
`
|
|
13802
|
+
);
|
|
12072
13803
|
}
|
|
12073
13804
|
onAgentChanged(payload) {
|
|
12074
13805
|
const a = this.hostedAgents.get(payload.agentImUserId);
|
|
@@ -12511,7 +14242,7 @@ var Runner = class extends EventEmitter3 {
|
|
|
12511
14242
|
function resolveUserAssetWorkspaceDir(workspaceId) {
|
|
12512
14243
|
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
12513
14244
|
if (override) return join17(override, workspaceId);
|
|
12514
|
-
const home =
|
|
14245
|
+
const home = homedir8();
|
|
12515
14246
|
const desktop = join17(home, "Desktop");
|
|
12516
14247
|
const base = existsSync13(desktop) ? desktop : home;
|
|
12517
14248
|
return join17(base, "Prismer Assets", workspaceId);
|
|
@@ -12630,7 +14361,7 @@ function buildDaemonCommand() {
|
|
|
12630
14361
|
while (!configExists(paths)) {
|
|
12631
14362
|
if (stopRequested) return;
|
|
12632
14363
|
getUI().info(`[daemon] waiting for ${paths.configFile} \u2014 run \`prismer setup\` to create it`);
|
|
12633
|
-
await
|
|
14364
|
+
await sleep3(5e3);
|
|
12634
14365
|
}
|
|
12635
14366
|
runner = new Runner({
|
|
12636
14367
|
startLocalServer: opts.localServer !== false,
|
|
@@ -12662,7 +14393,7 @@ function buildDaemonCommand() {
|
|
|
12662
14393
|
if (existingPid && pidAlive2(existingPid)) {
|
|
12663
14394
|
exitWithError(`Daemon already running (pid ${existingPid}). Use \`prismer daemon stop\` first.`);
|
|
12664
14395
|
}
|
|
12665
|
-
if (!existsSync14(paths.logsDir))
|
|
14396
|
+
if (!existsSync14(paths.logsDir)) mkdirSync10(paths.logsDir, { recursive: true });
|
|
12666
14397
|
const logFile = join18(paths.logsDir, "daemon.log");
|
|
12667
14398
|
const fd = openSync2(logFile, "a");
|
|
12668
14399
|
const args = [process.argv[1], "daemon", "run"];
|
|
@@ -12676,7 +14407,7 @@ function buildDaemonCommand() {
|
|
|
12676
14407
|
child.unref();
|
|
12677
14408
|
const deadline = Date.now() + 5e3;
|
|
12678
14409
|
while (Date.now() < deadline) {
|
|
12679
|
-
await
|
|
14410
|
+
await sleep3(200);
|
|
12680
14411
|
const pid = readPidFile(paths);
|
|
12681
14412
|
if (pid && pidAlive2(pid)) {
|
|
12682
14413
|
if (opts.json) printJson({ ok: true, pid, logFile });
|
|
@@ -12710,7 +14441,7 @@ function buildDaemonCommand() {
|
|
|
12710
14441
|
getUI().ok("Daemon stopped", `pid ${pid}`);
|
|
12711
14442
|
return;
|
|
12712
14443
|
}
|
|
12713
|
-
await
|
|
14444
|
+
await sleep3(200);
|
|
12714
14445
|
}
|
|
12715
14446
|
process.stderr.write(`Daemon did not exit within timeout; pid ${pid} may still be alive.
|
|
12716
14447
|
`);
|
|
@@ -12791,7 +14522,7 @@ async function tailFromEnd(path9, lines) {
|
|
|
12791
14522
|
async function followFile(path9) {
|
|
12792
14523
|
let offset = statSync4(path9).size;
|
|
12793
14524
|
for (; ; ) {
|
|
12794
|
-
await
|
|
14525
|
+
await sleep3(1e3);
|
|
12795
14526
|
const size = statSync4(path9).size;
|
|
12796
14527
|
if (size < offset) offset = 0;
|
|
12797
14528
|
if (size === offset) continue;
|
|
@@ -12805,7 +14536,7 @@ async function followFile(path9) {
|
|
|
12805
14536
|
init_util();
|
|
12806
14537
|
import { Command as Command9 } from "commander";
|
|
12807
14538
|
import { createReadStream as createReadStream2, existsSync as existsSync15 } from "fs";
|
|
12808
|
-
import { homedir as
|
|
14539
|
+
import { homedir as homedir9 } from "os";
|
|
12809
14540
|
import { join as join19 } from "path";
|
|
12810
14541
|
import { createInterface } from "readline";
|
|
12811
14542
|
var DEFAULT_LIMIT2 = 50;
|
|
@@ -12838,7 +14569,7 @@ function buildEventsStatsCommand() {
|
|
|
12838
14569
|
const events = await readEvents(file, filters);
|
|
12839
14570
|
printJson({
|
|
12840
14571
|
ok: true,
|
|
12841
|
-
data: { stats:
|
|
14572
|
+
data: { stats: summarize3(events), count: events.length, limit: filters.limit },
|
|
12842
14573
|
checks: { file, exists: true, filters: cleanFilters(filters) }
|
|
12843
14574
|
});
|
|
12844
14575
|
});
|
|
@@ -12847,7 +14578,7 @@ function addEventOptions(cmd) {
|
|
|
12847
14578
|
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)");
|
|
12848
14579
|
}
|
|
12849
14580
|
function eventsPath() {
|
|
12850
|
-
return join19(process.env.PRISMER_HOME ?? join19(
|
|
14581
|
+
return join19(process.env.PRISMER_HOME ?? join19(homedir9(), ".prismer"), "para", "events.jsonl");
|
|
12851
14582
|
}
|
|
12852
14583
|
function unavailable(file) {
|
|
12853
14584
|
return {
|
|
@@ -12895,7 +14626,7 @@ function field(event, names) {
|
|
|
12895
14626
|
}
|
|
12896
14627
|
return void 0;
|
|
12897
14628
|
}
|
|
12898
|
-
function
|
|
14629
|
+
function summarize3(events) {
|
|
12899
14630
|
const byFamily = {};
|
|
12900
14631
|
const byType = {};
|
|
12901
14632
|
const byAgentId = {};
|
|
@@ -13209,7 +14940,7 @@ import { Command as Command11 } from "commander";
|
|
|
13209
14940
|
// src/pair.ts
|
|
13210
14941
|
import { generateKeyPairSync } from "crypto";
|
|
13211
14942
|
import { hostname as hostname2 } from "os";
|
|
13212
|
-
import { setTimeout as
|
|
14943
|
+
import { setTimeout as sleep4 } from "timers/promises";
|
|
13213
14944
|
import qrcode from "qrcode";
|
|
13214
14945
|
|
|
13215
14946
|
// src/daemon-id.ts
|
|
@@ -13296,7 +15027,7 @@ Scan with Lumin to approve, or open: ${offer.qrUrl}
|
|
|
13296
15027
|
const pollIntervalMs = opts.pollIntervalMs ?? 5e3;
|
|
13297
15028
|
for (let i = 0; i < maxAttempts; i += 1) {
|
|
13298
15029
|
if (i > 0 || !localOnlyMode) {
|
|
13299
|
-
await
|
|
15030
|
+
await sleep4(pollIntervalMs);
|
|
13300
15031
|
}
|
|
13301
15032
|
const res = await cloud.request(
|
|
13302
15033
|
"GET",
|
|
@@ -13377,7 +15108,7 @@ function buildPairCommand() {
|
|
|
13377
15108
|
|
|
13378
15109
|
// src/cli/commands/profile.ts
|
|
13379
15110
|
import { Command as Command12 } from "commander";
|
|
13380
|
-
import { existsSync as existsSync17, readFileSync as readFileSync12, writeFileSync as
|
|
15111
|
+
import { existsSync as existsSync17, readFileSync as readFileSync12, writeFileSync as writeFileSync10 } from "fs";
|
|
13381
15112
|
import { tmpdir } from "os";
|
|
13382
15113
|
import { join as join20 } from "path";
|
|
13383
15114
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
@@ -13504,7 +15235,7 @@ function buildProfileCommand() {
|
|
|
13504
15235
|
`/api/im/agent_profiles/${encodeURIComponent(profileId)}`
|
|
13505
15236
|
);
|
|
13506
15237
|
const tmpFile = join20(tmpdir(), `prismer-profile-${profileId}.json`);
|
|
13507
|
-
|
|
15238
|
+
writeFileSync10(tmpFile, JSON.stringify(profile.config, null, 2), "utf8");
|
|
13508
15239
|
const editor = process.env.EDITOR || "vi";
|
|
13509
15240
|
const ed = spawnSync4(editor, [tmpFile], { stdio: "inherit" });
|
|
13510
15241
|
if (ed.status !== 0) exitWithError(`editor exited ${ed.status}`, { code: "editor_failed" });
|
|
@@ -13545,10 +15276,10 @@ async function resolveDefaultWorkspaceId(cloud) {
|
|
|
13545
15276
|
// src/cli/commands/reset.ts
|
|
13546
15277
|
import { Command as Command13 } from "commander";
|
|
13547
15278
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
13548
|
-
import { existsSync as existsSync18, mkdirSync as
|
|
13549
|
-
import { homedir as
|
|
13550
|
-
import { dirname as
|
|
13551
|
-
import { setTimeout as
|
|
15279
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync11, rmSync, renameSync as renameSync2 } from "fs";
|
|
15280
|
+
import { homedir as homedir10 } from "os";
|
|
15281
|
+
import { dirname as dirname12, join as join21 } from "path";
|
|
15282
|
+
import { setTimeout as sleep5 } from "timers/promises";
|
|
13552
15283
|
init_util();
|
|
13553
15284
|
init_ui();
|
|
13554
15285
|
function buildResetCommand() {
|
|
@@ -13601,8 +15332,8 @@ function buildResetCommand() {
|
|
|
13601
15332
|
rmSync(paths.root, { recursive: true, force: true });
|
|
13602
15333
|
} else {
|
|
13603
15334
|
if (!archivePath) throw new Error("internal: archivePath missing");
|
|
13604
|
-
if (!existsSync18(
|
|
13605
|
-
|
|
15335
|
+
if (!existsSync18(dirname12(archivePath))) {
|
|
15336
|
+
mkdirSync11(dirname12(archivePath), { recursive: true });
|
|
13606
15337
|
}
|
|
13607
15338
|
renameSync2(paths.root, archivePath);
|
|
13608
15339
|
}
|
|
@@ -13612,8 +15343,8 @@ function buildResetCommand() {
|
|
|
13612
15343
|
rmSync(assetSyncRoot, { recursive: true, force: true });
|
|
13613
15344
|
} else {
|
|
13614
15345
|
if (!assetSyncArchivePath) throw new Error("internal: assetSyncArchivePath missing");
|
|
13615
|
-
if (!existsSync18(
|
|
13616
|
-
|
|
15346
|
+
if (!existsSync18(dirname12(assetSyncArchivePath))) {
|
|
15347
|
+
mkdirSync11(dirname12(assetSyncArchivePath), { recursive: true });
|
|
13617
15348
|
}
|
|
13618
15349
|
renameSync2(assetSyncRoot, assetSyncArchivePath);
|
|
13619
15350
|
}
|
|
@@ -13623,13 +15354,13 @@ function buildResetCommand() {
|
|
|
13623
15354
|
rmSync(hermesHome, { recursive: true, force: true });
|
|
13624
15355
|
} else {
|
|
13625
15356
|
if (!hermesArchivePath) throw new Error("internal: hermesArchivePath missing");
|
|
13626
|
-
if (!existsSync18(
|
|
13627
|
-
|
|
15357
|
+
if (!existsSync18(dirname12(hermesArchivePath))) {
|
|
15358
|
+
mkdirSync11(dirname12(hermesArchivePath), { recursive: true });
|
|
13628
15359
|
}
|
|
13629
15360
|
renameSync2(hermesHome, hermesArchivePath);
|
|
13630
15361
|
}
|
|
13631
15362
|
}
|
|
13632
|
-
|
|
15363
|
+
mkdirSync11(paths.root, { recursive: true });
|
|
13633
15364
|
if (opts.json) {
|
|
13634
15365
|
printJson({ ok: true, reset: true, plan });
|
|
13635
15366
|
return;
|
|
@@ -13678,7 +15409,7 @@ async function stopProcess(pid, timeoutMs, label) {
|
|
|
13678
15409
|
const deadline = Date.now() + timeoutMs;
|
|
13679
15410
|
while (Date.now() < deadline) {
|
|
13680
15411
|
if (!pidAlive2(pid)) return;
|
|
13681
|
-
await
|
|
15412
|
+
await sleep5(100);
|
|
13682
15413
|
}
|
|
13683
15414
|
try {
|
|
13684
15415
|
process.kill(pid, "SIGKILL");
|
|
@@ -13688,7 +15419,7 @@ async function stopProcess(pid, timeoutMs, label) {
|
|
|
13688
15419
|
const killDeadline = Date.now() + 1e3;
|
|
13689
15420
|
while (Date.now() < killDeadline) {
|
|
13690
15421
|
if (!pidAlive2(pid)) return;
|
|
13691
|
-
await
|
|
15422
|
+
await sleep5(100);
|
|
13692
15423
|
}
|
|
13693
15424
|
throw new Error(`${label} pid ${pid} did not exit within ${timeoutMs + 1e3}ms`);
|
|
13694
15425
|
}
|
|
@@ -13698,13 +15429,13 @@ function timestamp() {
|
|
|
13698
15429
|
function resolveAssetSyncRoot() {
|
|
13699
15430
|
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
13700
15431
|
if (override) return override;
|
|
13701
|
-
const home =
|
|
15432
|
+
const home = homedir10();
|
|
13702
15433
|
const desktop = join21(home, "Desktop");
|
|
13703
15434
|
const base = existsSync18(desktop) ? desktop : home;
|
|
13704
15435
|
return join21(base, "Prismer Assets");
|
|
13705
15436
|
}
|
|
13706
15437
|
function resolveHermesHome() {
|
|
13707
|
-
return process.env.HERMES_HOME || join21(
|
|
15438
|
+
return process.env.HERMES_HOME || join21(homedir10(), ".hermes");
|
|
13708
15439
|
}
|
|
13709
15440
|
function findHermesGatewayPids2() {
|
|
13710
15441
|
try {
|
|
@@ -14259,7 +15990,7 @@ function buildSkillCommand() {
|
|
|
14259
15990
|
const cloud = mkCloud6();
|
|
14260
15991
|
const path9 = opts.agent ? `/api/im/agents/${encodeURIComponent(opts.agent)}/skills` : `/api/im/skills/${encodeURIComponent(slugOrId)}/install`;
|
|
14261
15992
|
const body = opts.agent ? { skillId: slugOrId, workspaceId: opts.workspaceId, version: opts.version } : { workspaceId: opts.workspaceId, version: opts.version };
|
|
14262
|
-
const data = await
|
|
15993
|
+
const data = await requestEnvelope2(cloud, "POST", path9, body, "skill_install_failed");
|
|
14263
15994
|
if (opts.json) {
|
|
14264
15995
|
printJson(data);
|
|
14265
15996
|
return;
|
|
@@ -14270,7 +16001,7 @@ function buildSkillCommand() {
|
|
|
14270
16001
|
const cloud = mkCloud6();
|
|
14271
16002
|
const path9 = opts.agent ? `/api/im/agents/${encodeURIComponent(opts.agent)}/skills` : `/api/im/skills/${encodeURIComponent(slugOrId)}/install`;
|
|
14272
16003
|
const body = opts.agent ? { skillId: slugOrId, workspaceId: opts.workspaceId } : void 0;
|
|
14273
|
-
const data = await
|
|
16004
|
+
const data = await requestEnvelope2(cloud, "DELETE", path9, body, "skill_uninstall_failed");
|
|
14274
16005
|
if (opts.json) {
|
|
14275
16006
|
printJson(data);
|
|
14276
16007
|
return;
|
|
@@ -14323,7 +16054,7 @@ function mkCloud6() {
|
|
|
14323
16054
|
const cfg = loadConfig(resolvePaths());
|
|
14324
16055
|
return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
14325
16056
|
}
|
|
14326
|
-
async function
|
|
16057
|
+
async function requestEnvelope2(cloud, method, path9, body, code) {
|
|
14327
16058
|
const res = await cloud.request(method, path9, {
|
|
14328
16059
|
body
|
|
14329
16060
|
});
|
|
@@ -14473,6 +16204,7 @@ function buildStatusCommand() {
|
|
|
14473
16204
|
let me = null;
|
|
14474
16205
|
let devices = null;
|
|
14475
16206
|
let agents = null;
|
|
16207
|
+
let diagnose = null;
|
|
14476
16208
|
try {
|
|
14477
16209
|
const meRes = await cloud.request("GET", "/api/im/me", { timeoutMs: 3e3 });
|
|
14478
16210
|
cloudOk = meRes.ok;
|
|
@@ -14485,19 +16217,33 @@ function buildStatusCommand() {
|
|
|
14485
16217
|
if (Array.isArray(wsList) && wsList.length > 0) {
|
|
14486
16218
|
const wsId = wsList[0]?.id;
|
|
14487
16219
|
if (wsId) {
|
|
14488
|
-
const
|
|
14489
|
-
if (
|
|
14490
|
-
const
|
|
14491
|
-
|
|
14492
|
-
|
|
14493
|
-
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
agents = agBody?.data;
|
|
16220
|
+
const rtRes = await cloud.request("GET", `/api/im/workspaces/${encodeURIComponent(wsId)}/runtime`, { timeoutMs: 3e3 });
|
|
16221
|
+
if (rtRes.ok) {
|
|
16222
|
+
const rtBody = rtRes.data;
|
|
16223
|
+
const snapshot = rtBody?.data;
|
|
16224
|
+
if (snapshot && Array.isArray(snapshot.devices)) {
|
|
16225
|
+
devices = snapshot.devices;
|
|
16226
|
+
agents = snapshot.devices.flatMap((d) => d.agents ?? []);
|
|
16227
|
+
}
|
|
14497
16228
|
}
|
|
14498
16229
|
}
|
|
14499
16230
|
}
|
|
14500
16231
|
}
|
|
16232
|
+
const daemonId = cfg.daemon_id;
|
|
16233
|
+
if (daemonId) {
|
|
16234
|
+
try {
|
|
16235
|
+
const diagRes = await cloud.request(
|
|
16236
|
+
"GET",
|
|
16237
|
+
`/api/im/runtime/diagnose?daemonId=${encodeURIComponent(daemonId)}`,
|
|
16238
|
+
{ timeoutMs: 3e3 }
|
|
16239
|
+
);
|
|
16240
|
+
if (diagRes.ok) {
|
|
16241
|
+
const body = diagRes.data;
|
|
16242
|
+
if (body?.data && typeof body.data === "object") diagnose = body.data;
|
|
16243
|
+
}
|
|
16244
|
+
} catch {
|
|
16245
|
+
}
|
|
16246
|
+
}
|
|
14501
16247
|
}
|
|
14502
16248
|
} catch {
|
|
14503
16249
|
cloudOk = false;
|
|
@@ -14514,7 +16260,8 @@ function buildStatusCommand() {
|
|
|
14514
16260
|
info: daemonStatus.info ?? {}
|
|
14515
16261
|
},
|
|
14516
16262
|
cloud: { base: cfg.cloud_api_base, reachable: cloudOk, me, devices, agents },
|
|
14517
|
-
local
|
|
16263
|
+
local,
|
|
16264
|
+
diagnose
|
|
14518
16265
|
};
|
|
14519
16266
|
if (opts.json) {
|
|
14520
16267
|
printJson(report);
|
|
@@ -14523,6 +16270,60 @@ function buildStatusCommand() {
|
|
|
14523
16270
|
printPretty(report);
|
|
14524
16271
|
});
|
|
14525
16272
|
}
|
|
16273
|
+
function computeDeviceIcon(lastSeenAt, now) {
|
|
16274
|
+
if (!lastSeenAt) return "\u25CB";
|
|
16275
|
+
const age = now.getTime() - Date.parse(lastSeenAt);
|
|
16276
|
+
if (!Number.isFinite(age) || age < 0) return "\u25CB";
|
|
16277
|
+
if (age < 5 * 6e4) return "\u25CF";
|
|
16278
|
+
if (age < 60 * 6e4) return "\u25D0";
|
|
16279
|
+
return "\u25CB";
|
|
16280
|
+
}
|
|
16281
|
+
function computeAgentIcon(status, lastHeartbeat, now) {
|
|
16282
|
+
const s = (status || "").toLowerCase();
|
|
16283
|
+
if (s === "error" || s === "failed") return "\u25C6";
|
|
16284
|
+
if (!lastHeartbeat) return s === "offline" ? "\u25CB" : "\u25D0";
|
|
16285
|
+
const age = now.getTime() - Date.parse(lastHeartbeat);
|
|
16286
|
+
if (!Number.isFinite(age) || age < 0 || age >= 60 * 6e4) return "\u25CB";
|
|
16287
|
+
if (age >= 5 * 6e4) return "\u25D0";
|
|
16288
|
+
return "\u25CF";
|
|
16289
|
+
}
|
|
16290
|
+
function pad(value, width) {
|
|
16291
|
+
if (value.length >= width) return value.slice(0, width);
|
|
16292
|
+
return value + " ".repeat(width - value.length);
|
|
16293
|
+
}
|
|
16294
|
+
function formatDeviceBindings(devices, now = /* @__PURE__ */ new Date()) {
|
|
16295
|
+
const lines = [];
|
|
16296
|
+
if (devices.length === 0) {
|
|
16297
|
+
lines.push(" No paired devices.");
|
|
16298
|
+
return lines;
|
|
16299
|
+
}
|
|
16300
|
+
const totalAgents = devices.reduce((s, d) => s + (d.agents?.length ?? 0), 0);
|
|
16301
|
+
const devWord = devices.length === 1 ? "device" : "devices";
|
|
16302
|
+
const agWord = totalAgents === 1 ? "agent" : "agents";
|
|
16303
|
+
lines.push(` Device & Agent Bindings (${devices.length} ${devWord} \xB7 ${totalAgents} ${agWord})`);
|
|
16304
|
+
lines.push("");
|
|
16305
|
+
for (let di = 0; di < devices.length; di++) {
|
|
16306
|
+
const d = devices[di];
|
|
16307
|
+
const dIcon = computeDeviceIcon(d.lastSeenAt, now);
|
|
16308
|
+
const dName = pad(d.name || d.deviceId, 40);
|
|
16309
|
+
const lastSeen = d.lastSeenAt ? formatRelative(d.lastSeenAt, now) : "never seen";
|
|
16310
|
+
lines.push(` ${dIcon} ${dName} ${lastSeen}`);
|
|
16311
|
+
const agents = d.agents ?? [];
|
|
16312
|
+
for (let ai = 0; ai < agents.length; ai++) {
|
|
16313
|
+
const a = agents[ai];
|
|
16314
|
+
const isLast = ai === agents.length - 1;
|
|
16315
|
+
const branch = isLast ? "\u2514\u2500" : "\u251C\u2500";
|
|
16316
|
+
const aIcon = computeAgentIcon(a.status, a.lastHeartbeat, now);
|
|
16317
|
+
const aName = pad(a.name || a.id, 24);
|
|
16318
|
+
const aStatus = pad(a.status || "?", 8);
|
|
16319
|
+
const aVersion = pad(a.version ? `v${a.version}` : "\u2014", 10);
|
|
16320
|
+
const tail = a.currentTaskId ? `task=${a.currentTaskId}` : a.lastHeartbeat ? `\u2764 ${formatRelative(a.lastHeartbeat, now)}` : "";
|
|
16321
|
+
lines.push(` ${branch} ${aIcon} ${aName} ${aStatus} ${aVersion} ${tail}`.trimEnd());
|
|
16322
|
+
}
|
|
16323
|
+
if (di < devices.length - 1) lines.push("");
|
|
16324
|
+
}
|
|
16325
|
+
return lines;
|
|
16326
|
+
}
|
|
14526
16327
|
async function readDaemonStatus() {
|
|
14527
16328
|
try {
|
|
14528
16329
|
const res = await fetch("http://127.0.0.1:3210/healthz", {
|
|
@@ -14595,16 +16396,7 @@ function printPretty(report) {
|
|
|
14595
16396
|
if (Array.isArray(report.cloud.devices) && report.cloud.devices.length > 0) {
|
|
14596
16397
|
const devs = report.cloud.devices;
|
|
14597
16398
|
ui.blank();
|
|
14598
|
-
|
|
14599
|
-
for (const d of devs) {
|
|
14600
|
-
const statusIcon = d.daemonStatus === "connected" ? "\u25CF" : "\u25CB";
|
|
14601
|
-
const kind = d.runtimeKind === "docker" ? "Local" : "K8s";
|
|
14602
|
-
const declared = d.hostedAgentSummary?.declared ?? 0;
|
|
14603
|
-
ui.line(` ${statusIcon} ${d.podName?.replace(/^daemon:/, "")?.slice(0, 28) ?? "?"} kind=${kind} agents=${declared}`);
|
|
14604
|
-
}
|
|
14605
|
-
}
|
|
14606
|
-
if (Array.isArray(report.cloud.agents) && report.cloud.agents.length > 0) {
|
|
14607
|
-
ui.line(` Hosted agents: ${report.cloud.agents.length}`);
|
|
16399
|
+
for (const ln of formatDeviceBindings(devs)) ui.line(ln);
|
|
14608
16400
|
}
|
|
14609
16401
|
if (report.local) {
|
|
14610
16402
|
ui.blank();
|
|
@@ -14614,11 +16406,63 @@ function printPretty(report) {
|
|
|
14614
16406
|
} else {
|
|
14615
16407
|
warn("Local DB", "unavailable");
|
|
14616
16408
|
}
|
|
16409
|
+
if (report.diagnose) {
|
|
16410
|
+
ui.blank();
|
|
16411
|
+
ui.header("Cloud dispatch reachability");
|
|
16412
|
+
ui.blank();
|
|
16413
|
+
printDiagnose(report.diagnose, report.daemon.pid ?? null);
|
|
16414
|
+
}
|
|
16415
|
+
}
|
|
16416
|
+
function printDiagnose(d, pid) {
|
|
16417
|
+
const ui = getUI();
|
|
16418
|
+
const heartbeatLine = d.lastHeartbeatAt ? `last heartbeat ${formatRelative(d.lastHeartbeatAt)}` : "no heartbeat seen";
|
|
16419
|
+
if (d.wsAlive) {
|
|
16420
|
+
ok("Daemon", `running${pid ? ` (pid ${pid})` : ""}`);
|
|
16421
|
+
ok("Cloud WS", `connected (${heartbeatLine})`);
|
|
16422
|
+
} else {
|
|
16423
|
+
warn("Daemon", `running${pid ? ` (pid ${pid})` : ""} \u2014 not visible to cloud WS`);
|
|
16424
|
+
fail("Cloud WS", `disconnected (${heartbeatLine})`);
|
|
16425
|
+
}
|
|
16426
|
+
if (d.gatewayUrl) {
|
|
16427
|
+
if (d.gatewayIsPrivate) {
|
|
16428
|
+
warn("HTTP gateway", `${d.gatewayUrl} (private IP \u2014 only reachable on same network)`);
|
|
16429
|
+
} else if (d.httpReachable) {
|
|
16430
|
+
ok(
|
|
16431
|
+
"HTTP gateway",
|
|
16432
|
+
`${d.gatewayUrl} (reachable${d.httpLatencyMs != null ? `, ${d.httpLatencyMs}ms` : ""})`
|
|
16433
|
+
);
|
|
16434
|
+
} else {
|
|
16435
|
+
fail("HTTP gateway", `${d.gatewayUrl} (unreachable${d.httpError ? ` \u2014 ${d.httpError}` : ""})`);
|
|
16436
|
+
}
|
|
16437
|
+
} else {
|
|
16438
|
+
ui.line(" HTTP gateway: not declared (WS-only daemon)");
|
|
16439
|
+
}
|
|
16440
|
+
const activeAgents = /* @__PURE__ */ (() => {
|
|
16441
|
+
return 0;
|
|
16442
|
+
})();
|
|
16443
|
+
if (activeAgents > 0) {
|
|
16444
|
+
ok("Active agents", String(activeAgents));
|
|
16445
|
+
}
|
|
16446
|
+
const recColour = d.recommendedTransport === "ws" ? ok : d.recommendedTransport === "http" ? warn : fail;
|
|
16447
|
+
recColour("Recommended transport", d.recommendedTransport);
|
|
16448
|
+
if (d.lastProbeAt) {
|
|
16449
|
+
ui.line(` Last transport probe: ${formatRelative(d.lastProbeAt)}`);
|
|
16450
|
+
}
|
|
16451
|
+
}
|
|
16452
|
+
function formatRelative(iso, now = /* @__PURE__ */ new Date()) {
|
|
16453
|
+
const ts = Date.parse(iso);
|
|
16454
|
+
if (!Number.isFinite(ts)) return "unknown";
|
|
16455
|
+
const diff = Math.max(0, now.getTime() - ts);
|
|
16456
|
+
if (diff < 3e4) return "just now";
|
|
16457
|
+
if (diff < 6e4) return `${Math.round(diff / 1e3)}s ago`;
|
|
16458
|
+
if (diff < 36e5) return `${Math.round(diff / 6e4)}m ago`;
|
|
16459
|
+
if (diff < 864e5) return `${Math.round(diff / 36e5)}h ago`;
|
|
16460
|
+
return `${Math.round(diff / 864e5)}d ago`;
|
|
14617
16461
|
}
|
|
14618
16462
|
|
|
14619
16463
|
// src/cli/commands/task.ts
|
|
14620
16464
|
import { Command as Command18 } from "commander";
|
|
14621
|
-
import { setTimeout as
|
|
16465
|
+
import { setTimeout as sleep6 } from "timers/promises";
|
|
14622
16466
|
init_util();
|
|
14623
16467
|
function describeStatus2(status) {
|
|
14624
16468
|
return status === 0 ? "network error" : `HTTP ${status}`;
|
|
@@ -14701,7 +16545,7 @@ function unwrap2(raw) {
|
|
|
14701
16545
|
async function pollUntilTerminal(cloud, taskId, timeoutMs) {
|
|
14702
16546
|
const deadline = Date.now() + (timeoutMs ?? 5 * 6e4);
|
|
14703
16547
|
while (Date.now() < deadline) {
|
|
14704
|
-
await
|
|
16548
|
+
await sleep6(1e3);
|
|
14705
16549
|
const res = await cloud.request(
|
|
14706
16550
|
"GET",
|
|
14707
16551
|
`/api/im/tasks/${encodeURIComponent(taskId)}`
|
|
@@ -14976,7 +16820,7 @@ async function readResponseError(res) {
|
|
|
14976
16820
|
|
|
14977
16821
|
// src/cli/index.ts
|
|
14978
16822
|
init_ui();
|
|
14979
|
-
var VERSION = "2.0.
|
|
16823
|
+
var VERSION = "2.0.5";
|
|
14980
16824
|
function buildProgram() {
|
|
14981
16825
|
const program = new Command20("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
|
|
14982
16826
|
program.addCommand(buildBannerCommand());
|