@prismer/runtime 2.0.4 → 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 +2067 -251
- package/dist/cli.js +1948 -132
- package/dist/index.cjs +2077 -259
- package/dist/index.d.cts +533 -181
- package/dist/index.d.ts +533 -181
- package/dist/index.js +1962 -144
- 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
|
});
|
|
@@ -2299,7 +2633,7 @@ var require_package = __commonJS({
|
|
|
2299
2633
|
"package.json"(exports, module) {
|
|
2300
2634
|
module.exports = {
|
|
2301
2635
|
name: "@prismer/runtime",
|
|
2302
|
-
version: "2.0.
|
|
2636
|
+
version: "2.0.5",
|
|
2303
2637
|
description: "Prismer Cloud daemon runtime \u2014 TS-only adapter host for hosted IM agents",
|
|
2304
2638
|
type: "module",
|
|
2305
2639
|
main: "dist/index.js",
|
|
@@ -2333,6 +2667,7 @@ var require_package = __commonJS({
|
|
|
2333
2667
|
},
|
|
2334
2668
|
dependencies: {
|
|
2335
2669
|
"@iarna/toml": "^2.2.5",
|
|
2670
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
2336
2671
|
"better-sqlite3": "^11.8.0",
|
|
2337
2672
|
commander: "^12.1.0",
|
|
2338
2673
|
qrcode: "^1.5.4",
|
|
@@ -2394,7 +2729,7 @@ __export(util_exports, {
|
|
|
2394
2729
|
warn: () => warn,
|
|
2395
2730
|
writePidFile: () => writePidFile
|
|
2396
2731
|
});
|
|
2397
|
-
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";
|
|
2398
2733
|
import { join as join15 } from "path";
|
|
2399
2734
|
function color(kind, text) {
|
|
2400
2735
|
if (process.env.NO_COLOR === "1" || process.env.NO_COLOR === "true") return text;
|
|
@@ -2492,7 +2827,7 @@ function pidFilePath(paths) {
|
|
|
2492
2827
|
return join15(paths.root, "daemon.pid");
|
|
2493
2828
|
}
|
|
2494
2829
|
function writePidFile(paths, pid) {
|
|
2495
|
-
|
|
2830
|
+
writeFileSync7(pidFilePath(paths), `${pid}
|
|
2496
2831
|
`, "utf8");
|
|
2497
2832
|
}
|
|
2498
2833
|
function readPidFile(paths) {
|
|
@@ -2715,9 +3050,9 @@ var WsClient = class extends EventEmitter {
|
|
|
2715
3050
|
|
|
2716
3051
|
// src/sync/store.ts
|
|
2717
3052
|
import Database2 from "better-sqlite3";
|
|
2718
|
-
import { existsSync as existsSync2, mkdirSync as
|
|
3053
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
2719
3054
|
import { dirname as dirname2 } from "path";
|
|
2720
|
-
var SCHEMA_VERSION =
|
|
3055
|
+
var SCHEMA_VERSION = 4;
|
|
2721
3056
|
var MIGRATIONS = [
|
|
2722
3057
|
{
|
|
2723
3058
|
version: 1,
|
|
@@ -2843,6 +3178,36 @@ var MIGRATIONS = [
|
|
|
2843
3178
|
CREATE INDEX IF NOT EXISTS idx_asset_meta_seq
|
|
2844
3179
|
ON asset_metadata_index(workspace_id, asset_index_seq);
|
|
2845
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
|
+
`
|
|
2846
3211
|
}
|
|
2847
3212
|
];
|
|
2848
3213
|
function runSql(db, sql) {
|
|
@@ -2850,7 +3215,7 @@ function runSql(db, sql) {
|
|
|
2850
3215
|
}
|
|
2851
3216
|
function openLocalDb(path9) {
|
|
2852
3217
|
if (path9 !== ":memory:" && !existsSync2(dirname2(path9))) {
|
|
2853
|
-
|
|
3218
|
+
mkdirSync3(dirname2(path9), { recursive: true });
|
|
2854
3219
|
}
|
|
2855
3220
|
const db = new Database2(path9);
|
|
2856
3221
|
db.pragma("journal_mode = WAL");
|
|
@@ -3087,7 +3452,7 @@ function listRoleTemplates() {
|
|
|
3087
3452
|
|
|
3088
3453
|
// src/config.ts
|
|
3089
3454
|
import * as TOML from "@iarna/toml";
|
|
3090
|
-
import { existsSync as existsSync3, mkdirSync as
|
|
3455
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
3091
3456
|
import { homedir as homedir3 } from "os";
|
|
3092
3457
|
import { dirname as dirname3, join as join4 } from "path";
|
|
3093
3458
|
import { z as z3 } from "zod";
|
|
@@ -3154,13 +3519,13 @@ function loadConfig(paths = resolvePaths()) {
|
|
|
3154
3519
|
}
|
|
3155
3520
|
function saveConfig(config, paths = resolvePaths()) {
|
|
3156
3521
|
if (!existsSync3(paths.root)) {
|
|
3157
|
-
|
|
3522
|
+
mkdirSync4(paths.root, { recursive: true });
|
|
3158
3523
|
}
|
|
3159
3524
|
if (!existsSync3(dirname3(paths.configFile))) {
|
|
3160
|
-
|
|
3525
|
+
mkdirSync4(dirname3(paths.configFile), { recursive: true });
|
|
3161
3526
|
}
|
|
3162
3527
|
ConfigSchema.parse(config);
|
|
3163
|
-
|
|
3528
|
+
writeFileSync3(paths.configFile, TOML.stringify(config), "utf8");
|
|
3164
3529
|
}
|
|
3165
3530
|
function deriveWsUrl(httpBase) {
|
|
3166
3531
|
const u = new URL(httpBase);
|
|
@@ -3333,7 +3698,7 @@ function envelope(type, payload, requestId) {
|
|
|
3333
3698
|
// src/asset-cache.ts
|
|
3334
3699
|
import { createHash as createHash2 } from "crypto";
|
|
3335
3700
|
import { promises as dnsp } from "dns";
|
|
3336
|
-
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";
|
|
3337
3702
|
import { isIP, isIPv4, isIPv6 } from "net";
|
|
3338
3703
|
import { dirname as dirname4, join as join5 } from "path";
|
|
3339
3704
|
import { Agent as UndiciAgent } from "undici";
|
|
@@ -3359,7 +3724,7 @@ var AssetCache = class {
|
|
|
3359
3724
|
this.cacheDir = opts.cacheDir;
|
|
3360
3725
|
this.maxBytes = opts.maxBytes ?? 5 * 1024 * 1024 * 1024;
|
|
3361
3726
|
if (!existsSync4(this.cacheDir)) {
|
|
3362
|
-
|
|
3727
|
+
mkdirSync5(this.cacheDir, { recursive: true });
|
|
3363
3728
|
}
|
|
3364
3729
|
}
|
|
3365
3730
|
/** Local file path for a content hash. Files split by first 2 chars for fs scaling. */
|
|
@@ -3434,10 +3799,10 @@ var AssetCache = class {
|
|
|
3434
3799
|
}
|
|
3435
3800
|
const localPath = this.pathFor(hash);
|
|
3436
3801
|
if (!existsSync4(dirname4(localPath))) {
|
|
3437
|
-
|
|
3802
|
+
mkdirSync5(dirname4(localPath), { recursive: true });
|
|
3438
3803
|
}
|
|
3439
3804
|
const tmpPath = `${localPath}.${process.pid}.${Date.now()}.tmp`;
|
|
3440
|
-
|
|
3805
|
+
writeFileSync4(tmpPath, buf);
|
|
3441
3806
|
renameSync(tmpPath, localPath);
|
|
3442
3807
|
const mime = res.headers.get("content-type");
|
|
3443
3808
|
const now = Date.now();
|
|
@@ -3498,10 +3863,10 @@ var AssetCache = class {
|
|
|
3498
3863
|
}
|
|
3499
3864
|
const localPath = this.pathFor(hash);
|
|
3500
3865
|
if (!existsSync4(dirname4(localPath))) {
|
|
3501
|
-
|
|
3866
|
+
mkdirSync5(dirname4(localPath), { recursive: true });
|
|
3502
3867
|
}
|
|
3503
3868
|
const tmpPath = `${localPath}.${process.pid}.${Date.now()}.tmp`;
|
|
3504
|
-
|
|
3869
|
+
writeFileSync4(tmpPath, body);
|
|
3505
3870
|
renameSync(tmpPath, localPath);
|
|
3506
3871
|
const now = Date.now();
|
|
3507
3872
|
this.db.prepare(
|
|
@@ -3755,7 +4120,7 @@ async function fetchUrlWithGuards(rawUrl, opts) {
|
|
|
3755
4120
|
}
|
|
3756
4121
|
|
|
3757
4122
|
// src/daemon/asset/mirror.ts
|
|
3758
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
4123
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
3759
4124
|
import { join as join6 } from "path";
|
|
3760
4125
|
function rowToBinding(row) {
|
|
3761
4126
|
return {
|
|
@@ -3778,7 +4143,7 @@ var WorkspaceMirror = class {
|
|
|
3778
4143
|
this.cloud = opts.cloud;
|
|
3779
4144
|
this.workspaceId = opts.workspaceId;
|
|
3780
4145
|
if (!existsSync5(opts.workspaceStateDir)) {
|
|
3781
|
-
|
|
4146
|
+
mkdirSync6(opts.workspaceStateDir, { recursive: true });
|
|
3782
4147
|
}
|
|
3783
4148
|
this.cursorPath = join6(opts.workspaceStateDir, "asset-cursor.json");
|
|
3784
4149
|
}
|
|
@@ -3799,7 +4164,7 @@ var WorkspaceMirror = class {
|
|
|
3799
4164
|
cursor,
|
|
3800
4165
|
writtenAt: Date.now()
|
|
3801
4166
|
};
|
|
3802
|
-
|
|
4167
|
+
writeFileSync5(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
3803
4168
|
}
|
|
3804
4169
|
/**
|
|
3805
4170
|
* Pull workspace_file changes since the persisted cursor and upsert into
|
|
@@ -4208,6 +4573,262 @@ function extractHttpUrls(text) {
|
|
|
4208
4573
|
import { readFileSync as readFileSync5, promises as fsp3 } from "fs";
|
|
4209
4574
|
import * as path from "path";
|
|
4210
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
|
|
4211
4832
|
var DEFAULT_CONTEXT_MAX = 8e3;
|
|
4212
4833
|
var GOAL_CONTEXT_MAX = 4;
|
|
4213
4834
|
var MEMORY_DIGEST_MAX_BYTES = 4e3;
|
|
@@ -4223,6 +4844,8 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4223
4844
|
const { taskId, agentImUserId } = payload;
|
|
4224
4845
|
let resolvedHashes = [];
|
|
4225
4846
|
let reply;
|
|
4847
|
+
let heartbeatRef;
|
|
4848
|
+
let recorderRef;
|
|
4226
4849
|
const outboxBase = deps.paths?.runsDir ? path.join(deps.paths.runsDir, taskId) : null;
|
|
4227
4850
|
const outboxDir = outboxBase ? path.join(outboxBase, "_outbox") : null;
|
|
4228
4851
|
const workDir = outboxBase ? path.join(outboxBase, "workdir") : null;
|
|
@@ -4304,7 +4927,13 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4304
4927
|
resolvedHashes.push(...r.resolvedHashes);
|
|
4305
4928
|
rewrittenContext = payload.context.map((e, i) => ({ ...e, content: r.texts[i] }));
|
|
4306
4929
|
}
|
|
4307
|
-
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
|
+
});
|
|
4308
4937
|
resolvedHashes.push(...assetResolution.pinnedHashes);
|
|
4309
4938
|
const basePrompt = composePrompt(
|
|
4310
4939
|
rewrittenPrompt.text,
|
|
@@ -4339,9 +4968,44 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4339
4968
|
}
|
|
4340
4969
|
const operatingPrinciples = resolveOperatingPrinciples(profile.config);
|
|
4341
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);
|
|
4342
4984
|
const taskInput = {
|
|
4343
4985
|
taskId,
|
|
4344
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 } : {},
|
|
4345
5009
|
metadata: {
|
|
4346
5010
|
...payload.metadata,
|
|
4347
5011
|
conversationId: payload.conversationId,
|
|
@@ -4404,6 +5068,9 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4404
5068
|
}
|
|
4405
5069
|
}
|
|
4406
5070
|
const reaperAborted = deps.signal?.aborted && result.error?.code === "task_cancelled";
|
|
5071
|
+
const approvalRequested = Boolean(
|
|
5072
|
+
result.metadata?.approvalRequested
|
|
5073
|
+
);
|
|
4407
5074
|
let collectedAssetIds = [];
|
|
4408
5075
|
if (deps.outboxWatcher && outboxDir) {
|
|
4409
5076
|
try {
|
|
@@ -4415,14 +5082,23 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4415
5082
|
collectedAssetIds = deps.outboxWatcher.flushPending(taskId);
|
|
4416
5083
|
}
|
|
4417
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;
|
|
4418
5092
|
reply = {
|
|
4419
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.
|
|
4420
5099
|
ok: result.ok,
|
|
4421
5100
|
output: result.output,
|
|
4422
|
-
error:
|
|
4423
|
-
code: "daemon_task_timeout",
|
|
4424
|
-
message: `Daemon-side reaper aborted after ${reaperLimitMs}ms inactivity (no progress events from adapter)`
|
|
4425
|
-
} : result.error,
|
|
5101
|
+
error: finalError,
|
|
4426
5102
|
...collectedAssetIds.length > 0 ? { assetIds: collectedAssetIds } : {},
|
|
4427
5103
|
metrics: result.metrics,
|
|
4428
5104
|
...assetResolution.observability.length > 0 || urlObservations.length > 0 ? { assetObservability: [...assetResolution.observability, ...urlObservations] } : {}
|
|
@@ -4444,6 +5120,18 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4444
5120
|
}
|
|
4445
5121
|
};
|
|
4446
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
|
+
}
|
|
4447
5135
|
for (const hash of new Set(resolvedHashes)) {
|
|
4448
5136
|
try {
|
|
4449
5137
|
deps.assetCache.unpin(hash);
|
|
@@ -4520,6 +5208,10 @@ function isTextLikeMime(mime) {
|
|
|
4520
5208
|
if (m.endsWith("+json") || m.endsWith("+xml") || m.endsWith("+csv")) return true;
|
|
4521
5209
|
return false;
|
|
4522
5210
|
}
|
|
5211
|
+
function isImageMime(mime) {
|
|
5212
|
+
if (!mime) return false;
|
|
5213
|
+
return mime.toLowerCase().split(";")[0].trim().startsWith("image/");
|
|
5214
|
+
}
|
|
4523
5215
|
var HASH_REF_RE = /(?:^|\s)#([^\s#]+)/g;
|
|
4524
5216
|
var HEX_COLOR_RE = /^[0-9a-fA-F]{3,8}$/;
|
|
4525
5217
|
var FILE_EXT_RE = /\.[a-zA-Z0-9]{1,10}$/;
|
|
@@ -4598,8 +5290,34 @@ async function resolveHashRefs(prompt, assetIndex, cloud) {
|
|
|
4598
5290
|
}
|
|
4599
5291
|
return { text: result, resolutions };
|
|
4600
5292
|
}
|
|
4601
|
-
|
|
4602
|
-
|
|
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
|
+
};
|
|
4603
5321
|
if (!refs || refs.length === 0) return out;
|
|
4604
5322
|
for (const ref of refs) {
|
|
4605
5323
|
let cached;
|
|
@@ -4664,7 +5382,42 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
4664
5382
|
continue;
|
|
4665
5383
|
}
|
|
4666
5384
|
}
|
|
4667
|
-
|
|
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
|
+
}
|
|
4668
5421
|
out.observability.push({
|
|
4669
5422
|
assetId: ref.assetId,
|
|
4670
5423
|
contentHash: ref.contentHash,
|
|
@@ -4675,6 +5428,110 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
4675
5428
|
}
|
|
4676
5429
|
return out;
|
|
4677
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
|
+
}
|
|
4678
5535
|
function logAssetResolveFailure(taskId, ref, error, mime = ref.mime) {
|
|
4679
5536
|
const message = error.replace(/\s+/g, " ").slice(0, 500);
|
|
4680
5537
|
process.stderr.write(
|
|
@@ -4689,8 +5546,19 @@ function formatInlineAssetBlock(ref, mime, body, strategy) {
|
|
|
4689
5546
|
${body}
|
|
4690
5547
|
---`;
|
|
4691
5548
|
}
|
|
4692
|
-
function
|
|
4693
|
-
|
|
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)}`;
|
|
4694
5562
|
}
|
|
4695
5563
|
function formatErrorAssetBlock(ref, mime, error) {
|
|
4696
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.`;
|
|
@@ -5175,6 +6043,7 @@ import { createServer } from "http";
|
|
|
5175
6043
|
import { randomUUID, createHash as createHash4, createHmac, timingSafeEqual as cryptoTimingSafeEqual } from "crypto";
|
|
5176
6044
|
import { promises as fs } from "fs";
|
|
5177
6045
|
import * as path2 from "path";
|
|
6046
|
+
import { homedir as homedir4 } from "os";
|
|
5178
6047
|
var LocalServer = class {
|
|
5179
6048
|
constructor(opts) {
|
|
5180
6049
|
this.opts = opts;
|
|
@@ -5286,6 +6155,11 @@ var LocalServer = class {
|
|
|
5286
6155
|
void this.handleSnapshot(req, res);
|
|
5287
6156
|
return;
|
|
5288
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
|
+
}
|
|
5289
6163
|
if (req.method === "POST" && url === "/local/asset/write") {
|
|
5290
6164
|
void this.handleAssetWrite(req, res);
|
|
5291
6165
|
return;
|
|
@@ -5363,6 +6237,54 @@ var LocalServer = class {
|
|
|
5363
6237
|
}
|
|
5364
6238
|
void req;
|
|
5365
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
|
+
}
|
|
5366
6288
|
/**
|
|
5367
6289
|
* POST /local/asset/write — agent-gen adapter RPC.
|
|
5368
6290
|
*
|
|
@@ -5634,6 +6556,39 @@ function validateAgentDispatchRequest(body) {
|
|
|
5634
6556
|
if (payload.attachments != null && !Array.isArray(payload.attachments)) return "attachments must be an array";
|
|
5635
6557
|
return null;
|
|
5636
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
|
+
}
|
|
5637
6592
|
async function walkAndDigest(root, current) {
|
|
5638
6593
|
const out = [];
|
|
5639
6594
|
let entries;
|
|
@@ -5677,7 +6632,7 @@ import { EventEmitter as EventEmitter3 } from "events";
|
|
|
5677
6632
|
import { createRequire as createRequire2 } from "module";
|
|
5678
6633
|
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
5679
6634
|
import { mkdir } from "fs/promises";
|
|
5680
|
-
import { homedir as
|
|
6635
|
+
import { homedir as homedir7, platform } from "os";
|
|
5681
6636
|
import { join as join14 } from "path";
|
|
5682
6637
|
|
|
5683
6638
|
// src/adapters/claude-code/index.ts
|
|
@@ -7775,7 +8730,7 @@ function parsePrismerUri(uri) {
|
|
|
7775
8730
|
}
|
|
7776
8731
|
|
|
7777
8732
|
// src/daemon/asset/metadata-index.ts
|
|
7778
|
-
import { existsSync as existsSync7, mkdirSync as
|
|
8733
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
7779
8734
|
import { join as join11 } from "path";
|
|
7780
8735
|
var DEFAULT_LIMIT = 8;
|
|
7781
8736
|
var PULL_PAGE_SIZE = 500;
|
|
@@ -7805,7 +8760,7 @@ var AssetMetadataIndex = class {
|
|
|
7805
8760
|
this.cloud = opts.cloud;
|
|
7806
8761
|
this.workspaceId = opts.workspaceId;
|
|
7807
8762
|
if (!existsSync7(opts.workspaceStateDir)) {
|
|
7808
|
-
|
|
8763
|
+
mkdirSync8(opts.workspaceStateDir, { recursive: true });
|
|
7809
8764
|
}
|
|
7810
8765
|
this.cursorPath = join11(opts.workspaceStateDir, "asset-metadata-cursor.json");
|
|
7811
8766
|
}
|
|
@@ -7826,7 +8781,7 @@ var AssetMetadataIndex = class {
|
|
|
7826
8781
|
cursor,
|
|
7827
8782
|
writtenAt: Date.now()
|
|
7828
8783
|
};
|
|
7829
|
-
|
|
8784
|
+
writeFileSync6(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
7830
8785
|
}
|
|
7831
8786
|
/**
|
|
7832
8787
|
* Pull incremental asset metadata changes since the persisted cursor and
|
|
@@ -8153,7 +9108,7 @@ async function readJson3(req) {
|
|
|
8153
9108
|
|
|
8154
9109
|
// src/daemon/asset/origin/drop-folder.ts
|
|
8155
9110
|
import { promises as fs3 } from "fs";
|
|
8156
|
-
import { homedir as
|
|
9111
|
+
import { homedir as homedir6 } from "os";
|
|
8157
9112
|
import * as path5 from "path";
|
|
8158
9113
|
var DropFolderAdapter = class {
|
|
8159
9114
|
kind = "drop-folder";
|
|
@@ -8161,7 +9116,7 @@ var DropFolderAdapter = class {
|
|
|
8161
9116
|
rootDir;
|
|
8162
9117
|
constructor(opts = {}) {
|
|
8163
9118
|
this.workspaceDir = opts.workspaceDir;
|
|
8164
|
-
this.rootDir = opts.rootDir ?? path5.join(opts.homeDir ??
|
|
9119
|
+
this.rootDir = opts.rootDir ?? path5.join(opts.homeDir ?? homedir6(), ".prismer");
|
|
8165
9120
|
}
|
|
8166
9121
|
/**
|
|
8167
9122
|
* Long-lived async iterable wrapping `scanOnce` on a 1s polling interval.
|
|
@@ -8171,7 +9126,7 @@ var DropFolderAdapter = class {
|
|
|
8171
9126
|
while (true) {
|
|
8172
9127
|
const batch = await this.scanOnce(workspaceId);
|
|
8173
9128
|
for (const obs of batch) yield obs;
|
|
8174
|
-
await
|
|
9129
|
+
await sleep2(1e3);
|
|
8175
9130
|
}
|
|
8176
9131
|
}
|
|
8177
9132
|
/**
|
|
@@ -8320,7 +9275,7 @@ function mimeFromExtension(p) {
|
|
|
8320
9275
|
const ext = path5.extname(p).toLowerCase();
|
|
8321
9276
|
return MIME_BY_EXT[ext] ?? null;
|
|
8322
9277
|
}
|
|
8323
|
-
function
|
|
9278
|
+
function sleep2(ms) {
|
|
8324
9279
|
return new Promise((r) => setTimeout(r, ms));
|
|
8325
9280
|
}
|
|
8326
9281
|
|
|
@@ -9025,15 +9980,126 @@ function normalizeMime(mime) {
|
|
|
9025
9980
|
const normalized = mime.toLowerCase().split(";")[0]?.trim();
|
|
9026
9981
|
return normalized || null;
|
|
9027
9982
|
}
|
|
9028
|
-
function extensionOf(filename) {
|
|
9029
|
-
const name = filename.toLowerCase().trim();
|
|
9030
|
-
const idx = name.lastIndexOf(".");
|
|
9031
|
-
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;
|
|
9032
10097
|
}
|
|
9033
10098
|
|
|
9034
10099
|
// src/daemon/outbox-watcher.ts
|
|
9035
10100
|
var DEFAULT_INTERVAL_MS = 2e3;
|
|
9036
10101
|
var RESERVED_SUBDIR = "_uploaded";
|
|
10102
|
+
var REJECTED_SUBDIR = "_rejected";
|
|
9037
10103
|
var OutboxWatcher = class {
|
|
9038
10104
|
constructor(opts) {
|
|
9039
10105
|
this.opts = opts;
|
|
@@ -9208,6 +10274,7 @@ var OutboxWatcher = class {
|
|
|
9208
10274
|
for (const name of entries) {
|
|
9209
10275
|
if (name.startsWith(".")) continue;
|
|
9210
10276
|
if (name === RESERVED_SUBDIR) continue;
|
|
10277
|
+
if (name === REJECTED_SUBDIR) continue;
|
|
9211
10278
|
const full = path7.join(dir, name);
|
|
9212
10279
|
let st;
|
|
9213
10280
|
try {
|
|
@@ -9222,10 +10289,77 @@ var OutboxWatcher = class {
|
|
|
9222
10289
|
await this.upload(full, st, kind, task);
|
|
9223
10290
|
this.uploaded.add(key);
|
|
9224
10291
|
} catch (err) {
|
|
9225
|
-
|
|
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
|
+
}
|
|
9226
10303
|
}
|
|
9227
10304
|
}
|
|
9228
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
|
+
}
|
|
9229
10363
|
async upload(filePath, st, kind, task) {
|
|
9230
10364
|
const wsId = this.opts.workspaceId();
|
|
9231
10365
|
if (!wsId) {
|
|
@@ -9243,6 +10377,11 @@ var OutboxWatcher = class {
|
|
|
9243
10377
|
if (!policy.ok) {
|
|
9244
10378
|
throw new Error(`agent output rejected: ${policy.reason}`);
|
|
9245
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
|
+
}
|
|
9246
10385
|
const metadata = {
|
|
9247
10386
|
taskId: task.taskId,
|
|
9248
10387
|
...task.adapter ? { adapter: task.adapter } : {}
|
|
@@ -9446,6 +10585,352 @@ ${input.stderr}` : null
|
|
|
9446
10585
|
|
|
9447
10586
|
// src/daemon/runner.ts
|
|
9448
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
|
|
9449
10934
|
var DEFAULT_LOCAL_PORT = 3210;
|
|
9450
10935
|
var log7 = createLogger("Daemon");
|
|
9451
10936
|
var assetMetaLog = createLogger("AssetMeta");
|
|
@@ -9493,6 +10978,14 @@ var Runner = class extends EventEmitter3 {
|
|
|
9493
10978
|
// F16 (2026-05-20) — periodic skill resync timer (default 10min).
|
|
9494
10979
|
skillResyncTimer;
|
|
9495
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;
|
|
9496
10989
|
async start() {
|
|
9497
10990
|
if (this.state !== "idle") throw new Error(`Runner already in state ${this.state}`);
|
|
9498
10991
|
this.state = "starting";
|
|
@@ -9507,6 +11000,7 @@ var Runner = class extends EventEmitter3 {
|
|
|
9507
11000
|
process.env.PRISMER_API_KEY = this.config.api_key;
|
|
9508
11001
|
this.db = openLocalDb(this.paths.localDb);
|
|
9509
11002
|
this.cloud = new CloudClient({ baseUrl: this.config.cloud_api_base, apiKey: this.config.api_key });
|
|
11003
|
+
this.pendingReplyCache = createPendingReplyCache(this.db);
|
|
9510
11004
|
this.assetCache = new AssetCache({
|
|
9511
11005
|
db: this.db,
|
|
9512
11006
|
cloud: this.cloud,
|
|
@@ -10113,6 +11607,19 @@ var Runner = class extends EventEmitter3 {
|
|
|
10113
11607
|
process.stdout.write(`[daemon] ws authenticated, sending agent.host.declare (${this.hostedAgents.size} agents)
|
|
10114
11608
|
`);
|
|
10115
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
|
+
}
|
|
10116
11623
|
return;
|
|
10117
11624
|
}
|
|
10118
11625
|
this.handleIncoming(msg);
|
|
@@ -10159,12 +11666,90 @@ var Runner = class extends EventEmitter3 {
|
|
|
10159
11666
|
case "asset.changed":
|
|
10160
11667
|
void this.onAssetChanged(msg.payload);
|
|
10161
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;
|
|
10162
11678
|
default:
|
|
10163
11679
|
this.emit("unknown-message", msg);
|
|
10164
11680
|
}
|
|
10165
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
|
+
}
|
|
10166
11745
|
async onHostAcked(payload) {
|
|
10167
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
|
+
});
|
|
10168
11753
|
if (this.memoryWiring && this.workspaceId) {
|
|
10169
11754
|
syncMemoryFromCloud(this.memoryWiring, this.cloud, [this.workspaceId]).catch(
|
|
10170
11755
|
(err) => log7.error("Initial memory sync failed", err.message)
|
|
@@ -10346,14 +11931,68 @@ var Runner = class extends EventEmitter3 {
|
|
|
10346
11931
|
};
|
|
10347
11932
|
}
|
|
10348
11933
|
async postMessageDispatchReply(payload) {
|
|
10349
|
-
const
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
|
|
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;
|
|
10356
11991
|
}
|
|
11992
|
+
process.stdout.write(
|
|
11993
|
+
`[daemon] transport-probe reported transport=${probe.transport} gateway=${probe.gatewayUrl ?? "null"} private=${probe.gatewayIsPrivate}
|
|
11994
|
+
`
|
|
11995
|
+
);
|
|
10357
11996
|
}
|
|
10358
11997
|
onAgentChanged(payload) {
|
|
10359
11998
|
const a = this.hostedAgents.get(payload.agentImUserId);
|
|
@@ -10796,7 +12435,7 @@ var Runner = class extends EventEmitter3 {
|
|
|
10796
12435
|
function resolveUserAssetWorkspaceDir(workspaceId) {
|
|
10797
12436
|
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
10798
12437
|
if (override) return join14(override, workspaceId);
|
|
10799
|
-
const home =
|
|
12438
|
+
const home = homedir7();
|
|
10800
12439
|
const desktop = join14(home, "Desktop");
|
|
10801
12440
|
const base = existsSync9(desktop) ? desktop : home;
|
|
10802
12441
|
return join14(base, "Prismer Assets", workspaceId);
|
|
@@ -10876,7 +12515,7 @@ function safeJsonParse(raw) {
|
|
|
10876
12515
|
// src/pair.ts
|
|
10877
12516
|
import { generateKeyPairSync } from "crypto";
|
|
10878
12517
|
import { hostname as hostname2 } from "os";
|
|
10879
|
-
import { setTimeout as
|
|
12518
|
+
import { setTimeout as sleep3 } from "timers/promises";
|
|
10880
12519
|
import qrcode from "qrcode";
|
|
10881
12520
|
async function pair(opts) {
|
|
10882
12521
|
const paths = opts.paths ?? resolvePaths();
|
|
@@ -10945,7 +12584,7 @@ Scan with Lumin to approve, or open: ${offer.qrUrl}
|
|
|
10945
12584
|
const pollIntervalMs = opts.pollIntervalMs ?? 5e3;
|
|
10946
12585
|
for (let i = 0; i < maxAttempts; i += 1) {
|
|
10947
12586
|
if (i > 0 || !localOnlyMode) {
|
|
10948
|
-
await
|
|
12587
|
+
await sleep3(pollIntervalMs);
|
|
10949
12588
|
}
|
|
10950
12589
|
const res = await cloud.request(
|
|
10951
12590
|
"GET",
|
|
@@ -10989,13 +12628,13 @@ import { spawnSync as spawnSync2 } from "child_process";
|
|
|
10989
12628
|
import {
|
|
10990
12629
|
copyFileSync,
|
|
10991
12630
|
existsSync as existsSync12,
|
|
10992
|
-
mkdirSync as
|
|
12631
|
+
mkdirSync as mkdirSync9,
|
|
10993
12632
|
readFileSync as readFileSync10,
|
|
10994
12633
|
statSync as statSync3,
|
|
10995
|
-
writeFileSync as
|
|
12634
|
+
writeFileSync as writeFileSync8
|
|
10996
12635
|
} from "fs";
|
|
10997
|
-
import { homedir as
|
|
10998
|
-
import { dirname as
|
|
12636
|
+
import { homedir as homedir8 } from "os";
|
|
12637
|
+
import { dirname as dirname10, join as join16 } from "path";
|
|
10999
12638
|
import { Command } from "commander";
|
|
11000
12639
|
init_hermes();
|
|
11001
12640
|
init_openclaw();
|
|
@@ -11010,7 +12649,7 @@ var INSTALL_SPECS = {
|
|
|
11010
12649
|
binary: "claude",
|
|
11011
12650
|
hint: "Set ANTHROPIC_API_KEY in your shell profile, or rely on Claude Code OAuth login. Run `claude login` to sign in.",
|
|
11012
12651
|
authHints: ["ANTHROPIC_API_KEY or Claude Code OAuth login (`claude login`)"],
|
|
11013
|
-
hookTarget: { path: join16(
|
|
12652
|
+
hookTarget: { path: join16(homedir8(), ".claude", "hooks.json"), kind: "json-file" }
|
|
11014
12653
|
},
|
|
11015
12654
|
openclaw: {
|
|
11016
12655
|
name: "openclaw",
|
|
@@ -11019,7 +12658,7 @@ var INSTALL_SPECS = {
|
|
|
11019
12658
|
binary: "openclaw",
|
|
11020
12659
|
hint: "OpenClaw runs as a gateway. Configure ~/.openclaw/openclaw.json and start it with `openclaw gateway` before tasks dispatch.",
|
|
11021
12660
|
authHints: ["~/.openclaw/openclaw.json gateway.auth.bearerTokens", "Prismer daemon api_key"],
|
|
11022
|
-
hookTarget: { path: join16(
|
|
12661
|
+
hookTarget: { path: join16(homedir8(), ".openclaw", "hooks"), kind: "directory" }
|
|
11023
12662
|
},
|
|
11024
12663
|
codex: {
|
|
11025
12664
|
name: "codex",
|
|
@@ -11028,7 +12667,7 @@ var INSTALL_SPECS = {
|
|
|
11028
12667
|
binary: "codex",
|
|
11029
12668
|
hint: "Set OPENAI_API_KEY in your shell profile. Verify with `codex --help`.",
|
|
11030
12669
|
authHints: ["OPENAI_API_KEY or Codex CLI account login"],
|
|
11031
|
-
hookTarget: { path: join16(
|
|
12670
|
+
hookTarget: { path: join16(homedir8(), ".codex", "hooks.json"), kind: "json-file" }
|
|
11032
12671
|
},
|
|
11033
12672
|
hermes: {
|
|
11034
12673
|
name: "hermes",
|
|
@@ -11037,7 +12676,7 @@ var INSTALL_SPECS = {
|
|
|
11037
12676
|
binary: "hermes",
|
|
11038
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.",
|
|
11039
12678
|
authHints: ["~/.hermes/.env API_SERVER_KEY", "Hermes profile provider credentials"],
|
|
11040
|
-
hookTarget: { path: join16(
|
|
12679
|
+
hookTarget: { path: join16(homedir8(), ".hermes", "hooks.json"), kind: "json-file" }
|
|
11041
12680
|
}
|
|
11042
12681
|
};
|
|
11043
12682
|
function buildAdapterCommand() {
|
|
@@ -11252,15 +12891,15 @@ function runHooks(spec, opts) {
|
|
|
11252
12891
|
}
|
|
11253
12892
|
let backup;
|
|
11254
12893
|
if (planned.kind === "directory") {
|
|
11255
|
-
|
|
12894
|
+
mkdirSync9(planned.path, { recursive: true });
|
|
11256
12895
|
const markerPath = join16(planned.path, "prismer.json");
|
|
11257
12896
|
backup = backupIfExists(markerPath);
|
|
11258
|
-
|
|
12897
|
+
writeFileSync8(markerPath, JSON.stringify(prismerHookMarker(spec), null, 2) + "\n", "utf8");
|
|
11259
12898
|
} else {
|
|
11260
|
-
|
|
12899
|
+
mkdirSync9(dirname10(planned.path), { recursive: true });
|
|
11261
12900
|
backup = backupIfExists(planned.path);
|
|
11262
12901
|
const merged = mergeHookJson(planned.path, spec);
|
|
11263
|
-
|
|
12902
|
+
writeFileSync8(planned.path, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
11264
12903
|
}
|
|
11265
12904
|
return {
|
|
11266
12905
|
ok: true,
|
|
@@ -11327,7 +12966,7 @@ function inspectAuthEnv(spec) {
|
|
|
11327
12966
|
hints: spec.authHints ?? []
|
|
11328
12967
|
};
|
|
11329
12968
|
case "hermes": {
|
|
11330
|
-
const envFile = join16(
|
|
12969
|
+
const envFile = join16(homedir8(), ".hermes", ".env");
|
|
11331
12970
|
return {
|
|
11332
12971
|
ok: existsSync12(envFile) || Boolean(process.env.HERMES_API_KEY || process.env.API_SERVER_KEY),
|
|
11333
12972
|
present: [
|
|
@@ -11338,7 +12977,7 @@ function inspectAuthEnv(spec) {
|
|
|
11338
12977
|
};
|
|
11339
12978
|
}
|
|
11340
12979
|
case "openclaw": {
|
|
11341
|
-
const cfgFile = join16(
|
|
12980
|
+
const cfgFile = join16(homedir8(), ".openclaw", "openclaw.json");
|
|
11342
12981
|
return {
|
|
11343
12982
|
ok: existsSync12(cfgFile) || Boolean(process.env.OPENCLAW_API_KEY),
|
|
11344
12983
|
present: [
|
|
@@ -11358,7 +12997,7 @@ function inspectHook(spec) {
|
|
|
11358
12997
|
return { supported: false, markerPresent: false };
|
|
11359
12998
|
}
|
|
11360
12999
|
if (spec.name === "openclaw") {
|
|
11361
|
-
const jsonPath = join16(
|
|
13000
|
+
const jsonPath = join16(homedir8(), ".openclaw", "hooks.json");
|
|
11362
13001
|
const markerPath = join16(target.path, "prismer.json");
|
|
11363
13002
|
const jsonMarkerPresent = existsSync12(jsonPath) && fileContainsPrismerMarker(jsonPath);
|
|
11364
13003
|
const dirMarkerPresent = existsSync12(markerPath) && fileContainsPrismerMarker(markerPath);
|
|
@@ -11820,8 +13459,74 @@ function buildAgentCommand() {
|
|
|
11820
13459
|
}
|
|
11821
13460
|
getUI().ok("Skill install requested", `agent=${opts.agent} skill=${slugOrId}`);
|
|
11822
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" }));
|
|
11823
13510
|
return cmd;
|
|
11824
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
|
+
}
|
|
11825
13530
|
function readLocalAgents(localDb) {
|
|
11826
13531
|
const db = openLocalDb(localDb);
|
|
11827
13532
|
try {
|
|
@@ -11926,8 +13631,8 @@ function whichBinary2(bin) {
|
|
|
11926
13631
|
// src/cli/commands/asset.ts
|
|
11927
13632
|
import { createHash as createHash8 } from "crypto";
|
|
11928
13633
|
import { Command as Command3 } from "commander";
|
|
11929
|
-
import { existsSync as existsSync13, lstatSync, readdirSync, readFileSync as readFileSync11, writeFileSync as
|
|
11930
|
-
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";
|
|
11931
13636
|
init_util();
|
|
11932
13637
|
init_ui();
|
|
11933
13638
|
function buildAssetCommand() {
|
|
@@ -12110,7 +13815,7 @@ async function uploadAssetPath(inputPath, opts) {
|
|
|
12110
13815
|
if (files.length === 0) exitWithError(`directory has no uploadable files: ${inputPath}`);
|
|
12111
13816
|
const items = [];
|
|
12112
13817
|
for (const file of files) {
|
|
12113
|
-
const relDir =
|
|
13818
|
+
const relDir = dirname11(relative4(inputPath, file));
|
|
12114
13819
|
const folderPath = combineFolderPath(opts.folderPath, relDir === "." ? "" : relDir);
|
|
12115
13820
|
const response = await uploadAsset(file, { ...opts, folderPath });
|
|
12116
13821
|
items.push({ file, folderPath: folderPath ?? null, response });
|
|
@@ -12293,7 +13998,7 @@ async function downloadAsset(assetId, outPath) {
|
|
|
12293
13998
|
exitWithError(`asset download failed (${res.status}): ${errorMessage(body)}`);
|
|
12294
13999
|
}
|
|
12295
14000
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
12296
|
-
|
|
14001
|
+
writeFileSync9(outPath, bytes);
|
|
12297
14002
|
}
|
|
12298
14003
|
function parseMetadata(raw) {
|
|
12299
14004
|
if (!raw) return {};
|
|
@@ -12692,7 +14397,7 @@ function buildCookbookCommand() {
|
|
|
12692
14397
|
for (const suite of suites) {
|
|
12693
14398
|
checks.push(...await runSuite(suite, cloud, opts));
|
|
12694
14399
|
}
|
|
12695
|
-
const summary =
|
|
14400
|
+
const summary = summarize2(checks, Boolean(opts.strict));
|
|
12696
14401
|
if (opts.json) {
|
|
12697
14402
|
printJson(summary);
|
|
12698
14403
|
} else {
|
|
@@ -12857,7 +14562,7 @@ function parseSuites(raw) {
|
|
|
12857
14562
|
const expanded = selected.includes("all") ? ["status", "im", "task", "group", "asset", "sandbox"] : selected;
|
|
12858
14563
|
return [...new Set(expanded)];
|
|
12859
14564
|
}
|
|
12860
|
-
function
|
|
14565
|
+
function summarize2(checks, strict) {
|
|
12861
14566
|
const totals = { pass: 0, fail: 0, skip: 0 };
|
|
12862
14567
|
for (const check of checks) totals[check.status] += 1;
|
|
12863
14568
|
return {
|
|
@@ -12940,8 +14645,8 @@ function parsePositiveInt3(value) {
|
|
|
12940
14645
|
// src/cli/commands/daemon.ts
|
|
12941
14646
|
import { Command as Command8 } from "commander";
|
|
12942
14647
|
import { spawn as spawn5 } from "child_process";
|
|
12943
|
-
import { createReadStream, existsSync as existsSync14, mkdirSync as
|
|
12944
|
-
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";
|
|
12945
14650
|
import { join as join18 } from "path";
|
|
12946
14651
|
init_util();
|
|
12947
14652
|
init_ui();
|
|
@@ -12984,7 +14689,7 @@ function buildDaemonCommand() {
|
|
|
12984
14689
|
while (!configExists(paths)) {
|
|
12985
14690
|
if (stopRequested) return;
|
|
12986
14691
|
getUI().info(`[daemon] waiting for ${paths.configFile} \u2014 run \`prismer setup\` to create it`);
|
|
12987
|
-
await
|
|
14692
|
+
await sleep4(5e3);
|
|
12988
14693
|
}
|
|
12989
14694
|
runner = new Runner({
|
|
12990
14695
|
startLocalServer: opts.localServer !== false,
|
|
@@ -13016,7 +14721,7 @@ function buildDaemonCommand() {
|
|
|
13016
14721
|
if (existingPid && pidAlive2(existingPid)) {
|
|
13017
14722
|
exitWithError(`Daemon already running (pid ${existingPid}). Use \`prismer daemon stop\` first.`);
|
|
13018
14723
|
}
|
|
13019
|
-
if (!existsSync14(paths.logsDir))
|
|
14724
|
+
if (!existsSync14(paths.logsDir)) mkdirSync10(paths.logsDir, { recursive: true });
|
|
13020
14725
|
const logFile = join18(paths.logsDir, "daemon.log");
|
|
13021
14726
|
const fd = openSync2(logFile, "a");
|
|
13022
14727
|
const args = [process.argv[1], "daemon", "run"];
|
|
@@ -13030,7 +14735,7 @@ function buildDaemonCommand() {
|
|
|
13030
14735
|
child.unref();
|
|
13031
14736
|
const deadline = Date.now() + 5e3;
|
|
13032
14737
|
while (Date.now() < deadline) {
|
|
13033
|
-
await
|
|
14738
|
+
await sleep4(200);
|
|
13034
14739
|
const pid = readPidFile(paths);
|
|
13035
14740
|
if (pid && pidAlive2(pid)) {
|
|
13036
14741
|
if (opts.json) printJson({ ok: true, pid, logFile });
|
|
@@ -13064,7 +14769,7 @@ function buildDaemonCommand() {
|
|
|
13064
14769
|
getUI().ok("Daemon stopped", `pid ${pid}`);
|
|
13065
14770
|
return;
|
|
13066
14771
|
}
|
|
13067
|
-
await
|
|
14772
|
+
await sleep4(200);
|
|
13068
14773
|
}
|
|
13069
14774
|
process.stderr.write(`Daemon did not exit within timeout; pid ${pid} may still be alive.
|
|
13070
14775
|
`);
|
|
@@ -13145,7 +14850,7 @@ async function tailFromEnd(path9, lines) {
|
|
|
13145
14850
|
async function followFile(path9) {
|
|
13146
14851
|
let offset = statSync4(path9).size;
|
|
13147
14852
|
for (; ; ) {
|
|
13148
|
-
await
|
|
14853
|
+
await sleep4(1e3);
|
|
13149
14854
|
const size = statSync4(path9).size;
|
|
13150
14855
|
if (size < offset) offset = 0;
|
|
13151
14856
|
if (size === offset) continue;
|
|
@@ -13159,7 +14864,7 @@ async function followFile(path9) {
|
|
|
13159
14864
|
init_util();
|
|
13160
14865
|
import { Command as Command9 } from "commander";
|
|
13161
14866
|
import { createReadStream as createReadStream2, existsSync as existsSync15 } from "fs";
|
|
13162
|
-
import { homedir as
|
|
14867
|
+
import { homedir as homedir9 } from "os";
|
|
13163
14868
|
import { join as join19 } from "path";
|
|
13164
14869
|
import { createInterface } from "readline";
|
|
13165
14870
|
var DEFAULT_LIMIT2 = 50;
|
|
@@ -13192,7 +14897,7 @@ function buildEventsStatsCommand() {
|
|
|
13192
14897
|
const events = await readEvents(file, filters);
|
|
13193
14898
|
printJson({
|
|
13194
14899
|
ok: true,
|
|
13195
|
-
data: { stats:
|
|
14900
|
+
data: { stats: summarize3(events), count: events.length, limit: filters.limit },
|
|
13196
14901
|
checks: { file, exists: true, filters: cleanFilters(filters) }
|
|
13197
14902
|
});
|
|
13198
14903
|
});
|
|
@@ -13201,7 +14906,7 @@ function addEventOptions(cmd) {
|
|
|
13201
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)");
|
|
13202
14907
|
}
|
|
13203
14908
|
function eventsPath() {
|
|
13204
|
-
return join19(process.env.PRISMER_HOME ?? join19(
|
|
14909
|
+
return join19(process.env.PRISMER_HOME ?? join19(homedir9(), ".prismer"), "para", "events.jsonl");
|
|
13205
14910
|
}
|
|
13206
14911
|
function unavailable(file) {
|
|
13207
14912
|
return {
|
|
@@ -13249,7 +14954,7 @@ function field(event, names) {
|
|
|
13249
14954
|
}
|
|
13250
14955
|
return void 0;
|
|
13251
14956
|
}
|
|
13252
|
-
function
|
|
14957
|
+
function summarize3(events) {
|
|
13253
14958
|
const byFamily = {};
|
|
13254
14959
|
const byType = {};
|
|
13255
14960
|
const byAgentId = {};
|
|
@@ -13603,7 +15308,7 @@ function buildPairCommand() {
|
|
|
13603
15308
|
|
|
13604
15309
|
// src/cli/commands/profile.ts
|
|
13605
15310
|
import { Command as Command12 } from "commander";
|
|
13606
|
-
import { existsSync as existsSync17, readFileSync as readFileSync12, writeFileSync as
|
|
15311
|
+
import { existsSync as existsSync17, readFileSync as readFileSync12, writeFileSync as writeFileSync10 } from "fs";
|
|
13607
15312
|
import { tmpdir } from "os";
|
|
13608
15313
|
import { join as join20 } from "path";
|
|
13609
15314
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
@@ -13654,7 +15359,7 @@ function buildProfileCommand() {
|
|
|
13654
15359
|
`/api/im/agent_profiles/${encodeURIComponent(profileId)}`
|
|
13655
15360
|
);
|
|
13656
15361
|
const tmpFile = join20(tmpdir(), `prismer-profile-${profileId}.json`);
|
|
13657
|
-
|
|
15362
|
+
writeFileSync10(tmpFile, JSON.stringify(profile.config, null, 2), "utf8");
|
|
13658
15363
|
const editor = process.env.EDITOR || "vi";
|
|
13659
15364
|
const ed = spawnSync4(editor, [tmpFile], { stdio: "inherit" });
|
|
13660
15365
|
if (ed.status !== 0) exitWithError(`editor exited ${ed.status}`, { code: "editor_failed" });
|
|
@@ -13695,10 +15400,10 @@ async function resolveDefaultWorkspaceId(cloud) {
|
|
|
13695
15400
|
// src/cli/commands/reset.ts
|
|
13696
15401
|
import { Command as Command13 } from "commander";
|
|
13697
15402
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
13698
|
-
import { existsSync as existsSync18, mkdirSync as
|
|
13699
|
-
import { homedir as
|
|
13700
|
-
import { dirname as
|
|
13701
|
-
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";
|
|
13702
15407
|
init_util();
|
|
13703
15408
|
init_ui();
|
|
13704
15409
|
function buildResetCommand() {
|
|
@@ -13751,8 +15456,8 @@ function buildResetCommand() {
|
|
|
13751
15456
|
rmSync(paths.root, { recursive: true, force: true });
|
|
13752
15457
|
} else {
|
|
13753
15458
|
if (!archivePath) throw new Error("internal: archivePath missing");
|
|
13754
|
-
if (!existsSync18(
|
|
13755
|
-
|
|
15459
|
+
if (!existsSync18(dirname12(archivePath))) {
|
|
15460
|
+
mkdirSync11(dirname12(archivePath), { recursive: true });
|
|
13756
15461
|
}
|
|
13757
15462
|
renameSync2(paths.root, archivePath);
|
|
13758
15463
|
}
|
|
@@ -13762,8 +15467,8 @@ function buildResetCommand() {
|
|
|
13762
15467
|
rmSync(assetSyncRoot, { recursive: true, force: true });
|
|
13763
15468
|
} else {
|
|
13764
15469
|
if (!assetSyncArchivePath) throw new Error("internal: assetSyncArchivePath missing");
|
|
13765
|
-
if (!existsSync18(
|
|
13766
|
-
|
|
15470
|
+
if (!existsSync18(dirname12(assetSyncArchivePath))) {
|
|
15471
|
+
mkdirSync11(dirname12(assetSyncArchivePath), { recursive: true });
|
|
13767
15472
|
}
|
|
13768
15473
|
renameSync2(assetSyncRoot, assetSyncArchivePath);
|
|
13769
15474
|
}
|
|
@@ -13773,13 +15478,13 @@ function buildResetCommand() {
|
|
|
13773
15478
|
rmSync(hermesHome, { recursive: true, force: true });
|
|
13774
15479
|
} else {
|
|
13775
15480
|
if (!hermesArchivePath) throw new Error("internal: hermesArchivePath missing");
|
|
13776
|
-
if (!existsSync18(
|
|
13777
|
-
|
|
15481
|
+
if (!existsSync18(dirname12(hermesArchivePath))) {
|
|
15482
|
+
mkdirSync11(dirname12(hermesArchivePath), { recursive: true });
|
|
13778
15483
|
}
|
|
13779
15484
|
renameSync2(hermesHome, hermesArchivePath);
|
|
13780
15485
|
}
|
|
13781
15486
|
}
|
|
13782
|
-
|
|
15487
|
+
mkdirSync11(paths.root, { recursive: true });
|
|
13783
15488
|
if (opts.json) {
|
|
13784
15489
|
printJson({ ok: true, reset: true, plan });
|
|
13785
15490
|
return;
|
|
@@ -13828,7 +15533,7 @@ async function stopProcess(pid, timeoutMs, label) {
|
|
|
13828
15533
|
const deadline = Date.now() + timeoutMs;
|
|
13829
15534
|
while (Date.now() < deadline) {
|
|
13830
15535
|
if (!pidAlive2(pid)) return;
|
|
13831
|
-
await
|
|
15536
|
+
await sleep5(100);
|
|
13832
15537
|
}
|
|
13833
15538
|
try {
|
|
13834
15539
|
process.kill(pid, "SIGKILL");
|
|
@@ -13838,7 +15543,7 @@ async function stopProcess(pid, timeoutMs, label) {
|
|
|
13838
15543
|
const killDeadline = Date.now() + 1e3;
|
|
13839
15544
|
while (Date.now() < killDeadline) {
|
|
13840
15545
|
if (!pidAlive2(pid)) return;
|
|
13841
|
-
await
|
|
15546
|
+
await sleep5(100);
|
|
13842
15547
|
}
|
|
13843
15548
|
throw new Error(`${label} pid ${pid} did not exit within ${timeoutMs + 1e3}ms`);
|
|
13844
15549
|
}
|
|
@@ -13848,13 +15553,13 @@ function timestamp() {
|
|
|
13848
15553
|
function resolveAssetSyncRoot() {
|
|
13849
15554
|
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
13850
15555
|
if (override) return override;
|
|
13851
|
-
const home =
|
|
15556
|
+
const home = homedir10();
|
|
13852
15557
|
const desktop = join21(home, "Desktop");
|
|
13853
15558
|
const base = existsSync18(desktop) ? desktop : home;
|
|
13854
15559
|
return join21(base, "Prismer Assets");
|
|
13855
15560
|
}
|
|
13856
15561
|
function resolveHermesHome() {
|
|
13857
|
-
return process.env.HERMES_HOME || join21(
|
|
15562
|
+
return process.env.HERMES_HOME || join21(homedir10(), ".hermes");
|
|
13858
15563
|
}
|
|
13859
15564
|
function findHermesGatewayPids2() {
|
|
13860
15565
|
try {
|
|
@@ -14409,7 +16114,7 @@ function buildSkillCommand() {
|
|
|
14409
16114
|
const cloud = mkCloud6();
|
|
14410
16115
|
const path9 = opts.agent ? `/api/im/agents/${encodeURIComponent(opts.agent)}/skills` : `/api/im/skills/${encodeURIComponent(slugOrId)}/install`;
|
|
14411
16116
|
const body = opts.agent ? { skillId: slugOrId, workspaceId: opts.workspaceId, version: opts.version } : { workspaceId: opts.workspaceId, version: opts.version };
|
|
14412
|
-
const data = await
|
|
16117
|
+
const data = await requestEnvelope2(cloud, "POST", path9, body, "skill_install_failed");
|
|
14413
16118
|
if (opts.json) {
|
|
14414
16119
|
printJson(data);
|
|
14415
16120
|
return;
|
|
@@ -14420,7 +16125,7 @@ function buildSkillCommand() {
|
|
|
14420
16125
|
const cloud = mkCloud6();
|
|
14421
16126
|
const path9 = opts.agent ? `/api/im/agents/${encodeURIComponent(opts.agent)}/skills` : `/api/im/skills/${encodeURIComponent(slugOrId)}/install`;
|
|
14422
16127
|
const body = opts.agent ? { skillId: slugOrId, workspaceId: opts.workspaceId } : void 0;
|
|
14423
|
-
const data = await
|
|
16128
|
+
const data = await requestEnvelope2(cloud, "DELETE", path9, body, "skill_uninstall_failed");
|
|
14424
16129
|
if (opts.json) {
|
|
14425
16130
|
printJson(data);
|
|
14426
16131
|
return;
|
|
@@ -14473,7 +16178,7 @@ function mkCloud6() {
|
|
|
14473
16178
|
const cfg = loadConfig(resolvePaths());
|
|
14474
16179
|
return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
14475
16180
|
}
|
|
14476
|
-
async function
|
|
16181
|
+
async function requestEnvelope2(cloud, method, path9, body, code) {
|
|
14477
16182
|
const res = await cloud.request(method, path9, {
|
|
14478
16183
|
body
|
|
14479
16184
|
});
|
|
@@ -14623,6 +16328,7 @@ function buildStatusCommand() {
|
|
|
14623
16328
|
let me = null;
|
|
14624
16329
|
let devices = null;
|
|
14625
16330
|
let agents = null;
|
|
16331
|
+
let diagnose = null;
|
|
14626
16332
|
try {
|
|
14627
16333
|
const meRes = await cloud.request("GET", "/api/im/me", { timeoutMs: 3e3 });
|
|
14628
16334
|
cloudOk = meRes.ok;
|
|
@@ -14635,19 +16341,33 @@ function buildStatusCommand() {
|
|
|
14635
16341
|
if (Array.isArray(wsList) && wsList.length > 0) {
|
|
14636
16342
|
const wsId = wsList[0]?.id;
|
|
14637
16343
|
if (wsId) {
|
|
14638
|
-
const
|
|
14639
|
-
if (
|
|
14640
|
-
const
|
|
14641
|
-
|
|
14642
|
-
|
|
14643
|
-
|
|
14644
|
-
|
|
14645
|
-
|
|
14646
|
-
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
|
+
}
|
|
14647
16352
|
}
|
|
14648
16353
|
}
|
|
14649
16354
|
}
|
|
14650
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
|
+
}
|
|
14651
16371
|
}
|
|
14652
16372
|
} catch {
|
|
14653
16373
|
cloudOk = false;
|
|
@@ -14664,7 +16384,8 @@ function buildStatusCommand() {
|
|
|
14664
16384
|
info: daemonStatus.info ?? {}
|
|
14665
16385
|
},
|
|
14666
16386
|
cloud: { base: cfg.cloud_api_base, reachable: cloudOk, me, devices, agents },
|
|
14667
|
-
local
|
|
16387
|
+
local,
|
|
16388
|
+
diagnose
|
|
14668
16389
|
};
|
|
14669
16390
|
if (opts.json) {
|
|
14670
16391
|
printJson(report);
|
|
@@ -14673,6 +16394,60 @@ function buildStatusCommand() {
|
|
|
14673
16394
|
printPretty(report);
|
|
14674
16395
|
});
|
|
14675
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
|
+
}
|
|
14676
16451
|
async function readDaemonStatus() {
|
|
14677
16452
|
try {
|
|
14678
16453
|
const res = await fetch("http://127.0.0.1:3210/healthz", {
|
|
@@ -14745,16 +16520,7 @@ function printPretty(report) {
|
|
|
14745
16520
|
if (Array.isArray(report.cloud.devices) && report.cloud.devices.length > 0) {
|
|
14746
16521
|
const devs = report.cloud.devices;
|
|
14747
16522
|
ui.blank();
|
|
14748
|
-
|
|
14749
|
-
for (const d of devs) {
|
|
14750
|
-
const statusIcon = d.daemonStatus === "connected" ? "\u25CF" : "\u25CB";
|
|
14751
|
-
const kind = d.runtimeKind === "docker" ? "Local" : "K8s";
|
|
14752
|
-
const declared = d.hostedAgentSummary?.declared ?? 0;
|
|
14753
|
-
ui.line(` ${statusIcon} ${d.podName?.replace(/^daemon:/, "")?.slice(0, 28) ?? "?"} kind=${kind} agents=${declared}`);
|
|
14754
|
-
}
|
|
14755
|
-
}
|
|
14756
|
-
if (Array.isArray(report.cloud.agents) && report.cloud.agents.length > 0) {
|
|
14757
|
-
ui.line(` Hosted agents: ${report.cloud.agents.length}`);
|
|
16523
|
+
for (const ln of formatDeviceBindings(devs)) ui.line(ln);
|
|
14758
16524
|
}
|
|
14759
16525
|
if (report.local) {
|
|
14760
16526
|
ui.blank();
|
|
@@ -14764,11 +16530,63 @@ function printPretty(report) {
|
|
|
14764
16530
|
} else {
|
|
14765
16531
|
warn("Local DB", "unavailable");
|
|
14766
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`;
|
|
14767
16585
|
}
|
|
14768
16586
|
|
|
14769
16587
|
// src/cli/commands/task.ts
|
|
14770
16588
|
import { Command as Command18 } from "commander";
|
|
14771
|
-
import { setTimeout as
|
|
16589
|
+
import { setTimeout as sleep6 } from "timers/promises";
|
|
14772
16590
|
init_util();
|
|
14773
16591
|
function describeStatus2(status) {
|
|
14774
16592
|
return status === 0 ? "network error" : `HTTP ${status}`;
|
|
@@ -14851,7 +16669,7 @@ function unwrap2(raw) {
|
|
|
14851
16669
|
async function pollUntilTerminal(cloud, taskId, timeoutMs) {
|
|
14852
16670
|
const deadline = Date.now() + (timeoutMs ?? 5 * 6e4);
|
|
14853
16671
|
while (Date.now() < deadline) {
|
|
14854
|
-
await
|
|
16672
|
+
await sleep6(1e3);
|
|
14855
16673
|
const res = await cloud.request(
|
|
14856
16674
|
"GET",
|
|
14857
16675
|
`/api/im/tasks/${encodeURIComponent(taskId)}`
|
|
@@ -15126,7 +16944,7 @@ async function readResponseError(res) {
|
|
|
15126
16944
|
|
|
15127
16945
|
// src/cli/index.ts
|
|
15128
16946
|
init_ui();
|
|
15129
|
-
var VERSION = "2.0.
|
|
16947
|
+
var VERSION = "2.0.5";
|
|
15130
16948
|
function buildProgram() {
|
|
15131
16949
|
const program = new Command20("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
|
|
15132
16950
|
program.addCommand(buildBannerCommand());
|