dsh-agentic-pet 0.4.9 → 0.4.10
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/carrier/lib/main.js +48 -2
- package/client/pet-settings.js +308 -14
- package/lib/{service-BZbhT92u.js → driver-CpRgkle-.js} +3243 -2003
- package/lib/index.js +433 -48
- package/lib/tools.js +1 -1
- package/lib/types/actions.d.ts +28 -0
- package/lib/types/binding.d.ts +99 -0
- package/lib/types/driver.d.ts +33 -2
- package/lib/types/electron-lock.d.ts +25 -0
- package/lib/types/electron-runtime-status.d.ts +20 -0
- package/lib/types/inner-voice.d.ts +18 -1
- package/lib/types/journal.d.ts +58 -0
- package/lib/types/presence.d.ts +27 -0
- package/lib/types/rhythm.d.ts +21 -0
- package/lib/types/service.d.ts +6 -0
- package/lib/types/settings-page.d.ts +2 -1
- package/package.json +3 -3
- package/presets/pet-inner-voice.patch.yml +2 -0
- package/presets/pet.patch.yml +1 -0
package/lib/index.js
CHANGED
|
@@ -1,12 +1,75 @@
|
|
|
1
|
-
import { c as
|
|
1
|
+
import { _ as writePetName, c as parseClock, d as readAffectionRaw, f as readGlobalJson, g as writeGlobalJson, h as writeAffectionRaw, i as voiceBindingSnapshot, l as eventsFile, m as readPetName, o as installPetService, p as readJsonl, s as petHome, t as installPetDriver, u as globalFilePath, v as isPetPreset, y as patchTouchesElectronBinding } from "./driver-CpRgkle-.js";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { execFileSync, spawn } from "node:child_process";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
-
import { appendFileSync, existsSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { mkdir, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
const INSTALL_LOCK_MALFORMED_GRACE_MS = 6e4;
|
|
11
|
+
/** True when `pid` names a live process (EPERM means it exists but is not ours). */
|
|
12
|
+
function isProcessAlive(pid) {
|
|
13
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
14
|
+
try {
|
|
15
|
+
process.kill(pid, 0);
|
|
16
|
+
return true;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return error.code === "EPERM";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Parse a lock file; `undefined` means empty/malformed. */
|
|
22
|
+
function parseInstallLock(raw) {
|
|
23
|
+
try {
|
|
24
|
+
const value = JSON.parse(raw);
|
|
25
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
26
|
+
} catch {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Whether an existing lock may be taken over by a new installer.
|
|
32
|
+
* A dead owner is stale immediately; a live owner keeps its lock until the
|
|
33
|
+
* absolute cap (which also bounds PID-reuse false positives). A malformed
|
|
34
|
+
* record gets a short grace for a concurrent writer, then goes stale.
|
|
35
|
+
*/
|
|
36
|
+
function installLockStale(raw, mtimeMs, now, alive = isProcessAlive) {
|
|
37
|
+
if (now - mtimeMs > 18e5) return true;
|
|
38
|
+
const record = raw === void 0 ? void 0 : parseInstallLock(raw);
|
|
39
|
+
const pid = typeof record?.pid === "number" ? record.pid : void 0;
|
|
40
|
+
if (pid === void 0) return now - mtimeMs > INSTALL_LOCK_MALFORMED_GRACE_MS;
|
|
41
|
+
return !alive(pid);
|
|
42
|
+
}
|
|
43
|
+
// lib/types/electron-runtime-status.js
|
|
44
|
+
/**
|
|
45
|
+
* Persisted state of the Electron runtime bootstrap, shared by the installer
|
|
46
|
+
* path (writer) and the settings page (reader). The settings page is the only
|
|
47
|
+
* user-visible surface while the first install downloads ~158 MB, so this
|
|
48
|
+
* file is deliberately plain JSON in the pet home rather than process state.
|
|
49
|
+
* @module dsh-agentic-pet/electron-runtime-status
|
|
50
|
+
*/
|
|
51
|
+
function electronRuntimeStatusPath(home) {
|
|
52
|
+
return join(home, "whale598", "electron-runtime.json");
|
|
53
|
+
}
|
|
54
|
+
function readElectronRuntimeStatus(home) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(readFileSync(electronRuntimeStatusPath(home), "utf8"));
|
|
57
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
58
|
+
const status = parsed;
|
|
59
|
+
return typeof status.state === "string" ? status : void 0;
|
|
60
|
+
} catch {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function writeElectronRuntimeStatus(home, status) {
|
|
65
|
+
try {
|
|
66
|
+
const path = electronRuntimeStatusPath(home);
|
|
67
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
68
|
+
const tmp = `${path}.${String(process.pid)}.tmp`;
|
|
69
|
+
writeFileSync(tmp, `${JSON.stringify(status)}\n`, "utf8");
|
|
70
|
+
renameSync(tmp, path);
|
|
71
|
+
} catch {}
|
|
72
|
+
}
|
|
10
73
|
// lib/types/settings-page.js
|
|
11
74
|
/**
|
|
12
75
|
* The settings page's host half: local HTTP routes backing the desktop
|
|
@@ -46,6 +109,15 @@ const DEFAULT_SETTINGS = {
|
|
|
46
109
|
},
|
|
47
110
|
boundSessions: {}
|
|
48
111
|
},
|
|
112
|
+
rhythm: { quiet: null },
|
|
113
|
+
journal: {
|
|
114
|
+
enabled: true,
|
|
115
|
+
injectRecent: 3,
|
|
116
|
+
maxEntryChars: 120,
|
|
117
|
+
maxFileBytes: 262144,
|
|
118
|
+
allowUnboundAppend: false
|
|
119
|
+
},
|
|
120
|
+
presenceSummary: { enabled: false },
|
|
49
121
|
tts: { enabled: false },
|
|
50
122
|
debug: { cdp: false }
|
|
51
123
|
};
|
|
@@ -111,6 +183,18 @@ function mergeSettings(current, patch) {
|
|
|
111
183
|
...DEFAULT_SETTINGS.innerVoice,
|
|
112
184
|
...current.innerVoice
|
|
113
185
|
};
|
|
186
|
+
const rhythm = {
|
|
187
|
+
quiet: null,
|
|
188
|
+
...current.rhythm
|
|
189
|
+
};
|
|
190
|
+
const journal = {
|
|
191
|
+
...DEFAULT_SETTINGS.journal,
|
|
192
|
+
...current.journal
|
|
193
|
+
};
|
|
194
|
+
const presenceSummary = {
|
|
195
|
+
enabled: false,
|
|
196
|
+
...current.presenceSummary
|
|
197
|
+
};
|
|
114
198
|
const debug = {
|
|
115
199
|
...DEFAULT_SETTINGS.debug,
|
|
116
200
|
...current.debug
|
|
@@ -173,6 +257,37 @@ function mergeSettings(current, patch) {
|
|
|
173
257
|
innerVoice.boundSessions = record;
|
|
174
258
|
}
|
|
175
259
|
}
|
|
260
|
+
const patchRhythm = patch.rhythm;
|
|
261
|
+
if (typeof patchRhythm === "object" && patchRhythm !== null) {
|
|
262
|
+
const r = patchRhythm;
|
|
263
|
+
if (r.quiet === null) rhythm.quiet = null;
|
|
264
|
+
else if (typeof r.quiet === "object" && r.quiet !== null) {
|
|
265
|
+
const q = r.quiet;
|
|
266
|
+
const currentQuiet = current.rhythm?.quiet ?? void 0;
|
|
267
|
+
rhythm.quiet = {
|
|
268
|
+
from: typeof q.from === "string" && parseClock(q.from) !== void 0 ? q.from : currentQuiet?.from ?? "01:00",
|
|
269
|
+
to: typeof q.to === "string" && parseClock(q.to) !== void 0 ? q.to : currentQuiet?.to ?? "09:00",
|
|
270
|
+
allowTouchReply: typeof q.allowTouchReply === "boolean" ? q.allowTouchReply : currentQuiet?.allowTouchReply ?? true
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const patchJournal = patch.journal;
|
|
275
|
+
if (typeof patchJournal === "object" && patchJournal !== null) {
|
|
276
|
+
const j = patchJournal;
|
|
277
|
+
if (typeof j.enabled === "boolean") journal.enabled = j.enabled;
|
|
278
|
+
const inject = clampNumber(j.injectRecent, 1, 20);
|
|
279
|
+
if (inject !== void 0) journal.injectRecent = inject;
|
|
280
|
+
const chars = clampNumber(j.maxEntryChars, 40, 1e3);
|
|
281
|
+
if (chars !== void 0) journal.maxEntryChars = chars;
|
|
282
|
+
const bytes = clampNumber(j.maxFileBytes, 4096, 4194304);
|
|
283
|
+
if (bytes !== void 0) journal.maxFileBytes = bytes;
|
|
284
|
+
if (typeof j.allowUnboundAppend === "boolean") journal.allowUnboundAppend = j.allowUnboundAppend;
|
|
285
|
+
}
|
|
286
|
+
const patchPresence = patch.presenceSummary;
|
|
287
|
+
if (typeof patchPresence === "object" && patchPresence !== null) {
|
|
288
|
+
const p = patchPresence;
|
|
289
|
+
if (typeof p.enabled === "boolean") presenceSummary.enabled = p.enabled;
|
|
290
|
+
}
|
|
176
291
|
const patchDebug = patch.debug;
|
|
177
292
|
if (typeof patchDebug === "object" && patchDebug !== null) {
|
|
178
293
|
const d = patchDebug;
|
|
@@ -201,6 +316,9 @@ function mergeSettings(current, patch) {
|
|
|
201
316
|
...current,
|
|
202
317
|
proactivity,
|
|
203
318
|
innerVoice,
|
|
319
|
+
rhythm,
|
|
320
|
+
journal,
|
|
321
|
+
presenceSummary,
|
|
204
322
|
tts,
|
|
205
323
|
debug
|
|
206
324
|
};
|
|
@@ -225,7 +343,7 @@ function openPathInFileManager(path) {
|
|
|
225
343
|
* @param ctx - plugin context inside the runtime.
|
|
226
344
|
* @param home - the dsh home holding the settings/ledger/manifest files.
|
|
227
345
|
*/
|
|
228
|
-
function installSettingsPage(ctx, home, pet) {
|
|
346
|
+
function installSettingsPage(ctx, home, pet, control) {
|
|
229
347
|
const windowPath = globalFilePath(home, "pet-window.json");
|
|
230
348
|
/** Summarize EVERY live pet (id, online flag, normalized /status). */
|
|
231
349
|
const petStatus = async () => {
|
|
@@ -238,6 +356,56 @@ function installSettingsPage(ctx, home, pet) {
|
|
|
238
356
|
const readSettings = async () => {
|
|
239
357
|
return mergeSettings(await readGlobalJson(home, "pet-settings.json"), {});
|
|
240
358
|
};
|
|
359
|
+
/** Live header first, persisted header second. `checked:false` means the
|
|
360
|
+
* host exposed neither source — never turn that into a "missing session".
|
|
361
|
+
* `currentPreset` comes from the preset registry / projection cache, NOT
|
|
362
|
+
* from the immutable creation-time header field. */
|
|
363
|
+
const findSessionHeader = async (sessionId) => {
|
|
364
|
+
const getService = (name) => ctx.get(name);
|
|
365
|
+
const sessions = getService("sessions");
|
|
366
|
+
const agentPresets = getService("agentPresets");
|
|
367
|
+
const live = sessions?.get(sessionId);
|
|
368
|
+
if (live?.header !== void 0) {
|
|
369
|
+
let currentPreset;
|
|
370
|
+
try {
|
|
371
|
+
const preset = agentPresets?.presetForSession(live);
|
|
372
|
+
if (typeof preset === "string" && preset !== "") currentPreset = preset;
|
|
373
|
+
} catch {}
|
|
374
|
+
return {
|
|
375
|
+
header: live.header,
|
|
376
|
+
checked: true,
|
|
377
|
+
currentPreset
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const persistence = getService("sessionPersistence");
|
|
381
|
+
if (persistence === void 0) return {
|
|
382
|
+
header: void 0,
|
|
383
|
+
checked: sessions !== void 0,
|
|
384
|
+
currentPreset: void 0
|
|
385
|
+
};
|
|
386
|
+
try {
|
|
387
|
+
const header = (await persistence.list()).find((snapshot) => snapshot.header?.id === sessionId)?.header;
|
|
388
|
+
let currentPreset;
|
|
389
|
+
if (header !== void 0) {
|
|
390
|
+
const cache = getService("sessionProjectionCache");
|
|
391
|
+
try {
|
|
392
|
+
const preset = cache?.cachedSnapshot(header)?.values?.agentPreset;
|
|
393
|
+
if (typeof preset === "string" && preset !== "") currentPreset = preset;
|
|
394
|
+
} catch {}
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
header,
|
|
398
|
+
checked: true,
|
|
399
|
+
currentPreset
|
|
400
|
+
};
|
|
401
|
+
} catch {
|
|
402
|
+
return {
|
|
403
|
+
header: void 0,
|
|
404
|
+
checked: false,
|
|
405
|
+
currentPreset: void 0
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
};
|
|
241
409
|
const routeByPath = /* @__PURE__ */ new Map();
|
|
242
410
|
const route = (method, path, handler) => {
|
|
243
411
|
let handlers = routeByPath.get(path);
|
|
@@ -269,6 +437,18 @@ function installSettingsPage(ctx, home, pet) {
|
|
|
269
437
|
}
|
|
270
438
|
handlers.set(method, handler);
|
|
271
439
|
};
|
|
440
|
+
route("GET", "/api/pet-settings/binding-status", async (_req, res) => {
|
|
441
|
+
writeJson(res, 200, { status: voiceBindingSnapshot() });
|
|
442
|
+
});
|
|
443
|
+
route("POST", "/api/pet-settings/binding-test", async (req, res) => {
|
|
444
|
+
if (control === void 0) {
|
|
445
|
+
writeJson(res, 503, { error: "driver control unavailable" });
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const body = await readJsonBody(req);
|
|
449
|
+
const petId = typeof body?.petId === "string" && body.petId !== "" ? body.petId : "electron";
|
|
450
|
+
writeJson(res, 200, await control.testBinding(petId));
|
|
451
|
+
});
|
|
272
452
|
route("GET", "/api/pet-settings/read", async (_req, res) => {
|
|
273
453
|
const [settings, windowPosition, status] = await Promise.all([
|
|
274
454
|
readSettings(),
|
|
@@ -314,6 +494,12 @@ function installSettingsPage(ctx, home, pet) {
|
|
|
314
494
|
touchTotals
|
|
315
495
|
});
|
|
316
496
|
});
|
|
497
|
+
route("GET", "/api/pet-settings/electron-runtime", async (_req, res) => {
|
|
498
|
+
writeJson(res, 200, {
|
|
499
|
+
ok: true,
|
|
500
|
+
runtime: readElectronRuntimeStatus(home) ?? null
|
|
501
|
+
});
|
|
502
|
+
});
|
|
317
503
|
route("POST", "/api/pet-settings/update", async (req, res) => {
|
|
318
504
|
const body = await readJsonBody(req);
|
|
319
505
|
const patch = body === void 0 ? void 0 : body.patch;
|
|
@@ -327,11 +513,29 @@ function installSettingsPage(ctx, home, pet) {
|
|
|
327
513
|
delete patch.names;
|
|
328
514
|
}
|
|
329
515
|
const next = mergeSettings(await readSettings(), patch);
|
|
516
|
+
const warnings = [];
|
|
517
|
+
const patchTouchesBinding = patchTouchesElectronBinding(patch);
|
|
518
|
+
const bound = next.innerVoice?.boundSessions?.electron;
|
|
519
|
+
if (typeof bound === "string" && bound !== "") {
|
|
520
|
+
const found = await findSessionHeader(bound);
|
|
521
|
+
if (found.checked && found.header === void 0) warnings.push(`会话 ${bound} 不存在`);
|
|
522
|
+
else {
|
|
523
|
+
if (found.currentPreset !== void 0 && !isPetPreset(found.currentPreset)) {
|
|
524
|
+
if (patchTouchesBinding) {
|
|
525
|
+
writeJson(res, 400, { error: `这个会话的当前模式是「${found.currentPreset}」,没有 pet_say/pet_do。请改用宠物模式(「精简(仅表达)」或「普通(含工作工具)」)的会话,或新建一个宠物模式会话后再绑定。` });
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
warnings.push("当前已保存的绑定会话不是宠物模式;本次保存已生效,但绑定不会有气泡回应。");
|
|
529
|
+
}
|
|
530
|
+
if (found.currentPreset === void 0 && found.header !== void 0) warnings.push("无法确认这个会话的当前模式;如果它不是宠物模式,绑定后不会有气泡回应。");
|
|
531
|
+
}
|
|
532
|
+
}
|
|
330
533
|
await writeGlobalJson(home, "pet-settings.json", next);
|
|
331
534
|
ctx.emit("pet-settings/changed");
|
|
332
535
|
writeJson(res, 200, {
|
|
333
536
|
ok: true,
|
|
334
|
-
settings: next
|
|
537
|
+
settings: next,
|
|
538
|
+
warnings
|
|
335
539
|
});
|
|
336
540
|
});
|
|
337
541
|
route("POST", "/api/pet-settings/reset-affection", async (_req, res) => {
|
|
@@ -721,7 +925,6 @@ function apply(ctx) {
|
|
|
721
925
|
return;
|
|
722
926
|
}
|
|
723
927
|
};
|
|
724
|
-
const INSTALL_LOCK_STALE_MS = 18e5;
|
|
725
928
|
const installLockPath = (pkgDir) => join(pkgDir, ".dsh-pet-install.lock");
|
|
726
929
|
const acquireInstallLock = (pkgDir) => {
|
|
727
930
|
const lock = installLockPath(pkgDir);
|
|
@@ -734,63 +937,162 @@ function apply(ctx) {
|
|
|
734
937
|
try {
|
|
735
938
|
write();
|
|
736
939
|
return true;
|
|
940
|
+
} catch {}
|
|
941
|
+
let raw;
|
|
942
|
+
let mtimeMs = 0;
|
|
943
|
+
try {
|
|
944
|
+
raw = readFileSync(lock, "utf8");
|
|
945
|
+
mtimeMs = statSync(lock).mtimeMs;
|
|
737
946
|
} catch {
|
|
738
947
|
try {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
948
|
+
write();
|
|
949
|
+
return true;
|
|
950
|
+
} catch {
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
const owner = raw === void 0 ? void 0 : parseInstallLock(raw)?.pid;
|
|
955
|
+
const ownerPid = typeof owner === "number" ? owner : void 0;
|
|
956
|
+
if (!installLockStale(raw, mtimeMs, Date.now())) {
|
|
957
|
+
logCarrier(ownerPid === void 0 ? `electron install lock unreadable but fresh; waiting: ${lock}` : `electron install lock held by pid ${String(ownerPid)} (alive); waiting: ${lock}`);
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
const dead = ownerPid !== void 0 && !isProcessAlive(ownerPid);
|
|
961
|
+
logCarrier(`electron install lock stale (${ownerPid === void 0 ? "unreadable" : `pid ${String(ownerPid)} ${dead ? "dead" : "expired"}`}); taking over: ${lock}`);
|
|
962
|
+
try {
|
|
963
|
+
unlinkSync(lock);
|
|
964
|
+
} catch {}
|
|
965
|
+
try {
|
|
966
|
+
write();
|
|
967
|
+
return true;
|
|
968
|
+
} catch {
|
|
745
969
|
return false;
|
|
746
970
|
}
|
|
747
971
|
};
|
|
748
972
|
const releaseInstallLock = (pkgDir) => {
|
|
973
|
+
const lock = installLockPath(pkgDir);
|
|
974
|
+
try {
|
|
975
|
+
const owner = parseInstallLock(readFileSync(lock, "utf8"))?.pid;
|
|
976
|
+
if (typeof owner === "number" && owner !== process.pid) return;
|
|
977
|
+
} catch {}
|
|
749
978
|
try {
|
|
750
|
-
unlinkSync(
|
|
979
|
+
unlinkSync(lock);
|
|
751
980
|
} catch {}
|
|
752
981
|
};
|
|
982
|
+
const NPMMIRROR_ELECTRON = "https://npmmirror.com/mirrors/electron/";
|
|
983
|
+
const INSTALL_TIMEOUT_MS = 12e5;
|
|
984
|
+
let activeInstallChild;
|
|
985
|
+
const writeRuntimeStatus = (status) => {
|
|
986
|
+
writeElectronRuntimeStatus(petHome(), status);
|
|
987
|
+
};
|
|
753
988
|
/**
|
|
754
989
|
* Install the npm electron binary off the activation path. The package's
|
|
755
|
-
* lazy installer is a
|
|
990
|
+
* lazy installer is a large download; running it synchronously (which
|
|
756
991
|
* `require('electron')` does) freezes the host, so it runs as a child with
|
|
757
|
-
* progress in the spawn log
|
|
992
|
+
* progress in the spawn log. The lock is owner-liveness based: a host that
|
|
993
|
+
* dies mid-download never blocks the next activation.
|
|
758
994
|
*/
|
|
759
|
-
const installNpmElectron = async (pkgDir) => {
|
|
995
|
+
const installNpmElectron = async (pkgDir, attempt) => {
|
|
760
996
|
const installer = join(pkgDir, "install.js");
|
|
761
997
|
if (!existsSync(installer)) {
|
|
762
998
|
logCarrier(`electron binary missing and install.js not found: ${pkgDir}`);
|
|
763
|
-
|
|
999
|
+
writeRuntimeStatus({
|
|
1000
|
+
state: "unavailable",
|
|
1001
|
+
updatedAt: Date.now(),
|
|
1002
|
+
error: `install.js not found in ${pkgDir}`
|
|
1003
|
+
});
|
|
1004
|
+
return {
|
|
1005
|
+
kind: "unavailable",
|
|
1006
|
+
error: `install.js not found in ${pkgDir}`
|
|
1007
|
+
};
|
|
764
1008
|
}
|
|
765
1009
|
if (!acquireInstallLock(pkgDir)) {
|
|
766
|
-
|
|
767
|
-
|
|
1010
|
+
writeRuntimeStatus({
|
|
1011
|
+
state: "busy",
|
|
1012
|
+
updatedAt: Date.now()
|
|
1013
|
+
});
|
|
1014
|
+
return { kind: "busy" };
|
|
768
1015
|
}
|
|
769
|
-
|
|
1016
|
+
const explicitMirror = (process.env.ELECTRON_MIRROR ?? process.env.npm_config_electron_mirror ?? "").trim();
|
|
1017
|
+
const mirror = explicitMirror !== "" ? explicitMirror : attempt % 2 === 0 ? NPMMIRROR_ELECTRON : void 0;
|
|
1018
|
+
const source = explicitMirror !== "" ? "explicit" : mirror === void 0 ? "github" : "npmmirror";
|
|
1019
|
+
const startedAt = Date.now();
|
|
1020
|
+
writeRuntimeStatus({
|
|
1021
|
+
state: "downloading",
|
|
1022
|
+
updatedAt: startedAt,
|
|
1023
|
+
startedAt,
|
|
1024
|
+
attempt: attempt + 1,
|
|
1025
|
+
source
|
|
1026
|
+
});
|
|
1027
|
+
logCarrier(`electron binary missing; installing in background (attempt ${String(attempt + 1)}, source=${source}): ${pkgDir}`);
|
|
770
1028
|
return await new Promise((resolveInstall) => {
|
|
771
1029
|
let settled = false;
|
|
772
|
-
|
|
1030
|
+
let timeout;
|
|
1031
|
+
const finish = (result) => {
|
|
773
1032
|
if (settled) return;
|
|
774
1033
|
settled = true;
|
|
1034
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
1035
|
+
activeInstallChild = void 0;
|
|
775
1036
|
releaseInstallLock(pkgDir);
|
|
776
|
-
|
|
1037
|
+
if (result.kind === "installed") writeRuntimeStatus({
|
|
1038
|
+
state: "ready",
|
|
1039
|
+
updatedAt: Date.now(),
|
|
1040
|
+
startedAt,
|
|
1041
|
+
finishedAt: Date.now(),
|
|
1042
|
+
attempt: attempt + 1,
|
|
1043
|
+
source
|
|
1044
|
+
});
|
|
1045
|
+
else if (result.kind === "failed") writeRuntimeStatus({
|
|
1046
|
+
state: "failed",
|
|
1047
|
+
updatedAt: Date.now(),
|
|
1048
|
+
startedAt,
|
|
1049
|
+
attempt: attempt + 1,
|
|
1050
|
+
source,
|
|
1051
|
+
error: result.error
|
|
1052
|
+
});
|
|
1053
|
+
resolveInstall(result);
|
|
777
1054
|
};
|
|
778
1055
|
let stderr = "";
|
|
779
1056
|
let stdout = "";
|
|
780
1057
|
let lastProgress = 0;
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
],
|
|
792
|
-
|
|
793
|
-
|
|
1058
|
+
const env = {
|
|
1059
|
+
...process.env,
|
|
1060
|
+
ELECTRON_RUN_AS_NODE: "1"
|
|
1061
|
+
};
|
|
1062
|
+
if (mirror === void 0) {
|
|
1063
|
+
delete env.ELECTRON_MIRROR;
|
|
1064
|
+
delete env.npm_config_electron_mirror;
|
|
1065
|
+
} else env.ELECTRON_MIRROR = mirror;
|
|
1066
|
+
let child;
|
|
1067
|
+
try {
|
|
1068
|
+
child = spawn(process.execPath, [installer], {
|
|
1069
|
+
cwd: pkgDir,
|
|
1070
|
+
env,
|
|
1071
|
+
stdio: [
|
|
1072
|
+
"ignore",
|
|
1073
|
+
"pipe",
|
|
1074
|
+
"pipe"
|
|
1075
|
+
],
|
|
1076
|
+
windowsHide: true
|
|
1077
|
+
});
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1080
|
+
logCarrier(`electron install failed to start: ${message}`);
|
|
1081
|
+
finish({
|
|
1082
|
+
kind: "failed",
|
|
1083
|
+
error: message
|
|
1084
|
+
});
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
activeInstallChild = child;
|
|
1088
|
+
timeout = setTimeout(() => {
|
|
1089
|
+
logCarrier(`electron install timed out after ${String(INSTALL_TIMEOUT_MS / 6e4)} min (source=${source}); killing pid=${String(child.pid ?? "unknown")}`);
|
|
1090
|
+
child.kill();
|
|
1091
|
+
finish({
|
|
1092
|
+
kind: "failed",
|
|
1093
|
+
error: `timed out after ${String(INSTALL_TIMEOUT_MS / 6e4)} min`
|
|
1094
|
+
});
|
|
1095
|
+
}, INSTALL_TIMEOUT_MS);
|
|
794
1096
|
child.stdout?.setEncoding("utf8");
|
|
795
1097
|
child.stdout?.on("data", (chunk) => {
|
|
796
1098
|
stdout = `${stdout}${chunk}`.slice(-2e3);
|
|
@@ -807,22 +1109,35 @@ function apply(ctx) {
|
|
|
807
1109
|
});
|
|
808
1110
|
child.on("error", (error) => {
|
|
809
1111
|
logCarrier(`electron install failed to start: ${error.message}`);
|
|
810
|
-
finish(
|
|
1112
|
+
finish({
|
|
1113
|
+
kind: "failed",
|
|
1114
|
+
error: error.message
|
|
1115
|
+
});
|
|
811
1116
|
});
|
|
812
1117
|
child.on("exit", (code) => {
|
|
813
1118
|
if (code !== 0) {
|
|
814
|
-
|
|
815
|
-
|
|
1119
|
+
const detail = stderr.trim() || "no stderr";
|
|
1120
|
+
logCarrier(`electron install failed (exit ${String(code)}, source=${source}): ${detail}`);
|
|
1121
|
+
finish({
|
|
1122
|
+
kind: "failed",
|
|
1123
|
+
error: `exit ${String(code)}: ${detail}`
|
|
1124
|
+
});
|
|
816
1125
|
return;
|
|
817
1126
|
}
|
|
818
1127
|
const binary = npmElectronBinary(pkgDir);
|
|
819
1128
|
if (binary === void 0) {
|
|
820
1129
|
logCarrier(`electron install exited 0 but the binary is still missing: ${pkgDir}`);
|
|
821
|
-
finish(
|
|
1130
|
+
finish({
|
|
1131
|
+
kind: "failed",
|
|
1132
|
+
error: "binary still missing after a successful install"
|
|
1133
|
+
});
|
|
822
1134
|
return;
|
|
823
1135
|
}
|
|
824
1136
|
logCarrier(`electron installed: ${binary}`);
|
|
825
|
-
finish(
|
|
1137
|
+
finish({
|
|
1138
|
+
kind: "installed",
|
|
1139
|
+
binary: ensureElectronUsable(binary)
|
|
1140
|
+
});
|
|
826
1141
|
});
|
|
827
1142
|
});
|
|
828
1143
|
};
|
|
@@ -899,20 +1214,86 @@ function apply(ctx) {
|
|
|
899
1214
|
child.kill();
|
|
900
1215
|
}, "pet-surface: carrier shutdown");
|
|
901
1216
|
};
|
|
1217
|
+
const RETRY_DELAYS_MS = [
|
|
1218
|
+
3e4,
|
|
1219
|
+
6e4,
|
|
1220
|
+
12e4,
|
|
1221
|
+
3e5
|
|
1222
|
+
];
|
|
1223
|
+
let disposed = false;
|
|
1224
|
+
let carrierLaunched = false;
|
|
1225
|
+
let installInFlight = false;
|
|
1226
|
+
let installAttempt = 0;
|
|
1227
|
+
let retryTimer;
|
|
1228
|
+
const launchOnce = (url, carrierDir, binary) => {
|
|
1229
|
+
if (disposed || carrierLaunched) return;
|
|
1230
|
+
carrierLaunched = true;
|
|
1231
|
+
if (retryTimer !== void 0) {
|
|
1232
|
+
clearTimeout(retryTimer);
|
|
1233
|
+
retryTimer = void 0;
|
|
1234
|
+
}
|
|
1235
|
+
launchCarrier(url, carrierDir, binary);
|
|
1236
|
+
};
|
|
1237
|
+
const ensureCarrier = async (url, carrierDir, pkgDir) => {
|
|
1238
|
+
if (disposed || carrierLaunched) return;
|
|
1239
|
+
const resolution = resolveElectron();
|
|
1240
|
+
if (resolution.binary !== void 0) {
|
|
1241
|
+
writeRuntimeStatus({
|
|
1242
|
+
state: "ready",
|
|
1243
|
+
updatedAt: Date.now()
|
|
1244
|
+
});
|
|
1245
|
+
launchOnce(url, carrierDir, resolution.binary);
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
const nextPkgDir = resolution.npmPackageDir ?? pkgDir;
|
|
1249
|
+
if (installInFlight) return;
|
|
1250
|
+
installInFlight = true;
|
|
1251
|
+
const attempt = installAttempt;
|
|
1252
|
+
installAttempt += 1;
|
|
1253
|
+
const result = await installNpmElectron(nextPkgDir, attempt).catch((error) => ({
|
|
1254
|
+
kind: "failed",
|
|
1255
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1256
|
+
}));
|
|
1257
|
+
installInFlight = false;
|
|
1258
|
+
if (disposed || carrierLaunched) return;
|
|
1259
|
+
if (result.kind === "installed") {
|
|
1260
|
+
launchOnce(url, carrierDir, result.binary);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
if (result.kind === "unavailable") {
|
|
1264
|
+
logCarrier(`electron auto-install unavailable: ${result.error}`);
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
const reason = result.kind === "busy" ? "blocked by another activation" : `failed (${result.error})`;
|
|
1268
|
+
const delay = RETRY_DELAYS_MS[Math.min(installAttempt - 1, RETRY_DELAYS_MS.length - 1)];
|
|
1269
|
+
logCarrier(`electron install ${reason}; retrying in ${String(delay / 1e3)}s`);
|
|
1270
|
+
if (retryTimer !== void 0) clearTimeout(retryTimer);
|
|
1271
|
+
retryTimer = setTimeout(() => {
|
|
1272
|
+
ensureCarrier(url, carrierDir, nextPkgDir);
|
|
1273
|
+
}, delay);
|
|
1274
|
+
};
|
|
902
1275
|
const spawnPetCarrier = (url) => {
|
|
903
1276
|
const carrierDir = resolve(join(dirname(fileURLToPath(import.meta.url)), "..", "carrier"));
|
|
904
1277
|
if (process.env.DSH_PET_NO_SPAWN === "1" || !existsSync(join(carrierDir, "package.json"))) return;
|
|
905
1278
|
const resolution = resolveElectron();
|
|
906
1279
|
if (resolution.binary !== void 0) {
|
|
907
|
-
|
|
1280
|
+
writeRuntimeStatus({
|
|
1281
|
+
state: "ready",
|
|
1282
|
+
updatedAt: Date.now()
|
|
1283
|
+
});
|
|
1284
|
+
launchOnce(url, carrierDir, resolution.binary);
|
|
908
1285
|
return;
|
|
909
1286
|
}
|
|
910
|
-
if (resolution.npmPackageDir === void 0)
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1287
|
+
if (resolution.npmPackageDir === void 0) {
|
|
1288
|
+
logCarrier("no electron binary and no npm electron package; cannot auto-install");
|
|
1289
|
+
writeRuntimeStatus({
|
|
1290
|
+
state: "unavailable",
|
|
1291
|
+
updatedAt: Date.now(),
|
|
1292
|
+
error: "electron package not installed"
|
|
1293
|
+
});
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
ensureCarrier(url, carrierDir, resolution.npmPackageDir);
|
|
916
1297
|
};
|
|
917
1298
|
const settledReady = () => {
|
|
918
1299
|
const url = authenticatedRoot();
|
|
@@ -925,8 +1306,12 @@ function apply(ctx) {
|
|
|
925
1306
|
else settled.then(settledReady).catch(() => {});
|
|
926
1307
|
const home = petHome();
|
|
927
1308
|
const pet = installPetService(ctx, home);
|
|
928
|
-
installPetDriver(ctx, home, pet);
|
|
929
|
-
|
|
1309
|
+
installSettingsPage(ctx, home, pet, installPetDriver(ctx, home, pet));
|
|
1310
|
+
ctx.effect(() => () => {
|
|
1311
|
+
disposed = true;
|
|
1312
|
+
if (retryTimer !== void 0) clearTimeout(retryTimer);
|
|
1313
|
+
activeInstallChild?.kill();
|
|
1314
|
+
}, "pet-surface: electron install cleanup");
|
|
930
1315
|
ctx.effect(() => () => {
|
|
931
1316
|
readFile(petLocatorPath(), "utf8").then((raw) => {
|
|
932
1317
|
if (JSON.parse(raw).pid === process.pid) return unlink(petLocatorPath());
|
package/lib/tools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as createUnregisteredPetHttp, n as logStamp, r as registerPetTools, s as petHome } from "./driver-CpRgkle-.js";
|
|
2
2
|
import { appendFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
// lib/types/tools.js
|
package/lib/types/actions.d.ts
CHANGED
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
* catalog drift between pet implementations must die.
|
|
7
7
|
* @module dsh-agentic-pet/actions
|
|
8
8
|
*/
|
|
9
|
+
/** One body-owned catalog entry. `tags` are THIS body's vocabulary — the
|
|
10
|
+
* driver never defines a cross-body enum. */
|
|
11
|
+
export interface ActionCatalogEntry {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly tags: readonly string[];
|
|
14
|
+
readonly energy?: 'low' | 'mid' | 'high';
|
|
15
|
+
readonly durationHintMs?: number;
|
|
16
|
+
readonly textInFrame?: boolean;
|
|
17
|
+
}
|
|
9
18
|
/** A pet's advertised capabilities. `contractVersion: null` = a legacy
|
|
10
19
|
* implementation that predates the version field (v1 semantics: seven
|
|
11
20
|
* endpoints, no /world). `actions: null` = the pet publishes no catalog (a
|
|
@@ -14,6 +23,10 @@
|
|
|
14
23
|
export interface PetCapabilities {
|
|
15
24
|
readonly contractVersion: number | null;
|
|
16
25
|
readonly actions: readonly string[] | null;
|
|
26
|
+
readonly actionCatalog: readonly ActionCatalogEntry[] | null;
|
|
27
|
+
/** The body's own tag vocabulary, derived from its catalog when omitted. */
|
|
28
|
+
readonly actionTagVocabulary: readonly string[];
|
|
29
|
+
readonly tagged: boolean;
|
|
17
30
|
}
|
|
18
31
|
/** Defensive extraction from a raw /capabilities payload: implementations
|
|
19
32
|
* may omit or mistype anything. */
|
|
@@ -39,3 +52,18 @@ export declare function resolveActionRequest(requested: string, catalog: readonl
|
|
|
39
52
|
/** The error-message menu: long catalogs are previewed, with a pointer to
|
|
40
53
|
* pet_state {actions:true} for the full list. */
|
|
41
54
|
export declare function formatAvailableActions(available: readonly string[], cap?: number): string;
|
|
55
|
+
export type TagResolution = {
|
|
56
|
+
kind: 'ok';
|
|
57
|
+
action: string;
|
|
58
|
+
candidates: readonly string[];
|
|
59
|
+
} | {
|
|
60
|
+
kind: 'unknown';
|
|
61
|
+
availableTags: readonly string[];
|
|
62
|
+
availableActions: readonly string[];
|
|
63
|
+
};
|
|
64
|
+
/** Resolve a tag against the CURRENT body's own catalog. Multiple matches are
|
|
65
|
+
* a random draw (celebrate is never a tag candidate). No cross-body tag
|
|
66
|
+
* vocabulary is consulted. */
|
|
67
|
+
export declare function resolveTagRequest(tag: string, capabilities: PetCapabilities | null): TagResolution;
|
|
68
|
+
/** Tag menu for errors: an empty vocabulary names the fallback explicitly. */
|
|
69
|
+
export declare function formatAvailableTags(tags: readonly string[], cap?: number): string;
|