@prismer/runtime 2.0.4 → 2.0.6
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 +2123 -267
- package/dist/cli.js +2004 -148
- package/dist/index.cjs +2164 -306
- package/dist/index.d.cts +543 -182
- package/dist/index.d.ts +543 -182
- package/dist/index.js +2014 -156
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -154,6 +154,42 @@ import { fileURLToPath } from "url";
|
|
|
154
154
|
import Database from "better-sqlite3";
|
|
155
155
|
import * as YAML from "yaml";
|
|
156
156
|
import { z as z3 } from "zod";
|
|
157
|
+
function readPlainRecord(value) {
|
|
158
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
159
|
+
}
|
|
160
|
+
function readNonEmptyString(value) {
|
|
161
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
162
|
+
}
|
|
163
|
+
function sanitizeRoleSkillSlug(value) {
|
|
164
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "template";
|
|
165
|
+
}
|
|
166
|
+
function renderRoleTemplateSkill(skillName, agents, roleTemplate) {
|
|
167
|
+
const mcpServers = Array.isArray(roleTemplate?.mcpServers) ? roleTemplate.mcpServers : [];
|
|
168
|
+
const tools = /* @__PURE__ */ new Set();
|
|
169
|
+
for (const server of mcpServers) {
|
|
170
|
+
const rec = readPlainRecord(server);
|
|
171
|
+
const allowlist = Array.isArray(rec?.toolsAllowlist) ? rec.toolsAllowlist : [];
|
|
172
|
+
for (const tool of allowlist) {
|
|
173
|
+
if (typeof tool === "string" && tool.trim()) tools.add(tool.trim());
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const allowedTools = [...tools].join(" ");
|
|
177
|
+
const roleTemplateSlug = JSON.stringify(readNonEmptyString(roleTemplate?.slug) ?? null);
|
|
178
|
+
return `---
|
|
179
|
+
name: ${skillName}
|
|
180
|
+
description: Role-template operating playbook projected from Prismer RoleTemplate.
|
|
181
|
+
${allowedTools ? `allowed-tools: ${allowedTools}
|
|
182
|
+
` : ""}metadata:
|
|
183
|
+
prismer:
|
|
184
|
+
source: role-template
|
|
185
|
+
roleTemplateSlug: ${roleTemplateSlug}
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
# Role Template Playbook
|
|
189
|
+
|
|
190
|
+
${agents.trim()}
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
157
193
|
function parsePrismerGoals(value) {
|
|
158
194
|
if (!Array.isArray(value)) return [];
|
|
159
195
|
return value.map((entry) => {
|
|
@@ -563,13 +599,14 @@ function failure(status, body) {
|
|
|
563
599
|
error: { code: "adapter_dispatch_failed", message: `Hermes ${status}: ${body || "<no body>"}` }
|
|
564
600
|
};
|
|
565
601
|
}
|
|
566
|
-
async function consumeSse(body, task) {
|
|
602
|
+
async function consumeSse(body, task, state) {
|
|
567
603
|
const reader = body.getReader();
|
|
568
604
|
const decoder = new TextDecoder();
|
|
569
605
|
let buf = "";
|
|
570
606
|
let deltas = "";
|
|
571
607
|
let finalOutput;
|
|
572
608
|
let lastProgress = 0;
|
|
609
|
+
let approvalRequested = false;
|
|
573
610
|
for (; ; ) {
|
|
574
611
|
const readStart = Date.now();
|
|
575
612
|
const { value, done } = await reader.read();
|
|
@@ -618,6 +655,10 @@ async function consumeSse(body, task) {
|
|
|
618
655
|
const next = Math.min(0.99, lastProgress + 0.05);
|
|
619
656
|
lastProgress = next;
|
|
620
657
|
const toolName = typeof payload.tool === "string" ? payload.tool : typeof payload.name === "string" ? payload.name : void 0;
|
|
658
|
+
if (eventName === "tool.started" && toolName === APPROVAL_TOOL_NAME) {
|
|
659
|
+
approvalRequested = true;
|
|
660
|
+
if (state) state.approvalRequested = true;
|
|
661
|
+
}
|
|
621
662
|
const previewStr = typeof payload.preview === "string" ? payload.preview : payload.preview != null ? JSON.stringify(payload.preview).slice(0, 200) : void 0;
|
|
622
663
|
task.onProgress?.({
|
|
623
664
|
progress: next,
|
|
@@ -647,7 +688,53 @@ async function consumeSse(body, task) {
|
|
|
647
688
|
}
|
|
648
689
|
}
|
|
649
690
|
}
|
|
650
|
-
return
|
|
691
|
+
return {
|
|
692
|
+
output: finalOutput && finalOutput.length > 0 ? finalOutput : deltas,
|
|
693
|
+
approvalRequested
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
async function consumeChatCompletionsSse(body, task) {
|
|
697
|
+
const reader = body.getReader();
|
|
698
|
+
const decoder = new TextDecoder();
|
|
699
|
+
let buf = "";
|
|
700
|
+
let output = "";
|
|
701
|
+
let lastProgress = 0;
|
|
702
|
+
for (; ; ) {
|
|
703
|
+
const { value, done } = await reader.read();
|
|
704
|
+
if (done) break;
|
|
705
|
+
buf += decoder.decode(value, { stream: true });
|
|
706
|
+
const events = buf.split("\n\n");
|
|
707
|
+
buf = events.pop() ?? "";
|
|
708
|
+
for (const ev of events) {
|
|
709
|
+
const dataLine = ev.match(/^data: (.+)$/m);
|
|
710
|
+
if (!dataLine) continue;
|
|
711
|
+
const raw = dataLine[1].trim();
|
|
712
|
+
if (raw === "[DONE]") continue;
|
|
713
|
+
let payload;
|
|
714
|
+
try {
|
|
715
|
+
payload = JSON.parse(raw);
|
|
716
|
+
} catch (err) {
|
|
717
|
+
process.stderr.write(
|
|
718
|
+
`[hermes-adapter] chat-completions SSE parse skipped: ${err.message}
|
|
719
|
+
`
|
|
720
|
+
);
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
const delta = payload.choices?.[0]?.delta?.content;
|
|
724
|
+
if (typeof delta === "string") {
|
|
725
|
+
output += delta;
|
|
726
|
+
} else if (Array.isArray(delta)) {
|
|
727
|
+
for (const part of delta) {
|
|
728
|
+
if (part?.type === "text" && typeof part.text === "string") output += part.text;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (output.length > 0 && lastProgress < 0.99) {
|
|
732
|
+
lastProgress = Math.min(0.99, lastProgress + 0.05);
|
|
733
|
+
task.onProgress?.({ progress: lastProgress, message: "streaming" });
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return output;
|
|
651
738
|
}
|
|
652
739
|
async function checkHealth(baseUrl, apiKey) {
|
|
653
740
|
try {
|
|
@@ -728,13 +815,14 @@ function pidAlive(pid) {
|
|
|
728
815
|
function escapeRegExp(value) {
|
|
729
816
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
730
817
|
}
|
|
731
|
-
var PRISMER_IM_SKILL_NAME, PRISMER_IM_SKILL_CONTENT, HermesProfileConfigSchema, hermesAdapter, HermesService;
|
|
818
|
+
var PRISMER_IM_SKILL_NAME, PRISMER_ROLE_SKILL_PREFIX, PRISMER_IM_SKILL_CONTENT, HermesProfileConfigSchema, hermesAdapter, HermesService, APPROVAL_TOOL_NAME;
|
|
732
819
|
var init_hermes = __esm({
|
|
733
820
|
"src/adapters/hermes/index.ts"() {
|
|
734
821
|
"use strict";
|
|
735
822
|
init_contract();
|
|
736
823
|
init_skill_loader2();
|
|
737
824
|
PRISMER_IM_SKILL_NAME = "prismer-im-collab";
|
|
825
|
+
PRISMER_ROLE_SKILL_PREFIX = "prismer-role";
|
|
738
826
|
PRISMER_IM_SKILL_CONTENT = `---
|
|
739
827
|
name: prismer-im-collab
|
|
740
828
|
description: Coordinate reliably in Prismer conversations, use workspace assets through bounded MCP tools, and keep task work on the board.
|
|
@@ -1047,6 +1135,7 @@ No @ needed; the human is the only other party.
|
|
|
1047
1135
|
const startedAt = Date.now();
|
|
1048
1136
|
let runId;
|
|
1049
1137
|
const sysPrompt = typeof task.metadata?.systemPrompt === "string" ? task.metadata.systemPrompt : void 0;
|
|
1138
|
+
this.installRoleTemplateSkill();
|
|
1050
1139
|
const skillPrompt = await this.skillLoader.loadSystemPromptFragment().catch((err) => {
|
|
1051
1140
|
process.stderr.write(
|
|
1052
1141
|
`[hermes-adapter] failed to read skills for ${this.profileName}: ${err.message}
|
|
@@ -1054,7 +1143,21 @@ No @ needed; the human is the only other party.
|
|
|
1054
1143
|
);
|
|
1055
1144
|
return void 0;
|
|
1056
1145
|
});
|
|
1057
|
-
const
|
|
1146
|
+
const outboxDir = typeof task.metadata?.prismerOutboxDir === "string" ? task.metadata.prismerOutboxDir : void 0;
|
|
1147
|
+
const outboxDirective = outboxDir ? [
|
|
1148
|
+
"[Outbox directive \u2014 MANDATORY, overrides any prior outbox path]",
|
|
1149
|
+
"",
|
|
1150
|
+
"For THIS turn ONLY, your active outbox is:",
|
|
1151
|
+
` ${outboxDir}`,
|
|
1152
|
+
"",
|
|
1153
|
+
"Any user-deliverable file (PDF, image, CSV, archive, doc) MUST be",
|
|
1154
|
+
"written to that EXACT absolute path. Do NOT reuse the outbox path",
|
|
1155
|
+
"from any previous turn \u2014 those directories no longer accept new",
|
|
1156
|
+
"uploads. Do NOT copy files into the previous outbox; copy them",
|
|
1157
|
+
"into the path above. Files outside this directory will not become",
|
|
1158
|
+
'chat attachments and the user will see "no files were attached".'
|
|
1159
|
+
].join("\n") : "";
|
|
1160
|
+
const instructions = [outboxDirective, sysPrompt, skillPrompt].filter(Boolean).join("\n\n");
|
|
1058
1161
|
if (sysPrompt) {
|
|
1059
1162
|
try {
|
|
1060
1163
|
const profileDir = getHermesProfileDir(this.profileName);
|
|
@@ -1078,8 +1181,15 @@ No @ needed; the human is the only other party.
|
|
|
1078
1181
|
`
|
|
1079
1182
|
);
|
|
1080
1183
|
}
|
|
1184
|
+
const sseState = { approvalRequested: false };
|
|
1081
1185
|
try {
|
|
1082
1186
|
const nativeBridgePatch = await this.prepareNativeBridgePatch(task);
|
|
1187
|
+
const imageRefs = (task.assetRefs ?? []).filter(
|
|
1188
|
+
(r) => r.mime?.startsWith("image/")
|
|
1189
|
+
);
|
|
1190
|
+
if (imageRefs.length > 0) {
|
|
1191
|
+
return await this.dispatchMultimodalChat(task, instructions, imageRefs, startedAt);
|
|
1192
|
+
}
|
|
1083
1193
|
const createRes = await fetch(`${this.baseUrl}/v1/runs`, {
|
|
1084
1194
|
method: "POST",
|
|
1085
1195
|
headers: {
|
|
@@ -1130,10 +1240,10 @@ No @ needed; the human is the only other party.
|
|
|
1130
1240
|
}
|
|
1131
1241
|
};
|
|
1132
1242
|
}
|
|
1133
|
-
const
|
|
1243
|
+
const sseResult = await consumeSse(eventsRes.body, task, sseState);
|
|
1134
1244
|
return {
|
|
1135
1245
|
ok: true,
|
|
1136
|
-
output,
|
|
1246
|
+
output: sseResult.output,
|
|
1137
1247
|
metrics: { durationMs: Date.now() - startedAt },
|
|
1138
1248
|
metadata: {
|
|
1139
1249
|
hermes: this.bridgeSnapshot("dispatched", {
|
|
@@ -1141,7 +1251,13 @@ No @ needed; the human is the only other party.
|
|
|
1141
1251
|
runId,
|
|
1142
1252
|
baseUrl: this.baseUrl,
|
|
1143
1253
|
model: this.config.model
|
|
1144
|
-
})
|
|
1254
|
+
}),
|
|
1255
|
+
// Surfaced to dispatch.ts so a subsequent reaper kill on this
|
|
1256
|
+
// task is reclassified as `awaiting_human_approval` rather than
|
|
1257
|
+
// `daemon_task_timeout`. Lives under a stable key (not under
|
|
1258
|
+
// `hermes.*`) so non-hermes adapters can adopt the same signal
|
|
1259
|
+
// later without bleeding hermes-specific metadata shape.
|
|
1260
|
+
...sseResult.approvalRequested ? { approvalRequested: true } : {}
|
|
1145
1261
|
}
|
|
1146
1262
|
};
|
|
1147
1263
|
} catch (err) {
|
|
@@ -1151,6 +1267,7 @@ No @ needed; the human is the only other party.
|
|
|
1151
1267
|
);
|
|
1152
1268
|
const categorized = categorizeDispatchError(err, task.signal);
|
|
1153
1269
|
const bridgeKind = categorized.error?.code === "task_cancelled" ? "cancelled" : "failed";
|
|
1270
|
+
const approvalSignal = sseState.approvalRequested ? { approvalRequested: true } : {};
|
|
1154
1271
|
return {
|
|
1155
1272
|
...categorized,
|
|
1156
1273
|
metadata: {
|
|
@@ -1159,13 +1276,87 @@ No @ needed; the human is the only other party.
|
|
|
1159
1276
|
baseUrl: this.baseUrl,
|
|
1160
1277
|
model: this.config.model,
|
|
1161
1278
|
...bridgeKind === "failed" ? { error: err.message } : {}
|
|
1162
|
-
})
|
|
1279
|
+
}),
|
|
1280
|
+
...approvalSignal
|
|
1163
1281
|
}
|
|
1164
1282
|
};
|
|
1165
1283
|
} finally {
|
|
1166
1284
|
if (runId && this.currentRunId === runId) this.currentRunId = void 0;
|
|
1167
1285
|
}
|
|
1168
1286
|
}
|
|
1287
|
+
/**
|
|
1288
|
+
* 14b rev.3 §3.0.4 — Hermes multimodal path.
|
|
1289
|
+
*
|
|
1290
|
+
* Builds an OpenAI-shape chat/completions body where the user message
|
|
1291
|
+
* `content` is a multipart array (`{type:'text'}` + N × `{type:'image_url'}`).
|
|
1292
|
+
* Hermes `_normalize_multimodal_content` (api_server.py:170) collapses the
|
|
1293
|
+
* blocks into its native multimodal representation before routing into
|
|
1294
|
+
* the agent pipeline.
|
|
1295
|
+
*
|
|
1296
|
+
* `image_url.url` is the cdn URL when the daemon's reachability probe said
|
|
1297
|
+
* the upstream LLM can see it (D35 path A); otherwise it's a `data:` URI
|
|
1298
|
+
* carrying the base64 bytes the daemon pre-fetched (D35 path B fallback).
|
|
1299
|
+
* The adapter doesn't decide — `resolveAssetRefs` does, and the adapter
|
|
1300
|
+
* just shuttles `cdnUrl ?? data:base64` into the wire.
|
|
1301
|
+
*/
|
|
1302
|
+
async dispatchMultimodalChat(task, instructions, imageRefs, startedAt) {
|
|
1303
|
+
const userContent = [{ type: "text", text: task.prompt }];
|
|
1304
|
+
for (const r of imageRefs) {
|
|
1305
|
+
const url = r.reachable === "cdn" && r.cdnUrl ? r.cdnUrl : r.base64 ? `data:${r.mime ?? "image/png"};base64,${r.base64}` : r.cdnUrl ?? "";
|
|
1306
|
+
if (!url) {
|
|
1307
|
+
userContent.push({
|
|
1308
|
+
type: "text",
|
|
1309
|
+
text: `[image attachment id=${r.assetId} mime=${r.mime ?? "unknown"} unavailable: cdn not reachable and bytes exceeded inline cap]`
|
|
1310
|
+
});
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
userContent.push({ type: "image_url", image_url: { url, detail: "auto" } });
|
|
1314
|
+
}
|
|
1315
|
+
const messages = [];
|
|
1316
|
+
if (instructions) messages.push({ role: "system", content: instructions });
|
|
1317
|
+
messages.push({ role: "user", content: userContent });
|
|
1318
|
+
const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
1319
|
+
method: "POST",
|
|
1320
|
+
headers: {
|
|
1321
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
1322
|
+
"Content-Type": "application/json"
|
|
1323
|
+
},
|
|
1324
|
+
body: JSON.stringify({
|
|
1325
|
+
model: this.config.model,
|
|
1326
|
+
messages,
|
|
1327
|
+
stream: true
|
|
1328
|
+
}),
|
|
1329
|
+
signal: task.signal
|
|
1330
|
+
});
|
|
1331
|
+
if (!res.ok) {
|
|
1332
|
+
const body = await res.text().catch(() => "");
|
|
1333
|
+
return failure(res.status, body);
|
|
1334
|
+
}
|
|
1335
|
+
if (!res.body) {
|
|
1336
|
+
return {
|
|
1337
|
+
ok: false,
|
|
1338
|
+
error: {
|
|
1339
|
+
code: "adapter_dispatch_failed",
|
|
1340
|
+
message: "Hermes /v1/chat/completions returned no body"
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
const output = await consumeChatCompletionsSse(res.body, task);
|
|
1345
|
+
return {
|
|
1346
|
+
ok: true,
|
|
1347
|
+
output,
|
|
1348
|
+
metrics: { durationMs: Date.now() - startedAt },
|
|
1349
|
+
metadata: {
|
|
1350
|
+
hermes: this.bridgeSnapshot("dispatched", {
|
|
1351
|
+
endpoint: "/v1/chat/completions",
|
|
1352
|
+
multimodal: true,
|
|
1353
|
+
imageRefs: imageRefs.length,
|
|
1354
|
+
baseUrl: this.baseUrl,
|
|
1355
|
+
model: this.config.model
|
|
1356
|
+
})
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1169
1360
|
async shutdown() {
|
|
1170
1361
|
if (this.currentRunId) await this.stopRun(this.currentRunId);
|
|
1171
1362
|
}
|
|
@@ -1187,6 +1378,25 @@ No @ needed; the human is the only other party.
|
|
|
1187
1378
|
]);
|
|
1188
1379
|
return { ...kanbanPatch, ...goalsPatch };
|
|
1189
1380
|
}
|
|
1381
|
+
installRoleTemplateSkill() {
|
|
1382
|
+
const roleTemplate = readPlainRecord(this.config.roleTemplate);
|
|
1383
|
+
const hermesConfig = readPlainRecord(roleTemplate?.hermesConfig);
|
|
1384
|
+
const agents = readNonEmptyString(hermesConfig?.agents);
|
|
1385
|
+
if (!agents) return;
|
|
1386
|
+
const slug = sanitizeRoleSkillSlug(readNonEmptyString(roleTemplate?.slug) ?? "template");
|
|
1387
|
+
const skillName = `${PRISMER_ROLE_SKILL_PREFIX}-${slug}`;
|
|
1388
|
+
const content = renderRoleTemplateSkill(skillName, agents, roleTemplate);
|
|
1389
|
+
try {
|
|
1390
|
+
const skillDir = join2(this.skillLoader.getSkillsRoot(), skillName);
|
|
1391
|
+
mkdirSync(skillDir, { recursive: true });
|
|
1392
|
+
writeFileSync(join2(skillDir, "SKILL.md"), content, "utf8");
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
process.stderr.write(
|
|
1395
|
+
`[hermes-adapter] failed to install role-template skill ${skillName} for ${this.profileName}: ${err.message}
|
|
1396
|
+
`
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1190
1400
|
async prepareNativeKanbanPatch(task) {
|
|
1191
1401
|
const sourceKind = typeof task.metadata?.sourceKind === "string" ? task.metadata.sourceKind : null;
|
|
1192
1402
|
const parentTaskId = typeof task.metadata?.parentTaskId === "string" ? task.metadata.parentTaskId : null;
|
|
@@ -1295,6 +1505,7 @@ No @ needed; the human is the only other party.
|
|
|
1295
1505
|
};
|
|
1296
1506
|
}
|
|
1297
1507
|
};
|
|
1508
|
+
APPROVAL_TOOL_NAME = "prismer.approval.request_human_approval";
|
|
1298
1509
|
}
|
|
1299
1510
|
});
|
|
1300
1511
|
|
|
@@ -1327,8 +1538,61 @@ var init_skill_loader3 = __esm({
|
|
|
1327
1538
|
|
|
1328
1539
|
// src/adapters/openclaw/index.ts
|
|
1329
1540
|
import { z as z4 } from "zod";
|
|
1541
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1330
1542
|
import { homedir as homedir2 } from "os";
|
|
1331
1543
|
import { join as join3 } from "path";
|
|
1544
|
+
function readPlainRecord2(value) {
|
|
1545
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1546
|
+
}
|
|
1547
|
+
function readNonEmptyString2(value) {
|
|
1548
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1549
|
+
}
|
|
1550
|
+
function sanitizeRoleSkillSlug2(value) {
|
|
1551
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "template";
|
|
1552
|
+
}
|
|
1553
|
+
function renderRoleTemplateSkill2(skillName, agents, roleTemplate) {
|
|
1554
|
+
const mcpServers = Array.isArray(roleTemplate?.mcpServers) ? roleTemplate.mcpServers : [];
|
|
1555
|
+
const tools = /* @__PURE__ */ new Set();
|
|
1556
|
+
for (const server of mcpServers) {
|
|
1557
|
+
const rec = readPlainRecord2(server);
|
|
1558
|
+
const allowlist = Array.isArray(rec?.toolsAllowlist) ? rec.toolsAllowlist : [];
|
|
1559
|
+
for (const tool of allowlist) {
|
|
1560
|
+
if (typeof tool === "string" && tool.trim()) tools.add(tool.trim());
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
const allowedTools = [...tools].join(" ");
|
|
1564
|
+
const roleTemplateSlug = JSON.stringify(readNonEmptyString2(roleTemplate?.slug) ?? null);
|
|
1565
|
+
return `---
|
|
1566
|
+
name: ${skillName}
|
|
1567
|
+
description: Role-template operating playbook projected from Prismer RoleTemplate.
|
|
1568
|
+
${allowedTools ? `allowed-tools: ${allowedTools}
|
|
1569
|
+
` : ""}metadata:
|
|
1570
|
+
prismer:
|
|
1571
|
+
source: role-template
|
|
1572
|
+
roleTemplateSlug: ${roleTemplateSlug}
|
|
1573
|
+
---
|
|
1574
|
+
|
|
1575
|
+
# Role Template Playbook
|
|
1576
|
+
|
|
1577
|
+
${agents.trim()}
|
|
1578
|
+
`;
|
|
1579
|
+
}
|
|
1580
|
+
function pickResponsesSource(ref) {
|
|
1581
|
+
if (ref.reachable === "cdn" && ref.cdnUrl) {
|
|
1582
|
+
return { type: "url", url: ref.cdnUrl };
|
|
1583
|
+
}
|
|
1584
|
+
if (ref.base64) {
|
|
1585
|
+
return {
|
|
1586
|
+
type: "base64",
|
|
1587
|
+
data: ref.base64,
|
|
1588
|
+
media_type: ref.mime ?? "application/octet-stream"
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
if (ref.cdnUrl) {
|
|
1592
|
+
return { type: "url", url: ref.cdnUrl };
|
|
1593
|
+
}
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1332
1596
|
function getOpenClawSkillsDir(profile) {
|
|
1333
1597
|
const config = OpenClawProfileConfigSchema.parse(profile.config);
|
|
1334
1598
|
return resolveOpenClawSkillsDir(profile, config);
|
|
@@ -1349,12 +1613,13 @@ async function checkHealth2(baseUrl, apiKey) {
|
|
|
1349
1613
|
return false;
|
|
1350
1614
|
}
|
|
1351
1615
|
}
|
|
1352
|
-
var OpenClawProfileConfigSchema, openclawAdapter, OpenClawService;
|
|
1616
|
+
var PRISMER_ROLE_SKILL_PREFIX2, OpenClawProfileConfigSchema, openclawAdapter, OpenClawService;
|
|
1353
1617
|
var init_openclaw = __esm({
|
|
1354
1618
|
"src/adapters/openclaw/index.ts"() {
|
|
1355
1619
|
"use strict";
|
|
1356
1620
|
init_contract();
|
|
1357
1621
|
init_skill_loader3();
|
|
1622
|
+
PRISMER_ROLE_SKILL_PREFIX2 = "prismer-role";
|
|
1358
1623
|
OpenClawProfileConfigSchema = z4.object({
|
|
1359
1624
|
/** OpenClaw chat-completions HTTP port. */
|
|
1360
1625
|
port: z4.number().int().min(1).max(65535).default(18789),
|
|
@@ -1399,23 +1664,25 @@ var init_openclaw = __esm({
|
|
|
1399
1664
|
);
|
|
1400
1665
|
}
|
|
1401
1666
|
const skillsDir = resolveOpenClawSkillsDir(profile, config);
|
|
1402
|
-
return new OpenClawService(baseUrl, config.apiKey, config.model, new OpenClawSkillLoader(skillsDir));
|
|
1667
|
+
return new OpenClawService(baseUrl, config.apiKey, config.model, config, new OpenClawSkillLoader(skillsDir));
|
|
1403
1668
|
},
|
|
1404
1669
|
async health() {
|
|
1405
1670
|
return { available: true };
|
|
1406
1671
|
}
|
|
1407
1672
|
};
|
|
1408
1673
|
OpenClawService = class {
|
|
1409
|
-
constructor(baseUrl, apiKey, model, skillLoader) {
|
|
1674
|
+
constructor(baseUrl, apiKey, model, config, skillLoader) {
|
|
1410
1675
|
this.baseUrl = baseUrl;
|
|
1411
1676
|
this.apiKey = apiKey;
|
|
1412
1677
|
this.model = model;
|
|
1678
|
+
this.config = config;
|
|
1413
1679
|
this.skillLoader = skillLoader;
|
|
1414
1680
|
this.id = baseUrl;
|
|
1415
1681
|
}
|
|
1416
1682
|
baseUrl;
|
|
1417
1683
|
apiKey;
|
|
1418
1684
|
model;
|
|
1685
|
+
config;
|
|
1419
1686
|
skillLoader;
|
|
1420
1687
|
id;
|
|
1421
1688
|
async healthy() {
|
|
@@ -1424,17 +1691,47 @@ var init_openclaw = __esm({
|
|
|
1424
1691
|
async dispatch(task) {
|
|
1425
1692
|
const startedAt = Date.now();
|
|
1426
1693
|
const systemPrompt = typeof task.metadata?.systemPrompt === "string" ? task.metadata.systemPrompt : void 0;
|
|
1694
|
+
this.installRoleTemplateSkill();
|
|
1427
1695
|
const skillPrompt = await this.skillLoader.loadSystemPromptFragment().catch((err) => {
|
|
1428
1696
|
process.stderr.write(`[openclaw-adapter] failed to read skills: ${err.message}
|
|
1429
1697
|
`);
|
|
1430
1698
|
return void 0;
|
|
1431
1699
|
});
|
|
1432
1700
|
const systemContent = [systemPrompt, skillPrompt].filter(Boolean).join("\n\n");
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1435
|
-
|
|
1701
|
+
const refs = task.assetRefs ?? [];
|
|
1702
|
+
const imageRefs = refs.filter((r) => r.mime?.startsWith("image/"));
|
|
1703
|
+
const fileRefs = refs.filter((r) => r.mime && !r.mime.startsWith("image/"));
|
|
1704
|
+
const inputContent = [
|
|
1705
|
+
{ type: "input_text", text: task.prompt }
|
|
1706
|
+
];
|
|
1707
|
+
for (const r of imageRefs) {
|
|
1708
|
+
const source = pickResponsesSource(r);
|
|
1709
|
+
if (!source) {
|
|
1710
|
+
inputContent.push({
|
|
1711
|
+
type: "input_text",
|
|
1712
|
+
text: `[image attachment id=${r.assetId} unavailable: cdn unreachable and bytes exceeded inline cap]`
|
|
1713
|
+
});
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1716
|
+
inputContent.push({ type: "input_image", source });
|
|
1717
|
+
}
|
|
1718
|
+
for (const r of fileRefs) {
|
|
1719
|
+
const source = pickResponsesSource(r);
|
|
1720
|
+
if (!source) {
|
|
1721
|
+
inputContent.push({
|
|
1722
|
+
type: "input_text",
|
|
1723
|
+
text: `[file attachment id=${r.assetId} mime=${r.mime ?? "unknown"} unavailable]`
|
|
1724
|
+
});
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
if (r.filename && source.type === "base64") {
|
|
1728
|
+
source.filename = r.filename;
|
|
1729
|
+
}
|
|
1730
|
+
inputContent.push({ type: "input_file", source });
|
|
1731
|
+
}
|
|
1732
|
+
const input = [{ type: "message", role: "user", content: inputContent }];
|
|
1436
1733
|
try {
|
|
1437
|
-
const res = await fetch(`${this.baseUrl}/v1/
|
|
1734
|
+
const res = await fetch(`${this.baseUrl}/v1/responses`, {
|
|
1438
1735
|
method: "POST",
|
|
1439
1736
|
headers: {
|
|
1440
1737
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -1442,7 +1739,11 @@ var init_openclaw = __esm({
|
|
|
1442
1739
|
},
|
|
1443
1740
|
body: JSON.stringify({
|
|
1444
1741
|
model: this.model,
|
|
1445
|
-
|
|
1742
|
+
input,
|
|
1743
|
+
// OpenClaw `/v1/responses` accepts top-level `instructions` for
|
|
1744
|
+
// the system prompt — keeping it out of the user message
|
|
1745
|
+
// preserves cleanish channel-context separation.
|
|
1746
|
+
...systemContent ? { instructions: systemContent } : {},
|
|
1446
1747
|
stream: false
|
|
1447
1748
|
}),
|
|
1448
1749
|
signal: task.signal
|
|
@@ -1458,7 +1759,21 @@ var init_openclaw = __esm({
|
|
|
1458
1759
|
};
|
|
1459
1760
|
}
|
|
1460
1761
|
const json = await res.json();
|
|
1461
|
-
let output =
|
|
1762
|
+
let output = "";
|
|
1763
|
+
if (Array.isArray(json.output)) {
|
|
1764
|
+
for (const item of json.output) {
|
|
1765
|
+
if (item?.type === "message" && Array.isArray(item.content)) {
|
|
1766
|
+
for (const part of item.content) {
|
|
1767
|
+
if (part?.type === "output_text" && typeof part.text === "string") {
|
|
1768
|
+
output += part.text;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
if (!output) {
|
|
1775
|
+
output = json.choices?.[0]?.message?.content ?? "";
|
|
1776
|
+
}
|
|
1462
1777
|
const ERR_PREFIX_RE = /^Cannot read properties of undefined \(reading '[^']+'\)\s*\n+/;
|
|
1463
1778
|
if (ERR_PREFIX_RE.test(output)) {
|
|
1464
1779
|
process.stderr.write(
|
|
@@ -1476,6 +1791,25 @@ var init_openclaw = __esm({
|
|
|
1476
1791
|
return categorizeDispatchError(err, task.signal);
|
|
1477
1792
|
}
|
|
1478
1793
|
}
|
|
1794
|
+
installRoleTemplateSkill() {
|
|
1795
|
+
const roleTemplate = readPlainRecord2(this.config.roleTemplate);
|
|
1796
|
+
const openclawConfig = readPlainRecord2(roleTemplate?.openclawConfig);
|
|
1797
|
+
const agents = readNonEmptyString2(openclawConfig?.agents);
|
|
1798
|
+
if (!agents) return;
|
|
1799
|
+
const slug = sanitizeRoleSkillSlug2(readNonEmptyString2(roleTemplate?.slug) ?? "template");
|
|
1800
|
+
const skillName = `${PRISMER_ROLE_SKILL_PREFIX2}-${slug}`;
|
|
1801
|
+
const content = renderRoleTemplateSkill2(skillName, agents, roleTemplate);
|
|
1802
|
+
try {
|
|
1803
|
+
const skillDir = join3(this.skillLoader.getSkillsRoot(), skillName);
|
|
1804
|
+
mkdirSync2(skillDir, { recursive: true });
|
|
1805
|
+
writeFileSync2(join3(skillDir, "SKILL.md"), content, "utf8");
|
|
1806
|
+
} catch (err) {
|
|
1807
|
+
process.stderr.write(
|
|
1808
|
+
`[openclaw-adapter] failed to install role-template skill ${skillName}: ${err.message}
|
|
1809
|
+
`
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1479
1813
|
};
|
|
1480
1814
|
}
|
|
1481
1815
|
});
|
|
@@ -1911,7 +2245,7 @@ var require_package = __commonJS({
|
|
|
1911
2245
|
"package.json"(exports, module) {
|
|
1912
2246
|
module.exports = {
|
|
1913
2247
|
name: "@prismer/runtime",
|
|
1914
|
-
version: "2.0.
|
|
2248
|
+
version: "2.0.6",
|
|
1915
2249
|
description: "Prismer Cloud daemon runtime \u2014 TS-only adapter host for hosted IM agents",
|
|
1916
2250
|
type: "module",
|
|
1917
2251
|
main: "dist/index.js",
|
|
@@ -1945,6 +2279,7 @@ var require_package = __commonJS({
|
|
|
1945
2279
|
},
|
|
1946
2280
|
dependencies: {
|
|
1947
2281
|
"@iarna/toml": "^2.2.5",
|
|
2282
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
1948
2283
|
"better-sqlite3": "^11.8.0",
|
|
1949
2284
|
commander: "^12.1.0",
|
|
1950
2285
|
qrcode: "^1.5.4",
|
|
@@ -2006,7 +2341,7 @@ __export(util_exports, {
|
|
|
2006
2341
|
warn: () => warn,
|
|
2007
2342
|
writePidFile: () => writePidFile
|
|
2008
2343
|
});
|
|
2009
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync, writeFileSync as
|
|
2344
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
2010
2345
|
import { join as join5 } from "path";
|
|
2011
2346
|
function color(kind, text) {
|
|
2012
2347
|
if (process.env.NO_COLOR === "1" || process.env.NO_COLOR === "true") return text;
|
|
@@ -2104,7 +2439,7 @@ function pidFilePath(paths) {
|
|
|
2104
2439
|
return join5(paths.root, "daemon.pid");
|
|
2105
2440
|
}
|
|
2106
2441
|
function writePidFile(paths, pid) {
|
|
2107
|
-
|
|
2442
|
+
writeFileSync4(pidFilePath(paths), `${pid}
|
|
2108
2443
|
`, "utf8");
|
|
2109
2444
|
}
|
|
2110
2445
|
function readPidFile(paths) {
|
|
@@ -2554,10 +2889,10 @@ import { spawnSync as spawnSync2 } from "child_process";
|
|
|
2554
2889
|
import {
|
|
2555
2890
|
copyFileSync,
|
|
2556
2891
|
existsSync as existsSync5,
|
|
2557
|
-
mkdirSync as
|
|
2892
|
+
mkdirSync as mkdirSync4,
|
|
2558
2893
|
readFileSync as readFileSync5,
|
|
2559
2894
|
statSync,
|
|
2560
|
-
writeFileSync as
|
|
2895
|
+
writeFileSync as writeFileSync5
|
|
2561
2896
|
} from "fs";
|
|
2562
2897
|
import { homedir as homedir4 } from "os";
|
|
2563
2898
|
import { dirname as dirname4, join as join6 } from "path";
|
|
@@ -3057,7 +3392,7 @@ init_openclaw();
|
|
|
3057
3392
|
|
|
3058
3393
|
// src/config.ts
|
|
3059
3394
|
import * as TOML from "@iarna/toml";
|
|
3060
|
-
import { existsSync as existsSync2, mkdirSync as
|
|
3395
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
3061
3396
|
import { homedir as homedir3 } from "os";
|
|
3062
3397
|
import { dirname as dirname2, join as join4 } from "path";
|
|
3063
3398
|
import { z as z5 } from "zod";
|
|
@@ -3124,13 +3459,13 @@ function loadConfig(paths = resolvePaths()) {
|
|
|
3124
3459
|
}
|
|
3125
3460
|
function saveConfig(config, paths = resolvePaths()) {
|
|
3126
3461
|
if (!existsSync2(paths.root)) {
|
|
3127
|
-
|
|
3462
|
+
mkdirSync3(paths.root, { recursive: true });
|
|
3128
3463
|
}
|
|
3129
3464
|
if (!existsSync2(dirname2(paths.configFile))) {
|
|
3130
|
-
|
|
3465
|
+
mkdirSync3(dirname2(paths.configFile), { recursive: true });
|
|
3131
3466
|
}
|
|
3132
3467
|
ConfigSchema.parse(config);
|
|
3133
|
-
|
|
3468
|
+
writeFileSync3(paths.configFile, TOML.stringify(config), "utf8");
|
|
3134
3469
|
}
|
|
3135
3470
|
function deriveWsUrl(httpBase) {
|
|
3136
3471
|
const u = new URL(httpBase);
|
|
@@ -3393,15 +3728,15 @@ function runHooks(spec, opts) {
|
|
|
3393
3728
|
}
|
|
3394
3729
|
let backup;
|
|
3395
3730
|
if (planned.kind === "directory") {
|
|
3396
|
-
|
|
3731
|
+
mkdirSync4(planned.path, { recursive: true });
|
|
3397
3732
|
const markerPath = join6(planned.path, "prismer.json");
|
|
3398
3733
|
backup = backupIfExists(markerPath);
|
|
3399
|
-
|
|
3734
|
+
writeFileSync5(markerPath, JSON.stringify(prismerHookMarker(spec), null, 2) + "\n", "utf8");
|
|
3400
3735
|
} else {
|
|
3401
|
-
|
|
3736
|
+
mkdirSync4(dirname4(planned.path), { recursive: true });
|
|
3402
3737
|
backup = backupIfExists(planned.path);
|
|
3403
3738
|
const merged = mergeHookJson(planned.path, spec);
|
|
3404
|
-
|
|
3739
|
+
writeFileSync5(planned.path, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
3405
3740
|
}
|
|
3406
3741
|
return {
|
|
3407
3742
|
ok: true,
|
|
@@ -3799,7 +4134,7 @@ function mapStatusToCode(status) {
|
|
|
3799
4134
|
|
|
3800
4135
|
// src/sync/store.ts
|
|
3801
4136
|
import Database2 from "better-sqlite3";
|
|
3802
|
-
import { existsSync as existsSync6, mkdirSync as
|
|
4137
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "fs";
|
|
3803
4138
|
import { dirname as dirname5 } from "path";
|
|
3804
4139
|
var MIGRATIONS = [
|
|
3805
4140
|
{
|
|
@@ -3926,6 +4261,36 @@ var MIGRATIONS = [
|
|
|
3926
4261
|
CREATE INDEX IF NOT EXISTS idx_asset_meta_seq
|
|
3927
4262
|
ON asset_metadata_index(workspace_id, asset_index_seq);
|
|
3928
4263
|
`
|
|
4264
|
+
},
|
|
4265
|
+
{
|
|
4266
|
+
version: 4,
|
|
4267
|
+
up: `
|
|
4268
|
+
-- Wave 4 / W3 \xA74.3 \u2014 pending dispatch replies for crash recovery.
|
|
4269
|
+
--
|
|
4270
|
+
-- The two-phase reply protocol writes a row here BEFORE calling
|
|
4271
|
+
-- POST /dispatch/:taskId/prepare so that a crash between prepare
|
|
4272
|
+
-- and commit leaves a recoverable trail. On daemon cold-start, any
|
|
4273
|
+
-- row with status='prepared' triggers an idempotent commit retry.
|
|
4274
|
+
--
|
|
4275
|
+
-- The server-side cutoff for orphaned 'prepared' rows is 1h
|
|
4276
|
+
-- (PREPARED_REPLY_ORPHAN_MS in dispatch-reply.service.ts); rows
|
|
4277
|
+
-- older than that will fail commit with DISPATCH_REPLY_INVALID_STATE
|
|
4278
|
+
-- and must be reported to the user as 'dispatch lost'.
|
|
4279
|
+
CREATE TABLE IF NOT EXISTS pending_dispatch_replies (
|
|
4280
|
+
idempotency_key TEXT PRIMARY KEY,
|
|
4281
|
+
task_id TEXT NOT NULL,
|
|
4282
|
+
reply_id TEXT,
|
|
4283
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
4284
|
+
payload_json TEXT NOT NULL,
|
|
4285
|
+
prepared_at INTEGER,
|
|
4286
|
+
created_at INTEGER NOT NULL,
|
|
4287
|
+
last_attempt_at INTEGER,
|
|
4288
|
+
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
4289
|
+
last_error TEXT
|
|
4290
|
+
);
|
|
4291
|
+
CREATE INDEX IF NOT EXISTS idx_pending_reply_task ON pending_dispatch_replies (task_id);
|
|
4292
|
+
CREATE INDEX IF NOT EXISTS idx_pending_reply_status ON pending_dispatch_replies (status, created_at);
|
|
4293
|
+
`
|
|
3929
4294
|
}
|
|
3930
4295
|
];
|
|
3931
4296
|
function runSql(db, sql) {
|
|
@@ -3933,7 +4298,7 @@ function runSql(db, sql) {
|
|
|
3933
4298
|
}
|
|
3934
4299
|
function openLocalDb(path9) {
|
|
3935
4300
|
if (path9 !== ":memory:" && !existsSync6(dirname5(path9))) {
|
|
3936
|
-
|
|
4301
|
+
mkdirSync5(dirname5(path9), { recursive: true });
|
|
3937
4302
|
}
|
|
3938
4303
|
const db = new Database2(path9);
|
|
3939
4304
|
db.pragma("journal_mode = WAL");
|
|
@@ -4255,8 +4620,74 @@ function buildAgentCommand() {
|
|
|
4255
4620
|
}
|
|
4256
4621
|
getUI().ok("Skill install requested", `agent=${opts.agent} skill=${slugOrId}`);
|
|
4257
4622
|
}, { code: "agent_install_skill_failed" }));
|
|
4623
|
+
cmd.command("snapshot <imUserId>").description("Create an agent-level snapshot").option("--include-memory", "Include memory dump reference").option("--label <label>", "Snapshot label").option("--json", "Output JSON").action(runAction(async (imUserId, opts) => {
|
|
4624
|
+
const cloud = makeCloudClient();
|
|
4625
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agents/${encodeURIComponent(imUserId)}/snapshot`, {
|
|
4626
|
+
includeMemory: Boolean(opts.includeMemory),
|
|
4627
|
+
label: opts.label
|
|
4628
|
+
}, "agent_snapshot_failed");
|
|
4629
|
+
if (opts.json) {
|
|
4630
|
+
printJson(data);
|
|
4631
|
+
return;
|
|
4632
|
+
}
|
|
4633
|
+
const rec = data;
|
|
4634
|
+
getUI().ok("Snapshot created", String(rec.id ?? "<unknown>"));
|
|
4635
|
+
getUI().line(`agent=${String(rec.agentImUserId ?? imUserId)} memory=${rec.includeMemory ? "included" : "stripped"}`);
|
|
4636
|
+
}, { code: "agent_snapshot_failed" }));
|
|
4637
|
+
cmd.command("publish <imUserId>").description("Publish an agent as an Agent Pack").requiredOption("--slug <slug>", "Agent Pack slug").option("--version <version>", "Package version", "1.0.0").option("--license <license>", "Package license", "proprietary").option("--title <title>", "Display title metadata").option("--description <description>", "Description metadata").option("--json", "Output JSON").action(runAction(async (imUserId, opts) => {
|
|
4638
|
+
const cloud = makeCloudClient();
|
|
4639
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agents/${encodeURIComponent(imUserId)}/publish`, {
|
|
4640
|
+
slug: opts.slug,
|
|
4641
|
+
version: opts.version,
|
|
4642
|
+
license: opts.license,
|
|
4643
|
+
metadata: {
|
|
4644
|
+
...opts.title ? { title: opts.title } : {},
|
|
4645
|
+
...opts.description ? { description: opts.description } : {}
|
|
4646
|
+
},
|
|
4647
|
+
stripMemory: true
|
|
4648
|
+
}, "agent_publish_failed");
|
|
4649
|
+
if (opts.json) {
|
|
4650
|
+
printJson(data);
|
|
4651
|
+
return;
|
|
4652
|
+
}
|
|
4653
|
+
const rec = data;
|
|
4654
|
+
getUI().ok("Agent Pack published", `${String(rec.slug ?? opts.slug)}@${String(rec.version ?? opts.version ?? "1.0.0")}`);
|
|
4655
|
+
getUI().line(`package=${String(rec.id ?? "<unknown>")}`);
|
|
4656
|
+
}, { code: "agent_publish_failed" }));
|
|
4657
|
+
cmd.command("fork <packIdOrSlug>").description("Fork an Agent Pack into a workspace").requiredOption("--workspace-id <id>", "Target workspace id").option("--display-name <name>", "New agent display name").option("--json", "Output JSON").action(runAction(async (packIdOrSlug, opts) => {
|
|
4658
|
+
const cloud = makeCloudClient();
|
|
4659
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agent-packs/${encodeURIComponent(packIdOrSlug)}/fork`, {
|
|
4660
|
+
targetWorkspaceId: opts.workspaceId,
|
|
4661
|
+
displayName: opts.displayName
|
|
4662
|
+
}, "agent_fork_failed");
|
|
4663
|
+
if (opts.json) {
|
|
4664
|
+
printJson(data);
|
|
4665
|
+
return;
|
|
4666
|
+
}
|
|
4667
|
+
const rec = data;
|
|
4668
|
+
getUI().ok("Agent Pack forked", `newAgent=${String(rec.newImUserId ?? "<unknown>")}`);
|
|
4669
|
+
getUI().line(`did=${String(rec.newDid ?? "not issued")}`);
|
|
4670
|
+
}, { code: "agent_fork_failed" }));
|
|
4258
4671
|
return cmd;
|
|
4259
4672
|
}
|
|
4673
|
+
function makeCloudClient() {
|
|
4674
|
+
const cfg = loadConfig(resolvePaths());
|
|
4675
|
+
return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
4676
|
+
}
|
|
4677
|
+
async function requestEnvelope(cloud, method, path9, body, code) {
|
|
4678
|
+
const res = await cloud.request(
|
|
4679
|
+
method,
|
|
4680
|
+
path9,
|
|
4681
|
+
body === void 0 ? void 0 : { body }
|
|
4682
|
+
);
|
|
4683
|
+
if (!res.ok || res.data?.ok === false) {
|
|
4684
|
+
const message = res.error?.message ?? res.data?.error?.message ?? "request failed";
|
|
4685
|
+
exitWithError(`${path9} failed (${res.status === 0 ? "network error" : `HTTP ${res.status}`}): ${message}`, {
|
|
4686
|
+
code: res.error?.code ?? res.data?.error?.code ?? code
|
|
4687
|
+
});
|
|
4688
|
+
}
|
|
4689
|
+
return res.data && typeof res.data === "object" && "data" in res.data ? res.data.data : res.data;
|
|
4690
|
+
}
|
|
4260
4691
|
function readLocalAgents(localDb) {
|
|
4261
4692
|
const db = openLocalDb(localDb);
|
|
4262
4693
|
try {
|
|
@@ -4361,13 +4792,13 @@ function whichBinary2(bin) {
|
|
|
4361
4792
|
// src/cli/commands/asset.ts
|
|
4362
4793
|
import { createHash as createHash2 } from "crypto";
|
|
4363
4794
|
import { Command as Command3 } from "commander";
|
|
4364
|
-
import { existsSync as existsSync10, lstatSync, readdirSync, readFileSync as readFileSync9, writeFileSync as
|
|
4795
|
+
import { existsSync as existsSync10, lstatSync, readdirSync, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
4365
4796
|
import { basename, dirname as dirname7, join as join10, relative } from "path";
|
|
4366
4797
|
|
|
4367
4798
|
// src/asset-cache.ts
|
|
4368
4799
|
import { createHash } from "crypto";
|
|
4369
4800
|
import { promises as dnsp } from "dns";
|
|
4370
|
-
import { existsSync as existsSync7, mkdirSync as
|
|
4801
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync6, renameSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
4371
4802
|
import { isIP, isIPv4, isIPv6 } from "net";
|
|
4372
4803
|
import { dirname as dirname6, join as join7 } from "path";
|
|
4373
4804
|
import { Agent as UndiciAgent } from "undici";
|
|
@@ -4393,7 +4824,7 @@ var AssetCache = class {
|
|
|
4393
4824
|
this.cacheDir = opts.cacheDir;
|
|
4394
4825
|
this.maxBytes = opts.maxBytes ?? 5 * 1024 * 1024 * 1024;
|
|
4395
4826
|
if (!existsSync7(this.cacheDir)) {
|
|
4396
|
-
|
|
4827
|
+
mkdirSync6(this.cacheDir, { recursive: true });
|
|
4397
4828
|
}
|
|
4398
4829
|
}
|
|
4399
4830
|
/** Local file path for a content hash. Files split by first 2 chars for fs scaling. */
|
|
@@ -4468,10 +4899,10 @@ var AssetCache = class {
|
|
|
4468
4899
|
}
|
|
4469
4900
|
const localPath = this.pathFor(hash);
|
|
4470
4901
|
if (!existsSync7(dirname6(localPath))) {
|
|
4471
|
-
|
|
4902
|
+
mkdirSync6(dirname6(localPath), { recursive: true });
|
|
4472
4903
|
}
|
|
4473
4904
|
const tmpPath = `${localPath}.${process.pid}.${Date.now()}.tmp`;
|
|
4474
|
-
|
|
4905
|
+
writeFileSync6(tmpPath, buf);
|
|
4475
4906
|
renameSync(tmpPath, localPath);
|
|
4476
4907
|
const mime = res.headers.get("content-type");
|
|
4477
4908
|
const now = Date.now();
|
|
@@ -4532,10 +4963,10 @@ var AssetCache = class {
|
|
|
4532
4963
|
}
|
|
4533
4964
|
const localPath = this.pathFor(hash);
|
|
4534
4965
|
if (!existsSync7(dirname6(localPath))) {
|
|
4535
|
-
|
|
4966
|
+
mkdirSync6(dirname6(localPath), { recursive: true });
|
|
4536
4967
|
}
|
|
4537
4968
|
const tmpPath = `${localPath}.${process.pid}.${Date.now()}.tmp`;
|
|
4538
|
-
|
|
4969
|
+
writeFileSync6(tmpPath, body);
|
|
4539
4970
|
renameSync(tmpPath, localPath);
|
|
4540
4971
|
const now = Date.now();
|
|
4541
4972
|
this.db.prepare(
|
|
@@ -4789,7 +5220,7 @@ async function fetchUrlWithGuards(rawUrl, opts) {
|
|
|
4789
5220
|
}
|
|
4790
5221
|
|
|
4791
5222
|
// src/daemon/asset/metadata-index.ts
|
|
4792
|
-
import { existsSync as existsSync8, mkdirSync as
|
|
5223
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
4793
5224
|
import { join as join8 } from "path";
|
|
4794
5225
|
var DEFAULT_LIMIT = 8;
|
|
4795
5226
|
var PULL_PAGE_SIZE = 500;
|
|
@@ -4819,7 +5250,7 @@ var AssetMetadataIndex = class {
|
|
|
4819
5250
|
this.cloud = opts.cloud;
|
|
4820
5251
|
this.workspaceId = opts.workspaceId;
|
|
4821
5252
|
if (!existsSync8(opts.workspaceStateDir)) {
|
|
4822
|
-
|
|
5253
|
+
mkdirSync7(opts.workspaceStateDir, { recursive: true });
|
|
4823
5254
|
}
|
|
4824
5255
|
this.cursorPath = join8(opts.workspaceStateDir, "asset-metadata-cursor.json");
|
|
4825
5256
|
}
|
|
@@ -4840,7 +5271,7 @@ var AssetMetadataIndex = class {
|
|
|
4840
5271
|
cursor,
|
|
4841
5272
|
writtenAt: Date.now()
|
|
4842
5273
|
};
|
|
4843
|
-
|
|
5274
|
+
writeFileSync7(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
4844
5275
|
}
|
|
4845
5276
|
/**
|
|
4846
5277
|
* Pull incremental asset metadata changes since the persisted cursor and
|
|
@@ -4957,7 +5388,7 @@ var AssetMetadataIndex = class {
|
|
|
4957
5388
|
};
|
|
4958
5389
|
|
|
4959
5390
|
// src/daemon/asset/mirror.ts
|
|
4960
|
-
import { existsSync as existsSync9, mkdirSync as
|
|
5391
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
4961
5392
|
import { join as join9 } from "path";
|
|
4962
5393
|
function rowToBinding(row) {
|
|
4963
5394
|
return {
|
|
@@ -4980,7 +5411,7 @@ var WorkspaceMirror = class {
|
|
|
4980
5411
|
this.cloud = opts.cloud;
|
|
4981
5412
|
this.workspaceId = opts.workspaceId;
|
|
4982
5413
|
if (!existsSync9(opts.workspaceStateDir)) {
|
|
4983
|
-
|
|
5414
|
+
mkdirSync8(opts.workspaceStateDir, { recursive: true });
|
|
4984
5415
|
}
|
|
4985
5416
|
this.cursorPath = join9(opts.workspaceStateDir, "asset-cursor.json");
|
|
4986
5417
|
}
|
|
@@ -5001,7 +5432,7 @@ var WorkspaceMirror = class {
|
|
|
5001
5432
|
cursor,
|
|
5002
5433
|
writtenAt: Date.now()
|
|
5003
5434
|
};
|
|
5004
|
-
|
|
5435
|
+
writeFileSync8(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
5005
5436
|
}
|
|
5006
5437
|
/**
|
|
5007
5438
|
* Pull workspace_file changes since the persisted cursor and upsert into
|
|
@@ -5439,7 +5870,7 @@ async function downloadAsset(assetId, outPath) {
|
|
|
5439
5870
|
exitWithError(`asset download failed (${res.status}): ${errorMessage(body)}`);
|
|
5440
5871
|
}
|
|
5441
5872
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
5442
|
-
|
|
5873
|
+
writeFileSync9(outPath, bytes);
|
|
5443
5874
|
}
|
|
5444
5875
|
function parseMetadata(raw) {
|
|
5445
5876
|
if (!raw) return {};
|
|
@@ -6086,8 +6517,8 @@ function parsePositiveInt3(value) {
|
|
|
6086
6517
|
// src/cli/commands/daemon.ts
|
|
6087
6518
|
import { Command as Command8 } from "commander";
|
|
6088
6519
|
import { spawn as spawn5 } from "child_process";
|
|
6089
|
-
import { createReadStream, existsSync as existsSync14, mkdirSync as
|
|
6090
|
-
import { setTimeout as
|
|
6520
|
+
import { createReadStream, existsSync as existsSync14, mkdirSync as mkdirSync10, openSync as openSync2, statSync as statSync4 } from "fs";
|
|
6521
|
+
import { setTimeout as sleep3 } from "timers/promises";
|
|
6091
6522
|
import { join as join18 } from "path";
|
|
6092
6523
|
|
|
6093
6524
|
// src/daemon/runner.ts
|
|
@@ -6095,7 +6526,7 @@ import { EventEmitter as EventEmitter3 } from "events";
|
|
|
6095
6526
|
import { createRequire as createRequire2 } from "module";
|
|
6096
6527
|
import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
|
|
6097
6528
|
import { mkdir } from "fs/promises";
|
|
6098
|
-
import { homedir as
|
|
6529
|
+
import { homedir as homedir8, platform } from "os";
|
|
6099
6530
|
import { join as join17 } from "path";
|
|
6100
6531
|
|
|
6101
6532
|
// src/adapters/registry.ts
|
|
@@ -6576,6 +7007,262 @@ function createLogger(tag) {
|
|
|
6576
7007
|
import { readFileSync as readFileSync10, promises as fsp3 } from "fs";
|
|
6577
7008
|
import * as path2 from "path";
|
|
6578
7009
|
init_skill_sync();
|
|
7010
|
+
|
|
7011
|
+
// src/daemon/task-heartbeat.ts
|
|
7012
|
+
var TaskHeartbeat = class {
|
|
7013
|
+
constructor(opts) {
|
|
7014
|
+
this.opts = opts;
|
|
7015
|
+
this.intervalMs = opts.intervalMs ?? 15e3;
|
|
7016
|
+
this.maxRetries = opts.maxRetries ?? 3;
|
|
7017
|
+
this.retryBackoffMs = opts.retryBackoffMs ?? 100;
|
|
7018
|
+
this.now = opts.now ?? Date.now;
|
|
7019
|
+
}
|
|
7020
|
+
opts;
|
|
7021
|
+
timer;
|
|
7022
|
+
version = 0;
|
|
7023
|
+
taskId;
|
|
7024
|
+
getPhase;
|
|
7025
|
+
/** Tracks the last step time pushed via touchStep() — informational. */
|
|
7026
|
+
lastStepAt;
|
|
7027
|
+
intervalMs;
|
|
7028
|
+
maxRetries;
|
|
7029
|
+
retryBackoffMs;
|
|
7030
|
+
now;
|
|
7031
|
+
/**
|
|
7032
|
+
* Begin heartbeating for `taskId`. `getPhase` is invoked at every tick to
|
|
7033
|
+
* pick up the live phase string — adapters can mutate their phase state
|
|
7034
|
+
* freely and the heartbeat will surface it on the next 15s boundary.
|
|
7035
|
+
*
|
|
7036
|
+
* Calling start() twice on the same instance replaces the active task
|
|
7037
|
+
* (previous timer is cleared). Returns silently when already started for
|
|
7038
|
+
* the same taskId.
|
|
7039
|
+
*/
|
|
7040
|
+
start(taskId, getPhase) {
|
|
7041
|
+
if (this.taskId === taskId && this.timer) return;
|
|
7042
|
+
this.stop();
|
|
7043
|
+
this.taskId = taskId;
|
|
7044
|
+
this.getPhase = getPhase;
|
|
7045
|
+
this.timer = setInterval(() => {
|
|
7046
|
+
void this.push();
|
|
7047
|
+
}, this.intervalMs);
|
|
7048
|
+
this.timer.unref?.();
|
|
7049
|
+
}
|
|
7050
|
+
/**
|
|
7051
|
+
* Stop the heartbeat loop. Idempotent.
|
|
7052
|
+
*/
|
|
7053
|
+
stop() {
|
|
7054
|
+
if (this.timer) {
|
|
7055
|
+
clearInterval(this.timer);
|
|
7056
|
+
this.timer = void 0;
|
|
7057
|
+
}
|
|
7058
|
+
this.taskId = void 0;
|
|
7059
|
+
this.getPhase = void 0;
|
|
7060
|
+
}
|
|
7061
|
+
/**
|
|
7062
|
+
* Phase transition — push immediately AND update what the timer-driven
|
|
7063
|
+
* tick will report next time. Adapter calls this whenever the lifecycle
|
|
7064
|
+
* advances (thinking → tool_use → reasoning → responding → ...).
|
|
7065
|
+
*
|
|
7066
|
+
* No-op when no task is active.
|
|
7067
|
+
*/
|
|
7068
|
+
setPhase(phase) {
|
|
7069
|
+
if (!this.taskId) return;
|
|
7070
|
+
this.getPhase = () => phase;
|
|
7071
|
+
void this.push();
|
|
7072
|
+
}
|
|
7073
|
+
/**
|
|
7074
|
+
* Mark "I just made observable progress." Updates lastStepAt; next push
|
|
7075
|
+
* (timer-driven or setPhase-driven) carries it. Lightweight — no immediate
|
|
7076
|
+
* send. Adapters call this around tool calls / token boundaries so the
|
|
7077
|
+
* cloud reaper can distinguish "alive but slow" from "truly stuck".
|
|
7078
|
+
*/
|
|
7079
|
+
touchStep() {
|
|
7080
|
+
this.lastStepAt = this.now();
|
|
7081
|
+
}
|
|
7082
|
+
/**
|
|
7083
|
+
* Manually trigger one heartbeat send (does not affect the timer). Mainly
|
|
7084
|
+
* useful for tests that don't want to await a 15s tick. Returns the
|
|
7085
|
+
* heartbeatVersion that was sent, or undefined if no task is active.
|
|
7086
|
+
*/
|
|
7087
|
+
async forceTick() {
|
|
7088
|
+
if (!this.taskId) return void 0;
|
|
7089
|
+
return this.push();
|
|
7090
|
+
}
|
|
7091
|
+
/** Test helper: current heartbeatVersion (last value sent, may be 0 before first tick). */
|
|
7092
|
+
get currentVersion() {
|
|
7093
|
+
return this.version;
|
|
7094
|
+
}
|
|
7095
|
+
async push() {
|
|
7096
|
+
if (!this.taskId || !this.getPhase) return void 0;
|
|
7097
|
+
const taskId = this.taskId;
|
|
7098
|
+
const phase = this.getPhase();
|
|
7099
|
+
const heartbeatVersion = ++this.version;
|
|
7100
|
+
const payload = {
|
|
7101
|
+
taskId,
|
|
7102
|
+
heartbeatVersion,
|
|
7103
|
+
currentPhase: phase,
|
|
7104
|
+
...this.lastStepAt !== void 0 ? { lastStepAt: this.lastStepAt } : {}
|
|
7105
|
+
};
|
|
7106
|
+
const msg = envelope("task.heartbeat", payload);
|
|
7107
|
+
let lastErr;
|
|
7108
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
|
|
7109
|
+
try {
|
|
7110
|
+
if (this.opts.ws.isOpen && !this.opts.ws.isOpen()) {
|
|
7111
|
+
return heartbeatVersion;
|
|
7112
|
+
}
|
|
7113
|
+
this.opts.ws.send(msg);
|
|
7114
|
+
return heartbeatVersion;
|
|
7115
|
+
} catch (err) {
|
|
7116
|
+
lastErr = err instanceof Error ? err : new Error(String(err));
|
|
7117
|
+
if (attempt < this.maxRetries) {
|
|
7118
|
+
const backoff = this.retryBackoffMs * Math.pow(2, attempt);
|
|
7119
|
+
await sleep(backoff);
|
|
7120
|
+
}
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7123
|
+
if (lastErr) {
|
|
7124
|
+
try {
|
|
7125
|
+
this.opts.onGiveUp?.(taskId, lastErr);
|
|
7126
|
+
} catch {
|
|
7127
|
+
}
|
|
7128
|
+
}
|
|
7129
|
+
return heartbeatVersion;
|
|
7130
|
+
}
|
|
7131
|
+
};
|
|
7132
|
+
function sleep(ms) {
|
|
7133
|
+
return new Promise((resolve3) => {
|
|
7134
|
+
const handle = setTimeout(resolve3, ms);
|
|
7135
|
+
handle.unref?.();
|
|
7136
|
+
});
|
|
7137
|
+
}
|
|
7138
|
+
|
|
7139
|
+
// src/daemon/step-recorder.ts
|
|
7140
|
+
var StepRecorder = class {
|
|
7141
|
+
constructor(opts) {
|
|
7142
|
+
this.opts = opts;
|
|
7143
|
+
this.reasoningThrottleMs = opts.reasoningThrottleMs ?? 500;
|
|
7144
|
+
this.now = opts.now ?? Date.now;
|
|
7145
|
+
}
|
|
7146
|
+
opts;
|
|
7147
|
+
seq = 0;
|
|
7148
|
+
reasoningBuf = { texts: [], firstAt: 0 };
|
|
7149
|
+
reasoningTimer;
|
|
7150
|
+
reasoningThrottleMs;
|
|
7151
|
+
now;
|
|
7152
|
+
/** Push a phase transition step. Immediate (no throttle). */
|
|
7153
|
+
recordPhaseChange(phase) {
|
|
7154
|
+
this.emit("phase_change", { phase });
|
|
7155
|
+
}
|
|
7156
|
+
/**
|
|
7157
|
+
* Push a tool-call step. Immediate.
|
|
7158
|
+
*
|
|
7159
|
+
* `toolCallId` is the daemon-side correlation id used to pair with the
|
|
7160
|
+
* subsequent recordToolResult call. Free-form; adapter decides.
|
|
7161
|
+
*/
|
|
7162
|
+
recordToolCall(toolName, input, toolCallId) {
|
|
7163
|
+
this.emit("tool_call", {
|
|
7164
|
+
toolName,
|
|
7165
|
+
inputSummary: summarize2(input),
|
|
7166
|
+
...toolCallId ? { toolCallId } : {}
|
|
7167
|
+
});
|
|
7168
|
+
}
|
|
7169
|
+
/** Push a tool-result step. Immediate. */
|
|
7170
|
+
recordToolResult(toolCallId, output) {
|
|
7171
|
+
this.emit("tool_result", {
|
|
7172
|
+
toolCallId,
|
|
7173
|
+
outputSummary: summarize2(output)
|
|
7174
|
+
});
|
|
7175
|
+
}
|
|
7176
|
+
/**
|
|
7177
|
+
* Buffer a reasoning chunk for batched dispatch. Multiple chunks within
|
|
7178
|
+
* `reasoningThrottleMs` are concatenated and emitted as a single
|
|
7179
|
+
* `reasoning_chunk` step when the timer fires.
|
|
7180
|
+
*/
|
|
7181
|
+
recordReasoningChunk(text) {
|
|
7182
|
+
if (!text) return;
|
|
7183
|
+
if (this.reasoningBuf.texts.length === 0) {
|
|
7184
|
+
this.reasoningBuf.firstAt = this.now();
|
|
7185
|
+
}
|
|
7186
|
+
this.reasoningBuf.texts.push(text);
|
|
7187
|
+
if (this.reasoningTimer) return;
|
|
7188
|
+
this.reasoningTimer = setTimeout(() => {
|
|
7189
|
+
this.flushReasoning();
|
|
7190
|
+
}, this.reasoningThrottleMs);
|
|
7191
|
+
this.reasoningTimer.unref?.();
|
|
7192
|
+
}
|
|
7193
|
+
/** Push an error step. Immediate. */
|
|
7194
|
+
recordError(message, payload) {
|
|
7195
|
+
this.emit("error", { message, ...payload ?? {} });
|
|
7196
|
+
}
|
|
7197
|
+
/**
|
|
7198
|
+
* Force any pending reasoning buffer out the door. Call on shutdown so
|
|
7199
|
+
* trailing tokens aren't lost.
|
|
7200
|
+
*/
|
|
7201
|
+
flush() {
|
|
7202
|
+
this.flushReasoning();
|
|
7203
|
+
}
|
|
7204
|
+
/** Test helper. */
|
|
7205
|
+
get currentSeq() {
|
|
7206
|
+
return this.seq;
|
|
7207
|
+
}
|
|
7208
|
+
flushReasoning() {
|
|
7209
|
+
if (this.reasoningTimer) {
|
|
7210
|
+
clearTimeout(this.reasoningTimer);
|
|
7211
|
+
this.reasoningTimer = void 0;
|
|
7212
|
+
}
|
|
7213
|
+
if (this.reasoningBuf.texts.length === 0) return;
|
|
7214
|
+
const text = this.reasoningBuf.texts.join("");
|
|
7215
|
+
const firstAt = this.reasoningBuf.firstAt;
|
|
7216
|
+
this.reasoningBuf = { texts: [], firstAt: 0 };
|
|
7217
|
+
this.emit("reasoning_chunk", { text }, firstAt);
|
|
7218
|
+
}
|
|
7219
|
+
emit(kind, payload, occurredAt) {
|
|
7220
|
+
const frame = {
|
|
7221
|
+
seq: ++this.seq,
|
|
7222
|
+
kind,
|
|
7223
|
+
payload,
|
|
7224
|
+
occurredAt: occurredAt ?? this.now()
|
|
7225
|
+
};
|
|
7226
|
+
const msg = envelope("task.step.append", {
|
|
7227
|
+
taskRunId: this.opts.taskRunId,
|
|
7228
|
+
step: frame
|
|
7229
|
+
});
|
|
7230
|
+
try {
|
|
7231
|
+
if (this.opts.ws.isOpen && !this.opts.ws.isOpen()) {
|
|
7232
|
+
try {
|
|
7233
|
+
this.opts.onSend?.(frame);
|
|
7234
|
+
} catch {
|
|
7235
|
+
}
|
|
7236
|
+
return;
|
|
7237
|
+
}
|
|
7238
|
+
this.opts.ws.send(msg);
|
|
7239
|
+
try {
|
|
7240
|
+
this.opts.onSend?.(frame);
|
|
7241
|
+
} catch {
|
|
7242
|
+
}
|
|
7243
|
+
} catch {
|
|
7244
|
+
}
|
|
7245
|
+
}
|
|
7246
|
+
};
|
|
7247
|
+
var MAX_SUMMARY_CHARS = 512;
|
|
7248
|
+
function summarize2(value) {
|
|
7249
|
+
let text;
|
|
7250
|
+
if (value == null) text = "";
|
|
7251
|
+
else if (typeof value === "string") text = value;
|
|
7252
|
+
else {
|
|
7253
|
+
try {
|
|
7254
|
+
text = JSON.stringify(value);
|
|
7255
|
+
} catch {
|
|
7256
|
+
text = String(value);
|
|
7257
|
+
}
|
|
7258
|
+
}
|
|
7259
|
+
if (text.length > MAX_SUMMARY_CHARS) {
|
|
7260
|
+
return `${text.slice(0, MAX_SUMMARY_CHARS)}\u2026(+${text.length - MAX_SUMMARY_CHARS} chars)`;
|
|
7261
|
+
}
|
|
7262
|
+
return text;
|
|
7263
|
+
}
|
|
7264
|
+
|
|
7265
|
+
// src/daemon/dispatch.ts
|
|
6579
7266
|
var DEFAULT_CONTEXT_MAX = 8e3;
|
|
6580
7267
|
var GOAL_CONTEXT_MAX = 4;
|
|
6581
7268
|
var MEMORY_DIGEST_MAX_BYTES = 4e3;
|
|
@@ -6591,6 +7278,8 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6591
7278
|
const { taskId, agentImUserId } = payload;
|
|
6592
7279
|
let resolvedHashes = [];
|
|
6593
7280
|
let reply;
|
|
7281
|
+
let heartbeatRef;
|
|
7282
|
+
let recorderRef;
|
|
6594
7283
|
const outboxBase = deps.paths?.runsDir ? path2.join(deps.paths.runsDir, taskId) : null;
|
|
6595
7284
|
const outboxDir = outboxBase ? path2.join(outboxBase, "_outbox") : null;
|
|
6596
7285
|
const workDir = outboxBase ? path2.join(outboxBase, "workdir") : null;
|
|
@@ -6672,7 +7361,13 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6672
7361
|
resolvedHashes.push(...r.resolvedHashes);
|
|
6673
7362
|
rewrittenContext = payload.context.map((e, i) => ({ ...e, content: r.texts[i] }));
|
|
6674
7363
|
}
|
|
6675
|
-
const assetResolution = await resolveAssetRefs(payload.assetRefs, deps.assetCache, taskId
|
|
7364
|
+
const assetResolution = await resolveAssetRefs(payload.assetRefs, deps.assetCache, taskId, {
|
|
7365
|
+
cloud: deps.cloud,
|
|
7366
|
+
enabled: deps.visionAux?.enabled !== false,
|
|
7367
|
+
cacheDir: deps.visionAux?.cacheDir ?? (deps.paths ? path2.join(deps.paths.cacheDir, "vision-cache") : void 0),
|
|
7368
|
+
timeoutMs: deps.visionAux?.timeoutMs,
|
|
7369
|
+
signal: deps.signal
|
|
7370
|
+
});
|
|
6676
7371
|
resolvedHashes.push(...assetResolution.pinnedHashes);
|
|
6677
7372
|
const basePrompt = composePrompt(
|
|
6678
7373
|
rewrittenPrompt.text,
|
|
@@ -6707,9 +7402,44 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6707
7402
|
}
|
|
6708
7403
|
const operatingPrinciples = resolveOperatingPrinciples(profile.config);
|
|
6709
7404
|
const composedSystemPrompt = [profileSystemPrompt, operatingPrinciples].filter((part) => typeof part === "string" && part.length > 0).join("\n\n");
|
|
7405
|
+
let activePhase = "thinking";
|
|
7406
|
+
const heartbeat = new TaskHeartbeat({
|
|
7407
|
+
ws: deps.ws,
|
|
7408
|
+
onGiveUp: (tid, err) => process.stderr.write(
|
|
7409
|
+
`[daemon] task.heartbeat give-up task=${tid}: ${err.message}
|
|
7410
|
+
`
|
|
7411
|
+
)
|
|
7412
|
+
});
|
|
7413
|
+
heartbeatRef = heartbeat;
|
|
7414
|
+
heartbeat.start(taskId, () => activePhase);
|
|
7415
|
+
const recorder = new StepRecorder({ ws: deps.ws, taskRunId: taskId });
|
|
7416
|
+
recorderRef = recorder;
|
|
7417
|
+
recorder.recordPhaseChange(activePhase);
|
|
6710
7418
|
const taskInput = {
|
|
6711
7419
|
taskId,
|
|
6712
7420
|
prompt: adapterPrompt,
|
|
7421
|
+
heartbeat: {
|
|
7422
|
+
setPhase: (phase) => {
|
|
7423
|
+
activePhase = phase;
|
|
7424
|
+
heartbeat.setPhase(phase);
|
|
7425
|
+
recorder.recordPhaseChange(phase);
|
|
7426
|
+
},
|
|
7427
|
+
touchStep: () => heartbeat.touchStep()
|
|
7428
|
+
},
|
|
7429
|
+
recorder: {
|
|
7430
|
+
recordPhaseChange: (phase) => {
|
|
7431
|
+
activePhase = phase;
|
|
7432
|
+
heartbeat.setPhase(phase);
|
|
7433
|
+
recorder.recordPhaseChange(phase);
|
|
7434
|
+
},
|
|
7435
|
+
recordToolCall: (toolName, input, toolCallId) => recorder.recordToolCall(toolName, input, toolCallId),
|
|
7436
|
+
recordToolResult: (toolCallId, output) => recorder.recordToolResult(toolCallId, output),
|
|
7437
|
+
recordReasoningChunk: (text) => recorder.recordReasoningChunk(text),
|
|
7438
|
+
recordError: (message, payload2) => recorder.recordError(message, payload2)
|
|
7439
|
+
},
|
|
7440
|
+
// 14b rev.3 §9 P3 — multimodal-aware adapters (Hermes, OpenClaw) pick
|
|
7441
|
+
// up the resolved bytes/URLs here; text-only adapters ignore the field.
|
|
7442
|
+
...assetResolution.resolvedRefs.length > 0 ? { assetRefs: assetResolution.resolvedRefs } : {},
|
|
6713
7443
|
metadata: {
|
|
6714
7444
|
...payload.metadata,
|
|
6715
7445
|
conversationId: payload.conversationId,
|
|
@@ -6772,6 +7502,9 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6772
7502
|
}
|
|
6773
7503
|
}
|
|
6774
7504
|
const reaperAborted = deps.signal?.aborted && result.error?.code === "task_cancelled";
|
|
7505
|
+
const approvalRequested = Boolean(
|
|
7506
|
+
result.metadata?.approvalRequested
|
|
7507
|
+
);
|
|
6775
7508
|
let collectedAssetIds = [];
|
|
6776
7509
|
if (deps.outboxWatcher && outboxDir) {
|
|
6777
7510
|
try {
|
|
@@ -6783,14 +7516,23 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6783
7516
|
collectedAssetIds = deps.outboxWatcher.flushPending(taskId);
|
|
6784
7517
|
}
|
|
6785
7518
|
const reaperLimitMs = typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? payload.timeoutMs : 5 * 6e4;
|
|
7519
|
+
const finalError = approvalRequested ? {
|
|
7520
|
+
code: "awaiting_human_approval",
|
|
7521
|
+
message: "Agent requested human approval and is suspended pending decision. Cloud will redispatch with `approval.decided` once the human responds."
|
|
7522
|
+
} : reaperAborted ? {
|
|
7523
|
+
code: "daemon_task_timeout",
|
|
7524
|
+
message: `Daemon-side reaper aborted after ${reaperLimitMs}ms inactivity (no progress events from adapter)`
|
|
7525
|
+
} : result.error;
|
|
6786
7526
|
reply = {
|
|
6787
7527
|
taskId,
|
|
7528
|
+
// When approval is pending we keep ok=false (the run did not produce a
|
|
7529
|
+
// completion-style output) but the cloud-side handler treats the
|
|
7530
|
+
// `awaiting_human_approval` code as non-terminal — task stays alive,
|
|
7531
|
+
// no red failure pill, and the next dispatch (after the human decides)
|
|
7532
|
+
// continues normally.
|
|
6788
7533
|
ok: result.ok,
|
|
6789
7534
|
output: result.output,
|
|
6790
|
-
error:
|
|
6791
|
-
code: "daemon_task_timeout",
|
|
6792
|
-
message: `Daemon-side reaper aborted after ${reaperLimitMs}ms inactivity (no progress events from adapter)`
|
|
6793
|
-
} : result.error,
|
|
7535
|
+
error: finalError,
|
|
6794
7536
|
...collectedAssetIds.length > 0 ? { assetIds: collectedAssetIds } : {},
|
|
6795
7537
|
metrics: result.metrics,
|
|
6796
7538
|
...assetResolution.observability.length > 0 || urlObservations.length > 0 ? { assetObservability: [...assetResolution.observability, ...urlObservations] } : {}
|
|
@@ -6812,6 +7554,18 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6812
7554
|
}
|
|
6813
7555
|
};
|
|
6814
7556
|
} finally {
|
|
7557
|
+
try {
|
|
7558
|
+
recorderRef?.flush();
|
|
7559
|
+
} catch (err) {
|
|
7560
|
+
process.stderr.write(`[daemon] step recorder flush failed task=${taskId}: ${err.message}
|
|
7561
|
+
`);
|
|
7562
|
+
}
|
|
7563
|
+
try {
|
|
7564
|
+
heartbeatRef?.stop();
|
|
7565
|
+
} catch (err) {
|
|
7566
|
+
process.stderr.write(`[daemon] heartbeat stop failed task=${taskId}: ${err.message}
|
|
7567
|
+
`);
|
|
7568
|
+
}
|
|
6815
7569
|
for (const hash of new Set(resolvedHashes)) {
|
|
6816
7570
|
try {
|
|
6817
7571
|
deps.assetCache.unpin(hash);
|
|
@@ -6888,6 +7642,10 @@ function isTextLikeMime(mime) {
|
|
|
6888
7642
|
if (m.endsWith("+json") || m.endsWith("+xml") || m.endsWith("+csv")) return true;
|
|
6889
7643
|
return false;
|
|
6890
7644
|
}
|
|
7645
|
+
function isImageMime(mime) {
|
|
7646
|
+
if (!mime) return false;
|
|
7647
|
+
return mime.toLowerCase().split(";")[0].trim().startsWith("image/");
|
|
7648
|
+
}
|
|
6891
7649
|
var HASH_REF_RE = /(?:^|\s)#([^\s#]+)/g;
|
|
6892
7650
|
var HEX_COLOR_RE = /^[0-9a-fA-F]{3,8}$/;
|
|
6893
7651
|
var FILE_EXT_RE = /\.[a-zA-Z0-9]{1,10}$/;
|
|
@@ -6966,14 +7724,40 @@ async function resolveHashRefs(prompt, assetIndex, cloud) {
|
|
|
6966
7724
|
}
|
|
6967
7725
|
return { text: result, resolutions };
|
|
6968
7726
|
}
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
7727
|
+
var ASSET_BASE64_INLINE_MAX_BYTES = 10 * 1024 * 1024;
|
|
7728
|
+
async function probeCdnReachable(cdnUrl, timeoutMs = 1500) {
|
|
7729
|
+
try {
|
|
7730
|
+
const u = new URL(cdnUrl);
|
|
7731
|
+
if (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname.startsWith("192.168.")) {
|
|
7732
|
+
return false;
|
|
7733
|
+
}
|
|
7734
|
+
} catch {
|
|
7735
|
+
return false;
|
|
7736
|
+
}
|
|
7737
|
+
try {
|
|
7738
|
+
const r = await fetch(cdnUrl, {
|
|
7739
|
+
method: "HEAD",
|
|
7740
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
7741
|
+
});
|
|
7742
|
+
if (r.ok) return true;
|
|
7743
|
+
return false;
|
|
7744
|
+
} catch {
|
|
7745
|
+
return false;
|
|
7746
|
+
}
|
|
7747
|
+
}
|
|
7748
|
+
async function resolveAssetRefs(refs, cache, taskId, visionAux) {
|
|
7749
|
+
const out = {
|
|
7750
|
+
promptBlocks: [],
|
|
7751
|
+
observability: [],
|
|
7752
|
+
pinnedHashes: [],
|
|
7753
|
+
resolvedRefs: []
|
|
7754
|
+
};
|
|
7755
|
+
if (!refs || refs.length === 0) return out;
|
|
7756
|
+
for (const ref of refs) {
|
|
7757
|
+
let cached;
|
|
7758
|
+
try {
|
|
7759
|
+
cached = await cache.getOrFetch(ref.contentHash, {
|
|
7760
|
+
workspaceIdHint: ref.workspaceId,
|
|
6977
7761
|
assetId: ref.assetId
|
|
6978
7762
|
});
|
|
6979
7763
|
} catch (err) {
|
|
@@ -7032,7 +7816,42 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
7032
7816
|
continue;
|
|
7033
7817
|
}
|
|
7034
7818
|
}
|
|
7035
|
-
|
|
7819
|
+
let reachable = "unknown";
|
|
7820
|
+
let base64;
|
|
7821
|
+
if (ref.cdnUrl) {
|
|
7822
|
+
const ok2 = await probeCdnReachable(ref.cdnUrl);
|
|
7823
|
+
if (ok2) {
|
|
7824
|
+
reachable = "cdn";
|
|
7825
|
+
}
|
|
7826
|
+
}
|
|
7827
|
+
if (reachable !== "cdn") {
|
|
7828
|
+
try {
|
|
7829
|
+
const buf = readFileSync10(cached.localPath);
|
|
7830
|
+
if (buf.byteLength <= ASSET_BASE64_INLINE_MAX_BYTES) {
|
|
7831
|
+
base64 = buf.toString("base64");
|
|
7832
|
+
reachable = "base64";
|
|
7833
|
+
} else {
|
|
7834
|
+
reachable = "unknown";
|
|
7835
|
+
}
|
|
7836
|
+
} catch (err) {
|
|
7837
|
+
const errMsg = `base64 fallback read failed: ${err.message}`;
|
|
7838
|
+
logAssetResolveFailure(taskId, ref, errMsg, effectiveMime);
|
|
7839
|
+
}
|
|
7840
|
+
}
|
|
7841
|
+
const resolvedRef = {
|
|
7842
|
+
...ref,
|
|
7843
|
+
mime: effectiveMime,
|
|
7844
|
+
localPath: cached.localPath,
|
|
7845
|
+
base64,
|
|
7846
|
+
reachable
|
|
7847
|
+
};
|
|
7848
|
+
out.resolvedRefs.push(resolvedRef);
|
|
7849
|
+
if (isImageMime(effectiveMime) && visionAux?.enabled) {
|
|
7850
|
+
const block = await describeImageAttachment(ref, effectiveMime, resolvedRef, visionAux, taskId);
|
|
7851
|
+
out.promptBlocks.push(block);
|
|
7852
|
+
} else {
|
|
7853
|
+
out.promptBlocks.push(formatAttachmentReminderBlock(ref, effectiveMime));
|
|
7854
|
+
}
|
|
7036
7855
|
out.observability.push({
|
|
7037
7856
|
assetId: ref.assetId,
|
|
7038
7857
|
contentHash: ref.contentHash,
|
|
@@ -7043,6 +7862,110 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
7043
7862
|
}
|
|
7044
7863
|
return out;
|
|
7045
7864
|
}
|
|
7865
|
+
async function describeImageAttachment(ref, mime, resolved, opts, taskId) {
|
|
7866
|
+
const cached = await readVisionAuxLocalCache(opts.cacheDir, ref.contentHash, mime);
|
|
7867
|
+
if (cached) return formatVisionAuxBlock(ref, mime, cached.description, true);
|
|
7868
|
+
const source = buildVisionAuxSource(resolved, mime);
|
|
7869
|
+
if (!source) {
|
|
7870
|
+
return formatVisionAuxUnavailableBlock(ref, mime, "no reachable URL or inline bytes");
|
|
7871
|
+
}
|
|
7872
|
+
try {
|
|
7873
|
+
const description = await callVisionAux(ref, mime, source, opts);
|
|
7874
|
+
await writeVisionAuxLocalCache(opts.cacheDir, ref.contentHash, mime, description);
|
|
7875
|
+
return formatVisionAuxBlock(ref, mime, description.description, false);
|
|
7876
|
+
} catch (err) {
|
|
7877
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7878
|
+
process.stderr.write(
|
|
7879
|
+
`[daemon] vision-aux failed task=${taskId ?? "unknown"} assetId=${ref.assetId} hash=${ref.contentHash}: ${message}
|
|
7880
|
+
`
|
|
7881
|
+
);
|
|
7882
|
+
return formatVisionAuxUnavailableBlock(ref, mime, message);
|
|
7883
|
+
}
|
|
7884
|
+
}
|
|
7885
|
+
function buildVisionAuxSource(resolved, mime) {
|
|
7886
|
+
if (resolved.reachable === "cdn" && resolved.cdnUrl) return { kind: "url", url: resolved.cdnUrl };
|
|
7887
|
+
if (resolved.base64 && mime) return { kind: "data_url", url: `data:${mime};base64,${resolved.base64}` };
|
|
7888
|
+
return null;
|
|
7889
|
+
}
|
|
7890
|
+
async function callVisionAux(ref, mime, source, opts) {
|
|
7891
|
+
const secret = process.env.VISION_AUX_INTERNAL_SECRET || process.env.INTERNAL_API_SECRET || process.env.PRISMER_INTERNAL_SECRET;
|
|
7892
|
+
const headers = secret ? { "x-prismer-internal-secret": secret } : void 0;
|
|
7893
|
+
const res = await opts.cloud.request(
|
|
7894
|
+
"POST",
|
|
7895
|
+
"/api/internal/vision-aux/describe",
|
|
7896
|
+
{
|
|
7897
|
+
body: {
|
|
7898
|
+
assetId: ref.assetId,
|
|
7899
|
+
contentHash: ref.contentHash,
|
|
7900
|
+
mime: mime ?? ref.mime ?? "image/unknown",
|
|
7901
|
+
source
|
|
7902
|
+
},
|
|
7903
|
+
headers,
|
|
7904
|
+
timeoutMs: opts.timeoutMs ?? 12e3,
|
|
7905
|
+
signal: opts.signal
|
|
7906
|
+
}
|
|
7907
|
+
);
|
|
7908
|
+
const env = res.data;
|
|
7909
|
+
if (!res.ok || env?.ok === false || !env?.data?.description) {
|
|
7910
|
+
const message = res.error?.message || (typeof env?.error === "string" ? env.error : env?.error?.message) || env?.message || `HTTP ${res.status}`;
|
|
7911
|
+
throw new Error(message);
|
|
7912
|
+
}
|
|
7913
|
+
const now = /* @__PURE__ */ new Date();
|
|
7914
|
+
const ttlSec = Number.isFinite(env.data.cacheTtlSec) ? Number(env.data.cacheTtlSec) : 300;
|
|
7915
|
+
return {
|
|
7916
|
+
description: env.data.description,
|
|
7917
|
+
modelUsed: env.data.modelUsed || "unknown",
|
|
7918
|
+
provider: env.data.provider || "vision-aux",
|
|
7919
|
+
generatedAt: now.toISOString(),
|
|
7920
|
+
expiresAt: new Date(now.getTime() + Math.max(1, ttlSec) * 1e3).toISOString(),
|
|
7921
|
+
mime: mime ?? ref.mime ?? "image/unknown"
|
|
7922
|
+
};
|
|
7923
|
+
}
|
|
7924
|
+
async function readVisionAuxLocalCache(cacheDir, contentHash, mime) {
|
|
7925
|
+
if (!cacheDir) return null;
|
|
7926
|
+
const file = visionAuxCachePath(cacheDir, contentHash);
|
|
7927
|
+
try {
|
|
7928
|
+
const raw = await fsp3.readFile(file, "utf8");
|
|
7929
|
+
const parsed = parseVisionAuxCache(raw);
|
|
7930
|
+
if (!parsed) {
|
|
7931
|
+
await fsp3.unlink(file).catch(() => void 0);
|
|
7932
|
+
return null;
|
|
7933
|
+
}
|
|
7934
|
+
if (parsed.mime !== mime) return null;
|
|
7935
|
+
const expiresAt = Date.parse(parsed.expiresAt);
|
|
7936
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) return null;
|
|
7937
|
+
return parsed;
|
|
7938
|
+
} catch (err) {
|
|
7939
|
+
if (err.code !== "ENOENT") {
|
|
7940
|
+
await fsp3.unlink(file).catch(() => void 0);
|
|
7941
|
+
}
|
|
7942
|
+
return null;
|
|
7943
|
+
}
|
|
7944
|
+
}
|
|
7945
|
+
async function writeVisionAuxLocalCache(cacheDir, contentHash, mime, description) {
|
|
7946
|
+
if (!cacheDir || !mime) return;
|
|
7947
|
+
const file = visionAuxCachePath(cacheDir, contentHash);
|
|
7948
|
+
await fsp3.mkdir(path2.dirname(file), { recursive: true });
|
|
7949
|
+
await fsp3.writeFile(file, JSON.stringify({ ...description, mime }, null, 2), "utf8");
|
|
7950
|
+
}
|
|
7951
|
+
function visionAuxCachePath(cacheDir, contentHash) {
|
|
7952
|
+
const safeHash = /^[a-f0-9]{32,128}$/i.test(contentHash) ? contentHash : createSafeCacheKey(contentHash);
|
|
7953
|
+
return path2.join(cacheDir, `${safeHash}.json`);
|
|
7954
|
+
}
|
|
7955
|
+
function createSafeCacheKey(value) {
|
|
7956
|
+
return Buffer.from(value).toString("base64url").slice(0, 128) || "unknown";
|
|
7957
|
+
}
|
|
7958
|
+
function parseVisionAuxCache(raw) {
|
|
7959
|
+
try {
|
|
7960
|
+
const parsed = JSON.parse(raw);
|
|
7961
|
+
if (typeof parsed.description === "string" && typeof parsed.modelUsed === "string" && typeof parsed.provider === "string" && typeof parsed.generatedAt === "string" && typeof parsed.expiresAt === "string" && typeof parsed.mime === "string") {
|
|
7962
|
+
return parsed;
|
|
7963
|
+
}
|
|
7964
|
+
} catch {
|
|
7965
|
+
return null;
|
|
7966
|
+
}
|
|
7967
|
+
return null;
|
|
7968
|
+
}
|
|
7046
7969
|
function logAssetResolveFailure(taskId, ref, error, mime = ref.mime) {
|
|
7047
7970
|
const message = error.replace(/\s+/g, " ").slice(0, 500);
|
|
7048
7971
|
process.stderr.write(
|
|
@@ -7057,8 +7980,19 @@ function formatInlineAssetBlock(ref, mime, body, strategy) {
|
|
|
7057
7980
|
${body}
|
|
7058
7981
|
---`;
|
|
7059
7982
|
}
|
|
7060
|
-
function
|
|
7061
|
-
|
|
7983
|
+
function formatAttachmentReminderBlock(ref, mime) {
|
|
7984
|
+
const name = ref.filename ? ` name=${ref.filename}` : "";
|
|
7985
|
+
return `[Attached file] id=${ref.assetId} mime=${mime ?? "unknown"}${name} \u2014 the user uploaded this file; multimodal adapters receive the bytes directly.`;
|
|
7986
|
+
}
|
|
7987
|
+
function formatVisionAuxBlock(ref, mime, description, cached) {
|
|
7988
|
+
const name = ref.filename ? `: ${ref.filename}` : "";
|
|
7989
|
+
const cacheLabel = cached ? " local-cache" : "";
|
|
7990
|
+
return `[Image attachment${name}] id=${ref.assetId} mime=${mime ?? "unknown"}${cacheLabel}
|
|
7991
|
+
${description.trim()}`;
|
|
7992
|
+
}
|
|
7993
|
+
function formatVisionAuxUnavailableBlock(ref, mime, reason) {
|
|
7994
|
+
const name = ref.filename ? `: ${ref.filename}` : "";
|
|
7995
|
+
return `[Image attachment${name}; description unavailable due to vision-aux error] id=${ref.assetId} mime=${mime ?? "unknown"} reason=${reason.slice(0, 180)}`;
|
|
7062
7996
|
}
|
|
7063
7997
|
function formatErrorAssetBlock(ref, mime, error) {
|
|
7064
7998
|
return `[Attached file] id=${ref.assetId} mime=${mime ?? "unknown"} ERROR: ${error} \u2014 this file failed to load on the daemon side. Tell the user the specific error and ask them to re-upload or paste the content directly; do not guess at the cause.`;
|
|
@@ -7400,6 +8334,7 @@ import { createServer } from "http";
|
|
|
7400
8334
|
import { randomUUID, createHash as createHash4, createHmac, timingSafeEqual as cryptoTimingSafeEqual } from "crypto";
|
|
7401
8335
|
import { promises as fs2 } from "fs";
|
|
7402
8336
|
import * as path3 from "path";
|
|
8337
|
+
import { homedir as homedir5 } from "os";
|
|
7403
8338
|
|
|
7404
8339
|
// src/daemon/message-dispatch.ts
|
|
7405
8340
|
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
@@ -7610,6 +8545,11 @@ var LocalServer = class {
|
|
|
7610
8545
|
void this.handleSnapshot(req, res);
|
|
7611
8546
|
return;
|
|
7612
8547
|
}
|
|
8548
|
+
const agentDumpMatch = /^\/v1\/agents\/([^/]+)\/dump-state$/.exec(url);
|
|
8549
|
+
if (req.method === "POST" && agentDumpMatch) {
|
|
8550
|
+
void this.handleAgentDumpState(req, res, decodeURIComponent(agentDumpMatch[1]));
|
|
8551
|
+
return;
|
|
8552
|
+
}
|
|
7613
8553
|
if (req.method === "POST" && url === "/local/asset/write") {
|
|
7614
8554
|
void this.handleAssetWrite(req, res);
|
|
7615
8555
|
return;
|
|
@@ -7687,6 +8627,54 @@ var LocalServer = class {
|
|
|
7687
8627
|
}
|
|
7688
8628
|
void req;
|
|
7689
8629
|
}
|
|
8630
|
+
/**
|
|
8631
|
+
* POST /v1/agents/:agentId/dump-state — agent-scoped daemon manifest.
|
|
8632
|
+
*
|
|
8633
|
+
* This is narrower than `/v1/snapshot`: it walks only the current agent's
|
|
8634
|
+
* profile/work dirs so cloud can attach the result to IMAgentSnapshot without
|
|
8635
|
+
* capturing unrelated agents hosted in the same daemon/container.
|
|
8636
|
+
*/
|
|
8637
|
+
async handleAgentDumpState(req, res, agentId) {
|
|
8638
|
+
const state = this.opts.getState();
|
|
8639
|
+
const agent = state.hostedAgents.find((item) => item.imUserId === agentId);
|
|
8640
|
+
if (!agent) {
|
|
8641
|
+
respond(res, 404, { error: "agent_not_hosted", agentId });
|
|
8642
|
+
return;
|
|
8643
|
+
}
|
|
8644
|
+
const roots = getAgentStateRoots(agent, this.opts.snapshotRoot ?? "/workspace");
|
|
8645
|
+
try {
|
|
8646
|
+
const manifests = await Promise.all(
|
|
8647
|
+
roots.map(async (root) => {
|
|
8648
|
+
const stat = await statOptional(root.rootPath);
|
|
8649
|
+
if (!stat) return { ...root, exists: false, files: [] };
|
|
8650
|
+
if (!stat.isDirectory()) return { ...root, exists: true, error: "not_directory", files: [] };
|
|
8651
|
+
return { ...root, exists: true, files: await walkAndDigest(root.rootPath, root.rootPath) };
|
|
8652
|
+
})
|
|
8653
|
+
);
|
|
8654
|
+
const files = manifests.flatMap(
|
|
8655
|
+
(manifest) => manifest.files.map((file) => ({
|
|
8656
|
+
...file,
|
|
8657
|
+
path: `${manifest.kind}/${file.path}`,
|
|
8658
|
+
rootKind: manifest.kind,
|
|
8659
|
+
rootPath: manifest.rootPath
|
|
8660
|
+
}))
|
|
8661
|
+
);
|
|
8662
|
+
respond(res, 200, {
|
|
8663
|
+
agentId,
|
|
8664
|
+
adapterName: agent.adapterName,
|
|
8665
|
+
dumpedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8666
|
+
roots: manifests,
|
|
8667
|
+
files
|
|
8668
|
+
});
|
|
8669
|
+
} catch (err) {
|
|
8670
|
+
respond(res, 500, {
|
|
8671
|
+
error: "agent_dump_state_failed",
|
|
8672
|
+
agentId,
|
|
8673
|
+
message: err instanceof Error ? err.message : String(err)
|
|
8674
|
+
});
|
|
8675
|
+
}
|
|
8676
|
+
void req;
|
|
8677
|
+
}
|
|
7690
8678
|
/**
|
|
7691
8679
|
* POST /local/asset/write — agent-gen adapter RPC.
|
|
7692
8680
|
*
|
|
@@ -7958,6 +8946,39 @@ function validateAgentDispatchRequest(body) {
|
|
|
7958
8946
|
if (payload.attachments != null && !Array.isArray(payload.attachments)) return "attachments must be an array";
|
|
7959
8947
|
return null;
|
|
7960
8948
|
}
|
|
8949
|
+
function getAgentStateRoots(agent, snapshotRoot) {
|
|
8950
|
+
const profileName = sanitizeProfileName(agent.name || agent.imUserId);
|
|
8951
|
+
const roots = [
|
|
8952
|
+
{
|
|
8953
|
+
kind: "workspace-agent",
|
|
8954
|
+
rootPath: path3.join(snapshotRoot, "agents", agent.imUserId)
|
|
8955
|
+
}
|
|
8956
|
+
];
|
|
8957
|
+
if (agent.adapterName === "hermes" || agent.adapterName === "claude-code") {
|
|
8958
|
+
roots.unshift({
|
|
8959
|
+
kind: "hermes-profile",
|
|
8960
|
+
rootPath: path3.join(process.env.HERMES_HOME || path3.join(homedir5(), ".hermes"), "profiles", profileName)
|
|
8961
|
+
});
|
|
8962
|
+
}
|
|
8963
|
+
if (agent.adapterName === "openclaw") {
|
|
8964
|
+
roots.unshift({
|
|
8965
|
+
kind: "openclaw-profile",
|
|
8966
|
+
rootPath: path3.join(process.env.OPENCLAW_HOME || path3.join(homedir5(), ".openclaw"), "profiles", profileName)
|
|
8967
|
+
});
|
|
8968
|
+
}
|
|
8969
|
+
return roots;
|
|
8970
|
+
}
|
|
8971
|
+
function sanitizeProfileName(value) {
|
|
8972
|
+
return value.replace(/[\\/]/g, "_").trim() || "default";
|
|
8973
|
+
}
|
|
8974
|
+
async function statOptional(rootPath) {
|
|
8975
|
+
try {
|
|
8976
|
+
return await fs2.stat(rootPath);
|
|
8977
|
+
} catch (err) {
|
|
8978
|
+
if (err.code === "ENOENT") return null;
|
|
8979
|
+
throw err;
|
|
8980
|
+
}
|
|
8981
|
+
}
|
|
7961
8982
|
async function walkAndDigest(root, current) {
|
|
7962
8983
|
const out = [];
|
|
7963
8984
|
let entries;
|
|
@@ -9745,7 +10766,7 @@ async function readJson3(req) {
|
|
|
9745
10766
|
|
|
9746
10767
|
// src/daemon/asset/origin/drop-folder.ts
|
|
9747
10768
|
import { promises as fs4 } from "fs";
|
|
9748
|
-
import { homedir as
|
|
10769
|
+
import { homedir as homedir7 } from "os";
|
|
9749
10770
|
import * as path6 from "path";
|
|
9750
10771
|
var DropFolderAdapter = class {
|
|
9751
10772
|
kind = "drop-folder";
|
|
@@ -9753,7 +10774,7 @@ var DropFolderAdapter = class {
|
|
|
9753
10774
|
rootDir;
|
|
9754
10775
|
constructor(opts = {}) {
|
|
9755
10776
|
this.workspaceDir = opts.workspaceDir;
|
|
9756
|
-
this.rootDir = opts.rootDir ?? path6.join(opts.homeDir ??
|
|
10777
|
+
this.rootDir = opts.rootDir ?? path6.join(opts.homeDir ?? homedir7(), ".prismer");
|
|
9757
10778
|
}
|
|
9758
10779
|
/**
|
|
9759
10780
|
* Long-lived async iterable wrapping `scanOnce` on a 1s polling interval.
|
|
@@ -9763,7 +10784,7 @@ var DropFolderAdapter = class {
|
|
|
9763
10784
|
while (true) {
|
|
9764
10785
|
const batch = await this.scanOnce(workspaceId);
|
|
9765
10786
|
for (const obs of batch) yield obs;
|
|
9766
|
-
await
|
|
10787
|
+
await sleep2(1e3);
|
|
9767
10788
|
}
|
|
9768
10789
|
}
|
|
9769
10790
|
/**
|
|
@@ -9912,7 +10933,7 @@ function mimeFromExtension(p) {
|
|
|
9912
10933
|
const ext = path6.extname(p).toLowerCase();
|
|
9913
10934
|
return MIME_BY_EXT[ext] ?? null;
|
|
9914
10935
|
}
|
|
9915
|
-
function
|
|
10936
|
+
function sleep2(ms) {
|
|
9916
10937
|
return new Promise((r) => setTimeout(r, ms));
|
|
9917
10938
|
}
|
|
9918
10939
|
|
|
@@ -10623,9 +11644,120 @@ function extensionOf(filename) {
|
|
|
10623
11644
|
return idx >= 0 ? name.slice(idx) : "";
|
|
10624
11645
|
}
|
|
10625
11646
|
|
|
11647
|
+
// src/daemon/asset/magic-bytes.ts
|
|
11648
|
+
var PDF_SIG = Buffer.from("%PDF-", "ascii");
|
|
11649
|
+
var PNG_SIG = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
11650
|
+
var JPEG_SIG = Buffer.from([255, 216, 255]);
|
|
11651
|
+
var GIF87 = Buffer.from("GIF87a", "ascii");
|
|
11652
|
+
var GIF89 = Buffer.from("GIF89a", "ascii");
|
|
11653
|
+
var ZIP_LOCAL = Buffer.from([80, 75, 3, 4]);
|
|
11654
|
+
var ZIP_EMPTY = Buffer.from([80, 75, 5, 6]);
|
|
11655
|
+
var ZIP_SPANNED = Buffer.from([80, 75, 7, 8]);
|
|
11656
|
+
function detectMagicBytes(bytes) {
|
|
11657
|
+
const head = bytes.subarray(0, Math.min(bytes.length, 4096));
|
|
11658
|
+
if (head.length === 0) return { detected: null, mime: null };
|
|
11659
|
+
if (head.length >= PDF_SIG.length && head.subarray(0, PDF_SIG.length).equals(PDF_SIG)) {
|
|
11660
|
+
return { detected: "pdf", mime: "application/pdf" };
|
|
11661
|
+
}
|
|
11662
|
+
if (head.length >= PNG_SIG.length && head.subarray(0, PNG_SIG.length).equals(PNG_SIG)) {
|
|
11663
|
+
return { detected: "png", mime: "image/png" };
|
|
11664
|
+
}
|
|
11665
|
+
if (head.length >= JPEG_SIG.length && head.subarray(0, JPEG_SIG.length).equals(JPEG_SIG)) {
|
|
11666
|
+
return { detected: "jpeg", mime: "image/jpeg" };
|
|
11667
|
+
}
|
|
11668
|
+
if (head.length >= GIF87.length && head.subarray(0, GIF87.length).equals(GIF87) || head.length >= GIF89.length && head.subarray(0, GIF89.length).equals(GIF89)) {
|
|
11669
|
+
return { detected: "gif", mime: "image/gif" };
|
|
11670
|
+
}
|
|
11671
|
+
if (head.length >= 4 && (head.subarray(0, 4).equals(ZIP_LOCAL) || head.subarray(0, 4).equals(ZIP_EMPTY) || head.subarray(0, 4).equals(ZIP_SPANNED))) {
|
|
11672
|
+
return { detected: "zip", mime: "application/zip" };
|
|
11673
|
+
}
|
|
11674
|
+
const text = head.toString("utf8");
|
|
11675
|
+
const trimmed = text.trimStart();
|
|
11676
|
+
if (/^<\?xml[^>]*>\s*<svg[\s>]/i.test(trimmed) || /^<svg[\s>]/i.test(trimmed)) {
|
|
11677
|
+
return { detected: "svg", mime: "image/svg+xml" };
|
|
11678
|
+
}
|
|
11679
|
+
if (/^<(!doctype html|html[\s>])/i.test(trimmed)) {
|
|
11680
|
+
return { detected: "html", mime: "text/html" };
|
|
11681
|
+
}
|
|
11682
|
+
if (/^[{[]/.test(trimmed)) {
|
|
11683
|
+
return { detected: "json", mime: "application/json" };
|
|
11684
|
+
}
|
|
11685
|
+
if (looksLikePrintableText(head)) {
|
|
11686
|
+
return { detected: "markdown-or-text", mime: "text/plain" };
|
|
11687
|
+
}
|
|
11688
|
+
return { detected: null, mime: null };
|
|
11689
|
+
}
|
|
11690
|
+
var ZIP_DERIVED_MIMES = /* @__PURE__ */ new Set([
|
|
11691
|
+
"application/zip",
|
|
11692
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
11693
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
11694
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
11695
|
+
"application/vnd.oasis.opendocument.text",
|
|
11696
|
+
"application/vnd.oasis.opendocument.spreadsheet",
|
|
11697
|
+
"application/vnd.oasis.opendocument.presentation",
|
|
11698
|
+
"application/epub+zip",
|
|
11699
|
+
"application/java-archive"
|
|
11700
|
+
]);
|
|
11701
|
+
function mimeMismatchReason(detected, declared) {
|
|
11702
|
+
if (detected.detected === null) return null;
|
|
11703
|
+
const dec = (declared ?? "").toLowerCase().split(";")[0]?.trim() ?? "";
|
|
11704
|
+
if (!dec || dec === "application/octet-stream") return null;
|
|
11705
|
+
switch (detected.detected) {
|
|
11706
|
+
case "pdf":
|
|
11707
|
+
return dec === "application/pdf" ? null : `declared ${dec} but content is application/pdf`;
|
|
11708
|
+
case "png":
|
|
11709
|
+
return dec === "image/png" ? null : `declared ${dec} but content is image/png`;
|
|
11710
|
+
case "jpeg":
|
|
11711
|
+
return dec === "image/jpeg" || dec === "image/jpg" ? null : `declared ${dec} but content is image/jpeg`;
|
|
11712
|
+
case "gif":
|
|
11713
|
+
return dec === "image/gif" ? null : `declared ${dec} but content is image/gif`;
|
|
11714
|
+
case "zip":
|
|
11715
|
+
if (ZIP_DERIVED_MIMES.has(dec)) return null;
|
|
11716
|
+
return `declared ${dec} but content is a ZIP-container (application/zip family)`;
|
|
11717
|
+
case "svg":
|
|
11718
|
+
if (dec === "image/svg+xml" || dec.startsWith("text/")) return null;
|
|
11719
|
+
return `declared ${dec} but content is image/svg+xml`;
|
|
11720
|
+
case "html":
|
|
11721
|
+
if (dec === "text/html" || dec === "application/xhtml+xml" || dec.startsWith("text/")) return null;
|
|
11722
|
+
if (dec === "application/pdf" || dec.startsWith("image/") || dec === "application/zip") {
|
|
11723
|
+
return `declared ${dec} but content is text/html`;
|
|
11724
|
+
}
|
|
11725
|
+
return null;
|
|
11726
|
+
case "json":
|
|
11727
|
+
if (dec === "application/json" || dec.startsWith("text/")) return null;
|
|
11728
|
+
if (dec === "application/pdf" || dec.startsWith("image/") || dec === "application/zip") {
|
|
11729
|
+
return `declared ${dec} but content is application/json`;
|
|
11730
|
+
}
|
|
11731
|
+
return null;
|
|
11732
|
+
case "markdown-or-text":
|
|
11733
|
+
if (dec === "application/pdf" || dec.startsWith("image/") || dec === "application/zip") {
|
|
11734
|
+
return `declared ${dec} but content is text (not a real ${dec} payload)`;
|
|
11735
|
+
}
|
|
11736
|
+
return null;
|
|
11737
|
+
default:
|
|
11738
|
+
return null;
|
|
11739
|
+
}
|
|
11740
|
+
}
|
|
11741
|
+
function looksLikePrintableText(buf) {
|
|
11742
|
+
if (buf.length === 0) return false;
|
|
11743
|
+
let printable = 0;
|
|
11744
|
+
let total = 0;
|
|
11745
|
+
for (let i = 0; i < buf.length; i++) {
|
|
11746
|
+
const b = buf[i];
|
|
11747
|
+
if (b === 0) return false;
|
|
11748
|
+
total++;
|
|
11749
|
+
if (b >= 32 && b <= 126 || b === 9 || b === 10 || b === 13 || // utf-8 lead bytes
|
|
11750
|
+
b >= 128) {
|
|
11751
|
+
printable++;
|
|
11752
|
+
}
|
|
11753
|
+
}
|
|
11754
|
+
return total > 0 && printable / total >= 0.95;
|
|
11755
|
+
}
|
|
11756
|
+
|
|
10626
11757
|
// src/daemon/outbox-watcher.ts
|
|
10627
11758
|
var DEFAULT_INTERVAL_MS = 2e3;
|
|
10628
11759
|
var RESERVED_SUBDIR = "_uploaded";
|
|
11760
|
+
var REJECTED_SUBDIR = "_rejected";
|
|
10629
11761
|
var OutboxWatcher = class {
|
|
10630
11762
|
constructor(opts) {
|
|
10631
11763
|
this.opts = opts;
|
|
@@ -10800,6 +11932,7 @@ var OutboxWatcher = class {
|
|
|
10800
11932
|
for (const name of entries) {
|
|
10801
11933
|
if (name.startsWith(".")) continue;
|
|
10802
11934
|
if (name === RESERVED_SUBDIR) continue;
|
|
11935
|
+
if (name === REJECTED_SUBDIR) continue;
|
|
10803
11936
|
const full = path8.join(dir, name);
|
|
10804
11937
|
let st;
|
|
10805
11938
|
try {
|
|
@@ -10814,10 +11947,77 @@ var OutboxWatcher = class {
|
|
|
10814
11947
|
await this.upload(full, st, kind, task);
|
|
10815
11948
|
this.uploaded.add(key);
|
|
10816
11949
|
} catch (err) {
|
|
10817
|
-
|
|
11950
|
+
const msg = err.message;
|
|
11951
|
+
this.log("warn", `upload ${full} failed: ${msg}`);
|
|
11952
|
+
if (/\bMIME_MISMATCH\b/i.test(msg) || /\bmime mismatch\b/i.test(msg)) {
|
|
11953
|
+
await this.quarantine(dir, full).catch(
|
|
11954
|
+
(moveErr) => this.log("warn", `quarantine ${full} failed: ${moveErr.message}`)
|
|
11955
|
+
);
|
|
11956
|
+
this.uploaded.add(key);
|
|
11957
|
+
await this.reportMimeMismatchToCloud(task, full, msg).catch(
|
|
11958
|
+
(reportErr) => this.log("warn", `report MIME_MISMATCH failed: ${reportErr.message}`)
|
|
11959
|
+
);
|
|
11960
|
+
}
|
|
10818
11961
|
}
|
|
10819
11962
|
}
|
|
10820
11963
|
}
|
|
11964
|
+
/**
|
|
11965
|
+
* F2B — report an OUTBOX_MIME_MISMATCH task event to cloud so the task log
|
|
11966
|
+
* gains an auditable record of the rejection. Without this, the failure is
|
|
11967
|
+
* silent (file quarantined locally, agent unaware) and the next dispatch
|
|
11968
|
+
* loops on the same bad strategy.
|
|
11969
|
+
*
|
|
11970
|
+
* Best-effort: every catch site swallows errors. We never want this
|
|
11971
|
+
* upstream call to block local quarantine or trigger a retry loop, because
|
|
11972
|
+
* the reject decision is already final by the time we get here.
|
|
11973
|
+
*
|
|
11974
|
+
* Endpoint contract: POST /api/im/tasks/:id/event
|
|
11975
|
+
* { code: 'OUTBOX_MIME_MISMATCH', payload: { file, daemonError }, message }
|
|
11976
|
+
* Only callable when `task?.taskId` is known — pre-dispatch outbox files
|
|
11977
|
+
* (no active task) are quarantined silently as before.
|
|
11978
|
+
*/
|
|
11979
|
+
async reportMimeMismatchToCloud(task, filePath, daemonErrorMessage) {
|
|
11980
|
+
if (!task?.taskId) return;
|
|
11981
|
+
const fileName = path8.basename(filePath);
|
|
11982
|
+
const reasonMatch = daemonErrorMessage.match(/MIME_MISMATCH[^:]*:\s*(.+)$/i);
|
|
11983
|
+
const reason = reasonMatch?.[1] ? reasonMatch[1].trim() : daemonErrorMessage;
|
|
11984
|
+
const body = {
|
|
11985
|
+
code: "OUTBOX_MIME_MISMATCH",
|
|
11986
|
+
payload: {
|
|
11987
|
+
file: fileName,
|
|
11988
|
+
reason,
|
|
11989
|
+
daemonError: daemonErrorMessage
|
|
11990
|
+
},
|
|
11991
|
+
message: `Your output file ${fileName} was rejected because its bytes do not match the claimed format. Use a real library (e.g. reportlab for PDF, openpyxl for XLSX) instead of renaming an extension.`
|
|
11992
|
+
};
|
|
11993
|
+
const res = await this.opts.cloud.request("POST", `/api/im/tasks/${encodeURIComponent(task.taskId)}/event`, {
|
|
11994
|
+
body
|
|
11995
|
+
});
|
|
11996
|
+
if (!res.ok) {
|
|
11997
|
+
throw new Error(`cloud rejected event: ${res.status} ${res.error?.code ?? ""} ${res.error?.message ?? ""}`);
|
|
11998
|
+
}
|
|
11999
|
+
}
|
|
12000
|
+
/**
|
|
12001
|
+
* Move a file under `<dir>/_rejected/` so it stops being a scan candidate
|
|
12002
|
+
* (the loop skips the reserved subdir) and operators can find the
|
|
12003
|
+
* offending artifact. Filename collision is rare per task; on collision
|
|
12004
|
+
* we append a millisecond suffix to keep the original observable.
|
|
12005
|
+
*/
|
|
12006
|
+
async quarantine(dir, full) {
|
|
12007
|
+
const rejectedDir = path8.join(dir, REJECTED_SUBDIR);
|
|
12008
|
+
await fs5.mkdir(rejectedDir, { recursive: true });
|
|
12009
|
+
const base = path8.basename(full);
|
|
12010
|
+
let dest = path8.join(rejectedDir, base);
|
|
12011
|
+
try {
|
|
12012
|
+
await fs5.access(dest);
|
|
12013
|
+
const ext = path8.extname(base);
|
|
12014
|
+
const stem = base.slice(0, base.length - ext.length);
|
|
12015
|
+
dest = path8.join(rejectedDir, `${stem}.${Date.now()}${ext}`);
|
|
12016
|
+
} catch {
|
|
12017
|
+
}
|
|
12018
|
+
await fs5.rename(full, dest);
|
|
12019
|
+
this.log("warn", `quarantined ${full} \u2192 ${dest}`);
|
|
12020
|
+
}
|
|
10821
12021
|
async upload(filePath, st, kind, task) {
|
|
10822
12022
|
const wsId = this.opts.workspaceId();
|
|
10823
12023
|
if (!wsId) {
|
|
@@ -10835,6 +12035,11 @@ var OutboxWatcher = class {
|
|
|
10835
12035
|
if (!policy.ok) {
|
|
10836
12036
|
throw new Error(`agent output rejected: ${policy.reason}`);
|
|
10837
12037
|
}
|
|
12038
|
+
const detection = detectMagicBytes(bytes);
|
|
12039
|
+
const reason = mimeMismatchReason(detection, policy.mime);
|
|
12040
|
+
if (reason) {
|
|
12041
|
+
throw new Error(`MIME_MISMATCH for ${fileName}: ${reason}`);
|
|
12042
|
+
}
|
|
10838
12043
|
const metadata = {
|
|
10839
12044
|
taskId: task.taskId,
|
|
10840
12045
|
...task.adapter ? { adapter: task.adapter } : {}
|
|
@@ -11188,6 +12393,350 @@ var WsClient = class extends EventEmitter2 {
|
|
|
11188
12393
|
}
|
|
11189
12394
|
};
|
|
11190
12395
|
|
|
12396
|
+
// src/daemon/pending-reply-cache.ts
|
|
12397
|
+
function fromRaw(r) {
|
|
12398
|
+
return {
|
|
12399
|
+
idempotencyKey: r.idempotency_key,
|
|
12400
|
+
taskId: r.task_id,
|
|
12401
|
+
replyId: r.reply_id,
|
|
12402
|
+
status: r.status,
|
|
12403
|
+
payloadJson: r.payload_json,
|
|
12404
|
+
createdAt: r.created_at,
|
|
12405
|
+
preparedAt: r.prepared_at,
|
|
12406
|
+
lastAttemptAt: r.last_attempt_at,
|
|
12407
|
+
attemptCount: r.attempt_count,
|
|
12408
|
+
lastError: r.last_error
|
|
12409
|
+
};
|
|
12410
|
+
}
|
|
12411
|
+
function createPendingReplyCache(db) {
|
|
12412
|
+
const insert = db.prepare(
|
|
12413
|
+
`INSERT OR REPLACE INTO pending_dispatch_replies
|
|
12414
|
+
(idempotency_key, task_id, reply_id, status, payload_json,
|
|
12415
|
+
prepared_at, created_at, last_attempt_at, attempt_count, last_error)
|
|
12416
|
+
VALUES (?, ?, NULL, 'pending', ?, NULL, ?, NULL, 0, NULL)`
|
|
12417
|
+
);
|
|
12418
|
+
const markPreparedStmt = db.prepare(
|
|
12419
|
+
`UPDATE pending_dispatch_replies
|
|
12420
|
+
SET reply_id = ?, status = 'prepared', prepared_at = ?,
|
|
12421
|
+
last_attempt_at = ?, attempt_count = attempt_count + 1,
|
|
12422
|
+
last_error = NULL
|
|
12423
|
+
WHERE idempotency_key = ?`
|
|
12424
|
+
);
|
|
12425
|
+
const markAttemptStmt = db.prepare(
|
|
12426
|
+
`UPDATE pending_dispatch_replies
|
|
12427
|
+
SET last_attempt_at = ?, attempt_count = attempt_count + 1,
|
|
12428
|
+
last_error = ?
|
|
12429
|
+
WHERE idempotency_key = ?`
|
|
12430
|
+
);
|
|
12431
|
+
const clearStmt = db.prepare(
|
|
12432
|
+
`DELETE FROM pending_dispatch_replies WHERE idempotency_key = ?`
|
|
12433
|
+
);
|
|
12434
|
+
const listRecoveryStmt = db.prepare(
|
|
12435
|
+
`SELECT * FROM pending_dispatch_replies
|
|
12436
|
+
WHERE status IN ('pending', 'prepared')
|
|
12437
|
+
ORDER BY created_at ASC`
|
|
12438
|
+
);
|
|
12439
|
+
const countStmt = db.prepare(
|
|
12440
|
+
`SELECT status, COUNT(*) as cnt FROM pending_dispatch_replies
|
|
12441
|
+
WHERE status IN ('pending','prepared') GROUP BY status`
|
|
12442
|
+
);
|
|
12443
|
+
const findStmt = db.prepare(
|
|
12444
|
+
`SELECT * FROM pending_dispatch_replies WHERE idempotency_key = ?`
|
|
12445
|
+
);
|
|
12446
|
+
return {
|
|
12447
|
+
savePending(idempotencyKey, taskId, payloadJson, now = Date.now()) {
|
|
12448
|
+
insert.run(idempotencyKey, taskId, payloadJson, now);
|
|
12449
|
+
},
|
|
12450
|
+
markPrepared(idempotencyKey, replyId, now = Date.now()) {
|
|
12451
|
+
markPreparedStmt.run(replyId, now, now, idempotencyKey);
|
|
12452
|
+
},
|
|
12453
|
+
markAttempt(idempotencyKey, now = Date.now(), error) {
|
|
12454
|
+
markAttemptStmt.run(now, error ?? null, idempotencyKey);
|
|
12455
|
+
},
|
|
12456
|
+
clearPending(idempotencyKey) {
|
|
12457
|
+
clearStmt.run(idempotencyKey);
|
|
12458
|
+
},
|
|
12459
|
+
listForRecovery() {
|
|
12460
|
+
const rows = listRecoveryStmt.all();
|
|
12461
|
+
return rows.map(fromRaw);
|
|
12462
|
+
},
|
|
12463
|
+
countByStatus() {
|
|
12464
|
+
const rows = countStmt.all();
|
|
12465
|
+
const result = { pending: 0, prepared: 0 };
|
|
12466
|
+
for (const r of rows) {
|
|
12467
|
+
if (r.status === "pending") result.pending = r.cnt;
|
|
12468
|
+
else if (r.status === "prepared") result.prepared = r.cnt;
|
|
12469
|
+
}
|
|
12470
|
+
return result;
|
|
12471
|
+
},
|
|
12472
|
+
findByIdempotencyKey(idempotencyKey) {
|
|
12473
|
+
const row = findStmt.get(idempotencyKey);
|
|
12474
|
+
return row ? fromRaw(row) : null;
|
|
12475
|
+
}
|
|
12476
|
+
};
|
|
12477
|
+
}
|
|
12478
|
+
function generateIdempotencyKey() {
|
|
12479
|
+
const c = globalThis.crypto;
|
|
12480
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
12481
|
+
const bytes = new Uint8Array(16);
|
|
12482
|
+
const cryptoNode = globalThis.crypto;
|
|
12483
|
+
if (cryptoNode?.getRandomValues) {
|
|
12484
|
+
cryptoNode.getRandomValues(bytes);
|
|
12485
|
+
} else {
|
|
12486
|
+
for (let i = 0; i < 16; i += 1) bytes[i] = Math.floor(Math.random() * 256);
|
|
12487
|
+
}
|
|
12488
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
12489
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
12490
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
12491
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
12492
|
+
}
|
|
12493
|
+
|
|
12494
|
+
// src/daemon/dispatch-reply-transport.ts
|
|
12495
|
+
var DispatchReplyAbortedError = class extends Error {
|
|
12496
|
+
constructor(replyId) {
|
|
12497
|
+
super(`dispatch reply ${replyId} aborted by server reaper (likely >1h since prepare)`);
|
|
12498
|
+
this.replyId = replyId;
|
|
12499
|
+
this.name = "DispatchReplyAbortedError";
|
|
12500
|
+
}
|
|
12501
|
+
replyId;
|
|
12502
|
+
code = "DISPATCH_REPLY_INVALID_STATE";
|
|
12503
|
+
};
|
|
12504
|
+
function defaultLog2(line) {
|
|
12505
|
+
process.stderr.write(`${line}
|
|
12506
|
+
`);
|
|
12507
|
+
}
|
|
12508
|
+
async function sendDispatchReplyTwoPhase(args) {
|
|
12509
|
+
const { taskId, payload, cloud, cache } = args;
|
|
12510
|
+
const log8 = args.log ?? defaultLog2;
|
|
12511
|
+
const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
|
|
12512
|
+
const payloadJson = JSON.stringify({ ...payload, _taskId: taskId, _idempotencyKey: idempotencyKey });
|
|
12513
|
+
cache.savePending(idempotencyKey, taskId, payloadJson);
|
|
12514
|
+
const prepareRes = await cloud.request("POST", `/api/im/dispatch/${encodeURIComponent(taskId)}/prepare`, {
|
|
12515
|
+
body: { ...payload, idempotencyKey },
|
|
12516
|
+
timeoutMs: 3e4
|
|
12517
|
+
});
|
|
12518
|
+
if (!prepareRes.ok || !prepareRes.data) {
|
|
12519
|
+
const errCode = prepareRes.error?.code ?? "unknown";
|
|
12520
|
+
const errMsg = prepareRes.error?.message ?? `prepare failed (status=${prepareRes.status})`;
|
|
12521
|
+
cache.markAttempt(idempotencyKey, Date.now(), `prepare: ${errCode}: ${errMsg}`);
|
|
12522
|
+
throw new Error(`dispatch prepare failed: ${errCode}: ${errMsg}`);
|
|
12523
|
+
}
|
|
12524
|
+
const envelope2 = prepareRes.data;
|
|
12525
|
+
const inner = "data" in envelope2 && envelope2.data ? envelope2.data : envelope2;
|
|
12526
|
+
if (!inner || typeof inner.replyId !== "string") {
|
|
12527
|
+
cache.markAttempt(idempotencyKey, Date.now(), "prepare: missing replyId in response");
|
|
12528
|
+
throw new Error("dispatch prepare returned no replyId");
|
|
12529
|
+
}
|
|
12530
|
+
if ("ok" in envelope2 && envelope2.ok === false) {
|
|
12531
|
+
const errCode = envelope2.error?.code ?? "prepare_failed";
|
|
12532
|
+
const errMsg = envelope2.error?.message ?? "prepare ok=false";
|
|
12533
|
+
cache.markAttempt(idempotencyKey, Date.now(), `prepare: ${errCode}: ${errMsg}`);
|
|
12534
|
+
throw new Error(`dispatch prepare failed: ${errCode}: ${errMsg}`);
|
|
12535
|
+
}
|
|
12536
|
+
cache.markPrepared(idempotencyKey, inner.replyId);
|
|
12537
|
+
log8(
|
|
12538
|
+
`[daemon] dispatch.prepare task=${taskId} replyId=${inner.replyId} status=${inner.status} idem=${idempotencyKey}`
|
|
12539
|
+
);
|
|
12540
|
+
if (inner.status === "committed") {
|
|
12541
|
+
cache.clearPending(idempotencyKey);
|
|
12542
|
+
return {
|
|
12543
|
+
taskId,
|
|
12544
|
+
replyId: inner.replyId,
|
|
12545
|
+
status: "committed",
|
|
12546
|
+
alreadyCommitted: true
|
|
12547
|
+
};
|
|
12548
|
+
}
|
|
12549
|
+
try {
|
|
12550
|
+
const result = await commitDispatchReply({ taskId, replyId: inner.replyId, cloud, log: log8 });
|
|
12551
|
+
cache.clearPending(idempotencyKey);
|
|
12552
|
+
return { taskId, replyId: inner.replyId, ...result };
|
|
12553
|
+
} catch (err) {
|
|
12554
|
+
if (err instanceof DispatchReplyAbortedError) {
|
|
12555
|
+
cache.clearPending(idempotencyKey);
|
|
12556
|
+
log8(`[daemon] dispatch.commit aborted task=${taskId} replyId=${inner.replyId} \u2014 dispatch lost`);
|
|
12557
|
+
return {
|
|
12558
|
+
taskId,
|
|
12559
|
+
replyId: inner.replyId,
|
|
12560
|
+
status: "aborted",
|
|
12561
|
+
alreadyCommitted: false
|
|
12562
|
+
};
|
|
12563
|
+
}
|
|
12564
|
+
cache.markAttempt(idempotencyKey, Date.now(), `commit: ${err.message}`);
|
|
12565
|
+
throw err;
|
|
12566
|
+
}
|
|
12567
|
+
}
|
|
12568
|
+
async function commitDispatchReply(args) {
|
|
12569
|
+
const log8 = args.log ?? defaultLog2;
|
|
12570
|
+
const res = await args.cloud.request("POST", `/api/im/dispatch/${encodeURIComponent(args.taskId)}/commit`, {
|
|
12571
|
+
body: { replyId: args.replyId },
|
|
12572
|
+
timeoutMs: 3e4
|
|
12573
|
+
});
|
|
12574
|
+
if (!res.ok || !res.data) {
|
|
12575
|
+
const code = res.error?.code ?? "unknown";
|
|
12576
|
+
const msg = res.error?.message ?? `commit failed (status=${res.status})`;
|
|
12577
|
+
if (code === "DISPATCH_REPLY_INVALID_STATE") {
|
|
12578
|
+
throw new DispatchReplyAbortedError(args.replyId);
|
|
12579
|
+
}
|
|
12580
|
+
throw new Error(`dispatch commit failed: ${code}: ${msg}`);
|
|
12581
|
+
}
|
|
12582
|
+
const env = res.data;
|
|
12583
|
+
const inner = "data" in env && env.data ? env.data : env;
|
|
12584
|
+
if ("ok" in env && env.ok === false) {
|
|
12585
|
+
const code = env.error?.code ?? "commit_failed";
|
|
12586
|
+
if (code === "DISPATCH_REPLY_INVALID_STATE") {
|
|
12587
|
+
throw new DispatchReplyAbortedError(args.replyId);
|
|
12588
|
+
}
|
|
12589
|
+
throw new Error(`dispatch commit failed: ${code}: ${env.error?.message ?? "commit ok=false"}`);
|
|
12590
|
+
}
|
|
12591
|
+
log8(
|
|
12592
|
+
`[daemon] dispatch.commit task=${args.taskId} replyId=${args.replyId} alreadyCommitted=${inner.alreadyCommitted ?? false}`
|
|
12593
|
+
);
|
|
12594
|
+
return {
|
|
12595
|
+
status: "committed",
|
|
12596
|
+
alreadyCommitted: Boolean(inner.alreadyCommitted),
|
|
12597
|
+
...inner.messageId ? { messageId: inner.messageId } : {}
|
|
12598
|
+
};
|
|
12599
|
+
}
|
|
12600
|
+
async function recoverPendingReplies(deps) {
|
|
12601
|
+
const log8 = deps.log ?? defaultLog2;
|
|
12602
|
+
const rows = deps.cache.listForRecovery();
|
|
12603
|
+
if (rows.length === 0) {
|
|
12604
|
+
return { attempted: 0, committed: 0, aborted: 0, failed: 0 };
|
|
12605
|
+
}
|
|
12606
|
+
log8(`[daemon] dispatch.recovery: ${rows.length} pending row(s) to replay`);
|
|
12607
|
+
let committed = 0;
|
|
12608
|
+
let aborted = 0;
|
|
12609
|
+
let failed = 0;
|
|
12610
|
+
for (const row of rows) {
|
|
12611
|
+
try {
|
|
12612
|
+
const result = await replayRow(row, deps);
|
|
12613
|
+
if (result.status === "committed") committed += 1;
|
|
12614
|
+
else if (result.status === "aborted") aborted += 1;
|
|
12615
|
+
} catch (err) {
|
|
12616
|
+
failed += 1;
|
|
12617
|
+
log8(
|
|
12618
|
+
`[daemon] dispatch.recovery FAIL idem=${row.idempotencyKey} task=${row.taskId}: ${err.message}`
|
|
12619
|
+
);
|
|
12620
|
+
}
|
|
12621
|
+
}
|
|
12622
|
+
log8(
|
|
12623
|
+
`[daemon] dispatch.recovery done attempted=${rows.length} committed=${committed} aborted=${aborted} failed=${failed}`
|
|
12624
|
+
);
|
|
12625
|
+
return { attempted: rows.length, committed, aborted, failed };
|
|
12626
|
+
}
|
|
12627
|
+
async function replayRow(row, deps) {
|
|
12628
|
+
const log8 = deps.log ?? defaultLog2;
|
|
12629
|
+
if (row.status === "prepared" && row.replyId) {
|
|
12630
|
+
try {
|
|
12631
|
+
const result2 = await commitDispatchReply({
|
|
12632
|
+
taskId: row.taskId,
|
|
12633
|
+
replyId: row.replyId,
|
|
12634
|
+
cloud: deps.cloud,
|
|
12635
|
+
log: log8
|
|
12636
|
+
});
|
|
12637
|
+
deps.cache.clearPending(row.idempotencyKey);
|
|
12638
|
+
return { status: result2.status };
|
|
12639
|
+
} catch (err) {
|
|
12640
|
+
if (err instanceof DispatchReplyAbortedError) {
|
|
12641
|
+
deps.cache.clearPending(row.idempotencyKey);
|
|
12642
|
+
return { status: "aborted" };
|
|
12643
|
+
}
|
|
12644
|
+
throw err;
|
|
12645
|
+
}
|
|
12646
|
+
}
|
|
12647
|
+
let parsed;
|
|
12648
|
+
try {
|
|
12649
|
+
parsed = JSON.parse(row.payloadJson);
|
|
12650
|
+
} catch (err) {
|
|
12651
|
+
deps.cache.clearPending(row.idempotencyKey);
|
|
12652
|
+
throw new Error(`pending row ${row.idempotencyKey} has unparseable payload: ${err.message}`);
|
|
12653
|
+
}
|
|
12654
|
+
const { _taskId, _idempotencyKey, ...payload } = parsed;
|
|
12655
|
+
const result = await sendDispatchReplyTwoPhase({
|
|
12656
|
+
taskId: row.taskId,
|
|
12657
|
+
payload,
|
|
12658
|
+
cloud: deps.cloud,
|
|
12659
|
+
cache: deps.cache,
|
|
12660
|
+
idempotencyKey: row.idempotencyKey,
|
|
12661
|
+
log: deps.log
|
|
12662
|
+
});
|
|
12663
|
+
return { status: result.status };
|
|
12664
|
+
}
|
|
12665
|
+
|
|
12666
|
+
// src/daemon/transport-probe.ts
|
|
12667
|
+
import { networkInterfaces } from "os";
|
|
12668
|
+
function isPrivateAddress(addr) {
|
|
12669
|
+
if (!addr) return true;
|
|
12670
|
+
const h = String(addr).trim().toLowerCase();
|
|
12671
|
+
if (!h) return true;
|
|
12672
|
+
if (h === "localhost" || h === "::1") return true;
|
|
12673
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
|
|
12674
|
+
if (m) {
|
|
12675
|
+
const a = Number(m[1]);
|
|
12676
|
+
const b = Number(m[2]);
|
|
12677
|
+
if (a === 10) return true;
|
|
12678
|
+
if (a === 127) return true;
|
|
12679
|
+
if (a === 169 && b === 254) return true;
|
|
12680
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
12681
|
+
if (a === 192 && b === 168) return true;
|
|
12682
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
12683
|
+
return false;
|
|
12684
|
+
}
|
|
12685
|
+
if (h.endsWith(".local")) return true;
|
|
12686
|
+
if (h.endsWith(".localhost")) return true;
|
|
12687
|
+
return false;
|
|
12688
|
+
}
|
|
12689
|
+
function buildTransportProbe(gatewayUrl) {
|
|
12690
|
+
if (!gatewayUrl) {
|
|
12691
|
+
return { transport: "ws", gatewayUrl: null, gatewayIsPrivate: true };
|
|
12692
|
+
}
|
|
12693
|
+
let host = null;
|
|
12694
|
+
try {
|
|
12695
|
+
host = new URL(gatewayUrl).hostname;
|
|
12696
|
+
} catch {
|
|
12697
|
+
host = null;
|
|
12698
|
+
}
|
|
12699
|
+
const gatewayIsPrivate = isPrivateAddress(host);
|
|
12700
|
+
return {
|
|
12701
|
+
transport: gatewayIsPrivate ? "ws" : "both",
|
|
12702
|
+
gatewayUrl,
|
|
12703
|
+
gatewayIsPrivate
|
|
12704
|
+
};
|
|
12705
|
+
}
|
|
12706
|
+
function pickLocalIPv4() {
|
|
12707
|
+
const nics = networkInterfaces();
|
|
12708
|
+
let firstPrivate = null;
|
|
12709
|
+
for (const list of Object.values(nics)) {
|
|
12710
|
+
if (!list) continue;
|
|
12711
|
+
for (const ni of list) {
|
|
12712
|
+
if (ni.internal || ni.family !== "IPv4") continue;
|
|
12713
|
+
if (!isPrivateAddress(ni.address)) return ni.address;
|
|
12714
|
+
if (!firstPrivate) firstPrivate = ni.address;
|
|
12715
|
+
}
|
|
12716
|
+
}
|
|
12717
|
+
return firstPrivate;
|
|
12718
|
+
}
|
|
12719
|
+
async function reportTransportProbe(cloud, daemonId, probe) {
|
|
12720
|
+
const body = {
|
|
12721
|
+
daemonId,
|
|
12722
|
+
transport: probe.transport,
|
|
12723
|
+
gatewayUrl: probe.gatewayUrl,
|
|
12724
|
+
gatewayIsPrivate: probe.gatewayIsPrivate
|
|
12725
|
+
};
|
|
12726
|
+
const res = await cloud.request("POST", "/api/im/runtime/transport-report", {
|
|
12727
|
+
body,
|
|
12728
|
+
timeoutMs: 5e3
|
|
12729
|
+
});
|
|
12730
|
+
if (!res.ok) {
|
|
12731
|
+
return {
|
|
12732
|
+
ok: false,
|
|
12733
|
+
status: res.status,
|
|
12734
|
+
error: res.error?.message ?? `transport-report failed (${res.status})`
|
|
12735
|
+
};
|
|
12736
|
+
}
|
|
12737
|
+
return { ok: true, status: res.status };
|
|
12738
|
+
}
|
|
12739
|
+
|
|
11191
12740
|
// src/daemon/runner.ts
|
|
11192
12741
|
var DEFAULT_LOCAL_PORT = 3210;
|
|
11193
12742
|
var log7 = createLogger("Daemon");
|
|
@@ -11229,6 +12778,7 @@ var Runner = class extends EventEmitter3 {
|
|
|
11229
12778
|
workspaceId = "";
|
|
11230
12779
|
wsConnected = false;
|
|
11231
12780
|
hostedAgents = /* @__PURE__ */ new Map();
|
|
12781
|
+
rejectedHostedAgentIds = /* @__PURE__ */ new Map();
|
|
11232
12782
|
runningTasks = /* @__PURE__ */ new Map();
|
|
11233
12783
|
lastTaskError;
|
|
11234
12784
|
heartbeatTimer;
|
|
@@ -11236,6 +12786,14 @@ var Runner = class extends EventEmitter3 {
|
|
|
11236
12786
|
// F16 (2026-05-20) — periodic skill resync timer (default 10min).
|
|
11237
12787
|
skillResyncTimer;
|
|
11238
12788
|
skillSyncInFlight = false;
|
|
12789
|
+
// Wave-4 E7 — local cache of in-flight two-phase dispatch replies. Survives
|
|
12790
|
+
// daemon crash so cold-start can resume `prepared` rows via idempotent
|
|
12791
|
+
// commit (server enforces (taskId, idempotencyKey) UNIQUE).
|
|
12792
|
+
pendingReplyCache;
|
|
12793
|
+
// One-shot guard so we only recover on the first post-authenticated tick,
|
|
12794
|
+
// not every reconnect (server-side commits are idempotent but the log noise
|
|
12795
|
+
// and DB pressure of re-running on every redeclare is wasteful).
|
|
12796
|
+
pendingReplyRecoveryDone = false;
|
|
11239
12797
|
async start() {
|
|
11240
12798
|
if (this.state !== "idle") throw new Error(`Runner already in state ${this.state}`);
|
|
11241
12799
|
this.state = "starting";
|
|
@@ -11250,6 +12808,7 @@ var Runner = class extends EventEmitter3 {
|
|
|
11250
12808
|
process.env.PRISMER_API_KEY = this.config.api_key;
|
|
11251
12809
|
this.db = openLocalDb(this.paths.localDb);
|
|
11252
12810
|
this.cloud = new CloudClient({ baseUrl: this.config.cloud_api_base, apiKey: this.config.api_key });
|
|
12811
|
+
this.pendingReplyCache = createPendingReplyCache(this.db);
|
|
11253
12812
|
this.assetCache = new AssetCache({
|
|
11254
12813
|
db: this.db,
|
|
11255
12814
|
cloud: this.cloud,
|
|
@@ -11665,6 +13224,9 @@ var Runner = class extends EventEmitter3 {
|
|
|
11665
13224
|
profilesByAgent.set(p.agent_im_user_id, list);
|
|
11666
13225
|
}
|
|
11667
13226
|
for (const a of agents) {
|
|
13227
|
+
if (this.rejectedHostedAgentIds.has(a.im_user_id)) {
|
|
13228
|
+
continue;
|
|
13229
|
+
}
|
|
11668
13230
|
let caps = [];
|
|
11669
13231
|
try {
|
|
11670
13232
|
caps = JSON.parse(a.capabilities);
|
|
@@ -11714,6 +13276,7 @@ var Runner = class extends EventEmitter3 {
|
|
|
11714
13276
|
* Used to populate the `agents` list in agent.host.declare.
|
|
11715
13277
|
*/
|
|
11716
13278
|
setHostedAgent(agent) {
|
|
13279
|
+
this.rejectedHostedAgentIds.delete(agent.imUserId);
|
|
11717
13280
|
this.hostedAgents.set(agent.imUserId, {
|
|
11718
13281
|
imUserId: agent.imUserId,
|
|
11719
13282
|
name: agent.name,
|
|
@@ -11746,6 +13309,7 @@ var Runner = class extends EventEmitter3 {
|
|
|
11746
13309
|
);
|
|
11747
13310
|
});
|
|
11748
13311
|
tx(payload);
|
|
13312
|
+
this.rejectedHostedAgentIds.delete(payload.imUserId);
|
|
11749
13313
|
this.loadAgentsFromDb();
|
|
11750
13314
|
if (this.wsConnected) this.sendDeclare();
|
|
11751
13315
|
return {
|
|
@@ -11856,6 +13420,19 @@ var Runner = class extends EventEmitter3 {
|
|
|
11856
13420
|
process.stdout.write(`[daemon] ws authenticated, sending agent.host.declare (${this.hostedAgents.size} agents)
|
|
11857
13421
|
`);
|
|
11858
13422
|
this.sendDeclare();
|
|
13423
|
+
if (!this.pendingReplyRecoveryDone) {
|
|
13424
|
+
this.pendingReplyRecoveryDone = true;
|
|
13425
|
+
void recoverPendingReplies({ cloud: this.cloud, cache: this.pendingReplyCache }).then((summary) => {
|
|
13426
|
+
if (summary.attempted === 0) return;
|
|
13427
|
+
process.stdout.write(
|
|
13428
|
+
`[daemon] dispatch.recovery summary attempted=${summary.attempted} committed=${summary.committed} aborted=${summary.aborted} failed=${summary.failed}
|
|
13429
|
+
`
|
|
13430
|
+
);
|
|
13431
|
+
}).catch((err) => {
|
|
13432
|
+
process.stderr.write(`[daemon] dispatch.recovery threw: ${err.message}
|
|
13433
|
+
`);
|
|
13434
|
+
});
|
|
13435
|
+
}
|
|
11859
13436
|
return;
|
|
11860
13437
|
}
|
|
11861
13438
|
this.handleIncoming(msg);
|
|
@@ -11902,12 +13479,91 @@ var Runner = class extends EventEmitter3 {
|
|
|
11902
13479
|
case "asset.changed":
|
|
11903
13480
|
void this.onAssetChanged(msg.payload);
|
|
11904
13481
|
return;
|
|
13482
|
+
// v2.0 §4.8.1 (Wave 4-E4) — webhook reverse-channel RPC. Cloud's
|
|
13483
|
+
// dispatchExternalMention pushes an AgentDispatchRequest plus an
|
|
13484
|
+
// embedded `_rpcId`; the daemon runs the same handler as the
|
|
13485
|
+
// HTTP /dispatch path and echoes the reply back over WS with the
|
|
13486
|
+
// identical `_rpcId` so cloud's WsRpcService can settle the
|
|
13487
|
+
// pending promise.
|
|
13488
|
+
case "webhook.dispatch.request":
|
|
13489
|
+
void this.onWebhookDispatch(msg.payload);
|
|
13490
|
+
return;
|
|
11905
13491
|
default:
|
|
11906
13492
|
this.emit("unknown-message", msg);
|
|
11907
13493
|
}
|
|
11908
13494
|
}
|
|
13495
|
+
/**
|
|
13496
|
+
* v2.0 §4.8.1 — webhook dispatch over WS reverse channel.
|
|
13497
|
+
*
|
|
13498
|
+
* Acks via `webhook.dispatch.reply { _rpcId, ok, acceptedAt }` immediately
|
|
13499
|
+
* after `handleAgentMessageDispatch()` returns its synchronous response
|
|
13500
|
+
* — the actual reply (the agent's reply text/attachments) still flows
|
|
13501
|
+
* through `postMessageDispatchReply()` which posts to
|
|
13502
|
+
* `/api/im/dispatch/reply` after the adapter finishes. That mirrors the
|
|
13503
|
+
* HTTP /dispatch contract: ack first, asynchronous final reply via the
|
|
13504
|
+
* dispatch-reply REST endpoint.
|
|
13505
|
+
*
|
|
13506
|
+
* The `_rpcId` field is the only addition versus the HTTP path; we strip
|
|
13507
|
+
* it before passing the payload to the dispatch handler so existing
|
|
13508
|
+
* validation logic doesn't trip on an unknown field.
|
|
13509
|
+
*/
|
|
13510
|
+
async onWebhookDispatch(payload) {
|
|
13511
|
+
const rpcId = payload._rpcId;
|
|
13512
|
+
if (!rpcId) {
|
|
13513
|
+
process.stderr.write(`[daemon] webhook.dispatch.request without _rpcId \u2014 dropped
|
|
13514
|
+
`);
|
|
13515
|
+
return;
|
|
13516
|
+
}
|
|
13517
|
+
const { _rpcId: _, ...request } = payload;
|
|
13518
|
+
try {
|
|
13519
|
+
const handle = handleAgentMessageDispatch(request, {
|
|
13520
|
+
findAgent: (agentImUserId) => this.findMessageDispatchAgent(agentImUserId),
|
|
13521
|
+
postReply: (replyPayload) => this.postMessageDispatchReply(replyPayload),
|
|
13522
|
+
onError: (err, req) => {
|
|
13523
|
+
process.stderr.write(
|
|
13524
|
+
`[daemon] webhook.dispatch.request failed message=${req.messageId} agent=${req.mentionedAgentImUserId}: ${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
13525
|
+
`
|
|
13526
|
+
);
|
|
13527
|
+
}
|
|
13528
|
+
});
|
|
13529
|
+
this.ws.send({
|
|
13530
|
+
type: "webhook.dispatch.reply",
|
|
13531
|
+
payload: {
|
|
13532
|
+
_rpcId: rpcId,
|
|
13533
|
+
ok: handle.response.ok,
|
|
13534
|
+
acceptedAt: handle.response.acceptedAt,
|
|
13535
|
+
error: handle.response.error
|
|
13536
|
+
}
|
|
13537
|
+
});
|
|
13538
|
+
void handle.done.catch((err) => {
|
|
13539
|
+
process.stderr.write(
|
|
13540
|
+
`[daemon] webhook.dispatch final reply failed message=${request.messageId}: ${err instanceof Error ? err.message : String(err)}
|
|
13541
|
+
`
|
|
13542
|
+
);
|
|
13543
|
+
});
|
|
13544
|
+
} catch (err) {
|
|
13545
|
+
this.ws.send({
|
|
13546
|
+
type: "webhook.dispatch.reply",
|
|
13547
|
+
payload: {
|
|
13548
|
+
_rpcId: rpcId,
|
|
13549
|
+
ok: false,
|
|
13550
|
+
error: {
|
|
13551
|
+
code: "daemon_dispatch_failed",
|
|
13552
|
+
message: err instanceof Error ? err.message : String(err)
|
|
13553
|
+
}
|
|
13554
|
+
}
|
|
13555
|
+
});
|
|
13556
|
+
}
|
|
13557
|
+
}
|
|
11909
13558
|
async onHostAcked(payload) {
|
|
11910
13559
|
this.workspaceId = payload.workspaceId;
|
|
13560
|
+
const ownershipLockChanged = await this.applyHostAckOwnershipLock(payload);
|
|
13561
|
+
void this.reportTransport().catch((err) => {
|
|
13562
|
+
process.stderr.write(
|
|
13563
|
+
`[daemon] transport-probe report failed: ${err.message}
|
|
13564
|
+
`
|
|
13565
|
+
);
|
|
13566
|
+
});
|
|
11911
13567
|
if (this.memoryWiring && this.workspaceId) {
|
|
11912
13568
|
syncMemoryFromCloud(this.memoryWiring, this.cloud, [this.workspaceId]).catch(
|
|
11913
13569
|
(err) => log7.error("Initial memory sync failed", err.message)
|
|
@@ -11917,7 +13573,9 @@ var Runner = class extends EventEmitter3 {
|
|
|
11917
13573
|
(err) => log7.error("Asset sync failed", err.message)
|
|
11918
13574
|
);
|
|
11919
13575
|
await this.ensureDropFolderRuntime(this.workspaceId);
|
|
11920
|
-
|
|
13576
|
+
const profilesToSync = payload.profilesToSync ?? [];
|
|
13577
|
+
const profilesToDelete = payload.profilesToDelete ?? [];
|
|
13578
|
+
for (const id of profilesToSync) {
|
|
11921
13579
|
try {
|
|
11922
13580
|
await this.servicePool.drop(id);
|
|
11923
13581
|
await this.syncProfileFromCloud(id);
|
|
@@ -11925,7 +13583,7 @@ var Runner = class extends EventEmitter3 {
|
|
|
11925
13583
|
this.emit("sync-error", err);
|
|
11926
13584
|
}
|
|
11927
13585
|
}
|
|
11928
|
-
for (const id of
|
|
13586
|
+
for (const id of profilesToDelete) {
|
|
11929
13587
|
const row = this.db.prepare("SELECT agent_im_user_id FROM agent_profiles WHERE id = ?").get(id);
|
|
11930
13588
|
this.db.prepare("DELETE FROM agent_profiles WHERE id = ?").run(id);
|
|
11931
13589
|
if (row) {
|
|
@@ -11934,10 +13592,37 @@ var Runner = class extends EventEmitter3 {
|
|
|
11934
13592
|
process.stderr.write(`[daemon] tombstone profile=${id}
|
|
11935
13593
|
`);
|
|
11936
13594
|
}
|
|
11937
|
-
if (
|
|
11938
|
-
if ((
|
|
13595
|
+
if (profilesToDelete.length) this.loadAgentsFromDb();
|
|
13596
|
+
if ((ownershipLockChanged || profilesToSync.length > 0 || profilesToDelete.length > 0) && this.wsConnected) {
|
|
13597
|
+
this.sendDeclare();
|
|
13598
|
+
}
|
|
11939
13599
|
this.emit("host-acked", payload);
|
|
11940
13600
|
}
|
|
13601
|
+
async applyHostAckOwnershipLock(payload) {
|
|
13602
|
+
const rejected = payload.rejectedAgents ?? [];
|
|
13603
|
+
if (rejected.length === 0) return false;
|
|
13604
|
+
let changed = false;
|
|
13605
|
+
for (const rejection of rejected) {
|
|
13606
|
+
if (!rejection?.imUserId) continue;
|
|
13607
|
+
const agentImUserId = rejection.imUserId;
|
|
13608
|
+
this.rejectedHostedAgentIds.set(agentImUserId, {
|
|
13609
|
+
...rejection,
|
|
13610
|
+
rejectedAt: Date.now()
|
|
13611
|
+
});
|
|
13612
|
+
const wasHosted = this.hostedAgents.delete(agentImUserId);
|
|
13613
|
+
const profiles = this.db.prepare("SELECT id FROM agent_profiles WHERE agent_im_user_id = ?").all(agentImUserId);
|
|
13614
|
+
const deleted = this.db.prepare("DELETE FROM agents WHERE im_user_id = ?").run(agentImUserId);
|
|
13615
|
+
await Promise.all(profiles.map((profile) => this.servicePool.drop(profile.id)));
|
|
13616
|
+
if (wasHosted || deleted.changes > 0) {
|
|
13617
|
+
changed = true;
|
|
13618
|
+
process.stderr.write(
|
|
13619
|
+
`[daemon] ownership rejected agent=${agentImUserId} reason=${rejection.reason} owner=${rejection.ownerDaemonId ?? "(unknown)"} \u2014 stop declaring locally
|
|
13620
|
+
`
|
|
13621
|
+
);
|
|
13622
|
+
}
|
|
13623
|
+
}
|
|
13624
|
+
return changed;
|
|
13625
|
+
}
|
|
11941
13626
|
async onTaskDispatch(payload, requestId) {
|
|
11942
13627
|
const targetDaemonId = readTargetDaemonId(payload);
|
|
11943
13628
|
if (targetDaemonId && targetDaemonId !== this.config.daemon_id) {
|
|
@@ -12089,15 +13774,69 @@ var Runner = class extends EventEmitter3 {
|
|
|
12089
13774
|
};
|
|
12090
13775
|
}
|
|
12091
13776
|
async postMessageDispatchReply(payload) {
|
|
12092
|
-
const
|
|
12093
|
-
|
|
12094
|
-
|
|
12095
|
-
|
|
12096
|
-
|
|
12097
|
-
|
|
12098
|
-
|
|
13777
|
+
const taskId = `external:${payload.replyToMessageId}`;
|
|
13778
|
+
try {
|
|
13779
|
+
const result = await sendDispatchReplyTwoPhase({
|
|
13780
|
+
taskId,
|
|
13781
|
+
payload: {
|
|
13782
|
+
conversationId: payload.conversationId,
|
|
13783
|
+
replyToken: payload.replyToken,
|
|
13784
|
+
replyToMessageId: payload.replyToMessageId,
|
|
13785
|
+
agentImUserId: payload.agentImUserId,
|
|
13786
|
+
status: payload.status,
|
|
13787
|
+
...payload.replyText !== void 0 ? { replyText: payload.replyText } : {},
|
|
13788
|
+
...payload.attachments ? { attachments: payload.attachments } : {},
|
|
13789
|
+
assetIds: payload.attachments?.map((a) => a.assetId).filter((id) => typeof id === "string") ?? [],
|
|
13790
|
+
completedAt: payload.completedAt,
|
|
13791
|
+
...payload.error ? { error: payload.error } : {}
|
|
13792
|
+
},
|
|
13793
|
+
cloud: this.cloud,
|
|
13794
|
+
cache: this.pendingReplyCache
|
|
13795
|
+
});
|
|
13796
|
+
if (result.status === "aborted") {
|
|
13797
|
+
throw new Error(`dispatch reply aborted by server reaper (replyId=${result.replyId})`);
|
|
13798
|
+
}
|
|
13799
|
+
} catch (err) {
|
|
13800
|
+
throw new Error(
|
|
13801
|
+
err.message?.length ? err.message : `dispatch reply failed`
|
|
13802
|
+
);
|
|
12099
13803
|
}
|
|
12100
13804
|
}
|
|
13805
|
+
/**
|
|
13806
|
+
* v2.0 §4.8.1 (Wave 4-E4) — submit the transport-probe report. Called
|
|
13807
|
+
* from `onHostAcked` so the daemon's container row exists in cloud
|
|
13808
|
+
* before we POST to /runtime/transport-report.
|
|
13809
|
+
*
|
|
13810
|
+
* The gatewayUrl reported here is `http://<local-ipv4>:<localPort>`
|
|
13811
|
+
* (typically `http://192.168.x.x:3210`). For a daemon without a local
|
|
13812
|
+
* HTTP server (startLocalServer=false in tests) we still post the
|
|
13813
|
+
* probe with `gatewayUrl=null` so cloud knows transport='ws' is the
|
|
13814
|
+
* only path.
|
|
13815
|
+
*/
|
|
13816
|
+
async reportTransport() {
|
|
13817
|
+
const hasLocalServer = this.opts.startLocalServer !== false;
|
|
13818
|
+
let gatewayUrl = null;
|
|
13819
|
+
if (hasLocalServer) {
|
|
13820
|
+
const localIp = pickLocalIPv4();
|
|
13821
|
+
if (localIp) {
|
|
13822
|
+
const port = this.opts.localPort ?? DEFAULT_LOCAL_PORT;
|
|
13823
|
+
gatewayUrl = `http://${localIp}:${port}`;
|
|
13824
|
+
}
|
|
13825
|
+
}
|
|
13826
|
+
const probe = buildTransportProbe(gatewayUrl);
|
|
13827
|
+
const result = await reportTransportProbe(this.cloud, this.config.daemon_id, probe);
|
|
13828
|
+
if (!result.ok) {
|
|
13829
|
+
process.stderr.write(
|
|
13830
|
+
`[daemon] transport-probe report rejected status=${result.status} error=${result.error ?? "-"}
|
|
13831
|
+
`
|
|
13832
|
+
);
|
|
13833
|
+
return;
|
|
13834
|
+
}
|
|
13835
|
+
process.stdout.write(
|
|
13836
|
+
`[daemon] transport-probe reported transport=${probe.transport} gateway=${probe.gatewayUrl ?? "null"} private=${probe.gatewayIsPrivate}
|
|
13837
|
+
`
|
|
13838
|
+
);
|
|
13839
|
+
}
|
|
12101
13840
|
onAgentChanged(payload) {
|
|
12102
13841
|
const a = this.hostedAgents.get(payload.agentImUserId);
|
|
12103
13842
|
if (!a) return;
|
|
@@ -12166,18 +13905,22 @@ var Runner = class extends EventEmitter3 {
|
|
|
12166
13905
|
const name = agent?.card?.name || agent?.displayName || agent?.username || profile.agentImUserId;
|
|
12167
13906
|
const now = Date.now();
|
|
12168
13907
|
const tx = this.db.transaction(() => {
|
|
12169
|
-
this.
|
|
12170
|
-
|
|
12171
|
-
|
|
12172
|
-
|
|
12173
|
-
|
|
12174
|
-
|
|
12175
|
-
|
|
12176
|
-
|
|
12177
|
-
|
|
12178
|
-
|
|
12179
|
-
|
|
12180
|
-
|
|
13908
|
+
if (this.rejectedHostedAgentIds.has(profile.agentImUserId)) {
|
|
13909
|
+
this.db.prepare("DELETE FROM agents WHERE im_user_id = ?").run(profile.agentImUserId);
|
|
13910
|
+
} else {
|
|
13911
|
+
this.db.prepare(
|
|
13912
|
+
`INSERT OR REPLACE INTO agents
|
|
13913
|
+
(im_user_id, workspace_id, name, adapter_name, capabilities, status, version, synced_at, dirty)
|
|
13914
|
+
VALUES (?, ?, ?, ?, ?, 'offline', 1, ?, 0)`
|
|
13915
|
+
).run(
|
|
13916
|
+
profile.agentImUserId,
|
|
13917
|
+
profile.workspaceId,
|
|
13918
|
+
name,
|
|
13919
|
+
profile.adapterName,
|
|
13920
|
+
JSON.stringify(capabilities),
|
|
13921
|
+
now
|
|
13922
|
+
);
|
|
13923
|
+
}
|
|
12181
13924
|
this.db.prepare(
|
|
12182
13925
|
`INSERT OR REPLACE INTO agent_profiles
|
|
12183
13926
|
(id, workspace_id, agent_im_user_id, adapter_name, name, config, version, synced_at, dirty, deleted_at)
|
|
@@ -12539,7 +14282,7 @@ var Runner = class extends EventEmitter3 {
|
|
|
12539
14282
|
function resolveUserAssetWorkspaceDir(workspaceId) {
|
|
12540
14283
|
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
12541
14284
|
if (override) return join17(override, workspaceId);
|
|
12542
|
-
const home =
|
|
14285
|
+
const home = homedir8();
|
|
12543
14286
|
const desktop = join17(home, "Desktop");
|
|
12544
14287
|
const base = existsSync13(desktop) ? desktop : home;
|
|
12545
14288
|
return join17(base, "Prismer Assets", workspaceId);
|
|
@@ -12658,7 +14401,7 @@ function buildDaemonCommand() {
|
|
|
12658
14401
|
while (!configExists(paths)) {
|
|
12659
14402
|
if (stopRequested) return;
|
|
12660
14403
|
getUI().info(`[daemon] waiting for ${paths.configFile} \u2014 run \`prismer setup\` to create it`);
|
|
12661
|
-
await
|
|
14404
|
+
await sleep3(5e3);
|
|
12662
14405
|
}
|
|
12663
14406
|
runner = new Runner({
|
|
12664
14407
|
startLocalServer: opts.localServer !== false,
|
|
@@ -12690,7 +14433,7 @@ function buildDaemonCommand() {
|
|
|
12690
14433
|
if (existingPid && pidAlive2(existingPid)) {
|
|
12691
14434
|
exitWithError(`Daemon already running (pid ${existingPid}). Use \`prismer daemon stop\` first.`);
|
|
12692
14435
|
}
|
|
12693
|
-
if (!existsSync14(paths.logsDir))
|
|
14436
|
+
if (!existsSync14(paths.logsDir)) mkdirSync10(paths.logsDir, { recursive: true });
|
|
12694
14437
|
const logFile = join18(paths.logsDir, "daemon.log");
|
|
12695
14438
|
const fd = openSync2(logFile, "a");
|
|
12696
14439
|
const args = [process.argv[1], "daemon", "run"];
|
|
@@ -12704,7 +14447,7 @@ function buildDaemonCommand() {
|
|
|
12704
14447
|
child.unref();
|
|
12705
14448
|
const deadline = Date.now() + 5e3;
|
|
12706
14449
|
while (Date.now() < deadline) {
|
|
12707
|
-
await
|
|
14450
|
+
await sleep3(200);
|
|
12708
14451
|
const pid = readPidFile(paths);
|
|
12709
14452
|
if (pid && pidAlive2(pid)) {
|
|
12710
14453
|
if (opts.json) printJson({ ok: true, pid, logFile });
|
|
@@ -12738,7 +14481,7 @@ function buildDaemonCommand() {
|
|
|
12738
14481
|
getUI().ok("Daemon stopped", `pid ${pid}`);
|
|
12739
14482
|
return;
|
|
12740
14483
|
}
|
|
12741
|
-
await
|
|
14484
|
+
await sleep3(200);
|
|
12742
14485
|
}
|
|
12743
14486
|
process.stderr.write(`Daemon did not exit within timeout; pid ${pid} may still be alive.
|
|
12744
14487
|
`);
|
|
@@ -12819,7 +14562,7 @@ async function tailFromEnd(path9, lines) {
|
|
|
12819
14562
|
async function followFile(path9) {
|
|
12820
14563
|
let offset = statSync4(path9).size;
|
|
12821
14564
|
for (; ; ) {
|
|
12822
|
-
await
|
|
14565
|
+
await sleep3(1e3);
|
|
12823
14566
|
const size = statSync4(path9).size;
|
|
12824
14567
|
if (size < offset) offset = 0;
|
|
12825
14568
|
if (size === offset) continue;
|
|
@@ -12833,7 +14576,7 @@ async function followFile(path9) {
|
|
|
12833
14576
|
init_util();
|
|
12834
14577
|
import { Command as Command9 } from "commander";
|
|
12835
14578
|
import { createReadStream as createReadStream2, existsSync as existsSync15 } from "fs";
|
|
12836
|
-
import { homedir as
|
|
14579
|
+
import { homedir as homedir9 } from "os";
|
|
12837
14580
|
import { join as join19 } from "path";
|
|
12838
14581
|
import { createInterface } from "readline";
|
|
12839
14582
|
var DEFAULT_LIMIT2 = 50;
|
|
@@ -12866,7 +14609,7 @@ function buildEventsStatsCommand() {
|
|
|
12866
14609
|
const events = await readEvents(file, filters);
|
|
12867
14610
|
printJson({
|
|
12868
14611
|
ok: true,
|
|
12869
|
-
data: { stats:
|
|
14612
|
+
data: { stats: summarize3(events), count: events.length, limit: filters.limit },
|
|
12870
14613
|
checks: { file, exists: true, filters: cleanFilters(filters) }
|
|
12871
14614
|
});
|
|
12872
14615
|
});
|
|
@@ -12875,7 +14618,7 @@ function addEventOptions(cmd) {
|
|
|
12875
14618
|
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)");
|
|
12876
14619
|
}
|
|
12877
14620
|
function eventsPath() {
|
|
12878
|
-
return join19(process.env.PRISMER_HOME ?? join19(
|
|
14621
|
+
return join19(process.env.PRISMER_HOME ?? join19(homedir9(), ".prismer"), "para", "events.jsonl");
|
|
12879
14622
|
}
|
|
12880
14623
|
function unavailable(file) {
|
|
12881
14624
|
return {
|
|
@@ -12923,7 +14666,7 @@ function field(event, names) {
|
|
|
12923
14666
|
}
|
|
12924
14667
|
return void 0;
|
|
12925
14668
|
}
|
|
12926
|
-
function
|
|
14669
|
+
function summarize3(events) {
|
|
12927
14670
|
const byFamily = {};
|
|
12928
14671
|
const byType = {};
|
|
12929
14672
|
const byAgentId = {};
|
|
@@ -13237,7 +14980,7 @@ import { Command as Command11 } from "commander";
|
|
|
13237
14980
|
// src/pair.ts
|
|
13238
14981
|
import { generateKeyPairSync } from "crypto";
|
|
13239
14982
|
import { hostname as hostname2 } from "os";
|
|
13240
|
-
import { setTimeout as
|
|
14983
|
+
import { setTimeout as sleep4 } from "timers/promises";
|
|
13241
14984
|
import qrcode from "qrcode";
|
|
13242
14985
|
|
|
13243
14986
|
// src/daemon-id.ts
|
|
@@ -13324,7 +15067,7 @@ Scan with Lumin to approve, or open: ${offer.qrUrl}
|
|
|
13324
15067
|
const pollIntervalMs = opts.pollIntervalMs ?? 5e3;
|
|
13325
15068
|
for (let i = 0; i < maxAttempts; i += 1) {
|
|
13326
15069
|
if (i > 0 || !localOnlyMode) {
|
|
13327
|
-
await
|
|
15070
|
+
await sleep4(pollIntervalMs);
|
|
13328
15071
|
}
|
|
13329
15072
|
const res = await cloud.request(
|
|
13330
15073
|
"GET",
|
|
@@ -13405,7 +15148,7 @@ function buildPairCommand() {
|
|
|
13405
15148
|
|
|
13406
15149
|
// src/cli/commands/profile.ts
|
|
13407
15150
|
import { Command as Command12 } from "commander";
|
|
13408
|
-
import { existsSync as existsSync17, readFileSync as readFileSync12, writeFileSync as
|
|
15151
|
+
import { existsSync as existsSync17, readFileSync as readFileSync12, writeFileSync as writeFileSync10 } from "fs";
|
|
13409
15152
|
import { tmpdir } from "os";
|
|
13410
15153
|
import { join as join20 } from "path";
|
|
13411
15154
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
@@ -13532,7 +15275,7 @@ function buildProfileCommand() {
|
|
|
13532
15275
|
`/api/im/agent_profiles/${encodeURIComponent(profileId)}`
|
|
13533
15276
|
);
|
|
13534
15277
|
const tmpFile = join20(tmpdir(), `prismer-profile-${profileId}.json`);
|
|
13535
|
-
|
|
15278
|
+
writeFileSync10(tmpFile, JSON.stringify(profile.config, null, 2), "utf8");
|
|
13536
15279
|
const editor = process.env.EDITOR || "vi";
|
|
13537
15280
|
const ed = spawnSync4(editor, [tmpFile], { stdio: "inherit" });
|
|
13538
15281
|
if (ed.status !== 0) exitWithError(`editor exited ${ed.status}`, { code: "editor_failed" });
|
|
@@ -13573,10 +15316,10 @@ async function resolveDefaultWorkspaceId(cloud) {
|
|
|
13573
15316
|
// src/cli/commands/reset.ts
|
|
13574
15317
|
import { Command as Command13 } from "commander";
|
|
13575
15318
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
13576
|
-
import { existsSync as existsSync18, mkdirSync as
|
|
13577
|
-
import { homedir as
|
|
13578
|
-
import { dirname as
|
|
13579
|
-
import { setTimeout as
|
|
15319
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync11, rmSync, renameSync as renameSync2 } from "fs";
|
|
15320
|
+
import { homedir as homedir10 } from "os";
|
|
15321
|
+
import { dirname as dirname12, join as join21 } from "path";
|
|
15322
|
+
import { setTimeout as sleep5 } from "timers/promises";
|
|
13580
15323
|
init_util();
|
|
13581
15324
|
init_ui();
|
|
13582
15325
|
function buildResetCommand() {
|
|
@@ -13629,8 +15372,8 @@ function buildResetCommand() {
|
|
|
13629
15372
|
rmSync(paths.root, { recursive: true, force: true });
|
|
13630
15373
|
} else {
|
|
13631
15374
|
if (!archivePath) throw new Error("internal: archivePath missing");
|
|
13632
|
-
if (!existsSync18(
|
|
13633
|
-
|
|
15375
|
+
if (!existsSync18(dirname12(archivePath))) {
|
|
15376
|
+
mkdirSync11(dirname12(archivePath), { recursive: true });
|
|
13634
15377
|
}
|
|
13635
15378
|
renameSync2(paths.root, archivePath);
|
|
13636
15379
|
}
|
|
@@ -13640,8 +15383,8 @@ function buildResetCommand() {
|
|
|
13640
15383
|
rmSync(assetSyncRoot, { recursive: true, force: true });
|
|
13641
15384
|
} else {
|
|
13642
15385
|
if (!assetSyncArchivePath) throw new Error("internal: assetSyncArchivePath missing");
|
|
13643
|
-
if (!existsSync18(
|
|
13644
|
-
|
|
15386
|
+
if (!existsSync18(dirname12(assetSyncArchivePath))) {
|
|
15387
|
+
mkdirSync11(dirname12(assetSyncArchivePath), { recursive: true });
|
|
13645
15388
|
}
|
|
13646
15389
|
renameSync2(assetSyncRoot, assetSyncArchivePath);
|
|
13647
15390
|
}
|
|
@@ -13651,13 +15394,13 @@ function buildResetCommand() {
|
|
|
13651
15394
|
rmSync(hermesHome, { recursive: true, force: true });
|
|
13652
15395
|
} else {
|
|
13653
15396
|
if (!hermesArchivePath) throw new Error("internal: hermesArchivePath missing");
|
|
13654
|
-
if (!existsSync18(
|
|
13655
|
-
|
|
15397
|
+
if (!existsSync18(dirname12(hermesArchivePath))) {
|
|
15398
|
+
mkdirSync11(dirname12(hermesArchivePath), { recursive: true });
|
|
13656
15399
|
}
|
|
13657
15400
|
renameSync2(hermesHome, hermesArchivePath);
|
|
13658
15401
|
}
|
|
13659
15402
|
}
|
|
13660
|
-
|
|
15403
|
+
mkdirSync11(paths.root, { recursive: true });
|
|
13661
15404
|
if (opts.json) {
|
|
13662
15405
|
printJson({ ok: true, reset: true, plan });
|
|
13663
15406
|
return;
|
|
@@ -13706,7 +15449,7 @@ async function stopProcess(pid, timeoutMs, label) {
|
|
|
13706
15449
|
const deadline = Date.now() + timeoutMs;
|
|
13707
15450
|
while (Date.now() < deadline) {
|
|
13708
15451
|
if (!pidAlive2(pid)) return;
|
|
13709
|
-
await
|
|
15452
|
+
await sleep5(100);
|
|
13710
15453
|
}
|
|
13711
15454
|
try {
|
|
13712
15455
|
process.kill(pid, "SIGKILL");
|
|
@@ -13716,7 +15459,7 @@ async function stopProcess(pid, timeoutMs, label) {
|
|
|
13716
15459
|
const killDeadline = Date.now() + 1e3;
|
|
13717
15460
|
while (Date.now() < killDeadline) {
|
|
13718
15461
|
if (!pidAlive2(pid)) return;
|
|
13719
|
-
await
|
|
15462
|
+
await sleep5(100);
|
|
13720
15463
|
}
|
|
13721
15464
|
throw new Error(`${label} pid ${pid} did not exit within ${timeoutMs + 1e3}ms`);
|
|
13722
15465
|
}
|
|
@@ -13726,13 +15469,13 @@ function timestamp() {
|
|
|
13726
15469
|
function resolveAssetSyncRoot() {
|
|
13727
15470
|
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
13728
15471
|
if (override) return override;
|
|
13729
|
-
const home =
|
|
15472
|
+
const home = homedir10();
|
|
13730
15473
|
const desktop = join21(home, "Desktop");
|
|
13731
15474
|
const base = existsSync18(desktop) ? desktop : home;
|
|
13732
15475
|
return join21(base, "Prismer Assets");
|
|
13733
15476
|
}
|
|
13734
15477
|
function resolveHermesHome() {
|
|
13735
|
-
return process.env.HERMES_HOME || join21(
|
|
15478
|
+
return process.env.HERMES_HOME || join21(homedir10(), ".hermes");
|
|
13736
15479
|
}
|
|
13737
15480
|
function findHermesGatewayPids2() {
|
|
13738
15481
|
try {
|
|
@@ -14287,7 +16030,7 @@ function buildSkillCommand() {
|
|
|
14287
16030
|
const cloud = mkCloud6();
|
|
14288
16031
|
const path9 = opts.agent ? `/api/im/agents/${encodeURIComponent(opts.agent)}/skills` : `/api/im/skills/${encodeURIComponent(slugOrId)}/install`;
|
|
14289
16032
|
const body = opts.agent ? { skillId: slugOrId, workspaceId: opts.workspaceId, version: opts.version } : { workspaceId: opts.workspaceId, version: opts.version };
|
|
14290
|
-
const data = await
|
|
16033
|
+
const data = await requestEnvelope2(cloud, "POST", path9, body, "skill_install_failed");
|
|
14291
16034
|
if (opts.json) {
|
|
14292
16035
|
printJson(data);
|
|
14293
16036
|
return;
|
|
@@ -14298,7 +16041,7 @@ function buildSkillCommand() {
|
|
|
14298
16041
|
const cloud = mkCloud6();
|
|
14299
16042
|
const path9 = opts.agent ? `/api/im/agents/${encodeURIComponent(opts.agent)}/skills` : `/api/im/skills/${encodeURIComponent(slugOrId)}/install`;
|
|
14300
16043
|
const body = opts.agent ? { skillId: slugOrId, workspaceId: opts.workspaceId } : void 0;
|
|
14301
|
-
const data = await
|
|
16044
|
+
const data = await requestEnvelope2(cloud, "DELETE", path9, body, "skill_uninstall_failed");
|
|
14302
16045
|
if (opts.json) {
|
|
14303
16046
|
printJson(data);
|
|
14304
16047
|
return;
|
|
@@ -14351,7 +16094,7 @@ function mkCloud6() {
|
|
|
14351
16094
|
const cfg = loadConfig(resolvePaths());
|
|
14352
16095
|
return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
14353
16096
|
}
|
|
14354
|
-
async function
|
|
16097
|
+
async function requestEnvelope2(cloud, method, path9, body, code) {
|
|
14355
16098
|
const res = await cloud.request(method, path9, {
|
|
14356
16099
|
body
|
|
14357
16100
|
});
|
|
@@ -14501,6 +16244,7 @@ function buildStatusCommand() {
|
|
|
14501
16244
|
let me = null;
|
|
14502
16245
|
let devices = null;
|
|
14503
16246
|
let agents = null;
|
|
16247
|
+
let diagnose = null;
|
|
14504
16248
|
try {
|
|
14505
16249
|
const meRes = await cloud.request("GET", "/api/im/me", { timeoutMs: 3e3 });
|
|
14506
16250
|
cloudOk = meRes.ok;
|
|
@@ -14513,19 +16257,33 @@ function buildStatusCommand() {
|
|
|
14513
16257
|
if (Array.isArray(wsList) && wsList.length > 0) {
|
|
14514
16258
|
const wsId = wsList[0]?.id;
|
|
14515
16259
|
if (wsId) {
|
|
14516
|
-
const
|
|
14517
|
-
if (
|
|
14518
|
-
const
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
agents = agBody?.data;
|
|
16260
|
+
const rtRes = await cloud.request("GET", `/api/im/workspaces/${encodeURIComponent(wsId)}/runtime`, { timeoutMs: 3e3 });
|
|
16261
|
+
if (rtRes.ok) {
|
|
16262
|
+
const rtBody = rtRes.data;
|
|
16263
|
+
const snapshot = rtBody?.data;
|
|
16264
|
+
if (snapshot && Array.isArray(snapshot.devices)) {
|
|
16265
|
+
devices = snapshot.devices;
|
|
16266
|
+
agents = snapshot.devices.flatMap((d) => d.agents ?? []);
|
|
16267
|
+
}
|
|
14525
16268
|
}
|
|
14526
16269
|
}
|
|
14527
16270
|
}
|
|
14528
16271
|
}
|
|
16272
|
+
const daemonId = cfg.daemon_id;
|
|
16273
|
+
if (daemonId) {
|
|
16274
|
+
try {
|
|
16275
|
+
const diagRes = await cloud.request(
|
|
16276
|
+
"GET",
|
|
16277
|
+
`/api/im/runtime/diagnose?daemonId=${encodeURIComponent(daemonId)}`,
|
|
16278
|
+
{ timeoutMs: 3e3 }
|
|
16279
|
+
);
|
|
16280
|
+
if (diagRes.ok) {
|
|
16281
|
+
const body = diagRes.data;
|
|
16282
|
+
if (body?.data && typeof body.data === "object") diagnose = body.data;
|
|
16283
|
+
}
|
|
16284
|
+
} catch {
|
|
16285
|
+
}
|
|
16286
|
+
}
|
|
14529
16287
|
}
|
|
14530
16288
|
} catch {
|
|
14531
16289
|
cloudOk = false;
|
|
@@ -14542,7 +16300,8 @@ function buildStatusCommand() {
|
|
|
14542
16300
|
info: daemonStatus.info ?? {}
|
|
14543
16301
|
},
|
|
14544
16302
|
cloud: { base: cfg.cloud_api_base, reachable: cloudOk, me, devices, agents },
|
|
14545
|
-
local
|
|
16303
|
+
local,
|
|
16304
|
+
diagnose
|
|
14546
16305
|
};
|
|
14547
16306
|
if (opts.json) {
|
|
14548
16307
|
printJson(report);
|
|
@@ -14551,6 +16310,60 @@ function buildStatusCommand() {
|
|
|
14551
16310
|
printPretty(report);
|
|
14552
16311
|
});
|
|
14553
16312
|
}
|
|
16313
|
+
function computeDeviceIcon(lastSeenAt, now) {
|
|
16314
|
+
if (!lastSeenAt) return "\u25CB";
|
|
16315
|
+
const age = now.getTime() - Date.parse(lastSeenAt);
|
|
16316
|
+
if (!Number.isFinite(age) || age < 0) return "\u25CB";
|
|
16317
|
+
if (age < 5 * 6e4) return "\u25CF";
|
|
16318
|
+
if (age < 60 * 6e4) return "\u25D0";
|
|
16319
|
+
return "\u25CB";
|
|
16320
|
+
}
|
|
16321
|
+
function computeAgentIcon(status, lastHeartbeat, now) {
|
|
16322
|
+
const s = (status || "").toLowerCase();
|
|
16323
|
+
if (s === "error" || s === "failed") return "\u25C6";
|
|
16324
|
+
if (!lastHeartbeat) return s === "offline" ? "\u25CB" : "\u25D0";
|
|
16325
|
+
const age = now.getTime() - Date.parse(lastHeartbeat);
|
|
16326
|
+
if (!Number.isFinite(age) || age < 0 || age >= 60 * 6e4) return "\u25CB";
|
|
16327
|
+
if (age >= 5 * 6e4) return "\u25D0";
|
|
16328
|
+
return "\u25CF";
|
|
16329
|
+
}
|
|
16330
|
+
function pad(value, width) {
|
|
16331
|
+
if (value.length >= width) return value.slice(0, width);
|
|
16332
|
+
return value + " ".repeat(width - value.length);
|
|
16333
|
+
}
|
|
16334
|
+
function formatDeviceBindings(devices, now = /* @__PURE__ */ new Date()) {
|
|
16335
|
+
const lines = [];
|
|
16336
|
+
if (devices.length === 0) {
|
|
16337
|
+
lines.push(" No paired devices.");
|
|
16338
|
+
return lines;
|
|
16339
|
+
}
|
|
16340
|
+
const totalAgents = devices.reduce((s, d) => s + (d.agents?.length ?? 0), 0);
|
|
16341
|
+
const devWord = devices.length === 1 ? "device" : "devices";
|
|
16342
|
+
const agWord = totalAgents === 1 ? "agent" : "agents";
|
|
16343
|
+
lines.push(` Device & Agent Bindings (${devices.length} ${devWord} \xB7 ${totalAgents} ${agWord})`);
|
|
16344
|
+
lines.push("");
|
|
16345
|
+
for (let di = 0; di < devices.length; di++) {
|
|
16346
|
+
const d = devices[di];
|
|
16347
|
+
const dIcon = computeDeviceIcon(d.lastSeenAt, now);
|
|
16348
|
+
const dName = pad(d.name || d.deviceId, 40);
|
|
16349
|
+
const lastSeen = d.lastSeenAt ? formatRelative(d.lastSeenAt, now) : "never seen";
|
|
16350
|
+
lines.push(` ${dIcon} ${dName} ${lastSeen}`);
|
|
16351
|
+
const agents = d.agents ?? [];
|
|
16352
|
+
for (let ai = 0; ai < agents.length; ai++) {
|
|
16353
|
+
const a = agents[ai];
|
|
16354
|
+
const isLast = ai === agents.length - 1;
|
|
16355
|
+
const branch = isLast ? "\u2514\u2500" : "\u251C\u2500";
|
|
16356
|
+
const aIcon = computeAgentIcon(a.status, a.lastHeartbeat, now);
|
|
16357
|
+
const aName = pad(a.name || a.id, 24);
|
|
16358
|
+
const aStatus = pad(a.status || "?", 8);
|
|
16359
|
+
const aVersion = pad(a.version ? `v${a.version}` : "\u2014", 10);
|
|
16360
|
+
const tail = a.currentTaskId ? `task=${a.currentTaskId}` : a.lastHeartbeat ? `\u2764 ${formatRelative(a.lastHeartbeat, now)}` : "";
|
|
16361
|
+
lines.push(` ${branch} ${aIcon} ${aName} ${aStatus} ${aVersion} ${tail}`.trimEnd());
|
|
16362
|
+
}
|
|
16363
|
+
if (di < devices.length - 1) lines.push("");
|
|
16364
|
+
}
|
|
16365
|
+
return lines;
|
|
16366
|
+
}
|
|
14554
16367
|
async function readDaemonStatus() {
|
|
14555
16368
|
try {
|
|
14556
16369
|
const res = await fetch("http://127.0.0.1:3210/healthz", {
|
|
@@ -14623,16 +16436,7 @@ function printPretty(report) {
|
|
|
14623
16436
|
if (Array.isArray(report.cloud.devices) && report.cloud.devices.length > 0) {
|
|
14624
16437
|
const devs = report.cloud.devices;
|
|
14625
16438
|
ui.blank();
|
|
14626
|
-
|
|
14627
|
-
for (const d of devs) {
|
|
14628
|
-
const statusIcon = d.daemonStatus === "connected" ? "\u25CF" : "\u25CB";
|
|
14629
|
-
const kind = d.runtimeKind === "docker" ? "Local" : "K8s";
|
|
14630
|
-
const declared = d.hostedAgentSummary?.declared ?? 0;
|
|
14631
|
-
ui.line(` ${statusIcon} ${d.podName?.replace(/^daemon:/, "")?.slice(0, 28) ?? "?"} kind=${kind} agents=${declared}`);
|
|
14632
|
-
}
|
|
14633
|
-
}
|
|
14634
|
-
if (Array.isArray(report.cloud.agents) && report.cloud.agents.length > 0) {
|
|
14635
|
-
ui.line(` Hosted agents: ${report.cloud.agents.length}`);
|
|
16439
|
+
for (const ln of formatDeviceBindings(devs)) ui.line(ln);
|
|
14636
16440
|
}
|
|
14637
16441
|
if (report.local) {
|
|
14638
16442
|
ui.blank();
|
|
@@ -14642,11 +16446,63 @@ function printPretty(report) {
|
|
|
14642
16446
|
} else {
|
|
14643
16447
|
warn("Local DB", "unavailable");
|
|
14644
16448
|
}
|
|
16449
|
+
if (report.diagnose) {
|
|
16450
|
+
ui.blank();
|
|
16451
|
+
ui.header("Cloud dispatch reachability");
|
|
16452
|
+
ui.blank();
|
|
16453
|
+
printDiagnose(report.diagnose, report.daemon.pid ?? null);
|
|
16454
|
+
}
|
|
16455
|
+
}
|
|
16456
|
+
function printDiagnose(d, pid) {
|
|
16457
|
+
const ui = getUI();
|
|
16458
|
+
const heartbeatLine = d.lastHeartbeatAt ? `last heartbeat ${formatRelative(d.lastHeartbeatAt)}` : "no heartbeat seen";
|
|
16459
|
+
if (d.wsAlive) {
|
|
16460
|
+
ok("Daemon", `running${pid ? ` (pid ${pid})` : ""}`);
|
|
16461
|
+
ok("Cloud WS", `connected (${heartbeatLine})`);
|
|
16462
|
+
} else {
|
|
16463
|
+
warn("Daemon", `running${pid ? ` (pid ${pid})` : ""} \u2014 not visible to cloud WS`);
|
|
16464
|
+
fail("Cloud WS", `disconnected (${heartbeatLine})`);
|
|
16465
|
+
}
|
|
16466
|
+
if (d.gatewayUrl) {
|
|
16467
|
+
if (d.gatewayIsPrivate) {
|
|
16468
|
+
warn("HTTP gateway", `${d.gatewayUrl} (private IP \u2014 only reachable on same network)`);
|
|
16469
|
+
} else if (d.httpReachable) {
|
|
16470
|
+
ok(
|
|
16471
|
+
"HTTP gateway",
|
|
16472
|
+
`${d.gatewayUrl} (reachable${d.httpLatencyMs != null ? `, ${d.httpLatencyMs}ms` : ""})`
|
|
16473
|
+
);
|
|
16474
|
+
} else {
|
|
16475
|
+
fail("HTTP gateway", `${d.gatewayUrl} (unreachable${d.httpError ? ` \u2014 ${d.httpError}` : ""})`);
|
|
16476
|
+
}
|
|
16477
|
+
} else {
|
|
16478
|
+
ui.line(" HTTP gateway: not declared (WS-only daemon)");
|
|
16479
|
+
}
|
|
16480
|
+
const activeAgents = /* @__PURE__ */ (() => {
|
|
16481
|
+
return 0;
|
|
16482
|
+
})();
|
|
16483
|
+
if (activeAgents > 0) {
|
|
16484
|
+
ok("Active agents", String(activeAgents));
|
|
16485
|
+
}
|
|
16486
|
+
const recColour = d.recommendedTransport === "ws" ? ok : d.recommendedTransport === "http" ? warn : fail;
|
|
16487
|
+
recColour("Recommended transport", d.recommendedTransport);
|
|
16488
|
+
if (d.lastProbeAt) {
|
|
16489
|
+
ui.line(` Last transport probe: ${formatRelative(d.lastProbeAt)}`);
|
|
16490
|
+
}
|
|
16491
|
+
}
|
|
16492
|
+
function formatRelative(iso, now = /* @__PURE__ */ new Date()) {
|
|
16493
|
+
const ts = Date.parse(iso);
|
|
16494
|
+
if (!Number.isFinite(ts)) return "unknown";
|
|
16495
|
+
const diff = Math.max(0, now.getTime() - ts);
|
|
16496
|
+
if (diff < 3e4) return "just now";
|
|
16497
|
+
if (diff < 6e4) return `${Math.round(diff / 1e3)}s ago`;
|
|
16498
|
+
if (diff < 36e5) return `${Math.round(diff / 6e4)}m ago`;
|
|
16499
|
+
if (diff < 864e5) return `${Math.round(diff / 36e5)}h ago`;
|
|
16500
|
+
return `${Math.round(diff / 864e5)}d ago`;
|
|
14645
16501
|
}
|
|
14646
16502
|
|
|
14647
16503
|
// src/cli/commands/task.ts
|
|
14648
16504
|
import { Command as Command18 } from "commander";
|
|
14649
|
-
import { setTimeout as
|
|
16505
|
+
import { setTimeout as sleep6 } from "timers/promises";
|
|
14650
16506
|
init_util();
|
|
14651
16507
|
function describeStatus2(status) {
|
|
14652
16508
|
return status === 0 ? "network error" : `HTTP ${status}`;
|
|
@@ -14729,7 +16585,7 @@ function unwrap2(raw) {
|
|
|
14729
16585
|
async function pollUntilTerminal(cloud, taskId, timeoutMs) {
|
|
14730
16586
|
const deadline = Date.now() + (timeoutMs ?? 5 * 6e4);
|
|
14731
16587
|
while (Date.now() < deadline) {
|
|
14732
|
-
await
|
|
16588
|
+
await sleep6(1e3);
|
|
14733
16589
|
const res = await cloud.request(
|
|
14734
16590
|
"GET",
|
|
14735
16591
|
`/api/im/tasks/${encodeURIComponent(taskId)}`
|
|
@@ -15004,7 +16860,7 @@ async function readResponseError(res) {
|
|
|
15004
16860
|
|
|
15005
16861
|
// src/cli/index.ts
|
|
15006
16862
|
init_ui();
|
|
15007
|
-
var VERSION = "2.0.
|
|
16863
|
+
var VERSION = "2.0.6";
|
|
15008
16864
|
function buildProgram() {
|
|
15009
16865
|
const program = new Command20("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
|
|
15010
16866
|
program.addCommand(buildBannerCommand());
|