@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/index.js
CHANGED
|
@@ -153,6 +153,42 @@ import { fileURLToPath } from "url";
|
|
|
153
153
|
import Database from "better-sqlite3";
|
|
154
154
|
import * as YAML from "yaml";
|
|
155
155
|
import { z } from "zod";
|
|
156
|
+
function readPlainRecord(value) {
|
|
157
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
158
|
+
}
|
|
159
|
+
function readNonEmptyString(value) {
|
|
160
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
161
|
+
}
|
|
162
|
+
function sanitizeRoleSkillSlug(value) {
|
|
163
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "template";
|
|
164
|
+
}
|
|
165
|
+
function renderRoleTemplateSkill(skillName, agents, roleTemplate) {
|
|
166
|
+
const mcpServers = Array.isArray(roleTemplate?.mcpServers) ? roleTemplate.mcpServers : [];
|
|
167
|
+
const tools = /* @__PURE__ */ new Set();
|
|
168
|
+
for (const server of mcpServers) {
|
|
169
|
+
const rec = readPlainRecord(server);
|
|
170
|
+
const allowlist = Array.isArray(rec?.toolsAllowlist) ? rec.toolsAllowlist : [];
|
|
171
|
+
for (const tool of allowlist) {
|
|
172
|
+
if (typeof tool === "string" && tool.trim()) tools.add(tool.trim());
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const allowedTools = [...tools].join(" ");
|
|
176
|
+
const roleTemplateSlug = JSON.stringify(readNonEmptyString(roleTemplate?.slug) ?? null);
|
|
177
|
+
return `---
|
|
178
|
+
name: ${skillName}
|
|
179
|
+
description: Role-template operating playbook projected from Prismer RoleTemplate.
|
|
180
|
+
${allowedTools ? `allowed-tools: ${allowedTools}
|
|
181
|
+
` : ""}metadata:
|
|
182
|
+
prismer:
|
|
183
|
+
source: role-template
|
|
184
|
+
roleTemplateSlug: ${roleTemplateSlug}
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
# Role Template Playbook
|
|
188
|
+
|
|
189
|
+
${agents.trim()}
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
156
192
|
function parsePrismerGoals(value) {
|
|
157
193
|
if (!Array.isArray(value)) return [];
|
|
158
194
|
return value.map((entry) => {
|
|
@@ -562,13 +598,14 @@ function failure(status, body) {
|
|
|
562
598
|
error: { code: "adapter_dispatch_failed", message: `Hermes ${status}: ${body || "<no body>"}` }
|
|
563
599
|
};
|
|
564
600
|
}
|
|
565
|
-
async function consumeSse(body, task) {
|
|
601
|
+
async function consumeSse(body, task, state) {
|
|
566
602
|
const reader = body.getReader();
|
|
567
603
|
const decoder = new TextDecoder();
|
|
568
604
|
let buf = "";
|
|
569
605
|
let deltas = "";
|
|
570
606
|
let finalOutput;
|
|
571
607
|
let lastProgress = 0;
|
|
608
|
+
let approvalRequested = false;
|
|
572
609
|
for (; ; ) {
|
|
573
610
|
const readStart = Date.now();
|
|
574
611
|
const { value, done } = await reader.read();
|
|
@@ -617,6 +654,10 @@ async function consumeSse(body, task) {
|
|
|
617
654
|
const next = Math.min(0.99, lastProgress + 0.05);
|
|
618
655
|
lastProgress = next;
|
|
619
656
|
const toolName = typeof payload.tool === "string" ? payload.tool : typeof payload.name === "string" ? payload.name : void 0;
|
|
657
|
+
if (eventName === "tool.started" && toolName === APPROVAL_TOOL_NAME) {
|
|
658
|
+
approvalRequested = true;
|
|
659
|
+
if (state) state.approvalRequested = true;
|
|
660
|
+
}
|
|
620
661
|
const previewStr = typeof payload.preview === "string" ? payload.preview : payload.preview != null ? JSON.stringify(payload.preview).slice(0, 200) : void 0;
|
|
621
662
|
task.onProgress?.({
|
|
622
663
|
progress: next,
|
|
@@ -646,7 +687,53 @@ async function consumeSse(body, task) {
|
|
|
646
687
|
}
|
|
647
688
|
}
|
|
648
689
|
}
|
|
649
|
-
return
|
|
690
|
+
return {
|
|
691
|
+
output: finalOutput && finalOutput.length > 0 ? finalOutput : deltas,
|
|
692
|
+
approvalRequested
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
async function consumeChatCompletionsSse(body, task) {
|
|
696
|
+
const reader = body.getReader();
|
|
697
|
+
const decoder = new TextDecoder();
|
|
698
|
+
let buf = "";
|
|
699
|
+
let output = "";
|
|
700
|
+
let lastProgress = 0;
|
|
701
|
+
for (; ; ) {
|
|
702
|
+
const { value, done } = await reader.read();
|
|
703
|
+
if (done) break;
|
|
704
|
+
buf += decoder.decode(value, { stream: true });
|
|
705
|
+
const events = buf.split("\n\n");
|
|
706
|
+
buf = events.pop() ?? "";
|
|
707
|
+
for (const ev of events) {
|
|
708
|
+
const dataLine = ev.match(/^data: (.+)$/m);
|
|
709
|
+
if (!dataLine) continue;
|
|
710
|
+
const raw = dataLine[1].trim();
|
|
711
|
+
if (raw === "[DONE]") continue;
|
|
712
|
+
let payload;
|
|
713
|
+
try {
|
|
714
|
+
payload = JSON.parse(raw);
|
|
715
|
+
} catch (err) {
|
|
716
|
+
process.stderr.write(
|
|
717
|
+
`[hermes-adapter] chat-completions SSE parse skipped: ${err.message}
|
|
718
|
+
`
|
|
719
|
+
);
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
const delta = payload.choices?.[0]?.delta?.content;
|
|
723
|
+
if (typeof delta === "string") {
|
|
724
|
+
output += delta;
|
|
725
|
+
} else if (Array.isArray(delta)) {
|
|
726
|
+
for (const part of delta) {
|
|
727
|
+
if (part?.type === "text" && typeof part.text === "string") output += part.text;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (output.length > 0 && lastProgress < 0.99) {
|
|
731
|
+
lastProgress = Math.min(0.99, lastProgress + 0.05);
|
|
732
|
+
task.onProgress?.({ progress: lastProgress, message: "streaming" });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return output;
|
|
650
737
|
}
|
|
651
738
|
async function checkHealth(baseUrl, apiKey) {
|
|
652
739
|
try {
|
|
@@ -727,13 +814,14 @@ function pidAlive(pid) {
|
|
|
727
814
|
function escapeRegExp(value) {
|
|
728
815
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
729
816
|
}
|
|
730
|
-
var PRISMER_IM_SKILL_NAME, PRISMER_IM_SKILL_CONTENT, HermesProfileConfigSchema, hermesAdapter, HermesService;
|
|
817
|
+
var PRISMER_IM_SKILL_NAME, PRISMER_ROLE_SKILL_PREFIX, PRISMER_IM_SKILL_CONTENT, HermesProfileConfigSchema, hermesAdapter, HermesService, APPROVAL_TOOL_NAME;
|
|
731
818
|
var init_hermes = __esm({
|
|
732
819
|
"src/adapters/hermes/index.ts"() {
|
|
733
820
|
"use strict";
|
|
734
821
|
init_contract();
|
|
735
822
|
init_skill_loader2();
|
|
736
823
|
PRISMER_IM_SKILL_NAME = "prismer-im-collab";
|
|
824
|
+
PRISMER_ROLE_SKILL_PREFIX = "prismer-role";
|
|
737
825
|
PRISMER_IM_SKILL_CONTENT = `---
|
|
738
826
|
name: prismer-im-collab
|
|
739
827
|
description: Coordinate reliably in Prismer conversations, use workspace assets through bounded MCP tools, and keep task work on the board.
|
|
@@ -1046,6 +1134,7 @@ No @ needed; the human is the only other party.
|
|
|
1046
1134
|
const startedAt = Date.now();
|
|
1047
1135
|
let runId;
|
|
1048
1136
|
const sysPrompt = typeof task.metadata?.systemPrompt === "string" ? task.metadata.systemPrompt : void 0;
|
|
1137
|
+
this.installRoleTemplateSkill();
|
|
1049
1138
|
const skillPrompt = await this.skillLoader.loadSystemPromptFragment().catch((err) => {
|
|
1050
1139
|
process.stderr.write(
|
|
1051
1140
|
`[hermes-adapter] failed to read skills for ${this.profileName}: ${err.message}
|
|
@@ -1053,7 +1142,21 @@ No @ needed; the human is the only other party.
|
|
|
1053
1142
|
);
|
|
1054
1143
|
return void 0;
|
|
1055
1144
|
});
|
|
1056
|
-
const
|
|
1145
|
+
const outboxDir = typeof task.metadata?.prismerOutboxDir === "string" ? task.metadata.prismerOutboxDir : void 0;
|
|
1146
|
+
const outboxDirective = outboxDir ? [
|
|
1147
|
+
"[Outbox directive \u2014 MANDATORY, overrides any prior outbox path]",
|
|
1148
|
+
"",
|
|
1149
|
+
"For THIS turn ONLY, your active outbox is:",
|
|
1150
|
+
` ${outboxDir}`,
|
|
1151
|
+
"",
|
|
1152
|
+
"Any user-deliverable file (PDF, image, CSV, archive, doc) MUST be",
|
|
1153
|
+
"written to that EXACT absolute path. Do NOT reuse the outbox path",
|
|
1154
|
+
"from any previous turn \u2014 those directories no longer accept new",
|
|
1155
|
+
"uploads. Do NOT copy files into the previous outbox; copy them",
|
|
1156
|
+
"into the path above. Files outside this directory will not become",
|
|
1157
|
+
'chat attachments and the user will see "no files were attached".'
|
|
1158
|
+
].join("\n") : "";
|
|
1159
|
+
const instructions = [outboxDirective, sysPrompt, skillPrompt].filter(Boolean).join("\n\n");
|
|
1057
1160
|
if (sysPrompt) {
|
|
1058
1161
|
try {
|
|
1059
1162
|
const profileDir = getHermesProfileDir(this.profileName);
|
|
@@ -1077,8 +1180,15 @@ No @ needed; the human is the only other party.
|
|
|
1077
1180
|
`
|
|
1078
1181
|
);
|
|
1079
1182
|
}
|
|
1183
|
+
const sseState = { approvalRequested: false };
|
|
1080
1184
|
try {
|
|
1081
1185
|
const nativeBridgePatch = await this.prepareNativeBridgePatch(task);
|
|
1186
|
+
const imageRefs = (task.assetRefs ?? []).filter(
|
|
1187
|
+
(r) => r.mime?.startsWith("image/")
|
|
1188
|
+
);
|
|
1189
|
+
if (imageRefs.length > 0) {
|
|
1190
|
+
return await this.dispatchMultimodalChat(task, instructions, imageRefs, startedAt);
|
|
1191
|
+
}
|
|
1082
1192
|
const createRes = await fetch(`${this.baseUrl}/v1/runs`, {
|
|
1083
1193
|
method: "POST",
|
|
1084
1194
|
headers: {
|
|
@@ -1129,10 +1239,10 @@ No @ needed; the human is the only other party.
|
|
|
1129
1239
|
}
|
|
1130
1240
|
};
|
|
1131
1241
|
}
|
|
1132
|
-
const
|
|
1242
|
+
const sseResult = await consumeSse(eventsRes.body, task, sseState);
|
|
1133
1243
|
return {
|
|
1134
1244
|
ok: true,
|
|
1135
|
-
output,
|
|
1245
|
+
output: sseResult.output,
|
|
1136
1246
|
metrics: { durationMs: Date.now() - startedAt },
|
|
1137
1247
|
metadata: {
|
|
1138
1248
|
hermes: this.bridgeSnapshot("dispatched", {
|
|
@@ -1140,7 +1250,13 @@ No @ needed; the human is the only other party.
|
|
|
1140
1250
|
runId,
|
|
1141
1251
|
baseUrl: this.baseUrl,
|
|
1142
1252
|
model: this.config.model
|
|
1143
|
-
})
|
|
1253
|
+
}),
|
|
1254
|
+
// Surfaced to dispatch.ts so a subsequent reaper kill on this
|
|
1255
|
+
// task is reclassified as `awaiting_human_approval` rather than
|
|
1256
|
+
// `daemon_task_timeout`. Lives under a stable key (not under
|
|
1257
|
+
// `hermes.*`) so non-hermes adapters can adopt the same signal
|
|
1258
|
+
// later without bleeding hermes-specific metadata shape.
|
|
1259
|
+
...sseResult.approvalRequested ? { approvalRequested: true } : {}
|
|
1144
1260
|
}
|
|
1145
1261
|
};
|
|
1146
1262
|
} catch (err) {
|
|
@@ -1150,6 +1266,7 @@ No @ needed; the human is the only other party.
|
|
|
1150
1266
|
);
|
|
1151
1267
|
const categorized = categorizeDispatchError(err, task.signal);
|
|
1152
1268
|
const bridgeKind = categorized.error?.code === "task_cancelled" ? "cancelled" : "failed";
|
|
1269
|
+
const approvalSignal = sseState.approvalRequested ? { approvalRequested: true } : {};
|
|
1153
1270
|
return {
|
|
1154
1271
|
...categorized,
|
|
1155
1272
|
metadata: {
|
|
@@ -1158,13 +1275,87 @@ No @ needed; the human is the only other party.
|
|
|
1158
1275
|
baseUrl: this.baseUrl,
|
|
1159
1276
|
model: this.config.model,
|
|
1160
1277
|
...bridgeKind === "failed" ? { error: err.message } : {}
|
|
1161
|
-
})
|
|
1278
|
+
}),
|
|
1279
|
+
...approvalSignal
|
|
1162
1280
|
}
|
|
1163
1281
|
};
|
|
1164
1282
|
} finally {
|
|
1165
1283
|
if (runId && this.currentRunId === runId) this.currentRunId = void 0;
|
|
1166
1284
|
}
|
|
1167
1285
|
}
|
|
1286
|
+
/**
|
|
1287
|
+
* 14b rev.3 §3.0.4 — Hermes multimodal path.
|
|
1288
|
+
*
|
|
1289
|
+
* Builds an OpenAI-shape chat/completions body where the user message
|
|
1290
|
+
* `content` is a multipart array (`{type:'text'}` + N × `{type:'image_url'}`).
|
|
1291
|
+
* Hermes `_normalize_multimodal_content` (api_server.py:170) collapses the
|
|
1292
|
+
* blocks into its native multimodal representation before routing into
|
|
1293
|
+
* the agent pipeline.
|
|
1294
|
+
*
|
|
1295
|
+
* `image_url.url` is the cdn URL when the daemon's reachability probe said
|
|
1296
|
+
* the upstream LLM can see it (D35 path A); otherwise it's a `data:` URI
|
|
1297
|
+
* carrying the base64 bytes the daemon pre-fetched (D35 path B fallback).
|
|
1298
|
+
* The adapter doesn't decide — `resolveAssetRefs` does, and the adapter
|
|
1299
|
+
* just shuttles `cdnUrl ?? data:base64` into the wire.
|
|
1300
|
+
*/
|
|
1301
|
+
async dispatchMultimodalChat(task, instructions, imageRefs, startedAt) {
|
|
1302
|
+
const userContent = [{ type: "text", text: task.prompt }];
|
|
1303
|
+
for (const r of imageRefs) {
|
|
1304
|
+
const url = r.reachable === "cdn" && r.cdnUrl ? r.cdnUrl : r.base64 ? `data:${r.mime ?? "image/png"};base64,${r.base64}` : r.cdnUrl ?? "";
|
|
1305
|
+
if (!url) {
|
|
1306
|
+
userContent.push({
|
|
1307
|
+
type: "text",
|
|
1308
|
+
text: `[image attachment id=${r.assetId} mime=${r.mime ?? "unknown"} unavailable: cdn not reachable and bytes exceeded inline cap]`
|
|
1309
|
+
});
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
userContent.push({ type: "image_url", image_url: { url, detail: "auto" } });
|
|
1313
|
+
}
|
|
1314
|
+
const messages = [];
|
|
1315
|
+
if (instructions) messages.push({ role: "system", content: instructions });
|
|
1316
|
+
messages.push({ role: "user", content: userContent });
|
|
1317
|
+
const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
1318
|
+
method: "POST",
|
|
1319
|
+
headers: {
|
|
1320
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
1321
|
+
"Content-Type": "application/json"
|
|
1322
|
+
},
|
|
1323
|
+
body: JSON.stringify({
|
|
1324
|
+
model: this.config.model,
|
|
1325
|
+
messages,
|
|
1326
|
+
stream: true
|
|
1327
|
+
}),
|
|
1328
|
+
signal: task.signal
|
|
1329
|
+
});
|
|
1330
|
+
if (!res.ok) {
|
|
1331
|
+
const body = await res.text().catch(() => "");
|
|
1332
|
+
return failure(res.status, body);
|
|
1333
|
+
}
|
|
1334
|
+
if (!res.body) {
|
|
1335
|
+
return {
|
|
1336
|
+
ok: false,
|
|
1337
|
+
error: {
|
|
1338
|
+
code: "adapter_dispatch_failed",
|
|
1339
|
+
message: "Hermes /v1/chat/completions returned no body"
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
const output = await consumeChatCompletionsSse(res.body, task);
|
|
1344
|
+
return {
|
|
1345
|
+
ok: true,
|
|
1346
|
+
output,
|
|
1347
|
+
metrics: { durationMs: Date.now() - startedAt },
|
|
1348
|
+
metadata: {
|
|
1349
|
+
hermes: this.bridgeSnapshot("dispatched", {
|
|
1350
|
+
endpoint: "/v1/chat/completions",
|
|
1351
|
+
multimodal: true,
|
|
1352
|
+
imageRefs: imageRefs.length,
|
|
1353
|
+
baseUrl: this.baseUrl,
|
|
1354
|
+
model: this.config.model
|
|
1355
|
+
})
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1168
1359
|
async shutdown() {
|
|
1169
1360
|
if (this.currentRunId) await this.stopRun(this.currentRunId);
|
|
1170
1361
|
}
|
|
@@ -1186,6 +1377,25 @@ No @ needed; the human is the only other party.
|
|
|
1186
1377
|
]);
|
|
1187
1378
|
return { ...kanbanPatch, ...goalsPatch };
|
|
1188
1379
|
}
|
|
1380
|
+
installRoleTemplateSkill() {
|
|
1381
|
+
const roleTemplate = readPlainRecord(this.config.roleTemplate);
|
|
1382
|
+
const hermesConfig = readPlainRecord(roleTemplate?.hermesConfig);
|
|
1383
|
+
const agents = readNonEmptyString(hermesConfig?.agents);
|
|
1384
|
+
if (!agents) return;
|
|
1385
|
+
const slug = sanitizeRoleSkillSlug(readNonEmptyString(roleTemplate?.slug) ?? "template");
|
|
1386
|
+
const skillName = `${PRISMER_ROLE_SKILL_PREFIX}-${slug}`;
|
|
1387
|
+
const content = renderRoleTemplateSkill(skillName, agents, roleTemplate);
|
|
1388
|
+
try {
|
|
1389
|
+
const skillDir = join2(this.skillLoader.getSkillsRoot(), skillName);
|
|
1390
|
+
mkdirSync(skillDir, { recursive: true });
|
|
1391
|
+
writeFileSync(join2(skillDir, "SKILL.md"), content, "utf8");
|
|
1392
|
+
} catch (err) {
|
|
1393
|
+
process.stderr.write(
|
|
1394
|
+
`[hermes-adapter] failed to install role-template skill ${skillName} for ${this.profileName}: ${err.message}
|
|
1395
|
+
`
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1189
1399
|
async prepareNativeKanbanPatch(task) {
|
|
1190
1400
|
const sourceKind = typeof task.metadata?.sourceKind === "string" ? task.metadata.sourceKind : null;
|
|
1191
1401
|
const parentTaskId = typeof task.metadata?.parentTaskId === "string" ? task.metadata.parentTaskId : null;
|
|
@@ -1294,6 +1504,7 @@ No @ needed; the human is the only other party.
|
|
|
1294
1504
|
};
|
|
1295
1505
|
}
|
|
1296
1506
|
};
|
|
1507
|
+
APPROVAL_TOOL_NAME = "prismer.approval.request_human_approval";
|
|
1297
1508
|
}
|
|
1298
1509
|
});
|
|
1299
1510
|
|
|
@@ -1326,8 +1537,61 @@ var init_skill_loader3 = __esm({
|
|
|
1326
1537
|
|
|
1327
1538
|
// src/adapters/openclaw/index.ts
|
|
1328
1539
|
import { z as z2 } from "zod";
|
|
1540
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1329
1541
|
import { homedir as homedir2 } from "os";
|
|
1330
1542
|
import { join as join3 } from "path";
|
|
1543
|
+
function readPlainRecord2(value) {
|
|
1544
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1545
|
+
}
|
|
1546
|
+
function readNonEmptyString2(value) {
|
|
1547
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1548
|
+
}
|
|
1549
|
+
function sanitizeRoleSkillSlug2(value) {
|
|
1550
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "template";
|
|
1551
|
+
}
|
|
1552
|
+
function renderRoleTemplateSkill2(skillName, agents, roleTemplate) {
|
|
1553
|
+
const mcpServers = Array.isArray(roleTemplate?.mcpServers) ? roleTemplate.mcpServers : [];
|
|
1554
|
+
const tools = /* @__PURE__ */ new Set();
|
|
1555
|
+
for (const server of mcpServers) {
|
|
1556
|
+
const rec = readPlainRecord2(server);
|
|
1557
|
+
const allowlist = Array.isArray(rec?.toolsAllowlist) ? rec.toolsAllowlist : [];
|
|
1558
|
+
for (const tool of allowlist) {
|
|
1559
|
+
if (typeof tool === "string" && tool.trim()) tools.add(tool.trim());
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
const allowedTools = [...tools].join(" ");
|
|
1563
|
+
const roleTemplateSlug = JSON.stringify(readNonEmptyString2(roleTemplate?.slug) ?? null);
|
|
1564
|
+
return `---
|
|
1565
|
+
name: ${skillName}
|
|
1566
|
+
description: Role-template operating playbook projected from Prismer RoleTemplate.
|
|
1567
|
+
${allowedTools ? `allowed-tools: ${allowedTools}
|
|
1568
|
+
` : ""}metadata:
|
|
1569
|
+
prismer:
|
|
1570
|
+
source: role-template
|
|
1571
|
+
roleTemplateSlug: ${roleTemplateSlug}
|
|
1572
|
+
---
|
|
1573
|
+
|
|
1574
|
+
# Role Template Playbook
|
|
1575
|
+
|
|
1576
|
+
${agents.trim()}
|
|
1577
|
+
`;
|
|
1578
|
+
}
|
|
1579
|
+
function pickResponsesSource(ref) {
|
|
1580
|
+
if (ref.reachable === "cdn" && ref.cdnUrl) {
|
|
1581
|
+
return { type: "url", url: ref.cdnUrl };
|
|
1582
|
+
}
|
|
1583
|
+
if (ref.base64) {
|
|
1584
|
+
return {
|
|
1585
|
+
type: "base64",
|
|
1586
|
+
data: ref.base64,
|
|
1587
|
+
media_type: ref.mime ?? "application/octet-stream"
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
if (ref.cdnUrl) {
|
|
1591
|
+
return { type: "url", url: ref.cdnUrl };
|
|
1592
|
+
}
|
|
1593
|
+
return null;
|
|
1594
|
+
}
|
|
1331
1595
|
function getOpenClawSkillsDir(profile) {
|
|
1332
1596
|
const config = OpenClawProfileConfigSchema.parse(profile.config);
|
|
1333
1597
|
return resolveOpenClawSkillsDir(profile, config);
|
|
@@ -1348,12 +1612,13 @@ async function checkHealth2(baseUrl, apiKey) {
|
|
|
1348
1612
|
return false;
|
|
1349
1613
|
}
|
|
1350
1614
|
}
|
|
1351
|
-
var OpenClawProfileConfigSchema, openclawAdapter, OpenClawService;
|
|
1615
|
+
var PRISMER_ROLE_SKILL_PREFIX2, OpenClawProfileConfigSchema, openclawAdapter, OpenClawService;
|
|
1352
1616
|
var init_openclaw = __esm({
|
|
1353
1617
|
"src/adapters/openclaw/index.ts"() {
|
|
1354
1618
|
"use strict";
|
|
1355
1619
|
init_contract();
|
|
1356
1620
|
init_skill_loader3();
|
|
1621
|
+
PRISMER_ROLE_SKILL_PREFIX2 = "prismer-role";
|
|
1357
1622
|
OpenClawProfileConfigSchema = z2.object({
|
|
1358
1623
|
/** OpenClaw chat-completions HTTP port. */
|
|
1359
1624
|
port: z2.number().int().min(1).max(65535).default(18789),
|
|
@@ -1398,23 +1663,25 @@ var init_openclaw = __esm({
|
|
|
1398
1663
|
);
|
|
1399
1664
|
}
|
|
1400
1665
|
const skillsDir = resolveOpenClawSkillsDir(profile, config);
|
|
1401
|
-
return new OpenClawService(baseUrl, config.apiKey, config.model, new OpenClawSkillLoader(skillsDir));
|
|
1666
|
+
return new OpenClawService(baseUrl, config.apiKey, config.model, config, new OpenClawSkillLoader(skillsDir));
|
|
1402
1667
|
},
|
|
1403
1668
|
async health() {
|
|
1404
1669
|
return { available: true };
|
|
1405
1670
|
}
|
|
1406
1671
|
};
|
|
1407
1672
|
OpenClawService = class {
|
|
1408
|
-
constructor(baseUrl, apiKey, model, skillLoader) {
|
|
1673
|
+
constructor(baseUrl, apiKey, model, config, skillLoader) {
|
|
1409
1674
|
this.baseUrl = baseUrl;
|
|
1410
1675
|
this.apiKey = apiKey;
|
|
1411
1676
|
this.model = model;
|
|
1677
|
+
this.config = config;
|
|
1412
1678
|
this.skillLoader = skillLoader;
|
|
1413
1679
|
this.id = baseUrl;
|
|
1414
1680
|
}
|
|
1415
1681
|
baseUrl;
|
|
1416
1682
|
apiKey;
|
|
1417
1683
|
model;
|
|
1684
|
+
config;
|
|
1418
1685
|
skillLoader;
|
|
1419
1686
|
id;
|
|
1420
1687
|
async healthy() {
|
|
@@ -1423,17 +1690,47 @@ var init_openclaw = __esm({
|
|
|
1423
1690
|
async dispatch(task) {
|
|
1424
1691
|
const startedAt = Date.now();
|
|
1425
1692
|
const systemPrompt = typeof task.metadata?.systemPrompt === "string" ? task.metadata.systemPrompt : void 0;
|
|
1693
|
+
this.installRoleTemplateSkill();
|
|
1426
1694
|
const skillPrompt = await this.skillLoader.loadSystemPromptFragment().catch((err) => {
|
|
1427
1695
|
process.stderr.write(`[openclaw-adapter] failed to read skills: ${err.message}
|
|
1428
1696
|
`);
|
|
1429
1697
|
return void 0;
|
|
1430
1698
|
});
|
|
1431
1699
|
const systemContent = [systemPrompt, skillPrompt].filter(Boolean).join("\n\n");
|
|
1432
|
-
const
|
|
1433
|
-
|
|
1434
|
-
|
|
1700
|
+
const refs = task.assetRefs ?? [];
|
|
1701
|
+
const imageRefs = refs.filter((r) => r.mime?.startsWith("image/"));
|
|
1702
|
+
const fileRefs = refs.filter((r) => r.mime && !r.mime.startsWith("image/"));
|
|
1703
|
+
const inputContent = [
|
|
1704
|
+
{ type: "input_text", text: task.prompt }
|
|
1705
|
+
];
|
|
1706
|
+
for (const r of imageRefs) {
|
|
1707
|
+
const source = pickResponsesSource(r);
|
|
1708
|
+
if (!source) {
|
|
1709
|
+
inputContent.push({
|
|
1710
|
+
type: "input_text",
|
|
1711
|
+
text: `[image attachment id=${r.assetId} unavailable: cdn unreachable and bytes exceeded inline cap]`
|
|
1712
|
+
});
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
inputContent.push({ type: "input_image", source });
|
|
1716
|
+
}
|
|
1717
|
+
for (const r of fileRefs) {
|
|
1718
|
+
const source = pickResponsesSource(r);
|
|
1719
|
+
if (!source) {
|
|
1720
|
+
inputContent.push({
|
|
1721
|
+
type: "input_text",
|
|
1722
|
+
text: `[file attachment id=${r.assetId} mime=${r.mime ?? "unknown"} unavailable]`
|
|
1723
|
+
});
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
if (r.filename && source.type === "base64") {
|
|
1727
|
+
source.filename = r.filename;
|
|
1728
|
+
}
|
|
1729
|
+
inputContent.push({ type: "input_file", source });
|
|
1730
|
+
}
|
|
1731
|
+
const input = [{ type: "message", role: "user", content: inputContent }];
|
|
1435
1732
|
try {
|
|
1436
|
-
const res = await fetch(`${this.baseUrl}/v1/
|
|
1733
|
+
const res = await fetch(`${this.baseUrl}/v1/responses`, {
|
|
1437
1734
|
method: "POST",
|
|
1438
1735
|
headers: {
|
|
1439
1736
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -1441,7 +1738,11 @@ var init_openclaw = __esm({
|
|
|
1441
1738
|
},
|
|
1442
1739
|
body: JSON.stringify({
|
|
1443
1740
|
model: this.model,
|
|
1444
|
-
|
|
1741
|
+
input,
|
|
1742
|
+
// OpenClaw `/v1/responses` accepts top-level `instructions` for
|
|
1743
|
+
// the system prompt — keeping it out of the user message
|
|
1744
|
+
// preserves cleanish channel-context separation.
|
|
1745
|
+
...systemContent ? { instructions: systemContent } : {},
|
|
1445
1746
|
stream: false
|
|
1446
1747
|
}),
|
|
1447
1748
|
signal: task.signal
|
|
@@ -1457,7 +1758,21 @@ var init_openclaw = __esm({
|
|
|
1457
1758
|
};
|
|
1458
1759
|
}
|
|
1459
1760
|
const json = await res.json();
|
|
1460
|
-
let output =
|
|
1761
|
+
let output = "";
|
|
1762
|
+
if (Array.isArray(json.output)) {
|
|
1763
|
+
for (const item of json.output) {
|
|
1764
|
+
if (item?.type === "message" && Array.isArray(item.content)) {
|
|
1765
|
+
for (const part of item.content) {
|
|
1766
|
+
if (part?.type === "output_text" && typeof part.text === "string") {
|
|
1767
|
+
output += part.text;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
if (!output) {
|
|
1774
|
+
output = json.choices?.[0]?.message?.content ?? "";
|
|
1775
|
+
}
|
|
1461
1776
|
const ERR_PREFIX_RE = /^Cannot read properties of undefined \(reading '[^']+'\)\s*\n+/;
|
|
1462
1777
|
if (ERR_PREFIX_RE.test(output)) {
|
|
1463
1778
|
process.stderr.write(
|
|
@@ -1475,6 +1790,25 @@ var init_openclaw = __esm({
|
|
|
1475
1790
|
return categorizeDispatchError(err, task.signal);
|
|
1476
1791
|
}
|
|
1477
1792
|
}
|
|
1793
|
+
installRoleTemplateSkill() {
|
|
1794
|
+
const roleTemplate = readPlainRecord2(this.config.roleTemplate);
|
|
1795
|
+
const openclawConfig = readPlainRecord2(roleTemplate?.openclawConfig);
|
|
1796
|
+
const agents = readNonEmptyString2(openclawConfig?.agents);
|
|
1797
|
+
if (!agents) return;
|
|
1798
|
+
const slug = sanitizeRoleSkillSlug2(readNonEmptyString2(roleTemplate?.slug) ?? "template");
|
|
1799
|
+
const skillName = `${PRISMER_ROLE_SKILL_PREFIX2}-${slug}`;
|
|
1800
|
+
const content = renderRoleTemplateSkill2(skillName, agents, roleTemplate);
|
|
1801
|
+
try {
|
|
1802
|
+
const skillDir = join3(this.skillLoader.getSkillsRoot(), skillName);
|
|
1803
|
+
mkdirSync2(skillDir, { recursive: true });
|
|
1804
|
+
writeFileSync2(join3(skillDir, "SKILL.md"), content, "utf8");
|
|
1805
|
+
} catch (err) {
|
|
1806
|
+
process.stderr.write(
|
|
1807
|
+
`[openclaw-adapter] failed to install role-template skill ${skillName}: ${err.message}
|
|
1808
|
+
`
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1478
1812
|
};
|
|
1479
1813
|
}
|
|
1480
1814
|
});
|
|
@@ -1525,11 +1859,39 @@ async function syncAllAgentSkills(profiles, cloud, options = {}) {
|
|
|
1525
1859
|
}
|
|
1526
1860
|
async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, signal) {
|
|
1527
1861
|
if (!agentImUserId) return { synced: 0, skipped: 0, unchanged: 0 };
|
|
1528
|
-
|
|
1862
|
+
let data = await cloud.get(
|
|
1529
1863
|
`/api/im/skills/installed?agentId=${encodeURIComponent(agentImUserId)}`,
|
|
1530
1864
|
{ signal }
|
|
1531
1865
|
);
|
|
1532
|
-
|
|
1866
|
+
let entries = normalizeInstalledSkills(data);
|
|
1867
|
+
if (entries.length === 0 && (profile.adapterName === "hermes" || profile.adapterName === "openclaw")) {
|
|
1868
|
+
try {
|
|
1869
|
+
const backfill = await cloud.request(
|
|
1870
|
+
"POST",
|
|
1871
|
+
`/api/im/agents/${encodeURIComponent(agentImUserId)}/skills/install-builtins`,
|
|
1872
|
+
{ body: {}, signal }
|
|
1873
|
+
);
|
|
1874
|
+
if (backfill.ok && backfill.data && typeof backfill.data === "object" && "data" in backfill.data) {
|
|
1875
|
+
const installed = backfill.data.data?.installed ?? 0;
|
|
1876
|
+
if (installed > 0) {
|
|
1877
|
+
process.stdout.write(
|
|
1878
|
+
`[daemon] skill sync: backfilled ${installed} built-in skills for agent ${agentImUserId.slice(-8)}
|
|
1879
|
+
`
|
|
1880
|
+
);
|
|
1881
|
+
data = await cloud.get(
|
|
1882
|
+
`/api/im/skills/installed?agentId=${encodeURIComponent(agentImUserId)}`,
|
|
1883
|
+
{ signal }
|
|
1884
|
+
);
|
|
1885
|
+
entries = normalizeInstalledSkills(data);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
} catch (err) {
|
|
1889
|
+
process.stderr.write(
|
|
1890
|
+
`[daemon] skill sync: backfill skipped for ${agentImUserId.slice(-8)}: ${err.message}
|
|
1891
|
+
`
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1533
1895
|
const skillsRoot = resolveSkillsRoot(profile);
|
|
1534
1896
|
if (!skillsRoot) return { synced: 0, skipped: 0, unchanged: 0 };
|
|
1535
1897
|
let synced = 0;
|
|
@@ -2271,7 +2633,7 @@ var require_package = __commonJS({
|
|
|
2271
2633
|
"package.json"(exports, module) {
|
|
2272
2634
|
module.exports = {
|
|
2273
2635
|
name: "@prismer/runtime",
|
|
2274
|
-
version: "2.0.
|
|
2636
|
+
version: "2.0.5",
|
|
2275
2637
|
description: "Prismer Cloud daemon runtime \u2014 TS-only adapter host for hosted IM agents",
|
|
2276
2638
|
type: "module",
|
|
2277
2639
|
main: "dist/index.js",
|
|
@@ -2305,6 +2667,7 @@ var require_package = __commonJS({
|
|
|
2305
2667
|
},
|
|
2306
2668
|
dependencies: {
|
|
2307
2669
|
"@iarna/toml": "^2.2.5",
|
|
2670
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
2308
2671
|
"better-sqlite3": "^11.8.0",
|
|
2309
2672
|
commander: "^12.1.0",
|
|
2310
2673
|
qrcode: "^1.5.4",
|
|
@@ -2366,7 +2729,7 @@ __export(util_exports, {
|
|
|
2366
2729
|
warn: () => warn,
|
|
2367
2730
|
writePidFile: () => writePidFile
|
|
2368
2731
|
});
|
|
2369
|
-
import { existsSync as existsSync11, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as
|
|
2732
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync7 } from "fs";
|
|
2370
2733
|
import { join as join15 } from "path";
|
|
2371
2734
|
function color(kind, text) {
|
|
2372
2735
|
if (process.env.NO_COLOR === "1" || process.env.NO_COLOR === "true") return text;
|
|
@@ -2464,7 +2827,7 @@ function pidFilePath(paths) {
|
|
|
2464
2827
|
return join15(paths.root, "daemon.pid");
|
|
2465
2828
|
}
|
|
2466
2829
|
function writePidFile(paths, pid) {
|
|
2467
|
-
|
|
2830
|
+
writeFileSync7(pidFilePath(paths), `${pid}
|
|
2468
2831
|
`, "utf8");
|
|
2469
2832
|
}
|
|
2470
2833
|
function readPidFile(paths) {
|
|
@@ -2687,9 +3050,9 @@ var WsClient = class extends EventEmitter {
|
|
|
2687
3050
|
|
|
2688
3051
|
// src/sync/store.ts
|
|
2689
3052
|
import Database2 from "better-sqlite3";
|
|
2690
|
-
import { existsSync as existsSync2, mkdirSync as
|
|
3053
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
2691
3054
|
import { dirname as dirname2 } from "path";
|
|
2692
|
-
var SCHEMA_VERSION =
|
|
3055
|
+
var SCHEMA_VERSION = 4;
|
|
2693
3056
|
var MIGRATIONS = [
|
|
2694
3057
|
{
|
|
2695
3058
|
version: 1,
|
|
@@ -2815,6 +3178,36 @@ var MIGRATIONS = [
|
|
|
2815
3178
|
CREATE INDEX IF NOT EXISTS idx_asset_meta_seq
|
|
2816
3179
|
ON asset_metadata_index(workspace_id, asset_index_seq);
|
|
2817
3180
|
`
|
|
3181
|
+
},
|
|
3182
|
+
{
|
|
3183
|
+
version: 4,
|
|
3184
|
+
up: `
|
|
3185
|
+
-- Wave 4 / W3 \xA74.3 \u2014 pending dispatch replies for crash recovery.
|
|
3186
|
+
--
|
|
3187
|
+
-- The two-phase reply protocol writes a row here BEFORE calling
|
|
3188
|
+
-- POST /dispatch/:taskId/prepare so that a crash between prepare
|
|
3189
|
+
-- and commit leaves a recoverable trail. On daemon cold-start, any
|
|
3190
|
+
-- row with status='prepared' triggers an idempotent commit retry.
|
|
3191
|
+
--
|
|
3192
|
+
-- The server-side cutoff for orphaned 'prepared' rows is 1h
|
|
3193
|
+
-- (PREPARED_REPLY_ORPHAN_MS in dispatch-reply.service.ts); rows
|
|
3194
|
+
-- older than that will fail commit with DISPATCH_REPLY_INVALID_STATE
|
|
3195
|
+
-- and must be reported to the user as 'dispatch lost'.
|
|
3196
|
+
CREATE TABLE IF NOT EXISTS pending_dispatch_replies (
|
|
3197
|
+
idempotency_key TEXT PRIMARY KEY,
|
|
3198
|
+
task_id TEXT NOT NULL,
|
|
3199
|
+
reply_id TEXT,
|
|
3200
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
3201
|
+
payload_json TEXT NOT NULL,
|
|
3202
|
+
prepared_at INTEGER,
|
|
3203
|
+
created_at INTEGER NOT NULL,
|
|
3204
|
+
last_attempt_at INTEGER,
|
|
3205
|
+
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
3206
|
+
last_error TEXT
|
|
3207
|
+
);
|
|
3208
|
+
CREATE INDEX IF NOT EXISTS idx_pending_reply_task ON pending_dispatch_replies (task_id);
|
|
3209
|
+
CREATE INDEX IF NOT EXISTS idx_pending_reply_status ON pending_dispatch_replies (status, created_at);
|
|
3210
|
+
`
|
|
2818
3211
|
}
|
|
2819
3212
|
];
|
|
2820
3213
|
function runSql(db, sql) {
|
|
@@ -2822,7 +3215,7 @@ function runSql(db, sql) {
|
|
|
2822
3215
|
}
|
|
2823
3216
|
function openLocalDb(path9) {
|
|
2824
3217
|
if (path9 !== ":memory:" && !existsSync2(dirname2(path9))) {
|
|
2825
|
-
|
|
3218
|
+
mkdirSync3(dirname2(path9), { recursive: true });
|
|
2826
3219
|
}
|
|
2827
3220
|
const db = new Database2(path9);
|
|
2828
3221
|
db.pragma("journal_mode = WAL");
|
|
@@ -3059,7 +3452,7 @@ function listRoleTemplates() {
|
|
|
3059
3452
|
|
|
3060
3453
|
// src/config.ts
|
|
3061
3454
|
import * as TOML from "@iarna/toml";
|
|
3062
|
-
import { existsSync as existsSync3, mkdirSync as
|
|
3455
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
3063
3456
|
import { homedir as homedir3 } from "os";
|
|
3064
3457
|
import { dirname as dirname3, join as join4 } from "path";
|
|
3065
3458
|
import { z as z3 } from "zod";
|
|
@@ -3126,13 +3519,13 @@ function loadConfig(paths = resolvePaths()) {
|
|
|
3126
3519
|
}
|
|
3127
3520
|
function saveConfig(config, paths = resolvePaths()) {
|
|
3128
3521
|
if (!existsSync3(paths.root)) {
|
|
3129
|
-
|
|
3522
|
+
mkdirSync4(paths.root, { recursive: true });
|
|
3130
3523
|
}
|
|
3131
3524
|
if (!existsSync3(dirname3(paths.configFile))) {
|
|
3132
|
-
|
|
3525
|
+
mkdirSync4(dirname3(paths.configFile), { recursive: true });
|
|
3133
3526
|
}
|
|
3134
3527
|
ConfigSchema.parse(config);
|
|
3135
|
-
|
|
3528
|
+
writeFileSync3(paths.configFile, TOML.stringify(config), "utf8");
|
|
3136
3529
|
}
|
|
3137
3530
|
function deriveWsUrl(httpBase) {
|
|
3138
3531
|
const u = new URL(httpBase);
|
|
@@ -3305,7 +3698,7 @@ function envelope(type, payload, requestId) {
|
|
|
3305
3698
|
// src/asset-cache.ts
|
|
3306
3699
|
import { createHash as createHash2 } from "crypto";
|
|
3307
3700
|
import { promises as dnsp } from "dns";
|
|
3308
|
-
import { existsSync as existsSync4, mkdirSync as
|
|
3701
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync3, renameSync, statSync, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
3309
3702
|
import { isIP, isIPv4, isIPv6 } from "net";
|
|
3310
3703
|
import { dirname as dirname4, join as join5 } from "path";
|
|
3311
3704
|
import { Agent as UndiciAgent } from "undici";
|
|
@@ -3331,7 +3724,7 @@ var AssetCache = class {
|
|
|
3331
3724
|
this.cacheDir = opts.cacheDir;
|
|
3332
3725
|
this.maxBytes = opts.maxBytes ?? 5 * 1024 * 1024 * 1024;
|
|
3333
3726
|
if (!existsSync4(this.cacheDir)) {
|
|
3334
|
-
|
|
3727
|
+
mkdirSync5(this.cacheDir, { recursive: true });
|
|
3335
3728
|
}
|
|
3336
3729
|
}
|
|
3337
3730
|
/** Local file path for a content hash. Files split by first 2 chars for fs scaling. */
|
|
@@ -3406,10 +3799,10 @@ var AssetCache = class {
|
|
|
3406
3799
|
}
|
|
3407
3800
|
const localPath = this.pathFor(hash);
|
|
3408
3801
|
if (!existsSync4(dirname4(localPath))) {
|
|
3409
|
-
|
|
3802
|
+
mkdirSync5(dirname4(localPath), { recursive: true });
|
|
3410
3803
|
}
|
|
3411
3804
|
const tmpPath = `${localPath}.${process.pid}.${Date.now()}.tmp`;
|
|
3412
|
-
|
|
3805
|
+
writeFileSync4(tmpPath, buf);
|
|
3413
3806
|
renameSync(tmpPath, localPath);
|
|
3414
3807
|
const mime = res.headers.get("content-type");
|
|
3415
3808
|
const now = Date.now();
|
|
@@ -3470,10 +3863,10 @@ var AssetCache = class {
|
|
|
3470
3863
|
}
|
|
3471
3864
|
const localPath = this.pathFor(hash);
|
|
3472
3865
|
if (!existsSync4(dirname4(localPath))) {
|
|
3473
|
-
|
|
3866
|
+
mkdirSync5(dirname4(localPath), { recursive: true });
|
|
3474
3867
|
}
|
|
3475
3868
|
const tmpPath = `${localPath}.${process.pid}.${Date.now()}.tmp`;
|
|
3476
|
-
|
|
3869
|
+
writeFileSync4(tmpPath, body);
|
|
3477
3870
|
renameSync(tmpPath, localPath);
|
|
3478
3871
|
const now = Date.now();
|
|
3479
3872
|
this.db.prepare(
|
|
@@ -3727,7 +4120,7 @@ async function fetchUrlWithGuards(rawUrl, opts) {
|
|
|
3727
4120
|
}
|
|
3728
4121
|
|
|
3729
4122
|
// src/daemon/asset/mirror.ts
|
|
3730
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
4123
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
3731
4124
|
import { join as join6 } from "path";
|
|
3732
4125
|
function rowToBinding(row) {
|
|
3733
4126
|
return {
|
|
@@ -3750,7 +4143,7 @@ var WorkspaceMirror = class {
|
|
|
3750
4143
|
this.cloud = opts.cloud;
|
|
3751
4144
|
this.workspaceId = opts.workspaceId;
|
|
3752
4145
|
if (!existsSync5(opts.workspaceStateDir)) {
|
|
3753
|
-
|
|
4146
|
+
mkdirSync6(opts.workspaceStateDir, { recursive: true });
|
|
3754
4147
|
}
|
|
3755
4148
|
this.cursorPath = join6(opts.workspaceStateDir, "asset-cursor.json");
|
|
3756
4149
|
}
|
|
@@ -3771,7 +4164,7 @@ var WorkspaceMirror = class {
|
|
|
3771
4164
|
cursor,
|
|
3772
4165
|
writtenAt: Date.now()
|
|
3773
4166
|
};
|
|
3774
|
-
|
|
4167
|
+
writeFileSync5(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
3775
4168
|
}
|
|
3776
4169
|
/**
|
|
3777
4170
|
* Pull workspace_file changes since the persisted cursor and upsert into
|
|
@@ -4180,6 +4573,262 @@ function extractHttpUrls(text) {
|
|
|
4180
4573
|
import { readFileSync as readFileSync5, promises as fsp3 } from "fs";
|
|
4181
4574
|
import * as path from "path";
|
|
4182
4575
|
init_skill_sync();
|
|
4576
|
+
|
|
4577
|
+
// src/daemon/task-heartbeat.ts
|
|
4578
|
+
var TaskHeartbeat = class {
|
|
4579
|
+
constructor(opts) {
|
|
4580
|
+
this.opts = opts;
|
|
4581
|
+
this.intervalMs = opts.intervalMs ?? 15e3;
|
|
4582
|
+
this.maxRetries = opts.maxRetries ?? 3;
|
|
4583
|
+
this.retryBackoffMs = opts.retryBackoffMs ?? 100;
|
|
4584
|
+
this.now = opts.now ?? Date.now;
|
|
4585
|
+
}
|
|
4586
|
+
opts;
|
|
4587
|
+
timer;
|
|
4588
|
+
version = 0;
|
|
4589
|
+
taskId;
|
|
4590
|
+
getPhase;
|
|
4591
|
+
/** Tracks the last step time pushed via touchStep() — informational. */
|
|
4592
|
+
lastStepAt;
|
|
4593
|
+
intervalMs;
|
|
4594
|
+
maxRetries;
|
|
4595
|
+
retryBackoffMs;
|
|
4596
|
+
now;
|
|
4597
|
+
/**
|
|
4598
|
+
* Begin heartbeating for `taskId`. `getPhase` is invoked at every tick to
|
|
4599
|
+
* pick up the live phase string — adapters can mutate their phase state
|
|
4600
|
+
* freely and the heartbeat will surface it on the next 15s boundary.
|
|
4601
|
+
*
|
|
4602
|
+
* Calling start() twice on the same instance replaces the active task
|
|
4603
|
+
* (previous timer is cleared). Returns silently when already started for
|
|
4604
|
+
* the same taskId.
|
|
4605
|
+
*/
|
|
4606
|
+
start(taskId, getPhase) {
|
|
4607
|
+
if (this.taskId === taskId && this.timer) return;
|
|
4608
|
+
this.stop();
|
|
4609
|
+
this.taskId = taskId;
|
|
4610
|
+
this.getPhase = getPhase;
|
|
4611
|
+
this.timer = setInterval(() => {
|
|
4612
|
+
void this.push();
|
|
4613
|
+
}, this.intervalMs);
|
|
4614
|
+
this.timer.unref?.();
|
|
4615
|
+
}
|
|
4616
|
+
/**
|
|
4617
|
+
* Stop the heartbeat loop. Idempotent.
|
|
4618
|
+
*/
|
|
4619
|
+
stop() {
|
|
4620
|
+
if (this.timer) {
|
|
4621
|
+
clearInterval(this.timer);
|
|
4622
|
+
this.timer = void 0;
|
|
4623
|
+
}
|
|
4624
|
+
this.taskId = void 0;
|
|
4625
|
+
this.getPhase = void 0;
|
|
4626
|
+
}
|
|
4627
|
+
/**
|
|
4628
|
+
* Phase transition — push immediately AND update what the timer-driven
|
|
4629
|
+
* tick will report next time. Adapter calls this whenever the lifecycle
|
|
4630
|
+
* advances (thinking → tool_use → reasoning → responding → ...).
|
|
4631
|
+
*
|
|
4632
|
+
* No-op when no task is active.
|
|
4633
|
+
*/
|
|
4634
|
+
setPhase(phase) {
|
|
4635
|
+
if (!this.taskId) return;
|
|
4636
|
+
this.getPhase = () => phase;
|
|
4637
|
+
void this.push();
|
|
4638
|
+
}
|
|
4639
|
+
/**
|
|
4640
|
+
* Mark "I just made observable progress." Updates lastStepAt; next push
|
|
4641
|
+
* (timer-driven or setPhase-driven) carries it. Lightweight — no immediate
|
|
4642
|
+
* send. Adapters call this around tool calls / token boundaries so the
|
|
4643
|
+
* cloud reaper can distinguish "alive but slow" from "truly stuck".
|
|
4644
|
+
*/
|
|
4645
|
+
touchStep() {
|
|
4646
|
+
this.lastStepAt = this.now();
|
|
4647
|
+
}
|
|
4648
|
+
/**
|
|
4649
|
+
* Manually trigger one heartbeat send (does not affect the timer). Mainly
|
|
4650
|
+
* useful for tests that don't want to await a 15s tick. Returns the
|
|
4651
|
+
* heartbeatVersion that was sent, or undefined if no task is active.
|
|
4652
|
+
*/
|
|
4653
|
+
async forceTick() {
|
|
4654
|
+
if (!this.taskId) return void 0;
|
|
4655
|
+
return this.push();
|
|
4656
|
+
}
|
|
4657
|
+
/** Test helper: current heartbeatVersion (last value sent, may be 0 before first tick). */
|
|
4658
|
+
get currentVersion() {
|
|
4659
|
+
return this.version;
|
|
4660
|
+
}
|
|
4661
|
+
async push() {
|
|
4662
|
+
if (!this.taskId || !this.getPhase) return void 0;
|
|
4663
|
+
const taskId = this.taskId;
|
|
4664
|
+
const phase = this.getPhase();
|
|
4665
|
+
const heartbeatVersion = ++this.version;
|
|
4666
|
+
const payload = {
|
|
4667
|
+
taskId,
|
|
4668
|
+
heartbeatVersion,
|
|
4669
|
+
currentPhase: phase,
|
|
4670
|
+
...this.lastStepAt !== void 0 ? { lastStepAt: this.lastStepAt } : {}
|
|
4671
|
+
};
|
|
4672
|
+
const msg = envelope("task.heartbeat", payload);
|
|
4673
|
+
let lastErr;
|
|
4674
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
|
|
4675
|
+
try {
|
|
4676
|
+
if (this.opts.ws.isOpen && !this.opts.ws.isOpen()) {
|
|
4677
|
+
return heartbeatVersion;
|
|
4678
|
+
}
|
|
4679
|
+
this.opts.ws.send(msg);
|
|
4680
|
+
return heartbeatVersion;
|
|
4681
|
+
} catch (err) {
|
|
4682
|
+
lastErr = err instanceof Error ? err : new Error(String(err));
|
|
4683
|
+
if (attempt < this.maxRetries) {
|
|
4684
|
+
const backoff = this.retryBackoffMs * Math.pow(2, attempt);
|
|
4685
|
+
await sleep(backoff);
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
if (lastErr) {
|
|
4690
|
+
try {
|
|
4691
|
+
this.opts.onGiveUp?.(taskId, lastErr);
|
|
4692
|
+
} catch {
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
return heartbeatVersion;
|
|
4696
|
+
}
|
|
4697
|
+
};
|
|
4698
|
+
function sleep(ms) {
|
|
4699
|
+
return new Promise((resolve3) => {
|
|
4700
|
+
const handle = setTimeout(resolve3, ms);
|
|
4701
|
+
handle.unref?.();
|
|
4702
|
+
});
|
|
4703
|
+
}
|
|
4704
|
+
|
|
4705
|
+
// src/daemon/step-recorder.ts
|
|
4706
|
+
var StepRecorder = class {
|
|
4707
|
+
constructor(opts) {
|
|
4708
|
+
this.opts = opts;
|
|
4709
|
+
this.reasoningThrottleMs = opts.reasoningThrottleMs ?? 500;
|
|
4710
|
+
this.now = opts.now ?? Date.now;
|
|
4711
|
+
}
|
|
4712
|
+
opts;
|
|
4713
|
+
seq = 0;
|
|
4714
|
+
reasoningBuf = { texts: [], firstAt: 0 };
|
|
4715
|
+
reasoningTimer;
|
|
4716
|
+
reasoningThrottleMs;
|
|
4717
|
+
now;
|
|
4718
|
+
/** Push a phase transition step. Immediate (no throttle). */
|
|
4719
|
+
recordPhaseChange(phase) {
|
|
4720
|
+
this.emit("phase_change", { phase });
|
|
4721
|
+
}
|
|
4722
|
+
/**
|
|
4723
|
+
* Push a tool-call step. Immediate.
|
|
4724
|
+
*
|
|
4725
|
+
* `toolCallId` is the daemon-side correlation id used to pair with the
|
|
4726
|
+
* subsequent recordToolResult call. Free-form; adapter decides.
|
|
4727
|
+
*/
|
|
4728
|
+
recordToolCall(toolName, input, toolCallId) {
|
|
4729
|
+
this.emit("tool_call", {
|
|
4730
|
+
toolName,
|
|
4731
|
+
inputSummary: summarize(input),
|
|
4732
|
+
...toolCallId ? { toolCallId } : {}
|
|
4733
|
+
});
|
|
4734
|
+
}
|
|
4735
|
+
/** Push a tool-result step. Immediate. */
|
|
4736
|
+
recordToolResult(toolCallId, output) {
|
|
4737
|
+
this.emit("tool_result", {
|
|
4738
|
+
toolCallId,
|
|
4739
|
+
outputSummary: summarize(output)
|
|
4740
|
+
});
|
|
4741
|
+
}
|
|
4742
|
+
/**
|
|
4743
|
+
* Buffer a reasoning chunk for batched dispatch. Multiple chunks within
|
|
4744
|
+
* `reasoningThrottleMs` are concatenated and emitted as a single
|
|
4745
|
+
* `reasoning_chunk` step when the timer fires.
|
|
4746
|
+
*/
|
|
4747
|
+
recordReasoningChunk(text) {
|
|
4748
|
+
if (!text) return;
|
|
4749
|
+
if (this.reasoningBuf.texts.length === 0) {
|
|
4750
|
+
this.reasoningBuf.firstAt = this.now();
|
|
4751
|
+
}
|
|
4752
|
+
this.reasoningBuf.texts.push(text);
|
|
4753
|
+
if (this.reasoningTimer) return;
|
|
4754
|
+
this.reasoningTimer = setTimeout(() => {
|
|
4755
|
+
this.flushReasoning();
|
|
4756
|
+
}, this.reasoningThrottleMs);
|
|
4757
|
+
this.reasoningTimer.unref?.();
|
|
4758
|
+
}
|
|
4759
|
+
/** Push an error step. Immediate. */
|
|
4760
|
+
recordError(message, payload) {
|
|
4761
|
+
this.emit("error", { message, ...payload ?? {} });
|
|
4762
|
+
}
|
|
4763
|
+
/**
|
|
4764
|
+
* Force any pending reasoning buffer out the door. Call on shutdown so
|
|
4765
|
+
* trailing tokens aren't lost.
|
|
4766
|
+
*/
|
|
4767
|
+
flush() {
|
|
4768
|
+
this.flushReasoning();
|
|
4769
|
+
}
|
|
4770
|
+
/** Test helper. */
|
|
4771
|
+
get currentSeq() {
|
|
4772
|
+
return this.seq;
|
|
4773
|
+
}
|
|
4774
|
+
flushReasoning() {
|
|
4775
|
+
if (this.reasoningTimer) {
|
|
4776
|
+
clearTimeout(this.reasoningTimer);
|
|
4777
|
+
this.reasoningTimer = void 0;
|
|
4778
|
+
}
|
|
4779
|
+
if (this.reasoningBuf.texts.length === 0) return;
|
|
4780
|
+
const text = this.reasoningBuf.texts.join("");
|
|
4781
|
+
const firstAt = this.reasoningBuf.firstAt;
|
|
4782
|
+
this.reasoningBuf = { texts: [], firstAt: 0 };
|
|
4783
|
+
this.emit("reasoning_chunk", { text }, firstAt);
|
|
4784
|
+
}
|
|
4785
|
+
emit(kind, payload, occurredAt) {
|
|
4786
|
+
const frame = {
|
|
4787
|
+
seq: ++this.seq,
|
|
4788
|
+
kind,
|
|
4789
|
+
payload,
|
|
4790
|
+
occurredAt: occurredAt ?? this.now()
|
|
4791
|
+
};
|
|
4792
|
+
const msg = envelope("task.step.append", {
|
|
4793
|
+
taskRunId: this.opts.taskRunId,
|
|
4794
|
+
step: frame
|
|
4795
|
+
});
|
|
4796
|
+
try {
|
|
4797
|
+
if (this.opts.ws.isOpen && !this.opts.ws.isOpen()) {
|
|
4798
|
+
try {
|
|
4799
|
+
this.opts.onSend?.(frame);
|
|
4800
|
+
} catch {
|
|
4801
|
+
}
|
|
4802
|
+
return;
|
|
4803
|
+
}
|
|
4804
|
+
this.opts.ws.send(msg);
|
|
4805
|
+
try {
|
|
4806
|
+
this.opts.onSend?.(frame);
|
|
4807
|
+
} catch {
|
|
4808
|
+
}
|
|
4809
|
+
} catch {
|
|
4810
|
+
}
|
|
4811
|
+
}
|
|
4812
|
+
};
|
|
4813
|
+
var MAX_SUMMARY_CHARS = 512;
|
|
4814
|
+
function summarize(value) {
|
|
4815
|
+
let text;
|
|
4816
|
+
if (value == null) text = "";
|
|
4817
|
+
else if (typeof value === "string") text = value;
|
|
4818
|
+
else {
|
|
4819
|
+
try {
|
|
4820
|
+
text = JSON.stringify(value);
|
|
4821
|
+
} catch {
|
|
4822
|
+
text = String(value);
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
if (text.length > MAX_SUMMARY_CHARS) {
|
|
4826
|
+
return `${text.slice(0, MAX_SUMMARY_CHARS)}\u2026(+${text.length - MAX_SUMMARY_CHARS} chars)`;
|
|
4827
|
+
}
|
|
4828
|
+
return text;
|
|
4829
|
+
}
|
|
4830
|
+
|
|
4831
|
+
// src/daemon/dispatch.ts
|
|
4183
4832
|
var DEFAULT_CONTEXT_MAX = 8e3;
|
|
4184
4833
|
var GOAL_CONTEXT_MAX = 4;
|
|
4185
4834
|
var MEMORY_DIGEST_MAX_BYTES = 4e3;
|
|
@@ -4195,6 +4844,8 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4195
4844
|
const { taskId, agentImUserId } = payload;
|
|
4196
4845
|
let resolvedHashes = [];
|
|
4197
4846
|
let reply;
|
|
4847
|
+
let heartbeatRef;
|
|
4848
|
+
let recorderRef;
|
|
4198
4849
|
const outboxBase = deps.paths?.runsDir ? path.join(deps.paths.runsDir, taskId) : null;
|
|
4199
4850
|
const outboxDir = outboxBase ? path.join(outboxBase, "_outbox") : null;
|
|
4200
4851
|
const workDir = outboxBase ? path.join(outboxBase, "workdir") : null;
|
|
@@ -4276,7 +4927,13 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4276
4927
|
resolvedHashes.push(...r.resolvedHashes);
|
|
4277
4928
|
rewrittenContext = payload.context.map((e, i) => ({ ...e, content: r.texts[i] }));
|
|
4278
4929
|
}
|
|
4279
|
-
const assetResolution = await resolveAssetRefs(payload.assetRefs, deps.assetCache, taskId
|
|
4930
|
+
const assetResolution = await resolveAssetRefs(payload.assetRefs, deps.assetCache, taskId, {
|
|
4931
|
+
cloud: deps.cloud,
|
|
4932
|
+
enabled: deps.visionAux?.enabled !== false,
|
|
4933
|
+
cacheDir: deps.visionAux?.cacheDir ?? (deps.paths ? path.join(deps.paths.cacheDir, "vision-cache") : void 0),
|
|
4934
|
+
timeoutMs: deps.visionAux?.timeoutMs,
|
|
4935
|
+
signal: deps.signal
|
|
4936
|
+
});
|
|
4280
4937
|
resolvedHashes.push(...assetResolution.pinnedHashes);
|
|
4281
4938
|
const basePrompt = composePrompt(
|
|
4282
4939
|
rewrittenPrompt.text,
|
|
@@ -4311,9 +4968,44 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4311
4968
|
}
|
|
4312
4969
|
const operatingPrinciples = resolveOperatingPrinciples(profile.config);
|
|
4313
4970
|
const composedSystemPrompt = [profileSystemPrompt, operatingPrinciples].filter((part) => typeof part === "string" && part.length > 0).join("\n\n");
|
|
4971
|
+
let activePhase = "thinking";
|
|
4972
|
+
const heartbeat = new TaskHeartbeat({
|
|
4973
|
+
ws: deps.ws,
|
|
4974
|
+
onGiveUp: (tid, err) => process.stderr.write(
|
|
4975
|
+
`[daemon] task.heartbeat give-up task=${tid}: ${err.message}
|
|
4976
|
+
`
|
|
4977
|
+
)
|
|
4978
|
+
});
|
|
4979
|
+
heartbeatRef = heartbeat;
|
|
4980
|
+
heartbeat.start(taskId, () => activePhase);
|
|
4981
|
+
const recorder = new StepRecorder({ ws: deps.ws, taskRunId: taskId });
|
|
4982
|
+
recorderRef = recorder;
|
|
4983
|
+
recorder.recordPhaseChange(activePhase);
|
|
4314
4984
|
const taskInput = {
|
|
4315
4985
|
taskId,
|
|
4316
4986
|
prompt: adapterPrompt,
|
|
4987
|
+
heartbeat: {
|
|
4988
|
+
setPhase: (phase) => {
|
|
4989
|
+
activePhase = phase;
|
|
4990
|
+
heartbeat.setPhase(phase);
|
|
4991
|
+
recorder.recordPhaseChange(phase);
|
|
4992
|
+
},
|
|
4993
|
+
touchStep: () => heartbeat.touchStep()
|
|
4994
|
+
},
|
|
4995
|
+
recorder: {
|
|
4996
|
+
recordPhaseChange: (phase) => {
|
|
4997
|
+
activePhase = phase;
|
|
4998
|
+
heartbeat.setPhase(phase);
|
|
4999
|
+
recorder.recordPhaseChange(phase);
|
|
5000
|
+
},
|
|
5001
|
+
recordToolCall: (toolName, input, toolCallId) => recorder.recordToolCall(toolName, input, toolCallId),
|
|
5002
|
+
recordToolResult: (toolCallId, output) => recorder.recordToolResult(toolCallId, output),
|
|
5003
|
+
recordReasoningChunk: (text) => recorder.recordReasoningChunk(text),
|
|
5004
|
+
recordError: (message, payload2) => recorder.recordError(message, payload2)
|
|
5005
|
+
},
|
|
5006
|
+
// 14b rev.3 §9 P3 — multimodal-aware adapters (Hermes, OpenClaw) pick
|
|
5007
|
+
// up the resolved bytes/URLs here; text-only adapters ignore the field.
|
|
5008
|
+
...assetResolution.resolvedRefs.length > 0 ? { assetRefs: assetResolution.resolvedRefs } : {},
|
|
4317
5009
|
metadata: {
|
|
4318
5010
|
...payload.metadata,
|
|
4319
5011
|
conversationId: payload.conversationId,
|
|
@@ -4376,6 +5068,9 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4376
5068
|
}
|
|
4377
5069
|
}
|
|
4378
5070
|
const reaperAborted = deps.signal?.aborted && result.error?.code === "task_cancelled";
|
|
5071
|
+
const approvalRequested = Boolean(
|
|
5072
|
+
result.metadata?.approvalRequested
|
|
5073
|
+
);
|
|
4379
5074
|
let collectedAssetIds = [];
|
|
4380
5075
|
if (deps.outboxWatcher && outboxDir) {
|
|
4381
5076
|
try {
|
|
@@ -4387,14 +5082,23 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4387
5082
|
collectedAssetIds = deps.outboxWatcher.flushPending(taskId);
|
|
4388
5083
|
}
|
|
4389
5084
|
const reaperLimitMs = typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? payload.timeoutMs : 5 * 6e4;
|
|
5085
|
+
const finalError = approvalRequested ? {
|
|
5086
|
+
code: "awaiting_human_approval",
|
|
5087
|
+
message: "Agent requested human approval and is suspended pending decision. Cloud will redispatch with `approval.decided` once the human responds."
|
|
5088
|
+
} : reaperAborted ? {
|
|
5089
|
+
code: "daemon_task_timeout",
|
|
5090
|
+
message: `Daemon-side reaper aborted after ${reaperLimitMs}ms inactivity (no progress events from adapter)`
|
|
5091
|
+
} : result.error;
|
|
4390
5092
|
reply = {
|
|
4391
5093
|
taskId,
|
|
5094
|
+
// When approval is pending we keep ok=false (the run did not produce a
|
|
5095
|
+
// completion-style output) but the cloud-side handler treats the
|
|
5096
|
+
// `awaiting_human_approval` code as non-terminal — task stays alive,
|
|
5097
|
+
// no red failure pill, and the next dispatch (after the human decides)
|
|
5098
|
+
// continues normally.
|
|
4392
5099
|
ok: result.ok,
|
|
4393
5100
|
output: result.output,
|
|
4394
|
-
error:
|
|
4395
|
-
code: "daemon_task_timeout",
|
|
4396
|
-
message: `Daemon-side reaper aborted after ${reaperLimitMs}ms inactivity (no progress events from adapter)`
|
|
4397
|
-
} : result.error,
|
|
5101
|
+
error: finalError,
|
|
4398
5102
|
...collectedAssetIds.length > 0 ? { assetIds: collectedAssetIds } : {},
|
|
4399
5103
|
metrics: result.metrics,
|
|
4400
5104
|
...assetResolution.observability.length > 0 || urlObservations.length > 0 ? { assetObservability: [...assetResolution.observability, ...urlObservations] } : {}
|
|
@@ -4416,6 +5120,18 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4416
5120
|
}
|
|
4417
5121
|
};
|
|
4418
5122
|
} finally {
|
|
5123
|
+
try {
|
|
5124
|
+
recorderRef?.flush();
|
|
5125
|
+
} catch (err) {
|
|
5126
|
+
process.stderr.write(`[daemon] step recorder flush failed task=${taskId}: ${err.message}
|
|
5127
|
+
`);
|
|
5128
|
+
}
|
|
5129
|
+
try {
|
|
5130
|
+
heartbeatRef?.stop();
|
|
5131
|
+
} catch (err) {
|
|
5132
|
+
process.stderr.write(`[daemon] heartbeat stop failed task=${taskId}: ${err.message}
|
|
5133
|
+
`);
|
|
5134
|
+
}
|
|
4419
5135
|
for (const hash of new Set(resolvedHashes)) {
|
|
4420
5136
|
try {
|
|
4421
5137
|
deps.assetCache.unpin(hash);
|
|
@@ -4492,6 +5208,10 @@ function isTextLikeMime(mime) {
|
|
|
4492
5208
|
if (m.endsWith("+json") || m.endsWith("+xml") || m.endsWith("+csv")) return true;
|
|
4493
5209
|
return false;
|
|
4494
5210
|
}
|
|
5211
|
+
function isImageMime(mime) {
|
|
5212
|
+
if (!mime) return false;
|
|
5213
|
+
return mime.toLowerCase().split(";")[0].trim().startsWith("image/");
|
|
5214
|
+
}
|
|
4495
5215
|
var HASH_REF_RE = /(?:^|\s)#([^\s#]+)/g;
|
|
4496
5216
|
var HEX_COLOR_RE = /^[0-9a-fA-F]{3,8}$/;
|
|
4497
5217
|
var FILE_EXT_RE = /\.[a-zA-Z0-9]{1,10}$/;
|
|
@@ -4570,8 +5290,34 @@ async function resolveHashRefs(prompt, assetIndex, cloud) {
|
|
|
4570
5290
|
}
|
|
4571
5291
|
return { text: result, resolutions };
|
|
4572
5292
|
}
|
|
4573
|
-
|
|
4574
|
-
|
|
5293
|
+
var ASSET_BASE64_INLINE_MAX_BYTES = 10 * 1024 * 1024;
|
|
5294
|
+
async function probeCdnReachable(cdnUrl, timeoutMs = 1500) {
|
|
5295
|
+
try {
|
|
5296
|
+
const u = new URL(cdnUrl);
|
|
5297
|
+
if (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname.startsWith("192.168.")) {
|
|
5298
|
+
return false;
|
|
5299
|
+
}
|
|
5300
|
+
} catch {
|
|
5301
|
+
return false;
|
|
5302
|
+
}
|
|
5303
|
+
try {
|
|
5304
|
+
const r = await fetch(cdnUrl, {
|
|
5305
|
+
method: "HEAD",
|
|
5306
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
5307
|
+
});
|
|
5308
|
+
if (r.ok) return true;
|
|
5309
|
+
return false;
|
|
5310
|
+
} catch {
|
|
5311
|
+
return false;
|
|
5312
|
+
}
|
|
5313
|
+
}
|
|
5314
|
+
async function resolveAssetRefs(refs, cache, taskId, visionAux) {
|
|
5315
|
+
const out = {
|
|
5316
|
+
promptBlocks: [],
|
|
5317
|
+
observability: [],
|
|
5318
|
+
pinnedHashes: [],
|
|
5319
|
+
resolvedRefs: []
|
|
5320
|
+
};
|
|
4575
5321
|
if (!refs || refs.length === 0) return out;
|
|
4576
5322
|
for (const ref of refs) {
|
|
4577
5323
|
let cached;
|
|
@@ -4636,7 +5382,42 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
4636
5382
|
continue;
|
|
4637
5383
|
}
|
|
4638
5384
|
}
|
|
4639
|
-
|
|
5385
|
+
let reachable = "unknown";
|
|
5386
|
+
let base64;
|
|
5387
|
+
if (ref.cdnUrl) {
|
|
5388
|
+
const ok2 = await probeCdnReachable(ref.cdnUrl);
|
|
5389
|
+
if (ok2) {
|
|
5390
|
+
reachable = "cdn";
|
|
5391
|
+
}
|
|
5392
|
+
}
|
|
5393
|
+
if (reachable !== "cdn") {
|
|
5394
|
+
try {
|
|
5395
|
+
const buf = readFileSync5(cached.localPath);
|
|
5396
|
+
if (buf.byteLength <= ASSET_BASE64_INLINE_MAX_BYTES) {
|
|
5397
|
+
base64 = buf.toString("base64");
|
|
5398
|
+
reachable = "base64";
|
|
5399
|
+
} else {
|
|
5400
|
+
reachable = "unknown";
|
|
5401
|
+
}
|
|
5402
|
+
} catch (err) {
|
|
5403
|
+
const errMsg = `base64 fallback read failed: ${err.message}`;
|
|
5404
|
+
logAssetResolveFailure(taskId, ref, errMsg, effectiveMime);
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
const resolvedRef = {
|
|
5408
|
+
...ref,
|
|
5409
|
+
mime: effectiveMime,
|
|
5410
|
+
localPath: cached.localPath,
|
|
5411
|
+
base64,
|
|
5412
|
+
reachable
|
|
5413
|
+
};
|
|
5414
|
+
out.resolvedRefs.push(resolvedRef);
|
|
5415
|
+
if (isImageMime(effectiveMime) && visionAux?.enabled) {
|
|
5416
|
+
const block = await describeImageAttachment(ref, effectiveMime, resolvedRef, visionAux, taskId);
|
|
5417
|
+
out.promptBlocks.push(block);
|
|
5418
|
+
} else {
|
|
5419
|
+
out.promptBlocks.push(formatAttachmentReminderBlock(ref, effectiveMime));
|
|
5420
|
+
}
|
|
4640
5421
|
out.observability.push({
|
|
4641
5422
|
assetId: ref.assetId,
|
|
4642
5423
|
contentHash: ref.contentHash,
|
|
@@ -4647,6 +5428,110 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
4647
5428
|
}
|
|
4648
5429
|
return out;
|
|
4649
5430
|
}
|
|
5431
|
+
async function describeImageAttachment(ref, mime, resolved, opts, taskId) {
|
|
5432
|
+
const cached = await readVisionAuxLocalCache(opts.cacheDir, ref.contentHash, mime);
|
|
5433
|
+
if (cached) return formatVisionAuxBlock(ref, mime, cached.description, true);
|
|
5434
|
+
const source = buildVisionAuxSource(resolved, mime);
|
|
5435
|
+
if (!source) {
|
|
5436
|
+
return formatVisionAuxUnavailableBlock(ref, mime, "no reachable URL or inline bytes");
|
|
5437
|
+
}
|
|
5438
|
+
try {
|
|
5439
|
+
const description = await callVisionAux(ref, mime, source, opts);
|
|
5440
|
+
await writeVisionAuxLocalCache(opts.cacheDir, ref.contentHash, mime, description);
|
|
5441
|
+
return formatVisionAuxBlock(ref, mime, description.description, false);
|
|
5442
|
+
} catch (err) {
|
|
5443
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5444
|
+
process.stderr.write(
|
|
5445
|
+
`[daemon] vision-aux failed task=${taskId ?? "unknown"} assetId=${ref.assetId} hash=${ref.contentHash}: ${message}
|
|
5446
|
+
`
|
|
5447
|
+
);
|
|
5448
|
+
return formatVisionAuxUnavailableBlock(ref, mime, message);
|
|
5449
|
+
}
|
|
5450
|
+
}
|
|
5451
|
+
function buildVisionAuxSource(resolved, mime) {
|
|
5452
|
+
if (resolved.reachable === "cdn" && resolved.cdnUrl) return { kind: "url", url: resolved.cdnUrl };
|
|
5453
|
+
if (resolved.base64 && mime) return { kind: "data_url", url: `data:${mime};base64,${resolved.base64}` };
|
|
5454
|
+
return null;
|
|
5455
|
+
}
|
|
5456
|
+
async function callVisionAux(ref, mime, source, opts) {
|
|
5457
|
+
const secret = process.env.VISION_AUX_INTERNAL_SECRET || process.env.INTERNAL_API_SECRET || process.env.PRISMER_INTERNAL_SECRET;
|
|
5458
|
+
const headers = secret ? { "x-prismer-internal-secret": secret } : void 0;
|
|
5459
|
+
const res = await opts.cloud.request(
|
|
5460
|
+
"POST",
|
|
5461
|
+
"/api/internal/vision-aux/describe",
|
|
5462
|
+
{
|
|
5463
|
+
body: {
|
|
5464
|
+
assetId: ref.assetId,
|
|
5465
|
+
contentHash: ref.contentHash,
|
|
5466
|
+
mime: mime ?? ref.mime ?? "image/unknown",
|
|
5467
|
+
source
|
|
5468
|
+
},
|
|
5469
|
+
headers,
|
|
5470
|
+
timeoutMs: opts.timeoutMs ?? 12e3,
|
|
5471
|
+
signal: opts.signal
|
|
5472
|
+
}
|
|
5473
|
+
);
|
|
5474
|
+
const env = res.data;
|
|
5475
|
+
if (!res.ok || env?.ok === false || !env?.data?.description) {
|
|
5476
|
+
const message = res.error?.message || (typeof env?.error === "string" ? env.error : env?.error?.message) || env?.message || `HTTP ${res.status}`;
|
|
5477
|
+
throw new Error(message);
|
|
5478
|
+
}
|
|
5479
|
+
const now = /* @__PURE__ */ new Date();
|
|
5480
|
+
const ttlSec = Number.isFinite(env.data.cacheTtlSec) ? Number(env.data.cacheTtlSec) : 300;
|
|
5481
|
+
return {
|
|
5482
|
+
description: env.data.description,
|
|
5483
|
+
modelUsed: env.data.modelUsed || "unknown",
|
|
5484
|
+
provider: env.data.provider || "vision-aux",
|
|
5485
|
+
generatedAt: now.toISOString(),
|
|
5486
|
+
expiresAt: new Date(now.getTime() + Math.max(1, ttlSec) * 1e3).toISOString(),
|
|
5487
|
+
mime: mime ?? ref.mime ?? "image/unknown"
|
|
5488
|
+
};
|
|
5489
|
+
}
|
|
5490
|
+
async function readVisionAuxLocalCache(cacheDir, contentHash, mime) {
|
|
5491
|
+
if (!cacheDir) return null;
|
|
5492
|
+
const file = visionAuxCachePath(cacheDir, contentHash);
|
|
5493
|
+
try {
|
|
5494
|
+
const raw = await fsp3.readFile(file, "utf8");
|
|
5495
|
+
const parsed = parseVisionAuxCache(raw);
|
|
5496
|
+
if (!parsed) {
|
|
5497
|
+
await fsp3.unlink(file).catch(() => void 0);
|
|
5498
|
+
return null;
|
|
5499
|
+
}
|
|
5500
|
+
if (parsed.mime !== mime) return null;
|
|
5501
|
+
const expiresAt = Date.parse(parsed.expiresAt);
|
|
5502
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) return null;
|
|
5503
|
+
return parsed;
|
|
5504
|
+
} catch (err) {
|
|
5505
|
+
if (err.code !== "ENOENT") {
|
|
5506
|
+
await fsp3.unlink(file).catch(() => void 0);
|
|
5507
|
+
}
|
|
5508
|
+
return null;
|
|
5509
|
+
}
|
|
5510
|
+
}
|
|
5511
|
+
async function writeVisionAuxLocalCache(cacheDir, contentHash, mime, description) {
|
|
5512
|
+
if (!cacheDir || !mime) return;
|
|
5513
|
+
const file = visionAuxCachePath(cacheDir, contentHash);
|
|
5514
|
+
await fsp3.mkdir(path.dirname(file), { recursive: true });
|
|
5515
|
+
await fsp3.writeFile(file, JSON.stringify({ ...description, mime }, null, 2), "utf8");
|
|
5516
|
+
}
|
|
5517
|
+
function visionAuxCachePath(cacheDir, contentHash) {
|
|
5518
|
+
const safeHash = /^[a-f0-9]{32,128}$/i.test(contentHash) ? contentHash : createSafeCacheKey(contentHash);
|
|
5519
|
+
return path.join(cacheDir, `${safeHash}.json`);
|
|
5520
|
+
}
|
|
5521
|
+
function createSafeCacheKey(value) {
|
|
5522
|
+
return Buffer.from(value).toString("base64url").slice(0, 128) || "unknown";
|
|
5523
|
+
}
|
|
5524
|
+
function parseVisionAuxCache(raw) {
|
|
5525
|
+
try {
|
|
5526
|
+
const parsed = JSON.parse(raw);
|
|
5527
|
+
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") {
|
|
5528
|
+
return parsed;
|
|
5529
|
+
}
|
|
5530
|
+
} catch {
|
|
5531
|
+
return null;
|
|
5532
|
+
}
|
|
5533
|
+
return null;
|
|
5534
|
+
}
|
|
4650
5535
|
function logAssetResolveFailure(taskId, ref, error, mime = ref.mime) {
|
|
4651
5536
|
const message = error.replace(/\s+/g, " ").slice(0, 500);
|
|
4652
5537
|
process.stderr.write(
|
|
@@ -4661,8 +5546,19 @@ function formatInlineAssetBlock(ref, mime, body, strategy) {
|
|
|
4661
5546
|
${body}
|
|
4662
5547
|
---`;
|
|
4663
5548
|
}
|
|
4664
|
-
function
|
|
4665
|
-
|
|
5549
|
+
function formatAttachmentReminderBlock(ref, mime) {
|
|
5550
|
+
const name = ref.filename ? ` name=${ref.filename}` : "";
|
|
5551
|
+
return `[Attached file] id=${ref.assetId} mime=${mime ?? "unknown"}${name} \u2014 the user uploaded this file; multimodal adapters receive the bytes directly.`;
|
|
5552
|
+
}
|
|
5553
|
+
function formatVisionAuxBlock(ref, mime, description, cached) {
|
|
5554
|
+
const name = ref.filename ? `: ${ref.filename}` : "";
|
|
5555
|
+
const cacheLabel = cached ? " local-cache" : "";
|
|
5556
|
+
return `[Image attachment${name}] id=${ref.assetId} mime=${mime ?? "unknown"}${cacheLabel}
|
|
5557
|
+
${description.trim()}`;
|
|
5558
|
+
}
|
|
5559
|
+
function formatVisionAuxUnavailableBlock(ref, mime, reason) {
|
|
5560
|
+
const name = ref.filename ? `: ${ref.filename}` : "";
|
|
5561
|
+
return `[Image attachment${name}; description unavailable due to vision-aux error] id=${ref.assetId} mime=${mime ?? "unknown"} reason=${reason.slice(0, 180)}`;
|
|
4666
5562
|
}
|
|
4667
5563
|
function formatErrorAssetBlock(ref, mime, error) {
|
|
4668
5564
|
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.`;
|
|
@@ -5147,6 +6043,7 @@ import { createServer } from "http";
|
|
|
5147
6043
|
import { randomUUID, createHash as createHash4, createHmac, timingSafeEqual as cryptoTimingSafeEqual } from "crypto";
|
|
5148
6044
|
import { promises as fs } from "fs";
|
|
5149
6045
|
import * as path2 from "path";
|
|
6046
|
+
import { homedir as homedir4 } from "os";
|
|
5150
6047
|
var LocalServer = class {
|
|
5151
6048
|
constructor(opts) {
|
|
5152
6049
|
this.opts = opts;
|
|
@@ -5258,6 +6155,11 @@ var LocalServer = class {
|
|
|
5258
6155
|
void this.handleSnapshot(req, res);
|
|
5259
6156
|
return;
|
|
5260
6157
|
}
|
|
6158
|
+
const agentDumpMatch = /^\/v1\/agents\/([^/]+)\/dump-state$/.exec(url);
|
|
6159
|
+
if (req.method === "POST" && agentDumpMatch) {
|
|
6160
|
+
void this.handleAgentDumpState(req, res, decodeURIComponent(agentDumpMatch[1]));
|
|
6161
|
+
return;
|
|
6162
|
+
}
|
|
5261
6163
|
if (req.method === "POST" && url === "/local/asset/write") {
|
|
5262
6164
|
void this.handleAssetWrite(req, res);
|
|
5263
6165
|
return;
|
|
@@ -5335,6 +6237,54 @@ var LocalServer = class {
|
|
|
5335
6237
|
}
|
|
5336
6238
|
void req;
|
|
5337
6239
|
}
|
|
6240
|
+
/**
|
|
6241
|
+
* POST /v1/agents/:agentId/dump-state — agent-scoped daemon manifest.
|
|
6242
|
+
*
|
|
6243
|
+
* This is narrower than `/v1/snapshot`: it walks only the current agent's
|
|
6244
|
+
* profile/work dirs so cloud can attach the result to IMAgentSnapshot without
|
|
6245
|
+
* capturing unrelated agents hosted in the same daemon/container.
|
|
6246
|
+
*/
|
|
6247
|
+
async handleAgentDumpState(req, res, agentId) {
|
|
6248
|
+
const state = this.opts.getState();
|
|
6249
|
+
const agent = state.hostedAgents.find((item) => item.imUserId === agentId);
|
|
6250
|
+
if (!agent) {
|
|
6251
|
+
respond(res, 404, { error: "agent_not_hosted", agentId });
|
|
6252
|
+
return;
|
|
6253
|
+
}
|
|
6254
|
+
const roots = getAgentStateRoots(agent, this.opts.snapshotRoot ?? "/workspace");
|
|
6255
|
+
try {
|
|
6256
|
+
const manifests = await Promise.all(
|
|
6257
|
+
roots.map(async (root) => {
|
|
6258
|
+
const stat = await statOptional(root.rootPath);
|
|
6259
|
+
if (!stat) return { ...root, exists: false, files: [] };
|
|
6260
|
+
if (!stat.isDirectory()) return { ...root, exists: true, error: "not_directory", files: [] };
|
|
6261
|
+
return { ...root, exists: true, files: await walkAndDigest(root.rootPath, root.rootPath) };
|
|
6262
|
+
})
|
|
6263
|
+
);
|
|
6264
|
+
const files = manifests.flatMap(
|
|
6265
|
+
(manifest) => manifest.files.map((file) => ({
|
|
6266
|
+
...file,
|
|
6267
|
+
path: `${manifest.kind}/${file.path}`,
|
|
6268
|
+
rootKind: manifest.kind,
|
|
6269
|
+
rootPath: manifest.rootPath
|
|
6270
|
+
}))
|
|
6271
|
+
);
|
|
6272
|
+
respond(res, 200, {
|
|
6273
|
+
agentId,
|
|
6274
|
+
adapterName: agent.adapterName,
|
|
6275
|
+
dumpedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6276
|
+
roots: manifests,
|
|
6277
|
+
files
|
|
6278
|
+
});
|
|
6279
|
+
} catch (err) {
|
|
6280
|
+
respond(res, 500, {
|
|
6281
|
+
error: "agent_dump_state_failed",
|
|
6282
|
+
agentId,
|
|
6283
|
+
message: err instanceof Error ? err.message : String(err)
|
|
6284
|
+
});
|
|
6285
|
+
}
|
|
6286
|
+
void req;
|
|
6287
|
+
}
|
|
5338
6288
|
/**
|
|
5339
6289
|
* POST /local/asset/write — agent-gen adapter RPC.
|
|
5340
6290
|
*
|
|
@@ -5606,6 +6556,39 @@ function validateAgentDispatchRequest(body) {
|
|
|
5606
6556
|
if (payload.attachments != null && !Array.isArray(payload.attachments)) return "attachments must be an array";
|
|
5607
6557
|
return null;
|
|
5608
6558
|
}
|
|
6559
|
+
function getAgentStateRoots(agent, snapshotRoot) {
|
|
6560
|
+
const profileName = sanitizeProfileName(agent.name || agent.imUserId);
|
|
6561
|
+
const roots = [
|
|
6562
|
+
{
|
|
6563
|
+
kind: "workspace-agent",
|
|
6564
|
+
rootPath: path2.join(snapshotRoot, "agents", agent.imUserId)
|
|
6565
|
+
}
|
|
6566
|
+
];
|
|
6567
|
+
if (agent.adapterName === "hermes" || agent.adapterName === "claude-code") {
|
|
6568
|
+
roots.unshift({
|
|
6569
|
+
kind: "hermes-profile",
|
|
6570
|
+
rootPath: path2.join(process.env.HERMES_HOME || path2.join(homedir4(), ".hermes"), "profiles", profileName)
|
|
6571
|
+
});
|
|
6572
|
+
}
|
|
6573
|
+
if (agent.adapterName === "openclaw") {
|
|
6574
|
+
roots.unshift({
|
|
6575
|
+
kind: "openclaw-profile",
|
|
6576
|
+
rootPath: path2.join(process.env.OPENCLAW_HOME || path2.join(homedir4(), ".openclaw"), "profiles", profileName)
|
|
6577
|
+
});
|
|
6578
|
+
}
|
|
6579
|
+
return roots;
|
|
6580
|
+
}
|
|
6581
|
+
function sanitizeProfileName(value) {
|
|
6582
|
+
return value.replace(/[\\/]/g, "_").trim() || "default";
|
|
6583
|
+
}
|
|
6584
|
+
async function statOptional(rootPath) {
|
|
6585
|
+
try {
|
|
6586
|
+
return await fs.stat(rootPath);
|
|
6587
|
+
} catch (err) {
|
|
6588
|
+
if (err.code === "ENOENT") return null;
|
|
6589
|
+
throw err;
|
|
6590
|
+
}
|
|
6591
|
+
}
|
|
5609
6592
|
async function walkAndDigest(root, current) {
|
|
5610
6593
|
const out = [];
|
|
5611
6594
|
let entries;
|
|
@@ -5649,7 +6632,7 @@ import { EventEmitter as EventEmitter3 } from "events";
|
|
|
5649
6632
|
import { createRequire as createRequire2 } from "module";
|
|
5650
6633
|
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
5651
6634
|
import { mkdir } from "fs/promises";
|
|
5652
|
-
import { homedir as
|
|
6635
|
+
import { homedir as homedir7, platform } from "os";
|
|
5653
6636
|
import { join as join14 } from "path";
|
|
5654
6637
|
|
|
5655
6638
|
// src/adapters/claude-code/index.ts
|
|
@@ -7747,7 +8730,7 @@ function parsePrismerUri(uri) {
|
|
|
7747
8730
|
}
|
|
7748
8731
|
|
|
7749
8732
|
// src/daemon/asset/metadata-index.ts
|
|
7750
|
-
import { existsSync as existsSync7, mkdirSync as
|
|
8733
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
7751
8734
|
import { join as join11 } from "path";
|
|
7752
8735
|
var DEFAULT_LIMIT = 8;
|
|
7753
8736
|
var PULL_PAGE_SIZE = 500;
|
|
@@ -7777,7 +8760,7 @@ var AssetMetadataIndex = class {
|
|
|
7777
8760
|
this.cloud = opts.cloud;
|
|
7778
8761
|
this.workspaceId = opts.workspaceId;
|
|
7779
8762
|
if (!existsSync7(opts.workspaceStateDir)) {
|
|
7780
|
-
|
|
8763
|
+
mkdirSync8(opts.workspaceStateDir, { recursive: true });
|
|
7781
8764
|
}
|
|
7782
8765
|
this.cursorPath = join11(opts.workspaceStateDir, "asset-metadata-cursor.json");
|
|
7783
8766
|
}
|
|
@@ -7798,7 +8781,7 @@ var AssetMetadataIndex = class {
|
|
|
7798
8781
|
cursor,
|
|
7799
8782
|
writtenAt: Date.now()
|
|
7800
8783
|
};
|
|
7801
|
-
|
|
8784
|
+
writeFileSync6(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
7802
8785
|
}
|
|
7803
8786
|
/**
|
|
7804
8787
|
* Pull incremental asset metadata changes since the persisted cursor and
|
|
@@ -8125,7 +9108,7 @@ async function readJson3(req) {
|
|
|
8125
9108
|
|
|
8126
9109
|
// src/daemon/asset/origin/drop-folder.ts
|
|
8127
9110
|
import { promises as fs3 } from "fs";
|
|
8128
|
-
import { homedir as
|
|
9111
|
+
import { homedir as homedir6 } from "os";
|
|
8129
9112
|
import * as path5 from "path";
|
|
8130
9113
|
var DropFolderAdapter = class {
|
|
8131
9114
|
kind = "drop-folder";
|
|
@@ -8133,7 +9116,7 @@ var DropFolderAdapter = class {
|
|
|
8133
9116
|
rootDir;
|
|
8134
9117
|
constructor(opts = {}) {
|
|
8135
9118
|
this.workspaceDir = opts.workspaceDir;
|
|
8136
|
-
this.rootDir = opts.rootDir ?? path5.join(opts.homeDir ??
|
|
9119
|
+
this.rootDir = opts.rootDir ?? path5.join(opts.homeDir ?? homedir6(), ".prismer");
|
|
8137
9120
|
}
|
|
8138
9121
|
/**
|
|
8139
9122
|
* Long-lived async iterable wrapping `scanOnce` on a 1s polling interval.
|
|
@@ -8143,7 +9126,7 @@ var DropFolderAdapter = class {
|
|
|
8143
9126
|
while (true) {
|
|
8144
9127
|
const batch = await this.scanOnce(workspaceId);
|
|
8145
9128
|
for (const obs of batch) yield obs;
|
|
8146
|
-
await
|
|
9129
|
+
await sleep2(1e3);
|
|
8147
9130
|
}
|
|
8148
9131
|
}
|
|
8149
9132
|
/**
|
|
@@ -8292,7 +9275,7 @@ function mimeFromExtension(p) {
|
|
|
8292
9275
|
const ext = path5.extname(p).toLowerCase();
|
|
8293
9276
|
return MIME_BY_EXT[ext] ?? null;
|
|
8294
9277
|
}
|
|
8295
|
-
function
|
|
9278
|
+
function sleep2(ms) {
|
|
8296
9279
|
return new Promise((r) => setTimeout(r, ms));
|
|
8297
9280
|
}
|
|
8298
9281
|
|
|
@@ -8997,15 +9980,126 @@ function normalizeMime(mime) {
|
|
|
8997
9980
|
const normalized = mime.toLowerCase().split(";")[0]?.trim();
|
|
8998
9981
|
return normalized || null;
|
|
8999
9982
|
}
|
|
9000
|
-
function extensionOf(filename) {
|
|
9001
|
-
const name = filename.toLowerCase().trim();
|
|
9002
|
-
const idx = name.lastIndexOf(".");
|
|
9003
|
-
return idx >= 0 ? name.slice(idx) : "";
|
|
9983
|
+
function extensionOf(filename) {
|
|
9984
|
+
const name = filename.toLowerCase().trim();
|
|
9985
|
+
const idx = name.lastIndexOf(".");
|
|
9986
|
+
return idx >= 0 ? name.slice(idx) : "";
|
|
9987
|
+
}
|
|
9988
|
+
|
|
9989
|
+
// src/daemon/asset/magic-bytes.ts
|
|
9990
|
+
var PDF_SIG = Buffer.from("%PDF-", "ascii");
|
|
9991
|
+
var PNG_SIG = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
9992
|
+
var JPEG_SIG = Buffer.from([255, 216, 255]);
|
|
9993
|
+
var GIF87 = Buffer.from("GIF87a", "ascii");
|
|
9994
|
+
var GIF89 = Buffer.from("GIF89a", "ascii");
|
|
9995
|
+
var ZIP_LOCAL = Buffer.from([80, 75, 3, 4]);
|
|
9996
|
+
var ZIP_EMPTY = Buffer.from([80, 75, 5, 6]);
|
|
9997
|
+
var ZIP_SPANNED = Buffer.from([80, 75, 7, 8]);
|
|
9998
|
+
function detectMagicBytes(bytes) {
|
|
9999
|
+
const head = bytes.subarray(0, Math.min(bytes.length, 4096));
|
|
10000
|
+
if (head.length === 0) return { detected: null, mime: null };
|
|
10001
|
+
if (head.length >= PDF_SIG.length && head.subarray(0, PDF_SIG.length).equals(PDF_SIG)) {
|
|
10002
|
+
return { detected: "pdf", mime: "application/pdf" };
|
|
10003
|
+
}
|
|
10004
|
+
if (head.length >= PNG_SIG.length && head.subarray(0, PNG_SIG.length).equals(PNG_SIG)) {
|
|
10005
|
+
return { detected: "png", mime: "image/png" };
|
|
10006
|
+
}
|
|
10007
|
+
if (head.length >= JPEG_SIG.length && head.subarray(0, JPEG_SIG.length).equals(JPEG_SIG)) {
|
|
10008
|
+
return { detected: "jpeg", mime: "image/jpeg" };
|
|
10009
|
+
}
|
|
10010
|
+
if (head.length >= GIF87.length && head.subarray(0, GIF87.length).equals(GIF87) || head.length >= GIF89.length && head.subarray(0, GIF89.length).equals(GIF89)) {
|
|
10011
|
+
return { detected: "gif", mime: "image/gif" };
|
|
10012
|
+
}
|
|
10013
|
+
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))) {
|
|
10014
|
+
return { detected: "zip", mime: "application/zip" };
|
|
10015
|
+
}
|
|
10016
|
+
const text = head.toString("utf8");
|
|
10017
|
+
const trimmed = text.trimStart();
|
|
10018
|
+
if (/^<\?xml[^>]*>\s*<svg[\s>]/i.test(trimmed) || /^<svg[\s>]/i.test(trimmed)) {
|
|
10019
|
+
return { detected: "svg", mime: "image/svg+xml" };
|
|
10020
|
+
}
|
|
10021
|
+
if (/^<(!doctype html|html[\s>])/i.test(trimmed)) {
|
|
10022
|
+
return { detected: "html", mime: "text/html" };
|
|
10023
|
+
}
|
|
10024
|
+
if (/^[{[]/.test(trimmed)) {
|
|
10025
|
+
return { detected: "json", mime: "application/json" };
|
|
10026
|
+
}
|
|
10027
|
+
if (looksLikePrintableText(head)) {
|
|
10028
|
+
return { detected: "markdown-or-text", mime: "text/plain" };
|
|
10029
|
+
}
|
|
10030
|
+
return { detected: null, mime: null };
|
|
10031
|
+
}
|
|
10032
|
+
var ZIP_DERIVED_MIMES = /* @__PURE__ */ new Set([
|
|
10033
|
+
"application/zip",
|
|
10034
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
10035
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
10036
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
10037
|
+
"application/vnd.oasis.opendocument.text",
|
|
10038
|
+
"application/vnd.oasis.opendocument.spreadsheet",
|
|
10039
|
+
"application/vnd.oasis.opendocument.presentation",
|
|
10040
|
+
"application/epub+zip",
|
|
10041
|
+
"application/java-archive"
|
|
10042
|
+
]);
|
|
10043
|
+
function mimeMismatchReason(detected, declared) {
|
|
10044
|
+
if (detected.detected === null) return null;
|
|
10045
|
+
const dec = (declared ?? "").toLowerCase().split(";")[0]?.trim() ?? "";
|
|
10046
|
+
if (!dec || dec === "application/octet-stream") return null;
|
|
10047
|
+
switch (detected.detected) {
|
|
10048
|
+
case "pdf":
|
|
10049
|
+
return dec === "application/pdf" ? null : `declared ${dec} but content is application/pdf`;
|
|
10050
|
+
case "png":
|
|
10051
|
+
return dec === "image/png" ? null : `declared ${dec} but content is image/png`;
|
|
10052
|
+
case "jpeg":
|
|
10053
|
+
return dec === "image/jpeg" || dec === "image/jpg" ? null : `declared ${dec} but content is image/jpeg`;
|
|
10054
|
+
case "gif":
|
|
10055
|
+
return dec === "image/gif" ? null : `declared ${dec} but content is image/gif`;
|
|
10056
|
+
case "zip":
|
|
10057
|
+
if (ZIP_DERIVED_MIMES.has(dec)) return null;
|
|
10058
|
+
return `declared ${dec} but content is a ZIP-container (application/zip family)`;
|
|
10059
|
+
case "svg":
|
|
10060
|
+
if (dec === "image/svg+xml" || dec.startsWith("text/")) return null;
|
|
10061
|
+
return `declared ${dec} but content is image/svg+xml`;
|
|
10062
|
+
case "html":
|
|
10063
|
+
if (dec === "text/html" || dec === "application/xhtml+xml" || dec.startsWith("text/")) return null;
|
|
10064
|
+
if (dec === "application/pdf" || dec.startsWith("image/") || dec === "application/zip") {
|
|
10065
|
+
return `declared ${dec} but content is text/html`;
|
|
10066
|
+
}
|
|
10067
|
+
return null;
|
|
10068
|
+
case "json":
|
|
10069
|
+
if (dec === "application/json" || dec.startsWith("text/")) return null;
|
|
10070
|
+
if (dec === "application/pdf" || dec.startsWith("image/") || dec === "application/zip") {
|
|
10071
|
+
return `declared ${dec} but content is application/json`;
|
|
10072
|
+
}
|
|
10073
|
+
return null;
|
|
10074
|
+
case "markdown-or-text":
|
|
10075
|
+
if (dec === "application/pdf" || dec.startsWith("image/") || dec === "application/zip") {
|
|
10076
|
+
return `declared ${dec} but content is text (not a real ${dec} payload)`;
|
|
10077
|
+
}
|
|
10078
|
+
return null;
|
|
10079
|
+
default:
|
|
10080
|
+
return null;
|
|
10081
|
+
}
|
|
10082
|
+
}
|
|
10083
|
+
function looksLikePrintableText(buf) {
|
|
10084
|
+
if (buf.length === 0) return false;
|
|
10085
|
+
let printable = 0;
|
|
10086
|
+
let total = 0;
|
|
10087
|
+
for (let i = 0; i < buf.length; i++) {
|
|
10088
|
+
const b = buf[i];
|
|
10089
|
+
if (b === 0) return false;
|
|
10090
|
+
total++;
|
|
10091
|
+
if (b >= 32 && b <= 126 || b === 9 || b === 10 || b === 13 || // utf-8 lead bytes
|
|
10092
|
+
b >= 128) {
|
|
10093
|
+
printable++;
|
|
10094
|
+
}
|
|
10095
|
+
}
|
|
10096
|
+
return total > 0 && printable / total >= 0.95;
|
|
9004
10097
|
}
|
|
9005
10098
|
|
|
9006
10099
|
// src/daemon/outbox-watcher.ts
|
|
9007
10100
|
var DEFAULT_INTERVAL_MS = 2e3;
|
|
9008
10101
|
var RESERVED_SUBDIR = "_uploaded";
|
|
10102
|
+
var REJECTED_SUBDIR = "_rejected";
|
|
9009
10103
|
var OutboxWatcher = class {
|
|
9010
10104
|
constructor(opts) {
|
|
9011
10105
|
this.opts = opts;
|
|
@@ -9180,6 +10274,7 @@ var OutboxWatcher = class {
|
|
|
9180
10274
|
for (const name of entries) {
|
|
9181
10275
|
if (name.startsWith(".")) continue;
|
|
9182
10276
|
if (name === RESERVED_SUBDIR) continue;
|
|
10277
|
+
if (name === REJECTED_SUBDIR) continue;
|
|
9183
10278
|
const full = path7.join(dir, name);
|
|
9184
10279
|
let st;
|
|
9185
10280
|
try {
|
|
@@ -9194,10 +10289,77 @@ var OutboxWatcher = class {
|
|
|
9194
10289
|
await this.upload(full, st, kind, task);
|
|
9195
10290
|
this.uploaded.add(key);
|
|
9196
10291
|
} catch (err) {
|
|
9197
|
-
|
|
10292
|
+
const msg = err.message;
|
|
10293
|
+
this.log("warn", `upload ${full} failed: ${msg}`);
|
|
10294
|
+
if (/\bMIME_MISMATCH\b/i.test(msg) || /\bmime mismatch\b/i.test(msg)) {
|
|
10295
|
+
await this.quarantine(dir, full).catch(
|
|
10296
|
+
(moveErr) => this.log("warn", `quarantine ${full} failed: ${moveErr.message}`)
|
|
10297
|
+
);
|
|
10298
|
+
this.uploaded.add(key);
|
|
10299
|
+
await this.reportMimeMismatchToCloud(task, full, msg).catch(
|
|
10300
|
+
(reportErr) => this.log("warn", `report MIME_MISMATCH failed: ${reportErr.message}`)
|
|
10301
|
+
);
|
|
10302
|
+
}
|
|
9198
10303
|
}
|
|
9199
10304
|
}
|
|
9200
10305
|
}
|
|
10306
|
+
/**
|
|
10307
|
+
* F2B — report an OUTBOX_MIME_MISMATCH task event to cloud so the task log
|
|
10308
|
+
* gains an auditable record of the rejection. Without this, the failure is
|
|
10309
|
+
* silent (file quarantined locally, agent unaware) and the next dispatch
|
|
10310
|
+
* loops on the same bad strategy.
|
|
10311
|
+
*
|
|
10312
|
+
* Best-effort: every catch site swallows errors. We never want this
|
|
10313
|
+
* upstream call to block local quarantine or trigger a retry loop, because
|
|
10314
|
+
* the reject decision is already final by the time we get here.
|
|
10315
|
+
*
|
|
10316
|
+
* Endpoint contract: POST /api/im/tasks/:id/event
|
|
10317
|
+
* { code: 'OUTBOX_MIME_MISMATCH', payload: { file, daemonError }, message }
|
|
10318
|
+
* Only callable when `task?.taskId` is known — pre-dispatch outbox files
|
|
10319
|
+
* (no active task) are quarantined silently as before.
|
|
10320
|
+
*/
|
|
10321
|
+
async reportMimeMismatchToCloud(task, filePath, daemonErrorMessage) {
|
|
10322
|
+
if (!task?.taskId) return;
|
|
10323
|
+
const fileName = path7.basename(filePath);
|
|
10324
|
+
const reasonMatch = daemonErrorMessage.match(/MIME_MISMATCH[^:]*:\s*(.+)$/i);
|
|
10325
|
+
const reason = reasonMatch?.[1] ? reasonMatch[1].trim() : daemonErrorMessage;
|
|
10326
|
+
const body = {
|
|
10327
|
+
code: "OUTBOX_MIME_MISMATCH",
|
|
10328
|
+
payload: {
|
|
10329
|
+
file: fileName,
|
|
10330
|
+
reason,
|
|
10331
|
+
daemonError: daemonErrorMessage
|
|
10332
|
+
},
|
|
10333
|
+
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.`
|
|
10334
|
+
};
|
|
10335
|
+
const res = await this.opts.cloud.request("POST", `/api/im/tasks/${encodeURIComponent(task.taskId)}/event`, {
|
|
10336
|
+
body
|
|
10337
|
+
});
|
|
10338
|
+
if (!res.ok) {
|
|
10339
|
+
throw new Error(`cloud rejected event: ${res.status} ${res.error?.code ?? ""} ${res.error?.message ?? ""}`);
|
|
10340
|
+
}
|
|
10341
|
+
}
|
|
10342
|
+
/**
|
|
10343
|
+
* Move a file under `<dir>/_rejected/` so it stops being a scan candidate
|
|
10344
|
+
* (the loop skips the reserved subdir) and operators can find the
|
|
10345
|
+
* offending artifact. Filename collision is rare per task; on collision
|
|
10346
|
+
* we append a millisecond suffix to keep the original observable.
|
|
10347
|
+
*/
|
|
10348
|
+
async quarantine(dir, full) {
|
|
10349
|
+
const rejectedDir = path7.join(dir, REJECTED_SUBDIR);
|
|
10350
|
+
await fs4.mkdir(rejectedDir, { recursive: true });
|
|
10351
|
+
const base = path7.basename(full);
|
|
10352
|
+
let dest = path7.join(rejectedDir, base);
|
|
10353
|
+
try {
|
|
10354
|
+
await fs4.access(dest);
|
|
10355
|
+
const ext = path7.extname(base);
|
|
10356
|
+
const stem = base.slice(0, base.length - ext.length);
|
|
10357
|
+
dest = path7.join(rejectedDir, `${stem}.${Date.now()}${ext}`);
|
|
10358
|
+
} catch {
|
|
10359
|
+
}
|
|
10360
|
+
await fs4.rename(full, dest);
|
|
10361
|
+
this.log("warn", `quarantined ${full} \u2192 ${dest}`);
|
|
10362
|
+
}
|
|
9201
10363
|
async upload(filePath, st, kind, task) {
|
|
9202
10364
|
const wsId = this.opts.workspaceId();
|
|
9203
10365
|
if (!wsId) {
|
|
@@ -9215,6 +10377,11 @@ var OutboxWatcher = class {
|
|
|
9215
10377
|
if (!policy.ok) {
|
|
9216
10378
|
throw new Error(`agent output rejected: ${policy.reason}`);
|
|
9217
10379
|
}
|
|
10380
|
+
const detection = detectMagicBytes(bytes);
|
|
10381
|
+
const reason = mimeMismatchReason(detection, policy.mime);
|
|
10382
|
+
if (reason) {
|
|
10383
|
+
throw new Error(`MIME_MISMATCH for ${fileName}: ${reason}`);
|
|
10384
|
+
}
|
|
9218
10385
|
const metadata = {
|
|
9219
10386
|
taskId: task.taskId,
|
|
9220
10387
|
...task.adapter ? { adapter: task.adapter } : {}
|
|
@@ -9418,6 +10585,352 @@ ${input.stderr}` : null
|
|
|
9418
10585
|
|
|
9419
10586
|
// src/daemon/runner.ts
|
|
9420
10587
|
init_skill_sync();
|
|
10588
|
+
|
|
10589
|
+
// src/daemon/pending-reply-cache.ts
|
|
10590
|
+
function fromRaw(r) {
|
|
10591
|
+
return {
|
|
10592
|
+
idempotencyKey: r.idempotency_key,
|
|
10593
|
+
taskId: r.task_id,
|
|
10594
|
+
replyId: r.reply_id,
|
|
10595
|
+
status: r.status,
|
|
10596
|
+
payloadJson: r.payload_json,
|
|
10597
|
+
createdAt: r.created_at,
|
|
10598
|
+
preparedAt: r.prepared_at,
|
|
10599
|
+
lastAttemptAt: r.last_attempt_at,
|
|
10600
|
+
attemptCount: r.attempt_count,
|
|
10601
|
+
lastError: r.last_error
|
|
10602
|
+
};
|
|
10603
|
+
}
|
|
10604
|
+
function createPendingReplyCache(db) {
|
|
10605
|
+
const insert = db.prepare(
|
|
10606
|
+
`INSERT OR REPLACE INTO pending_dispatch_replies
|
|
10607
|
+
(idempotency_key, task_id, reply_id, status, payload_json,
|
|
10608
|
+
prepared_at, created_at, last_attempt_at, attempt_count, last_error)
|
|
10609
|
+
VALUES (?, ?, NULL, 'pending', ?, NULL, ?, NULL, 0, NULL)`
|
|
10610
|
+
);
|
|
10611
|
+
const markPreparedStmt = db.prepare(
|
|
10612
|
+
`UPDATE pending_dispatch_replies
|
|
10613
|
+
SET reply_id = ?, status = 'prepared', prepared_at = ?,
|
|
10614
|
+
last_attempt_at = ?, attempt_count = attempt_count + 1,
|
|
10615
|
+
last_error = NULL
|
|
10616
|
+
WHERE idempotency_key = ?`
|
|
10617
|
+
);
|
|
10618
|
+
const markAttemptStmt = db.prepare(
|
|
10619
|
+
`UPDATE pending_dispatch_replies
|
|
10620
|
+
SET last_attempt_at = ?, attempt_count = attempt_count + 1,
|
|
10621
|
+
last_error = ?
|
|
10622
|
+
WHERE idempotency_key = ?`
|
|
10623
|
+
);
|
|
10624
|
+
const clearStmt = db.prepare(
|
|
10625
|
+
`DELETE FROM pending_dispatch_replies WHERE idempotency_key = ?`
|
|
10626
|
+
);
|
|
10627
|
+
const listRecoveryStmt = db.prepare(
|
|
10628
|
+
`SELECT * FROM pending_dispatch_replies
|
|
10629
|
+
WHERE status IN ('pending', 'prepared')
|
|
10630
|
+
ORDER BY created_at ASC`
|
|
10631
|
+
);
|
|
10632
|
+
const countStmt = db.prepare(
|
|
10633
|
+
`SELECT status, COUNT(*) as cnt FROM pending_dispatch_replies
|
|
10634
|
+
WHERE status IN ('pending','prepared') GROUP BY status`
|
|
10635
|
+
);
|
|
10636
|
+
const findStmt = db.prepare(
|
|
10637
|
+
`SELECT * FROM pending_dispatch_replies WHERE idempotency_key = ?`
|
|
10638
|
+
);
|
|
10639
|
+
return {
|
|
10640
|
+
savePending(idempotencyKey, taskId, payloadJson, now = Date.now()) {
|
|
10641
|
+
insert.run(idempotencyKey, taskId, payloadJson, now);
|
|
10642
|
+
},
|
|
10643
|
+
markPrepared(idempotencyKey, replyId, now = Date.now()) {
|
|
10644
|
+
markPreparedStmt.run(replyId, now, now, idempotencyKey);
|
|
10645
|
+
},
|
|
10646
|
+
markAttempt(idempotencyKey, now = Date.now(), error) {
|
|
10647
|
+
markAttemptStmt.run(now, error ?? null, idempotencyKey);
|
|
10648
|
+
},
|
|
10649
|
+
clearPending(idempotencyKey) {
|
|
10650
|
+
clearStmt.run(idempotencyKey);
|
|
10651
|
+
},
|
|
10652
|
+
listForRecovery() {
|
|
10653
|
+
const rows = listRecoveryStmt.all();
|
|
10654
|
+
return rows.map(fromRaw);
|
|
10655
|
+
},
|
|
10656
|
+
countByStatus() {
|
|
10657
|
+
const rows = countStmt.all();
|
|
10658
|
+
const result = { pending: 0, prepared: 0 };
|
|
10659
|
+
for (const r of rows) {
|
|
10660
|
+
if (r.status === "pending") result.pending = r.cnt;
|
|
10661
|
+
else if (r.status === "prepared") result.prepared = r.cnt;
|
|
10662
|
+
}
|
|
10663
|
+
return result;
|
|
10664
|
+
},
|
|
10665
|
+
findByIdempotencyKey(idempotencyKey) {
|
|
10666
|
+
const row = findStmt.get(idempotencyKey);
|
|
10667
|
+
return row ? fromRaw(row) : null;
|
|
10668
|
+
}
|
|
10669
|
+
};
|
|
10670
|
+
}
|
|
10671
|
+
function generateIdempotencyKey() {
|
|
10672
|
+
const c = globalThis.crypto;
|
|
10673
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
10674
|
+
const bytes = new Uint8Array(16);
|
|
10675
|
+
const cryptoNode = globalThis.crypto;
|
|
10676
|
+
if (cryptoNode?.getRandomValues) {
|
|
10677
|
+
cryptoNode.getRandomValues(bytes);
|
|
10678
|
+
} else {
|
|
10679
|
+
for (let i = 0; i < 16; i += 1) bytes[i] = Math.floor(Math.random() * 256);
|
|
10680
|
+
}
|
|
10681
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
10682
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
10683
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
10684
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
10685
|
+
}
|
|
10686
|
+
|
|
10687
|
+
// src/daemon/dispatch-reply-transport.ts
|
|
10688
|
+
var DispatchReplyAbortedError = class extends Error {
|
|
10689
|
+
constructor(replyId) {
|
|
10690
|
+
super(`dispatch reply ${replyId} aborted by server reaper (likely >1h since prepare)`);
|
|
10691
|
+
this.replyId = replyId;
|
|
10692
|
+
this.name = "DispatchReplyAbortedError";
|
|
10693
|
+
}
|
|
10694
|
+
replyId;
|
|
10695
|
+
code = "DISPATCH_REPLY_INVALID_STATE";
|
|
10696
|
+
};
|
|
10697
|
+
function defaultLog2(line) {
|
|
10698
|
+
process.stderr.write(`${line}
|
|
10699
|
+
`);
|
|
10700
|
+
}
|
|
10701
|
+
async function sendDispatchReplyTwoPhase(args) {
|
|
10702
|
+
const { taskId, payload, cloud, cache } = args;
|
|
10703
|
+
const log8 = args.log ?? defaultLog2;
|
|
10704
|
+
const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
|
|
10705
|
+
const payloadJson = JSON.stringify({ ...payload, _taskId: taskId, _idempotencyKey: idempotencyKey });
|
|
10706
|
+
cache.savePending(idempotencyKey, taskId, payloadJson);
|
|
10707
|
+
const prepareRes = await cloud.request("POST", `/api/im/dispatch/${encodeURIComponent(taskId)}/prepare`, {
|
|
10708
|
+
body: { ...payload, idempotencyKey },
|
|
10709
|
+
timeoutMs: 3e4
|
|
10710
|
+
});
|
|
10711
|
+
if (!prepareRes.ok || !prepareRes.data) {
|
|
10712
|
+
const errCode = prepareRes.error?.code ?? "unknown";
|
|
10713
|
+
const errMsg = prepareRes.error?.message ?? `prepare failed (status=${prepareRes.status})`;
|
|
10714
|
+
cache.markAttempt(idempotencyKey, Date.now(), `prepare: ${errCode}: ${errMsg}`);
|
|
10715
|
+
throw new Error(`dispatch prepare failed: ${errCode}: ${errMsg}`);
|
|
10716
|
+
}
|
|
10717
|
+
const envelope2 = prepareRes.data;
|
|
10718
|
+
const inner = "data" in envelope2 && envelope2.data ? envelope2.data : envelope2;
|
|
10719
|
+
if (!inner || typeof inner.replyId !== "string") {
|
|
10720
|
+
cache.markAttempt(idempotencyKey, Date.now(), "prepare: missing replyId in response");
|
|
10721
|
+
throw new Error("dispatch prepare returned no replyId");
|
|
10722
|
+
}
|
|
10723
|
+
if ("ok" in envelope2 && envelope2.ok === false) {
|
|
10724
|
+
const errCode = envelope2.error?.code ?? "prepare_failed";
|
|
10725
|
+
const errMsg = envelope2.error?.message ?? "prepare ok=false";
|
|
10726
|
+
cache.markAttempt(idempotencyKey, Date.now(), `prepare: ${errCode}: ${errMsg}`);
|
|
10727
|
+
throw new Error(`dispatch prepare failed: ${errCode}: ${errMsg}`);
|
|
10728
|
+
}
|
|
10729
|
+
cache.markPrepared(idempotencyKey, inner.replyId);
|
|
10730
|
+
log8(
|
|
10731
|
+
`[daemon] dispatch.prepare task=${taskId} replyId=${inner.replyId} status=${inner.status} idem=${idempotencyKey}`
|
|
10732
|
+
);
|
|
10733
|
+
if (inner.status === "committed") {
|
|
10734
|
+
cache.clearPending(idempotencyKey);
|
|
10735
|
+
return {
|
|
10736
|
+
taskId,
|
|
10737
|
+
replyId: inner.replyId,
|
|
10738
|
+
status: "committed",
|
|
10739
|
+
alreadyCommitted: true
|
|
10740
|
+
};
|
|
10741
|
+
}
|
|
10742
|
+
try {
|
|
10743
|
+
const result = await commitDispatchReply({ taskId, replyId: inner.replyId, cloud, log: log8 });
|
|
10744
|
+
cache.clearPending(idempotencyKey);
|
|
10745
|
+
return { taskId, replyId: inner.replyId, ...result };
|
|
10746
|
+
} catch (err) {
|
|
10747
|
+
if (err instanceof DispatchReplyAbortedError) {
|
|
10748
|
+
cache.clearPending(idempotencyKey);
|
|
10749
|
+
log8(`[daemon] dispatch.commit aborted task=${taskId} replyId=${inner.replyId} \u2014 dispatch lost`);
|
|
10750
|
+
return {
|
|
10751
|
+
taskId,
|
|
10752
|
+
replyId: inner.replyId,
|
|
10753
|
+
status: "aborted",
|
|
10754
|
+
alreadyCommitted: false
|
|
10755
|
+
};
|
|
10756
|
+
}
|
|
10757
|
+
cache.markAttempt(idempotencyKey, Date.now(), `commit: ${err.message}`);
|
|
10758
|
+
throw err;
|
|
10759
|
+
}
|
|
10760
|
+
}
|
|
10761
|
+
async function commitDispatchReply(args) {
|
|
10762
|
+
const log8 = args.log ?? defaultLog2;
|
|
10763
|
+
const res = await args.cloud.request("POST", `/api/im/dispatch/${encodeURIComponent(args.taskId)}/commit`, {
|
|
10764
|
+
body: { replyId: args.replyId },
|
|
10765
|
+
timeoutMs: 3e4
|
|
10766
|
+
});
|
|
10767
|
+
if (!res.ok || !res.data) {
|
|
10768
|
+
const code = res.error?.code ?? "unknown";
|
|
10769
|
+
const msg = res.error?.message ?? `commit failed (status=${res.status})`;
|
|
10770
|
+
if (code === "DISPATCH_REPLY_INVALID_STATE") {
|
|
10771
|
+
throw new DispatchReplyAbortedError(args.replyId);
|
|
10772
|
+
}
|
|
10773
|
+
throw new Error(`dispatch commit failed: ${code}: ${msg}`);
|
|
10774
|
+
}
|
|
10775
|
+
const env = res.data;
|
|
10776
|
+
const inner = "data" in env && env.data ? env.data : env;
|
|
10777
|
+
if ("ok" in env && env.ok === false) {
|
|
10778
|
+
const code = env.error?.code ?? "commit_failed";
|
|
10779
|
+
if (code === "DISPATCH_REPLY_INVALID_STATE") {
|
|
10780
|
+
throw new DispatchReplyAbortedError(args.replyId);
|
|
10781
|
+
}
|
|
10782
|
+
throw new Error(`dispatch commit failed: ${code}: ${env.error?.message ?? "commit ok=false"}`);
|
|
10783
|
+
}
|
|
10784
|
+
log8(
|
|
10785
|
+
`[daemon] dispatch.commit task=${args.taskId} replyId=${args.replyId} alreadyCommitted=${inner.alreadyCommitted ?? false}`
|
|
10786
|
+
);
|
|
10787
|
+
return {
|
|
10788
|
+
status: "committed",
|
|
10789
|
+
alreadyCommitted: Boolean(inner.alreadyCommitted),
|
|
10790
|
+
...inner.messageId ? { messageId: inner.messageId } : {}
|
|
10791
|
+
};
|
|
10792
|
+
}
|
|
10793
|
+
async function recoverPendingReplies(deps) {
|
|
10794
|
+
const log8 = deps.log ?? defaultLog2;
|
|
10795
|
+
const rows = deps.cache.listForRecovery();
|
|
10796
|
+
if (rows.length === 0) {
|
|
10797
|
+
return { attempted: 0, committed: 0, aborted: 0, failed: 0 };
|
|
10798
|
+
}
|
|
10799
|
+
log8(`[daemon] dispatch.recovery: ${rows.length} pending row(s) to replay`);
|
|
10800
|
+
let committed = 0;
|
|
10801
|
+
let aborted = 0;
|
|
10802
|
+
let failed = 0;
|
|
10803
|
+
for (const row of rows) {
|
|
10804
|
+
try {
|
|
10805
|
+
const result = await replayRow(row, deps);
|
|
10806
|
+
if (result.status === "committed") committed += 1;
|
|
10807
|
+
else if (result.status === "aborted") aborted += 1;
|
|
10808
|
+
} catch (err) {
|
|
10809
|
+
failed += 1;
|
|
10810
|
+
log8(
|
|
10811
|
+
`[daemon] dispatch.recovery FAIL idem=${row.idempotencyKey} task=${row.taskId}: ${err.message}`
|
|
10812
|
+
);
|
|
10813
|
+
}
|
|
10814
|
+
}
|
|
10815
|
+
log8(
|
|
10816
|
+
`[daemon] dispatch.recovery done attempted=${rows.length} committed=${committed} aborted=${aborted} failed=${failed}`
|
|
10817
|
+
);
|
|
10818
|
+
return { attempted: rows.length, committed, aborted, failed };
|
|
10819
|
+
}
|
|
10820
|
+
async function replayRow(row, deps) {
|
|
10821
|
+
const log8 = deps.log ?? defaultLog2;
|
|
10822
|
+
if (row.status === "prepared" && row.replyId) {
|
|
10823
|
+
try {
|
|
10824
|
+
const result2 = await commitDispatchReply({
|
|
10825
|
+
taskId: row.taskId,
|
|
10826
|
+
replyId: row.replyId,
|
|
10827
|
+
cloud: deps.cloud,
|
|
10828
|
+
log: log8
|
|
10829
|
+
});
|
|
10830
|
+
deps.cache.clearPending(row.idempotencyKey);
|
|
10831
|
+
return { status: result2.status };
|
|
10832
|
+
} catch (err) {
|
|
10833
|
+
if (err instanceof DispatchReplyAbortedError) {
|
|
10834
|
+
deps.cache.clearPending(row.idempotencyKey);
|
|
10835
|
+
return { status: "aborted" };
|
|
10836
|
+
}
|
|
10837
|
+
throw err;
|
|
10838
|
+
}
|
|
10839
|
+
}
|
|
10840
|
+
let parsed;
|
|
10841
|
+
try {
|
|
10842
|
+
parsed = JSON.parse(row.payloadJson);
|
|
10843
|
+
} catch (err) {
|
|
10844
|
+
deps.cache.clearPending(row.idempotencyKey);
|
|
10845
|
+
throw new Error(`pending row ${row.idempotencyKey} has unparseable payload: ${err.message}`);
|
|
10846
|
+
}
|
|
10847
|
+
const { _taskId, _idempotencyKey, ...payload } = parsed;
|
|
10848
|
+
const result = await sendDispatchReplyTwoPhase({
|
|
10849
|
+
taskId: row.taskId,
|
|
10850
|
+
payload,
|
|
10851
|
+
cloud: deps.cloud,
|
|
10852
|
+
cache: deps.cache,
|
|
10853
|
+
idempotencyKey: row.idempotencyKey,
|
|
10854
|
+
log: deps.log
|
|
10855
|
+
});
|
|
10856
|
+
return { status: result.status };
|
|
10857
|
+
}
|
|
10858
|
+
|
|
10859
|
+
// src/daemon/transport-probe.ts
|
|
10860
|
+
import { networkInterfaces } from "os";
|
|
10861
|
+
function isPrivateAddress(addr) {
|
|
10862
|
+
if (!addr) return true;
|
|
10863
|
+
const h = String(addr).trim().toLowerCase();
|
|
10864
|
+
if (!h) return true;
|
|
10865
|
+
if (h === "localhost" || h === "::1") return true;
|
|
10866
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
|
|
10867
|
+
if (m) {
|
|
10868
|
+
const a = Number(m[1]);
|
|
10869
|
+
const b = Number(m[2]);
|
|
10870
|
+
if (a === 10) return true;
|
|
10871
|
+
if (a === 127) return true;
|
|
10872
|
+
if (a === 169 && b === 254) return true;
|
|
10873
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
10874
|
+
if (a === 192 && b === 168) return true;
|
|
10875
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
10876
|
+
return false;
|
|
10877
|
+
}
|
|
10878
|
+
if (h.endsWith(".local")) return true;
|
|
10879
|
+
if (h.endsWith(".localhost")) return true;
|
|
10880
|
+
return false;
|
|
10881
|
+
}
|
|
10882
|
+
function buildTransportProbe(gatewayUrl) {
|
|
10883
|
+
if (!gatewayUrl) {
|
|
10884
|
+
return { transport: "ws", gatewayUrl: null, gatewayIsPrivate: true };
|
|
10885
|
+
}
|
|
10886
|
+
let host = null;
|
|
10887
|
+
try {
|
|
10888
|
+
host = new URL(gatewayUrl).hostname;
|
|
10889
|
+
} catch {
|
|
10890
|
+
host = null;
|
|
10891
|
+
}
|
|
10892
|
+
const gatewayIsPrivate = isPrivateAddress(host);
|
|
10893
|
+
return {
|
|
10894
|
+
transport: gatewayIsPrivate ? "ws" : "both",
|
|
10895
|
+
gatewayUrl,
|
|
10896
|
+
gatewayIsPrivate
|
|
10897
|
+
};
|
|
10898
|
+
}
|
|
10899
|
+
function pickLocalIPv4() {
|
|
10900
|
+
const nics = networkInterfaces();
|
|
10901
|
+
let firstPrivate = null;
|
|
10902
|
+
for (const list of Object.values(nics)) {
|
|
10903
|
+
if (!list) continue;
|
|
10904
|
+
for (const ni of list) {
|
|
10905
|
+
if (ni.internal || ni.family !== "IPv4") continue;
|
|
10906
|
+
if (!isPrivateAddress(ni.address)) return ni.address;
|
|
10907
|
+
if (!firstPrivate) firstPrivate = ni.address;
|
|
10908
|
+
}
|
|
10909
|
+
}
|
|
10910
|
+
return firstPrivate;
|
|
10911
|
+
}
|
|
10912
|
+
async function reportTransportProbe(cloud, daemonId, probe) {
|
|
10913
|
+
const body = {
|
|
10914
|
+
daemonId,
|
|
10915
|
+
transport: probe.transport,
|
|
10916
|
+
gatewayUrl: probe.gatewayUrl,
|
|
10917
|
+
gatewayIsPrivate: probe.gatewayIsPrivate
|
|
10918
|
+
};
|
|
10919
|
+
const res = await cloud.request("POST", "/api/im/runtime/transport-report", {
|
|
10920
|
+
body,
|
|
10921
|
+
timeoutMs: 5e3
|
|
10922
|
+
});
|
|
10923
|
+
if (!res.ok) {
|
|
10924
|
+
return {
|
|
10925
|
+
ok: false,
|
|
10926
|
+
status: res.status,
|
|
10927
|
+
error: res.error?.message ?? `transport-report failed (${res.status})`
|
|
10928
|
+
};
|
|
10929
|
+
}
|
|
10930
|
+
return { ok: true, status: res.status };
|
|
10931
|
+
}
|
|
10932
|
+
|
|
10933
|
+
// src/daemon/runner.ts
|
|
9421
10934
|
var DEFAULT_LOCAL_PORT = 3210;
|
|
9422
10935
|
var log7 = createLogger("Daemon");
|
|
9423
10936
|
var assetMetaLog = createLogger("AssetMeta");
|
|
@@ -9465,6 +10978,14 @@ var Runner = class extends EventEmitter3 {
|
|
|
9465
10978
|
// F16 (2026-05-20) — periodic skill resync timer (default 10min).
|
|
9466
10979
|
skillResyncTimer;
|
|
9467
10980
|
skillSyncInFlight = false;
|
|
10981
|
+
// Wave-4 E7 — local cache of in-flight two-phase dispatch replies. Survives
|
|
10982
|
+
// daemon crash so cold-start can resume `prepared` rows via idempotent
|
|
10983
|
+
// commit (server enforces (taskId, idempotencyKey) UNIQUE).
|
|
10984
|
+
pendingReplyCache;
|
|
10985
|
+
// One-shot guard so we only recover on the first post-authenticated tick,
|
|
10986
|
+
// not every reconnect (server-side commits are idempotent but the log noise
|
|
10987
|
+
// and DB pressure of re-running on every redeclare is wasteful).
|
|
10988
|
+
pendingReplyRecoveryDone = false;
|
|
9468
10989
|
async start() {
|
|
9469
10990
|
if (this.state !== "idle") throw new Error(`Runner already in state ${this.state}`);
|
|
9470
10991
|
this.state = "starting";
|
|
@@ -9479,6 +11000,7 @@ var Runner = class extends EventEmitter3 {
|
|
|
9479
11000
|
process.env.PRISMER_API_KEY = this.config.api_key;
|
|
9480
11001
|
this.db = openLocalDb(this.paths.localDb);
|
|
9481
11002
|
this.cloud = new CloudClient({ baseUrl: this.config.cloud_api_base, apiKey: this.config.api_key });
|
|
11003
|
+
this.pendingReplyCache = createPendingReplyCache(this.db);
|
|
9482
11004
|
this.assetCache = new AssetCache({
|
|
9483
11005
|
db: this.db,
|
|
9484
11006
|
cloud: this.cloud,
|
|
@@ -10085,6 +11607,19 @@ var Runner = class extends EventEmitter3 {
|
|
|
10085
11607
|
process.stdout.write(`[daemon] ws authenticated, sending agent.host.declare (${this.hostedAgents.size} agents)
|
|
10086
11608
|
`);
|
|
10087
11609
|
this.sendDeclare();
|
|
11610
|
+
if (!this.pendingReplyRecoveryDone) {
|
|
11611
|
+
this.pendingReplyRecoveryDone = true;
|
|
11612
|
+
void recoverPendingReplies({ cloud: this.cloud, cache: this.pendingReplyCache }).then((summary) => {
|
|
11613
|
+
if (summary.attempted === 0) return;
|
|
11614
|
+
process.stdout.write(
|
|
11615
|
+
`[daemon] dispatch.recovery summary attempted=${summary.attempted} committed=${summary.committed} aborted=${summary.aborted} failed=${summary.failed}
|
|
11616
|
+
`
|
|
11617
|
+
);
|
|
11618
|
+
}).catch((err) => {
|
|
11619
|
+
process.stderr.write(`[daemon] dispatch.recovery threw: ${err.message}
|
|
11620
|
+
`);
|
|
11621
|
+
});
|
|
11622
|
+
}
|
|
10088
11623
|
return;
|
|
10089
11624
|
}
|
|
10090
11625
|
this.handleIncoming(msg);
|
|
@@ -10131,12 +11666,90 @@ var Runner = class extends EventEmitter3 {
|
|
|
10131
11666
|
case "asset.changed":
|
|
10132
11667
|
void this.onAssetChanged(msg.payload);
|
|
10133
11668
|
return;
|
|
11669
|
+
// v2.0 §4.8.1 (Wave 4-E4) — webhook reverse-channel RPC. Cloud's
|
|
11670
|
+
// dispatchExternalMention pushes an AgentDispatchRequest plus an
|
|
11671
|
+
// embedded `_rpcId`; the daemon runs the same handler as the
|
|
11672
|
+
// HTTP /dispatch path and echoes the reply back over WS with the
|
|
11673
|
+
// identical `_rpcId` so cloud's WsRpcService can settle the
|
|
11674
|
+
// pending promise.
|
|
11675
|
+
case "webhook.dispatch.request":
|
|
11676
|
+
void this.onWebhookDispatch(msg.payload);
|
|
11677
|
+
return;
|
|
10134
11678
|
default:
|
|
10135
11679
|
this.emit("unknown-message", msg);
|
|
10136
11680
|
}
|
|
10137
11681
|
}
|
|
11682
|
+
/**
|
|
11683
|
+
* v2.0 §4.8.1 — webhook dispatch over WS reverse channel.
|
|
11684
|
+
*
|
|
11685
|
+
* Acks via `webhook.dispatch.reply { _rpcId, ok, acceptedAt }` immediately
|
|
11686
|
+
* after `handleAgentMessageDispatch()` returns its synchronous response
|
|
11687
|
+
* — the actual reply (the agent's reply text/attachments) still flows
|
|
11688
|
+
* through `postMessageDispatchReply()` which posts to
|
|
11689
|
+
* `/api/im/dispatch/reply` after the adapter finishes. That mirrors the
|
|
11690
|
+
* HTTP /dispatch contract: ack first, asynchronous final reply via the
|
|
11691
|
+
* dispatch-reply REST endpoint.
|
|
11692
|
+
*
|
|
11693
|
+
* The `_rpcId` field is the only addition versus the HTTP path; we strip
|
|
11694
|
+
* it before passing the payload to the dispatch handler so existing
|
|
11695
|
+
* validation logic doesn't trip on an unknown field.
|
|
11696
|
+
*/
|
|
11697
|
+
async onWebhookDispatch(payload) {
|
|
11698
|
+
const rpcId = payload._rpcId;
|
|
11699
|
+
if (!rpcId) {
|
|
11700
|
+
process.stderr.write(`[daemon] webhook.dispatch.request without _rpcId \u2014 dropped
|
|
11701
|
+
`);
|
|
11702
|
+
return;
|
|
11703
|
+
}
|
|
11704
|
+
const { _rpcId: _, ...request } = payload;
|
|
11705
|
+
try {
|
|
11706
|
+
const handle = handleAgentMessageDispatch(request, {
|
|
11707
|
+
findAgent: (agentImUserId) => this.findMessageDispatchAgent(agentImUserId),
|
|
11708
|
+
postReply: (replyPayload) => this.postMessageDispatchReply(replyPayload),
|
|
11709
|
+
onError: (err, req) => {
|
|
11710
|
+
process.stderr.write(
|
|
11711
|
+
`[daemon] webhook.dispatch.request failed message=${req.messageId} agent=${req.mentionedAgentImUserId}: ${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
11712
|
+
`
|
|
11713
|
+
);
|
|
11714
|
+
}
|
|
11715
|
+
});
|
|
11716
|
+
this.ws.send({
|
|
11717
|
+
type: "webhook.dispatch.reply",
|
|
11718
|
+
payload: {
|
|
11719
|
+
_rpcId: rpcId,
|
|
11720
|
+
ok: handle.response.ok,
|
|
11721
|
+
acceptedAt: handle.response.acceptedAt,
|
|
11722
|
+
error: handle.response.error
|
|
11723
|
+
}
|
|
11724
|
+
});
|
|
11725
|
+
void handle.done.catch((err) => {
|
|
11726
|
+
process.stderr.write(
|
|
11727
|
+
`[daemon] webhook.dispatch final reply failed message=${request.messageId}: ${err instanceof Error ? err.message : String(err)}
|
|
11728
|
+
`
|
|
11729
|
+
);
|
|
11730
|
+
});
|
|
11731
|
+
} catch (err) {
|
|
11732
|
+
this.ws.send({
|
|
11733
|
+
type: "webhook.dispatch.reply",
|
|
11734
|
+
payload: {
|
|
11735
|
+
_rpcId: rpcId,
|
|
11736
|
+
ok: false,
|
|
11737
|
+
error: {
|
|
11738
|
+
code: "daemon_dispatch_failed",
|
|
11739
|
+
message: err instanceof Error ? err.message : String(err)
|
|
11740
|
+
}
|
|
11741
|
+
}
|
|
11742
|
+
});
|
|
11743
|
+
}
|
|
11744
|
+
}
|
|
10138
11745
|
async onHostAcked(payload) {
|
|
10139
11746
|
this.workspaceId = payload.workspaceId;
|
|
11747
|
+
void this.reportTransport().catch((err) => {
|
|
11748
|
+
process.stderr.write(
|
|
11749
|
+
`[daemon] transport-probe report failed: ${err.message}
|
|
11750
|
+
`
|
|
11751
|
+
);
|
|
11752
|
+
});
|
|
10140
11753
|
if (this.memoryWiring && this.workspaceId) {
|
|
10141
11754
|
syncMemoryFromCloud(this.memoryWiring, this.cloud, [this.workspaceId]).catch(
|
|
10142
11755
|
(err) => log7.error("Initial memory sync failed", err.message)
|
|
@@ -10318,14 +11931,68 @@ var Runner = class extends EventEmitter3 {
|
|
|
10318
11931
|
};
|
|
10319
11932
|
}
|
|
10320
11933
|
async postMessageDispatchReply(payload) {
|
|
10321
|
-
const
|
|
10322
|
-
|
|
10323
|
-
|
|
10324
|
-
|
|
10325
|
-
|
|
10326
|
-
|
|
10327
|
-
|
|
11934
|
+
const taskId = `external:${payload.replyToMessageId}`;
|
|
11935
|
+
try {
|
|
11936
|
+
const result = await sendDispatchReplyTwoPhase({
|
|
11937
|
+
taskId,
|
|
11938
|
+
payload: {
|
|
11939
|
+
conversationId: payload.conversationId,
|
|
11940
|
+
replyToken: payload.replyToken,
|
|
11941
|
+
replyToMessageId: payload.replyToMessageId,
|
|
11942
|
+
agentImUserId: payload.agentImUserId,
|
|
11943
|
+
status: payload.status,
|
|
11944
|
+
...payload.replyText !== void 0 ? { replyText: payload.replyText } : {},
|
|
11945
|
+
...payload.attachments ? { attachments: payload.attachments } : {},
|
|
11946
|
+
assetIds: payload.attachments?.map((a) => a.assetId).filter((id) => typeof id === "string") ?? [],
|
|
11947
|
+
completedAt: payload.completedAt,
|
|
11948
|
+
...payload.error ? { error: payload.error } : {}
|
|
11949
|
+
},
|
|
11950
|
+
cloud: this.cloud,
|
|
11951
|
+
cache: this.pendingReplyCache
|
|
11952
|
+
});
|
|
11953
|
+
if (result.status === "aborted") {
|
|
11954
|
+
throw new Error(`dispatch reply aborted by server reaper (replyId=${result.replyId})`);
|
|
11955
|
+
}
|
|
11956
|
+
} catch (err) {
|
|
11957
|
+
throw new Error(
|
|
11958
|
+
err.message?.length ? err.message : `dispatch reply failed`
|
|
11959
|
+
);
|
|
11960
|
+
}
|
|
11961
|
+
}
|
|
11962
|
+
/**
|
|
11963
|
+
* v2.0 §4.8.1 (Wave 4-E4) — submit the transport-probe report. Called
|
|
11964
|
+
* from `onHostAcked` so the daemon's container row exists in cloud
|
|
11965
|
+
* before we POST to /runtime/transport-report.
|
|
11966
|
+
*
|
|
11967
|
+
* The gatewayUrl reported here is `http://<local-ipv4>:<localPort>`
|
|
11968
|
+
* (typically `http://192.168.x.x:3210`). For a daemon without a local
|
|
11969
|
+
* HTTP server (startLocalServer=false in tests) we still post the
|
|
11970
|
+
* probe with `gatewayUrl=null` so cloud knows transport='ws' is the
|
|
11971
|
+
* only path.
|
|
11972
|
+
*/
|
|
11973
|
+
async reportTransport() {
|
|
11974
|
+
const hasLocalServer = this.opts.startLocalServer !== false;
|
|
11975
|
+
let gatewayUrl = null;
|
|
11976
|
+
if (hasLocalServer) {
|
|
11977
|
+
const localIp = pickLocalIPv4();
|
|
11978
|
+
if (localIp) {
|
|
11979
|
+
const port = this.opts.localPort ?? DEFAULT_LOCAL_PORT;
|
|
11980
|
+
gatewayUrl = `http://${localIp}:${port}`;
|
|
11981
|
+
}
|
|
11982
|
+
}
|
|
11983
|
+
const probe = buildTransportProbe(gatewayUrl);
|
|
11984
|
+
const result = await reportTransportProbe(this.cloud, this.config.daemon_id, probe);
|
|
11985
|
+
if (!result.ok) {
|
|
11986
|
+
process.stderr.write(
|
|
11987
|
+
`[daemon] transport-probe report rejected status=${result.status} error=${result.error ?? "-"}
|
|
11988
|
+
`
|
|
11989
|
+
);
|
|
11990
|
+
return;
|
|
10328
11991
|
}
|
|
11992
|
+
process.stdout.write(
|
|
11993
|
+
`[daemon] transport-probe reported transport=${probe.transport} gateway=${probe.gatewayUrl ?? "null"} private=${probe.gatewayIsPrivate}
|
|
11994
|
+
`
|
|
11995
|
+
);
|
|
10329
11996
|
}
|
|
10330
11997
|
onAgentChanged(payload) {
|
|
10331
11998
|
const a = this.hostedAgents.get(payload.agentImUserId);
|
|
@@ -10768,7 +12435,7 @@ var Runner = class extends EventEmitter3 {
|
|
|
10768
12435
|
function resolveUserAssetWorkspaceDir(workspaceId) {
|
|
10769
12436
|
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
10770
12437
|
if (override) return join14(override, workspaceId);
|
|
10771
|
-
const home =
|
|
12438
|
+
const home = homedir7();
|
|
10772
12439
|
const desktop = join14(home, "Desktop");
|
|
10773
12440
|
const base = existsSync9(desktop) ? desktop : home;
|
|
10774
12441
|
return join14(base, "Prismer Assets", workspaceId);
|
|
@@ -10848,7 +12515,7 @@ function safeJsonParse(raw) {
|
|
|
10848
12515
|
// src/pair.ts
|
|
10849
12516
|
import { generateKeyPairSync } from "crypto";
|
|
10850
12517
|
import { hostname as hostname2 } from "os";
|
|
10851
|
-
import { setTimeout as
|
|
12518
|
+
import { setTimeout as sleep3 } from "timers/promises";
|
|
10852
12519
|
import qrcode from "qrcode";
|
|
10853
12520
|
async function pair(opts) {
|
|
10854
12521
|
const paths = opts.paths ?? resolvePaths();
|
|
@@ -10917,7 +12584,7 @@ Scan with Lumin to approve, or open: ${offer.qrUrl}
|
|
|
10917
12584
|
const pollIntervalMs = opts.pollIntervalMs ?? 5e3;
|
|
10918
12585
|
for (let i = 0; i < maxAttempts; i += 1) {
|
|
10919
12586
|
if (i > 0 || !localOnlyMode) {
|
|
10920
|
-
await
|
|
12587
|
+
await sleep3(pollIntervalMs);
|
|
10921
12588
|
}
|
|
10922
12589
|
const res = await cloud.request(
|
|
10923
12590
|
"GET",
|
|
@@ -10961,13 +12628,13 @@ import { spawnSync as spawnSync2 } from "child_process";
|
|
|
10961
12628
|
import {
|
|
10962
12629
|
copyFileSync,
|
|
10963
12630
|
existsSync as existsSync12,
|
|
10964
|
-
mkdirSync as
|
|
12631
|
+
mkdirSync as mkdirSync9,
|
|
10965
12632
|
readFileSync as readFileSync10,
|
|
10966
12633
|
statSync as statSync3,
|
|
10967
|
-
writeFileSync as
|
|
12634
|
+
writeFileSync as writeFileSync8
|
|
10968
12635
|
} from "fs";
|
|
10969
|
-
import { homedir as
|
|
10970
|
-
import { dirname as
|
|
12636
|
+
import { homedir as homedir8 } from "os";
|
|
12637
|
+
import { dirname as dirname10, join as join16 } from "path";
|
|
10971
12638
|
import { Command } from "commander";
|
|
10972
12639
|
init_hermes();
|
|
10973
12640
|
init_openclaw();
|
|
@@ -10982,7 +12649,7 @@ var INSTALL_SPECS = {
|
|
|
10982
12649
|
binary: "claude",
|
|
10983
12650
|
hint: "Set ANTHROPIC_API_KEY in your shell profile, or rely on Claude Code OAuth login. Run `claude login` to sign in.",
|
|
10984
12651
|
authHints: ["ANTHROPIC_API_KEY or Claude Code OAuth login (`claude login`)"],
|
|
10985
|
-
hookTarget: { path: join16(
|
|
12652
|
+
hookTarget: { path: join16(homedir8(), ".claude", "hooks.json"), kind: "json-file" }
|
|
10986
12653
|
},
|
|
10987
12654
|
openclaw: {
|
|
10988
12655
|
name: "openclaw",
|
|
@@ -10991,7 +12658,7 @@ var INSTALL_SPECS = {
|
|
|
10991
12658
|
binary: "openclaw",
|
|
10992
12659
|
hint: "OpenClaw runs as a gateway. Configure ~/.openclaw/openclaw.json and start it with `openclaw gateway` before tasks dispatch.",
|
|
10993
12660
|
authHints: ["~/.openclaw/openclaw.json gateway.auth.bearerTokens", "Prismer daemon api_key"],
|
|
10994
|
-
hookTarget: { path: join16(
|
|
12661
|
+
hookTarget: { path: join16(homedir8(), ".openclaw", "hooks"), kind: "directory" }
|
|
10995
12662
|
},
|
|
10996
12663
|
codex: {
|
|
10997
12664
|
name: "codex",
|
|
@@ -11000,7 +12667,7 @@ var INSTALL_SPECS = {
|
|
|
11000
12667
|
binary: "codex",
|
|
11001
12668
|
hint: "Set OPENAI_API_KEY in your shell profile. Verify with `codex --help`.",
|
|
11002
12669
|
authHints: ["OPENAI_API_KEY or Codex CLI account login"],
|
|
11003
|
-
hookTarget: { path: join16(
|
|
12670
|
+
hookTarget: { path: join16(homedir8(), ".codex", "hooks.json"), kind: "json-file" }
|
|
11004
12671
|
},
|
|
11005
12672
|
hermes: {
|
|
11006
12673
|
name: "hermes",
|
|
@@ -11009,7 +12676,7 @@ var INSTALL_SPECS = {
|
|
|
11009
12676
|
binary: "hermes",
|
|
11010
12677
|
hint: "Hermes runs as a long-lived HTTP gateway in your own Python venv. Start with `hermes -p <profile> gateway` after configuring ~/.hermes/.env.",
|
|
11011
12678
|
authHints: ["~/.hermes/.env API_SERVER_KEY", "Hermes profile provider credentials"],
|
|
11012
|
-
hookTarget: { path: join16(
|
|
12679
|
+
hookTarget: { path: join16(homedir8(), ".hermes", "hooks.json"), kind: "json-file" }
|
|
11013
12680
|
}
|
|
11014
12681
|
};
|
|
11015
12682
|
function buildAdapterCommand() {
|
|
@@ -11224,15 +12891,15 @@ function runHooks(spec, opts) {
|
|
|
11224
12891
|
}
|
|
11225
12892
|
let backup;
|
|
11226
12893
|
if (planned.kind === "directory") {
|
|
11227
|
-
|
|
12894
|
+
mkdirSync9(planned.path, { recursive: true });
|
|
11228
12895
|
const markerPath = join16(planned.path, "prismer.json");
|
|
11229
12896
|
backup = backupIfExists(markerPath);
|
|
11230
|
-
|
|
12897
|
+
writeFileSync8(markerPath, JSON.stringify(prismerHookMarker(spec), null, 2) + "\n", "utf8");
|
|
11231
12898
|
} else {
|
|
11232
|
-
|
|
12899
|
+
mkdirSync9(dirname10(planned.path), { recursive: true });
|
|
11233
12900
|
backup = backupIfExists(planned.path);
|
|
11234
12901
|
const merged = mergeHookJson(planned.path, spec);
|
|
11235
|
-
|
|
12902
|
+
writeFileSync8(planned.path, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
11236
12903
|
}
|
|
11237
12904
|
return {
|
|
11238
12905
|
ok: true,
|
|
@@ -11299,7 +12966,7 @@ function inspectAuthEnv(spec) {
|
|
|
11299
12966
|
hints: spec.authHints ?? []
|
|
11300
12967
|
};
|
|
11301
12968
|
case "hermes": {
|
|
11302
|
-
const envFile = join16(
|
|
12969
|
+
const envFile = join16(homedir8(), ".hermes", ".env");
|
|
11303
12970
|
return {
|
|
11304
12971
|
ok: existsSync12(envFile) || Boolean(process.env.HERMES_API_KEY || process.env.API_SERVER_KEY),
|
|
11305
12972
|
present: [
|
|
@@ -11310,7 +12977,7 @@ function inspectAuthEnv(spec) {
|
|
|
11310
12977
|
};
|
|
11311
12978
|
}
|
|
11312
12979
|
case "openclaw": {
|
|
11313
|
-
const cfgFile = join16(
|
|
12980
|
+
const cfgFile = join16(homedir8(), ".openclaw", "openclaw.json");
|
|
11314
12981
|
return {
|
|
11315
12982
|
ok: existsSync12(cfgFile) || Boolean(process.env.OPENCLAW_API_KEY),
|
|
11316
12983
|
present: [
|
|
@@ -11330,7 +12997,7 @@ function inspectHook(spec) {
|
|
|
11330
12997
|
return { supported: false, markerPresent: false };
|
|
11331
12998
|
}
|
|
11332
12999
|
if (spec.name === "openclaw") {
|
|
11333
|
-
const jsonPath = join16(
|
|
13000
|
+
const jsonPath = join16(homedir8(), ".openclaw", "hooks.json");
|
|
11334
13001
|
const markerPath = join16(target.path, "prismer.json");
|
|
11335
13002
|
const jsonMarkerPresent = existsSync12(jsonPath) && fileContainsPrismerMarker(jsonPath);
|
|
11336
13003
|
const dirMarkerPresent = existsSync12(markerPath) && fileContainsPrismerMarker(markerPath);
|
|
@@ -11792,8 +13459,74 @@ function buildAgentCommand() {
|
|
|
11792
13459
|
}
|
|
11793
13460
|
getUI().ok("Skill install requested", `agent=${opts.agent} skill=${slugOrId}`);
|
|
11794
13461
|
}, { code: "agent_install_skill_failed" }));
|
|
13462
|
+
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) => {
|
|
13463
|
+
const cloud = makeCloudClient();
|
|
13464
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agents/${encodeURIComponent(imUserId)}/snapshot`, {
|
|
13465
|
+
includeMemory: Boolean(opts.includeMemory),
|
|
13466
|
+
label: opts.label
|
|
13467
|
+
}, "agent_snapshot_failed");
|
|
13468
|
+
if (opts.json) {
|
|
13469
|
+
printJson(data);
|
|
13470
|
+
return;
|
|
13471
|
+
}
|
|
13472
|
+
const rec = data;
|
|
13473
|
+
getUI().ok("Snapshot created", String(rec.id ?? "<unknown>"));
|
|
13474
|
+
getUI().line(`agent=${String(rec.agentImUserId ?? imUserId)} memory=${rec.includeMemory ? "included" : "stripped"}`);
|
|
13475
|
+
}, { code: "agent_snapshot_failed" }));
|
|
13476
|
+
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) => {
|
|
13477
|
+
const cloud = makeCloudClient();
|
|
13478
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agents/${encodeURIComponent(imUserId)}/publish`, {
|
|
13479
|
+
slug: opts.slug,
|
|
13480
|
+
version: opts.version,
|
|
13481
|
+
license: opts.license,
|
|
13482
|
+
metadata: {
|
|
13483
|
+
...opts.title ? { title: opts.title } : {},
|
|
13484
|
+
...opts.description ? { description: opts.description } : {}
|
|
13485
|
+
},
|
|
13486
|
+
stripMemory: true
|
|
13487
|
+
}, "agent_publish_failed");
|
|
13488
|
+
if (opts.json) {
|
|
13489
|
+
printJson(data);
|
|
13490
|
+
return;
|
|
13491
|
+
}
|
|
13492
|
+
const rec = data;
|
|
13493
|
+
getUI().ok("Agent Pack published", `${String(rec.slug ?? opts.slug)}@${String(rec.version ?? opts.version ?? "1.0.0")}`);
|
|
13494
|
+
getUI().line(`package=${String(rec.id ?? "<unknown>")}`);
|
|
13495
|
+
}, { code: "agent_publish_failed" }));
|
|
13496
|
+
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) => {
|
|
13497
|
+
const cloud = makeCloudClient();
|
|
13498
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agent-packs/${encodeURIComponent(packIdOrSlug)}/fork`, {
|
|
13499
|
+
targetWorkspaceId: opts.workspaceId,
|
|
13500
|
+
displayName: opts.displayName
|
|
13501
|
+
}, "agent_fork_failed");
|
|
13502
|
+
if (opts.json) {
|
|
13503
|
+
printJson(data);
|
|
13504
|
+
return;
|
|
13505
|
+
}
|
|
13506
|
+
const rec = data;
|
|
13507
|
+
getUI().ok("Agent Pack forked", `newAgent=${String(rec.newImUserId ?? "<unknown>")}`);
|
|
13508
|
+
getUI().line(`did=${String(rec.newDid ?? "not issued")}`);
|
|
13509
|
+
}, { code: "agent_fork_failed" }));
|
|
11795
13510
|
return cmd;
|
|
11796
13511
|
}
|
|
13512
|
+
function makeCloudClient() {
|
|
13513
|
+
const cfg = loadConfig(resolvePaths());
|
|
13514
|
+
return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
13515
|
+
}
|
|
13516
|
+
async function requestEnvelope(cloud, method, path9, body, code) {
|
|
13517
|
+
const res = await cloud.request(
|
|
13518
|
+
method,
|
|
13519
|
+
path9,
|
|
13520
|
+
body === void 0 ? void 0 : { body }
|
|
13521
|
+
);
|
|
13522
|
+
if (!res.ok || res.data?.ok === false) {
|
|
13523
|
+
const message = res.error?.message ?? res.data?.error?.message ?? "request failed";
|
|
13524
|
+
exitWithError(`${path9} failed (${res.status === 0 ? "network error" : `HTTP ${res.status}`}): ${message}`, {
|
|
13525
|
+
code: res.error?.code ?? res.data?.error?.code ?? code
|
|
13526
|
+
});
|
|
13527
|
+
}
|
|
13528
|
+
return res.data && typeof res.data === "object" && "data" in res.data ? res.data.data : res.data;
|
|
13529
|
+
}
|
|
11797
13530
|
function readLocalAgents(localDb) {
|
|
11798
13531
|
const db = openLocalDb(localDb);
|
|
11799
13532
|
try {
|
|
@@ -11898,8 +13631,8 @@ function whichBinary2(bin) {
|
|
|
11898
13631
|
// src/cli/commands/asset.ts
|
|
11899
13632
|
import { createHash as createHash8 } from "crypto";
|
|
11900
13633
|
import { Command as Command3 } from "commander";
|
|
11901
|
-
import { existsSync as existsSync13, lstatSync, readdirSync, readFileSync as readFileSync11, writeFileSync as
|
|
11902
|
-
import { basename as basename3, dirname as
|
|
13634
|
+
import { existsSync as existsSync13, lstatSync, readdirSync, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
|
|
13635
|
+
import { basename as basename3, dirname as dirname11, join as join17, relative as relative4 } from "path";
|
|
11903
13636
|
init_util();
|
|
11904
13637
|
init_ui();
|
|
11905
13638
|
function buildAssetCommand() {
|
|
@@ -12082,7 +13815,7 @@ async function uploadAssetPath(inputPath, opts) {
|
|
|
12082
13815
|
if (files.length === 0) exitWithError(`directory has no uploadable files: ${inputPath}`);
|
|
12083
13816
|
const items = [];
|
|
12084
13817
|
for (const file of files) {
|
|
12085
|
-
const relDir =
|
|
13818
|
+
const relDir = dirname11(relative4(inputPath, file));
|
|
12086
13819
|
const folderPath = combineFolderPath(opts.folderPath, relDir === "." ? "" : relDir);
|
|
12087
13820
|
const response = await uploadAsset(file, { ...opts, folderPath });
|
|
12088
13821
|
items.push({ file, folderPath: folderPath ?? null, response });
|
|
@@ -12265,7 +13998,7 @@ async function downloadAsset(assetId, outPath) {
|
|
|
12265
13998
|
exitWithError(`asset download failed (${res.status}): ${errorMessage(body)}`);
|
|
12266
13999
|
}
|
|
12267
14000
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
12268
|
-
|
|
14001
|
+
writeFileSync9(outPath, bytes);
|
|
12269
14002
|
}
|
|
12270
14003
|
function parseMetadata(raw) {
|
|
12271
14004
|
if (!raw) return {};
|
|
@@ -12664,7 +14397,7 @@ function buildCookbookCommand() {
|
|
|
12664
14397
|
for (const suite of suites) {
|
|
12665
14398
|
checks.push(...await runSuite(suite, cloud, opts));
|
|
12666
14399
|
}
|
|
12667
|
-
const summary =
|
|
14400
|
+
const summary = summarize2(checks, Boolean(opts.strict));
|
|
12668
14401
|
if (opts.json) {
|
|
12669
14402
|
printJson(summary);
|
|
12670
14403
|
} else {
|
|
@@ -12829,7 +14562,7 @@ function parseSuites(raw) {
|
|
|
12829
14562
|
const expanded = selected.includes("all") ? ["status", "im", "task", "group", "asset", "sandbox"] : selected;
|
|
12830
14563
|
return [...new Set(expanded)];
|
|
12831
14564
|
}
|
|
12832
|
-
function
|
|
14565
|
+
function summarize2(checks, strict) {
|
|
12833
14566
|
const totals = { pass: 0, fail: 0, skip: 0 };
|
|
12834
14567
|
for (const check of checks) totals[check.status] += 1;
|
|
12835
14568
|
return {
|
|
@@ -12912,8 +14645,8 @@ function parsePositiveInt3(value) {
|
|
|
12912
14645
|
// src/cli/commands/daemon.ts
|
|
12913
14646
|
import { Command as Command8 } from "commander";
|
|
12914
14647
|
import { spawn as spawn5 } from "child_process";
|
|
12915
|
-
import { createReadStream, existsSync as existsSync14, mkdirSync as
|
|
12916
|
-
import { setTimeout as
|
|
14648
|
+
import { createReadStream, existsSync as existsSync14, mkdirSync as mkdirSync10, openSync as openSync2, statSync as statSync4 } from "fs";
|
|
14649
|
+
import { setTimeout as sleep4 } from "timers/promises";
|
|
12917
14650
|
import { join as join18 } from "path";
|
|
12918
14651
|
init_util();
|
|
12919
14652
|
init_ui();
|
|
@@ -12956,7 +14689,7 @@ function buildDaemonCommand() {
|
|
|
12956
14689
|
while (!configExists(paths)) {
|
|
12957
14690
|
if (stopRequested) return;
|
|
12958
14691
|
getUI().info(`[daemon] waiting for ${paths.configFile} \u2014 run \`prismer setup\` to create it`);
|
|
12959
|
-
await
|
|
14692
|
+
await sleep4(5e3);
|
|
12960
14693
|
}
|
|
12961
14694
|
runner = new Runner({
|
|
12962
14695
|
startLocalServer: opts.localServer !== false,
|
|
@@ -12988,7 +14721,7 @@ function buildDaemonCommand() {
|
|
|
12988
14721
|
if (existingPid && pidAlive2(existingPid)) {
|
|
12989
14722
|
exitWithError(`Daemon already running (pid ${existingPid}). Use \`prismer daemon stop\` first.`);
|
|
12990
14723
|
}
|
|
12991
|
-
if (!existsSync14(paths.logsDir))
|
|
14724
|
+
if (!existsSync14(paths.logsDir)) mkdirSync10(paths.logsDir, { recursive: true });
|
|
12992
14725
|
const logFile = join18(paths.logsDir, "daemon.log");
|
|
12993
14726
|
const fd = openSync2(logFile, "a");
|
|
12994
14727
|
const args = [process.argv[1], "daemon", "run"];
|
|
@@ -13002,7 +14735,7 @@ function buildDaemonCommand() {
|
|
|
13002
14735
|
child.unref();
|
|
13003
14736
|
const deadline = Date.now() + 5e3;
|
|
13004
14737
|
while (Date.now() < deadline) {
|
|
13005
|
-
await
|
|
14738
|
+
await sleep4(200);
|
|
13006
14739
|
const pid = readPidFile(paths);
|
|
13007
14740
|
if (pid && pidAlive2(pid)) {
|
|
13008
14741
|
if (opts.json) printJson({ ok: true, pid, logFile });
|
|
@@ -13036,7 +14769,7 @@ function buildDaemonCommand() {
|
|
|
13036
14769
|
getUI().ok("Daemon stopped", `pid ${pid}`);
|
|
13037
14770
|
return;
|
|
13038
14771
|
}
|
|
13039
|
-
await
|
|
14772
|
+
await sleep4(200);
|
|
13040
14773
|
}
|
|
13041
14774
|
process.stderr.write(`Daemon did not exit within timeout; pid ${pid} may still be alive.
|
|
13042
14775
|
`);
|
|
@@ -13117,7 +14850,7 @@ async function tailFromEnd(path9, lines) {
|
|
|
13117
14850
|
async function followFile(path9) {
|
|
13118
14851
|
let offset = statSync4(path9).size;
|
|
13119
14852
|
for (; ; ) {
|
|
13120
|
-
await
|
|
14853
|
+
await sleep4(1e3);
|
|
13121
14854
|
const size = statSync4(path9).size;
|
|
13122
14855
|
if (size < offset) offset = 0;
|
|
13123
14856
|
if (size === offset) continue;
|
|
@@ -13131,7 +14864,7 @@ async function followFile(path9) {
|
|
|
13131
14864
|
init_util();
|
|
13132
14865
|
import { Command as Command9 } from "commander";
|
|
13133
14866
|
import { createReadStream as createReadStream2, existsSync as existsSync15 } from "fs";
|
|
13134
|
-
import { homedir as
|
|
14867
|
+
import { homedir as homedir9 } from "os";
|
|
13135
14868
|
import { join as join19 } from "path";
|
|
13136
14869
|
import { createInterface } from "readline";
|
|
13137
14870
|
var DEFAULT_LIMIT2 = 50;
|
|
@@ -13164,7 +14897,7 @@ function buildEventsStatsCommand() {
|
|
|
13164
14897
|
const events = await readEvents(file, filters);
|
|
13165
14898
|
printJson({
|
|
13166
14899
|
ok: true,
|
|
13167
|
-
data: { stats:
|
|
14900
|
+
data: { stats: summarize3(events), count: events.length, limit: filters.limit },
|
|
13168
14901
|
checks: { file, exists: true, filters: cleanFilters(filters) }
|
|
13169
14902
|
});
|
|
13170
14903
|
});
|
|
@@ -13173,7 +14906,7 @@ function addEventOptions(cmd) {
|
|
|
13173
14906
|
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)");
|
|
13174
14907
|
}
|
|
13175
14908
|
function eventsPath() {
|
|
13176
|
-
return join19(process.env.PRISMER_HOME ?? join19(
|
|
14909
|
+
return join19(process.env.PRISMER_HOME ?? join19(homedir9(), ".prismer"), "para", "events.jsonl");
|
|
13177
14910
|
}
|
|
13178
14911
|
function unavailable(file) {
|
|
13179
14912
|
return {
|
|
@@ -13221,7 +14954,7 @@ function field(event, names) {
|
|
|
13221
14954
|
}
|
|
13222
14955
|
return void 0;
|
|
13223
14956
|
}
|
|
13224
|
-
function
|
|
14957
|
+
function summarize3(events) {
|
|
13225
14958
|
const byFamily = {};
|
|
13226
14959
|
const byType = {};
|
|
13227
14960
|
const byAgentId = {};
|
|
@@ -13575,7 +15308,7 @@ function buildPairCommand() {
|
|
|
13575
15308
|
|
|
13576
15309
|
// src/cli/commands/profile.ts
|
|
13577
15310
|
import { Command as Command12 } from "commander";
|
|
13578
|
-
import { existsSync as existsSync17, readFileSync as readFileSync12, writeFileSync as
|
|
15311
|
+
import { existsSync as existsSync17, readFileSync as readFileSync12, writeFileSync as writeFileSync10 } from "fs";
|
|
13579
15312
|
import { tmpdir } from "os";
|
|
13580
15313
|
import { join as join20 } from "path";
|
|
13581
15314
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
@@ -13626,7 +15359,7 @@ function buildProfileCommand() {
|
|
|
13626
15359
|
`/api/im/agent_profiles/${encodeURIComponent(profileId)}`
|
|
13627
15360
|
);
|
|
13628
15361
|
const tmpFile = join20(tmpdir(), `prismer-profile-${profileId}.json`);
|
|
13629
|
-
|
|
15362
|
+
writeFileSync10(tmpFile, JSON.stringify(profile.config, null, 2), "utf8");
|
|
13630
15363
|
const editor = process.env.EDITOR || "vi";
|
|
13631
15364
|
const ed = spawnSync4(editor, [tmpFile], { stdio: "inherit" });
|
|
13632
15365
|
if (ed.status !== 0) exitWithError(`editor exited ${ed.status}`, { code: "editor_failed" });
|
|
@@ -13667,10 +15400,10 @@ async function resolveDefaultWorkspaceId(cloud) {
|
|
|
13667
15400
|
// src/cli/commands/reset.ts
|
|
13668
15401
|
import { Command as Command13 } from "commander";
|
|
13669
15402
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
13670
|
-
import { existsSync as existsSync18, mkdirSync as
|
|
13671
|
-
import { homedir as
|
|
13672
|
-
import { dirname as
|
|
13673
|
-
import { setTimeout as
|
|
15403
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync11, rmSync, renameSync as renameSync2 } from "fs";
|
|
15404
|
+
import { homedir as homedir10 } from "os";
|
|
15405
|
+
import { dirname as dirname12, join as join21 } from "path";
|
|
15406
|
+
import { setTimeout as sleep5 } from "timers/promises";
|
|
13674
15407
|
init_util();
|
|
13675
15408
|
init_ui();
|
|
13676
15409
|
function buildResetCommand() {
|
|
@@ -13723,8 +15456,8 @@ function buildResetCommand() {
|
|
|
13723
15456
|
rmSync(paths.root, { recursive: true, force: true });
|
|
13724
15457
|
} else {
|
|
13725
15458
|
if (!archivePath) throw new Error("internal: archivePath missing");
|
|
13726
|
-
if (!existsSync18(
|
|
13727
|
-
|
|
15459
|
+
if (!existsSync18(dirname12(archivePath))) {
|
|
15460
|
+
mkdirSync11(dirname12(archivePath), { recursive: true });
|
|
13728
15461
|
}
|
|
13729
15462
|
renameSync2(paths.root, archivePath);
|
|
13730
15463
|
}
|
|
@@ -13734,8 +15467,8 @@ function buildResetCommand() {
|
|
|
13734
15467
|
rmSync(assetSyncRoot, { recursive: true, force: true });
|
|
13735
15468
|
} else {
|
|
13736
15469
|
if (!assetSyncArchivePath) throw new Error("internal: assetSyncArchivePath missing");
|
|
13737
|
-
if (!existsSync18(
|
|
13738
|
-
|
|
15470
|
+
if (!existsSync18(dirname12(assetSyncArchivePath))) {
|
|
15471
|
+
mkdirSync11(dirname12(assetSyncArchivePath), { recursive: true });
|
|
13739
15472
|
}
|
|
13740
15473
|
renameSync2(assetSyncRoot, assetSyncArchivePath);
|
|
13741
15474
|
}
|
|
@@ -13745,13 +15478,13 @@ function buildResetCommand() {
|
|
|
13745
15478
|
rmSync(hermesHome, { recursive: true, force: true });
|
|
13746
15479
|
} else {
|
|
13747
15480
|
if (!hermesArchivePath) throw new Error("internal: hermesArchivePath missing");
|
|
13748
|
-
if (!existsSync18(
|
|
13749
|
-
|
|
15481
|
+
if (!existsSync18(dirname12(hermesArchivePath))) {
|
|
15482
|
+
mkdirSync11(dirname12(hermesArchivePath), { recursive: true });
|
|
13750
15483
|
}
|
|
13751
15484
|
renameSync2(hermesHome, hermesArchivePath);
|
|
13752
15485
|
}
|
|
13753
15486
|
}
|
|
13754
|
-
|
|
15487
|
+
mkdirSync11(paths.root, { recursive: true });
|
|
13755
15488
|
if (opts.json) {
|
|
13756
15489
|
printJson({ ok: true, reset: true, plan });
|
|
13757
15490
|
return;
|
|
@@ -13800,7 +15533,7 @@ async function stopProcess(pid, timeoutMs, label) {
|
|
|
13800
15533
|
const deadline = Date.now() + timeoutMs;
|
|
13801
15534
|
while (Date.now() < deadline) {
|
|
13802
15535
|
if (!pidAlive2(pid)) return;
|
|
13803
|
-
await
|
|
15536
|
+
await sleep5(100);
|
|
13804
15537
|
}
|
|
13805
15538
|
try {
|
|
13806
15539
|
process.kill(pid, "SIGKILL");
|
|
@@ -13810,7 +15543,7 @@ async function stopProcess(pid, timeoutMs, label) {
|
|
|
13810
15543
|
const killDeadline = Date.now() + 1e3;
|
|
13811
15544
|
while (Date.now() < killDeadline) {
|
|
13812
15545
|
if (!pidAlive2(pid)) return;
|
|
13813
|
-
await
|
|
15546
|
+
await sleep5(100);
|
|
13814
15547
|
}
|
|
13815
15548
|
throw new Error(`${label} pid ${pid} did not exit within ${timeoutMs + 1e3}ms`);
|
|
13816
15549
|
}
|
|
@@ -13820,13 +15553,13 @@ function timestamp() {
|
|
|
13820
15553
|
function resolveAssetSyncRoot() {
|
|
13821
15554
|
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
13822
15555
|
if (override) return override;
|
|
13823
|
-
const home =
|
|
15556
|
+
const home = homedir10();
|
|
13824
15557
|
const desktop = join21(home, "Desktop");
|
|
13825
15558
|
const base = existsSync18(desktop) ? desktop : home;
|
|
13826
15559
|
return join21(base, "Prismer Assets");
|
|
13827
15560
|
}
|
|
13828
15561
|
function resolveHermesHome() {
|
|
13829
|
-
return process.env.HERMES_HOME || join21(
|
|
15562
|
+
return process.env.HERMES_HOME || join21(homedir10(), ".hermes");
|
|
13830
15563
|
}
|
|
13831
15564
|
function findHermesGatewayPids2() {
|
|
13832
15565
|
try {
|
|
@@ -14381,7 +16114,7 @@ function buildSkillCommand() {
|
|
|
14381
16114
|
const cloud = mkCloud6();
|
|
14382
16115
|
const path9 = opts.agent ? `/api/im/agents/${encodeURIComponent(opts.agent)}/skills` : `/api/im/skills/${encodeURIComponent(slugOrId)}/install`;
|
|
14383
16116
|
const body = opts.agent ? { skillId: slugOrId, workspaceId: opts.workspaceId, version: opts.version } : { workspaceId: opts.workspaceId, version: opts.version };
|
|
14384
|
-
const data = await
|
|
16117
|
+
const data = await requestEnvelope2(cloud, "POST", path9, body, "skill_install_failed");
|
|
14385
16118
|
if (opts.json) {
|
|
14386
16119
|
printJson(data);
|
|
14387
16120
|
return;
|
|
@@ -14392,7 +16125,7 @@ function buildSkillCommand() {
|
|
|
14392
16125
|
const cloud = mkCloud6();
|
|
14393
16126
|
const path9 = opts.agent ? `/api/im/agents/${encodeURIComponent(opts.agent)}/skills` : `/api/im/skills/${encodeURIComponent(slugOrId)}/install`;
|
|
14394
16127
|
const body = opts.agent ? { skillId: slugOrId, workspaceId: opts.workspaceId } : void 0;
|
|
14395
|
-
const data = await
|
|
16128
|
+
const data = await requestEnvelope2(cloud, "DELETE", path9, body, "skill_uninstall_failed");
|
|
14396
16129
|
if (opts.json) {
|
|
14397
16130
|
printJson(data);
|
|
14398
16131
|
return;
|
|
@@ -14445,7 +16178,7 @@ function mkCloud6() {
|
|
|
14445
16178
|
const cfg = loadConfig(resolvePaths());
|
|
14446
16179
|
return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
14447
16180
|
}
|
|
14448
|
-
async function
|
|
16181
|
+
async function requestEnvelope2(cloud, method, path9, body, code) {
|
|
14449
16182
|
const res = await cloud.request(method, path9, {
|
|
14450
16183
|
body
|
|
14451
16184
|
});
|
|
@@ -14595,6 +16328,7 @@ function buildStatusCommand() {
|
|
|
14595
16328
|
let me = null;
|
|
14596
16329
|
let devices = null;
|
|
14597
16330
|
let agents = null;
|
|
16331
|
+
let diagnose = null;
|
|
14598
16332
|
try {
|
|
14599
16333
|
const meRes = await cloud.request("GET", "/api/im/me", { timeoutMs: 3e3 });
|
|
14600
16334
|
cloudOk = meRes.ok;
|
|
@@ -14607,19 +16341,33 @@ function buildStatusCommand() {
|
|
|
14607
16341
|
if (Array.isArray(wsList) && wsList.length > 0) {
|
|
14608
16342
|
const wsId = wsList[0]?.id;
|
|
14609
16343
|
if (wsId) {
|
|
14610
|
-
const
|
|
14611
|
-
if (
|
|
14612
|
-
const
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
14618
|
-
agents = agBody?.data;
|
|
16344
|
+
const rtRes = await cloud.request("GET", `/api/im/workspaces/${encodeURIComponent(wsId)}/runtime`, { timeoutMs: 3e3 });
|
|
16345
|
+
if (rtRes.ok) {
|
|
16346
|
+
const rtBody = rtRes.data;
|
|
16347
|
+
const snapshot = rtBody?.data;
|
|
16348
|
+
if (snapshot && Array.isArray(snapshot.devices)) {
|
|
16349
|
+
devices = snapshot.devices;
|
|
16350
|
+
agents = snapshot.devices.flatMap((d) => d.agents ?? []);
|
|
16351
|
+
}
|
|
14619
16352
|
}
|
|
14620
16353
|
}
|
|
14621
16354
|
}
|
|
14622
16355
|
}
|
|
16356
|
+
const daemonId = cfg.daemon_id;
|
|
16357
|
+
if (daemonId) {
|
|
16358
|
+
try {
|
|
16359
|
+
const diagRes = await cloud.request(
|
|
16360
|
+
"GET",
|
|
16361
|
+
`/api/im/runtime/diagnose?daemonId=${encodeURIComponent(daemonId)}`,
|
|
16362
|
+
{ timeoutMs: 3e3 }
|
|
16363
|
+
);
|
|
16364
|
+
if (diagRes.ok) {
|
|
16365
|
+
const body = diagRes.data;
|
|
16366
|
+
if (body?.data && typeof body.data === "object") diagnose = body.data;
|
|
16367
|
+
}
|
|
16368
|
+
} catch {
|
|
16369
|
+
}
|
|
16370
|
+
}
|
|
14623
16371
|
}
|
|
14624
16372
|
} catch {
|
|
14625
16373
|
cloudOk = false;
|
|
@@ -14636,7 +16384,8 @@ function buildStatusCommand() {
|
|
|
14636
16384
|
info: daemonStatus.info ?? {}
|
|
14637
16385
|
},
|
|
14638
16386
|
cloud: { base: cfg.cloud_api_base, reachable: cloudOk, me, devices, agents },
|
|
14639
|
-
local
|
|
16387
|
+
local,
|
|
16388
|
+
diagnose
|
|
14640
16389
|
};
|
|
14641
16390
|
if (opts.json) {
|
|
14642
16391
|
printJson(report);
|
|
@@ -14645,6 +16394,60 @@ function buildStatusCommand() {
|
|
|
14645
16394
|
printPretty(report);
|
|
14646
16395
|
});
|
|
14647
16396
|
}
|
|
16397
|
+
function computeDeviceIcon(lastSeenAt, now) {
|
|
16398
|
+
if (!lastSeenAt) return "\u25CB";
|
|
16399
|
+
const age = now.getTime() - Date.parse(lastSeenAt);
|
|
16400
|
+
if (!Number.isFinite(age) || age < 0) return "\u25CB";
|
|
16401
|
+
if (age < 5 * 6e4) return "\u25CF";
|
|
16402
|
+
if (age < 60 * 6e4) return "\u25D0";
|
|
16403
|
+
return "\u25CB";
|
|
16404
|
+
}
|
|
16405
|
+
function computeAgentIcon(status, lastHeartbeat, now) {
|
|
16406
|
+
const s = (status || "").toLowerCase();
|
|
16407
|
+
if (s === "error" || s === "failed") return "\u25C6";
|
|
16408
|
+
if (!lastHeartbeat) return s === "offline" ? "\u25CB" : "\u25D0";
|
|
16409
|
+
const age = now.getTime() - Date.parse(lastHeartbeat);
|
|
16410
|
+
if (!Number.isFinite(age) || age < 0 || age >= 60 * 6e4) return "\u25CB";
|
|
16411
|
+
if (age >= 5 * 6e4) return "\u25D0";
|
|
16412
|
+
return "\u25CF";
|
|
16413
|
+
}
|
|
16414
|
+
function pad(value, width) {
|
|
16415
|
+
if (value.length >= width) return value.slice(0, width);
|
|
16416
|
+
return value + " ".repeat(width - value.length);
|
|
16417
|
+
}
|
|
16418
|
+
function formatDeviceBindings(devices, now = /* @__PURE__ */ new Date()) {
|
|
16419
|
+
const lines = [];
|
|
16420
|
+
if (devices.length === 0) {
|
|
16421
|
+
lines.push(" No paired devices.");
|
|
16422
|
+
return lines;
|
|
16423
|
+
}
|
|
16424
|
+
const totalAgents = devices.reduce((s, d) => s + (d.agents?.length ?? 0), 0);
|
|
16425
|
+
const devWord = devices.length === 1 ? "device" : "devices";
|
|
16426
|
+
const agWord = totalAgents === 1 ? "agent" : "agents";
|
|
16427
|
+
lines.push(` Device & Agent Bindings (${devices.length} ${devWord} \xB7 ${totalAgents} ${agWord})`);
|
|
16428
|
+
lines.push("");
|
|
16429
|
+
for (let di = 0; di < devices.length; di++) {
|
|
16430
|
+
const d = devices[di];
|
|
16431
|
+
const dIcon = computeDeviceIcon(d.lastSeenAt, now);
|
|
16432
|
+
const dName = pad(d.name || d.deviceId, 40);
|
|
16433
|
+
const lastSeen = d.lastSeenAt ? formatRelative(d.lastSeenAt, now) : "never seen";
|
|
16434
|
+
lines.push(` ${dIcon} ${dName} ${lastSeen}`);
|
|
16435
|
+
const agents = d.agents ?? [];
|
|
16436
|
+
for (let ai = 0; ai < agents.length; ai++) {
|
|
16437
|
+
const a = agents[ai];
|
|
16438
|
+
const isLast = ai === agents.length - 1;
|
|
16439
|
+
const branch = isLast ? "\u2514\u2500" : "\u251C\u2500";
|
|
16440
|
+
const aIcon = computeAgentIcon(a.status, a.lastHeartbeat, now);
|
|
16441
|
+
const aName = pad(a.name || a.id, 24);
|
|
16442
|
+
const aStatus = pad(a.status || "?", 8);
|
|
16443
|
+
const aVersion = pad(a.version ? `v${a.version}` : "\u2014", 10);
|
|
16444
|
+
const tail = a.currentTaskId ? `task=${a.currentTaskId}` : a.lastHeartbeat ? `\u2764 ${formatRelative(a.lastHeartbeat, now)}` : "";
|
|
16445
|
+
lines.push(` ${branch} ${aIcon} ${aName} ${aStatus} ${aVersion} ${tail}`.trimEnd());
|
|
16446
|
+
}
|
|
16447
|
+
if (di < devices.length - 1) lines.push("");
|
|
16448
|
+
}
|
|
16449
|
+
return lines;
|
|
16450
|
+
}
|
|
14648
16451
|
async function readDaemonStatus() {
|
|
14649
16452
|
try {
|
|
14650
16453
|
const res = await fetch("http://127.0.0.1:3210/healthz", {
|
|
@@ -14717,16 +16520,7 @@ function printPretty(report) {
|
|
|
14717
16520
|
if (Array.isArray(report.cloud.devices) && report.cloud.devices.length > 0) {
|
|
14718
16521
|
const devs = report.cloud.devices;
|
|
14719
16522
|
ui.blank();
|
|
14720
|
-
|
|
14721
|
-
for (const d of devs) {
|
|
14722
|
-
const statusIcon = d.daemonStatus === "connected" ? "\u25CF" : "\u25CB";
|
|
14723
|
-
const kind = d.runtimeKind === "docker" ? "Local" : "K8s";
|
|
14724
|
-
const declared = d.hostedAgentSummary?.declared ?? 0;
|
|
14725
|
-
ui.line(` ${statusIcon} ${d.podName?.replace(/^daemon:/, "")?.slice(0, 28) ?? "?"} kind=${kind} agents=${declared}`);
|
|
14726
|
-
}
|
|
14727
|
-
}
|
|
14728
|
-
if (Array.isArray(report.cloud.agents) && report.cloud.agents.length > 0) {
|
|
14729
|
-
ui.line(` Hosted agents: ${report.cloud.agents.length}`);
|
|
16523
|
+
for (const ln of formatDeviceBindings(devs)) ui.line(ln);
|
|
14730
16524
|
}
|
|
14731
16525
|
if (report.local) {
|
|
14732
16526
|
ui.blank();
|
|
@@ -14736,11 +16530,63 @@ function printPretty(report) {
|
|
|
14736
16530
|
} else {
|
|
14737
16531
|
warn("Local DB", "unavailable");
|
|
14738
16532
|
}
|
|
16533
|
+
if (report.diagnose) {
|
|
16534
|
+
ui.blank();
|
|
16535
|
+
ui.header("Cloud dispatch reachability");
|
|
16536
|
+
ui.blank();
|
|
16537
|
+
printDiagnose(report.diagnose, report.daemon.pid ?? null);
|
|
16538
|
+
}
|
|
16539
|
+
}
|
|
16540
|
+
function printDiagnose(d, pid) {
|
|
16541
|
+
const ui = getUI();
|
|
16542
|
+
const heartbeatLine = d.lastHeartbeatAt ? `last heartbeat ${formatRelative(d.lastHeartbeatAt)}` : "no heartbeat seen";
|
|
16543
|
+
if (d.wsAlive) {
|
|
16544
|
+
ok("Daemon", `running${pid ? ` (pid ${pid})` : ""}`);
|
|
16545
|
+
ok("Cloud WS", `connected (${heartbeatLine})`);
|
|
16546
|
+
} else {
|
|
16547
|
+
warn("Daemon", `running${pid ? ` (pid ${pid})` : ""} \u2014 not visible to cloud WS`);
|
|
16548
|
+
fail2("Cloud WS", `disconnected (${heartbeatLine})`);
|
|
16549
|
+
}
|
|
16550
|
+
if (d.gatewayUrl) {
|
|
16551
|
+
if (d.gatewayIsPrivate) {
|
|
16552
|
+
warn("HTTP gateway", `${d.gatewayUrl} (private IP \u2014 only reachable on same network)`);
|
|
16553
|
+
} else if (d.httpReachable) {
|
|
16554
|
+
ok(
|
|
16555
|
+
"HTTP gateway",
|
|
16556
|
+
`${d.gatewayUrl} (reachable${d.httpLatencyMs != null ? `, ${d.httpLatencyMs}ms` : ""})`
|
|
16557
|
+
);
|
|
16558
|
+
} else {
|
|
16559
|
+
fail2("HTTP gateway", `${d.gatewayUrl} (unreachable${d.httpError ? ` \u2014 ${d.httpError}` : ""})`);
|
|
16560
|
+
}
|
|
16561
|
+
} else {
|
|
16562
|
+
ui.line(" HTTP gateway: not declared (WS-only daemon)");
|
|
16563
|
+
}
|
|
16564
|
+
const activeAgents = /* @__PURE__ */ (() => {
|
|
16565
|
+
return 0;
|
|
16566
|
+
})();
|
|
16567
|
+
if (activeAgents > 0) {
|
|
16568
|
+
ok("Active agents", String(activeAgents));
|
|
16569
|
+
}
|
|
16570
|
+
const recColour = d.recommendedTransport === "ws" ? ok : d.recommendedTransport === "http" ? warn : fail2;
|
|
16571
|
+
recColour("Recommended transport", d.recommendedTransport);
|
|
16572
|
+
if (d.lastProbeAt) {
|
|
16573
|
+
ui.line(` Last transport probe: ${formatRelative(d.lastProbeAt)}`);
|
|
16574
|
+
}
|
|
16575
|
+
}
|
|
16576
|
+
function formatRelative(iso, now = /* @__PURE__ */ new Date()) {
|
|
16577
|
+
const ts = Date.parse(iso);
|
|
16578
|
+
if (!Number.isFinite(ts)) return "unknown";
|
|
16579
|
+
const diff = Math.max(0, now.getTime() - ts);
|
|
16580
|
+
if (diff < 3e4) return "just now";
|
|
16581
|
+
if (diff < 6e4) return `${Math.round(diff / 1e3)}s ago`;
|
|
16582
|
+
if (diff < 36e5) return `${Math.round(diff / 6e4)}m ago`;
|
|
16583
|
+
if (diff < 864e5) return `${Math.round(diff / 36e5)}h ago`;
|
|
16584
|
+
return `${Math.round(diff / 864e5)}d ago`;
|
|
14739
16585
|
}
|
|
14740
16586
|
|
|
14741
16587
|
// src/cli/commands/task.ts
|
|
14742
16588
|
import { Command as Command18 } from "commander";
|
|
14743
|
-
import { setTimeout as
|
|
16589
|
+
import { setTimeout as sleep6 } from "timers/promises";
|
|
14744
16590
|
init_util();
|
|
14745
16591
|
function describeStatus2(status) {
|
|
14746
16592
|
return status === 0 ? "network error" : `HTTP ${status}`;
|
|
@@ -14823,7 +16669,7 @@ function unwrap2(raw) {
|
|
|
14823
16669
|
async function pollUntilTerminal(cloud, taskId, timeoutMs) {
|
|
14824
16670
|
const deadline = Date.now() + (timeoutMs ?? 5 * 6e4);
|
|
14825
16671
|
while (Date.now() < deadline) {
|
|
14826
|
-
await
|
|
16672
|
+
await sleep6(1e3);
|
|
14827
16673
|
const res = await cloud.request(
|
|
14828
16674
|
"GET",
|
|
14829
16675
|
`/api/im/tasks/${encodeURIComponent(taskId)}`
|
|
@@ -15098,7 +16944,7 @@ async function readResponseError(res) {
|
|
|
15098
16944
|
|
|
15099
16945
|
// src/cli/index.ts
|
|
15100
16946
|
init_ui();
|
|
15101
|
-
var VERSION = "2.0.
|
|
16947
|
+
var VERSION = "2.0.5";
|
|
15102
16948
|
function buildProgram() {
|
|
15103
16949
|
const program = new Command20("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
|
|
15104
16950
|
program.addCommand(buildBannerCommand());
|