dsh-agentic-pet 0.4.8 → 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 +327 -14
- package/lib/{service-Df8olC1E.js → driver-CpRgkle-.js} +3391 -1775
- package/lib/index.js +467 -48
- package/lib/tools.js +3 -3
- package/lib/types/actions.d.ts +28 -0
- package/lib/types/binding.d.ts +99 -0
- package/lib/types/driver.d.ts +42 -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 +91 -1
- package/lib/types/journal.d.ts +58 -0
- package/lib/types/presence.d.ts +27 -0
- package/lib/types/registry.d.ts +1 -1
- 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/lib/types/tools.d.ts +2 -2
- package/package.json +3 -3
- package/presets/pet-inner-voice.patch.yml +3 -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
|
|
@@ -37,8 +100,24 @@ const DEFAULT_SETTINGS = {
|
|
|
37
100
|
touchReplyProbability: .3,
|
|
38
101
|
touchReplyCooldownSeconds: 30,
|
|
39
102
|
selfWakeBusyPolicy: "fire",
|
|
103
|
+
petToPetDelivery: "queue",
|
|
104
|
+
petMessageLimits: {
|
|
105
|
+
minutes5: 5,
|
|
106
|
+
minutes10: 8,
|
|
107
|
+
minutes30: 10,
|
|
108
|
+
minutes60: 15
|
|
109
|
+
},
|
|
40
110
|
boundSessions: {}
|
|
41
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 },
|
|
42
121
|
tts: { enabled: false },
|
|
43
122
|
debug: { cdp: false }
|
|
44
123
|
};
|
|
@@ -104,6 +183,18 @@ function mergeSettings(current, patch) {
|
|
|
104
183
|
...DEFAULT_SETTINGS.innerVoice,
|
|
105
184
|
...current.innerVoice
|
|
106
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
|
+
};
|
|
107
198
|
const debug = {
|
|
108
199
|
...DEFAULT_SETTINGS.debug,
|
|
109
200
|
...current.debug
|
|
@@ -132,6 +223,33 @@ function mergeSettings(current, patch) {
|
|
|
132
223
|
const replyCooldown = clampNumber(v.touchReplyCooldownSeconds, 0, 600);
|
|
133
224
|
if (replyCooldown !== void 0) innerVoice.touchReplyCooldownSeconds = replyCooldown;
|
|
134
225
|
if (v.selfWakeBusyPolicy === "defer" || v.selfWakeBusyPolicy === "fire") innerVoice.selfWakeBusyPolicy = v.selfWakeBusyPolicy;
|
|
226
|
+
if (v.petToPetDelivery === "immediate" || v.petToPetDelivery === "queue") innerVoice.petToPetDelivery = v.petToPetDelivery;
|
|
227
|
+
const patchLimits = v.petMessageLimits;
|
|
228
|
+
if (typeof patchLimits === "object" && patchLimits !== null) {
|
|
229
|
+
const merged = {
|
|
230
|
+
minutes5: 5,
|
|
231
|
+
minutes10: 8,
|
|
232
|
+
minutes30: 10,
|
|
233
|
+
minutes60: 15,
|
|
234
|
+
...innerVoice.petMessageLimits
|
|
235
|
+
};
|
|
236
|
+
const base = {
|
|
237
|
+
minutes5: merged.minutes5 ?? 5,
|
|
238
|
+
minutes10: merged.minutes10 ?? 8,
|
|
239
|
+
minutes30: merged.minutes30 ?? 10,
|
|
240
|
+
minutes60: merged.minutes60 ?? 15
|
|
241
|
+
};
|
|
242
|
+
const clampLimit = (value, fallback) => {
|
|
243
|
+
const parsed = clampNumber(value, 0, 999);
|
|
244
|
+
return parsed === void 0 ? fallback : parsed;
|
|
245
|
+
};
|
|
246
|
+
innerVoice.petMessageLimits = {
|
|
247
|
+
minutes5: clampLimit(patchLimits.minutes5, base.minutes5),
|
|
248
|
+
minutes10: clampLimit(patchLimits.minutes10, base.minutes10),
|
|
249
|
+
minutes30: clampLimit(patchLimits.minutes30, base.minutes30),
|
|
250
|
+
minutes60: clampLimit(patchLimits.minutes60, base.minutes60)
|
|
251
|
+
};
|
|
252
|
+
}
|
|
135
253
|
const bound = v.boundSessions;
|
|
136
254
|
if (typeof bound === "object" && bound !== null) {
|
|
137
255
|
const record = {};
|
|
@@ -139,6 +257,37 @@ function mergeSettings(current, patch) {
|
|
|
139
257
|
innerVoice.boundSessions = record;
|
|
140
258
|
}
|
|
141
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
|
+
}
|
|
142
291
|
const patchDebug = patch.debug;
|
|
143
292
|
if (typeof patchDebug === "object" && patchDebug !== null) {
|
|
144
293
|
const d = patchDebug;
|
|
@@ -167,6 +316,9 @@ function mergeSettings(current, patch) {
|
|
|
167
316
|
...current,
|
|
168
317
|
proactivity,
|
|
169
318
|
innerVoice,
|
|
319
|
+
rhythm,
|
|
320
|
+
journal,
|
|
321
|
+
presenceSummary,
|
|
170
322
|
tts,
|
|
171
323
|
debug
|
|
172
324
|
};
|
|
@@ -191,7 +343,7 @@ function openPathInFileManager(path) {
|
|
|
191
343
|
* @param ctx - plugin context inside the runtime.
|
|
192
344
|
* @param home - the dsh home holding the settings/ledger/manifest files.
|
|
193
345
|
*/
|
|
194
|
-
function installSettingsPage(ctx, home, pet) {
|
|
346
|
+
function installSettingsPage(ctx, home, pet, control) {
|
|
195
347
|
const windowPath = globalFilePath(home, "pet-window.json");
|
|
196
348
|
/** Summarize EVERY live pet (id, online flag, normalized /status). */
|
|
197
349
|
const petStatus = async () => {
|
|
@@ -204,6 +356,56 @@ function installSettingsPage(ctx, home, pet) {
|
|
|
204
356
|
const readSettings = async () => {
|
|
205
357
|
return mergeSettings(await readGlobalJson(home, "pet-settings.json"), {});
|
|
206
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
|
+
};
|
|
207
409
|
const routeByPath = /* @__PURE__ */ new Map();
|
|
208
410
|
const route = (method, path, handler) => {
|
|
209
411
|
let handlers = routeByPath.get(path);
|
|
@@ -235,6 +437,18 @@ function installSettingsPage(ctx, home, pet) {
|
|
|
235
437
|
}
|
|
236
438
|
handlers.set(method, handler);
|
|
237
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
|
+
});
|
|
238
452
|
route("GET", "/api/pet-settings/read", async (_req, res) => {
|
|
239
453
|
const [settings, windowPosition, status] = await Promise.all([
|
|
240
454
|
readSettings(),
|
|
@@ -280,6 +494,12 @@ function installSettingsPage(ctx, home, pet) {
|
|
|
280
494
|
touchTotals
|
|
281
495
|
});
|
|
282
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
|
+
});
|
|
283
503
|
route("POST", "/api/pet-settings/update", async (req, res) => {
|
|
284
504
|
const body = await readJsonBody(req);
|
|
285
505
|
const patch = body === void 0 ? void 0 : body.patch;
|
|
@@ -293,11 +513,29 @@ function installSettingsPage(ctx, home, pet) {
|
|
|
293
513
|
delete patch.names;
|
|
294
514
|
}
|
|
295
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
|
+
}
|
|
296
533
|
await writeGlobalJson(home, "pet-settings.json", next);
|
|
297
534
|
ctx.emit("pet-settings/changed");
|
|
298
535
|
writeJson(res, 200, {
|
|
299
536
|
ok: true,
|
|
300
|
-
settings: next
|
|
537
|
+
settings: next,
|
|
538
|
+
warnings
|
|
301
539
|
});
|
|
302
540
|
});
|
|
303
541
|
route("POST", "/api/pet-settings/reset-affection", async (_req, res) => {
|
|
@@ -687,7 +925,6 @@ function apply(ctx) {
|
|
|
687
925
|
return;
|
|
688
926
|
}
|
|
689
927
|
};
|
|
690
|
-
const INSTALL_LOCK_STALE_MS = 18e5;
|
|
691
928
|
const installLockPath = (pkgDir) => join(pkgDir, ".dsh-pet-install.lock");
|
|
692
929
|
const acquireInstallLock = (pkgDir) => {
|
|
693
930
|
const lock = installLockPath(pkgDir);
|
|
@@ -700,63 +937,162 @@ function apply(ctx) {
|
|
|
700
937
|
try {
|
|
701
938
|
write();
|
|
702
939
|
return true;
|
|
940
|
+
} catch {}
|
|
941
|
+
let raw;
|
|
942
|
+
let mtimeMs = 0;
|
|
943
|
+
try {
|
|
944
|
+
raw = readFileSync(lock, "utf8");
|
|
945
|
+
mtimeMs = statSync(lock).mtimeMs;
|
|
703
946
|
} catch {
|
|
704
947
|
try {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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 {
|
|
711
969
|
return false;
|
|
712
970
|
}
|
|
713
971
|
};
|
|
714
972
|
const releaseInstallLock = (pkgDir) => {
|
|
973
|
+
const lock = installLockPath(pkgDir);
|
|
715
974
|
try {
|
|
716
|
-
|
|
975
|
+
const owner = parseInstallLock(readFileSync(lock, "utf8"))?.pid;
|
|
976
|
+
if (typeof owner === "number" && owner !== process.pid) return;
|
|
977
|
+
} catch {}
|
|
978
|
+
try {
|
|
979
|
+
unlinkSync(lock);
|
|
717
980
|
} catch {}
|
|
718
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
|
+
};
|
|
719
988
|
/**
|
|
720
989
|
* Install the npm electron binary off the activation path. The package's
|
|
721
|
-
* lazy installer is a
|
|
990
|
+
* lazy installer is a large download; running it synchronously (which
|
|
722
991
|
* `require('electron')` does) freezes the host, so it runs as a child with
|
|
723
|
-
* 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.
|
|
724
994
|
*/
|
|
725
|
-
const installNpmElectron = async (pkgDir) => {
|
|
995
|
+
const installNpmElectron = async (pkgDir, attempt) => {
|
|
726
996
|
const installer = join(pkgDir, "install.js");
|
|
727
997
|
if (!existsSync(installer)) {
|
|
728
998
|
logCarrier(`electron binary missing and install.js not found: ${pkgDir}`);
|
|
729
|
-
|
|
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
|
+
};
|
|
730
1008
|
}
|
|
731
1009
|
if (!acquireInstallLock(pkgDir)) {
|
|
732
|
-
|
|
733
|
-
|
|
1010
|
+
writeRuntimeStatus({
|
|
1011
|
+
state: "busy",
|
|
1012
|
+
updatedAt: Date.now()
|
|
1013
|
+
});
|
|
1014
|
+
return { kind: "busy" };
|
|
734
1015
|
}
|
|
735
|
-
|
|
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}`);
|
|
736
1028
|
return await new Promise((resolveInstall) => {
|
|
737
1029
|
let settled = false;
|
|
738
|
-
|
|
1030
|
+
let timeout;
|
|
1031
|
+
const finish = (result) => {
|
|
739
1032
|
if (settled) return;
|
|
740
1033
|
settled = true;
|
|
1034
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
1035
|
+
activeInstallChild = void 0;
|
|
741
1036
|
releaseInstallLock(pkgDir);
|
|
742
|
-
|
|
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);
|
|
743
1054
|
};
|
|
744
1055
|
let stderr = "";
|
|
745
1056
|
let stdout = "";
|
|
746
1057
|
let lastProgress = 0;
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
],
|
|
758
|
-
|
|
759
|
-
|
|
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);
|
|
760
1096
|
child.stdout?.setEncoding("utf8");
|
|
761
1097
|
child.stdout?.on("data", (chunk) => {
|
|
762
1098
|
stdout = `${stdout}${chunk}`.slice(-2e3);
|
|
@@ -773,22 +1109,35 @@ function apply(ctx) {
|
|
|
773
1109
|
});
|
|
774
1110
|
child.on("error", (error) => {
|
|
775
1111
|
logCarrier(`electron install failed to start: ${error.message}`);
|
|
776
|
-
finish(
|
|
1112
|
+
finish({
|
|
1113
|
+
kind: "failed",
|
|
1114
|
+
error: error.message
|
|
1115
|
+
});
|
|
777
1116
|
});
|
|
778
1117
|
child.on("exit", (code) => {
|
|
779
1118
|
if (code !== 0) {
|
|
780
|
-
|
|
781
|
-
|
|
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
|
+
});
|
|
782
1125
|
return;
|
|
783
1126
|
}
|
|
784
1127
|
const binary = npmElectronBinary(pkgDir);
|
|
785
1128
|
if (binary === void 0) {
|
|
786
1129
|
logCarrier(`electron install exited 0 but the binary is still missing: ${pkgDir}`);
|
|
787
|
-
finish(
|
|
1130
|
+
finish({
|
|
1131
|
+
kind: "failed",
|
|
1132
|
+
error: "binary still missing after a successful install"
|
|
1133
|
+
});
|
|
788
1134
|
return;
|
|
789
1135
|
}
|
|
790
1136
|
logCarrier(`electron installed: ${binary}`);
|
|
791
|
-
finish(
|
|
1137
|
+
finish({
|
|
1138
|
+
kind: "installed",
|
|
1139
|
+
binary: ensureElectronUsable(binary)
|
|
1140
|
+
});
|
|
792
1141
|
});
|
|
793
1142
|
});
|
|
794
1143
|
};
|
|
@@ -865,20 +1214,86 @@ function apply(ctx) {
|
|
|
865
1214
|
child.kill();
|
|
866
1215
|
}, "pet-surface: carrier shutdown");
|
|
867
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
|
+
};
|
|
868
1275
|
const spawnPetCarrier = (url) => {
|
|
869
1276
|
const carrierDir = resolve(join(dirname(fileURLToPath(import.meta.url)), "..", "carrier"));
|
|
870
1277
|
if (process.env.DSH_PET_NO_SPAWN === "1" || !existsSync(join(carrierDir, "package.json"))) return;
|
|
871
1278
|
const resolution = resolveElectron();
|
|
872
1279
|
if (resolution.binary !== void 0) {
|
|
873
|
-
|
|
1280
|
+
writeRuntimeStatus({
|
|
1281
|
+
state: "ready",
|
|
1282
|
+
updatedAt: Date.now()
|
|
1283
|
+
});
|
|
1284
|
+
launchOnce(url, carrierDir, resolution.binary);
|
|
874
1285
|
return;
|
|
875
1286
|
}
|
|
876
|
-
if (resolution.npmPackageDir === void 0)
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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);
|
|
882
1297
|
};
|
|
883
1298
|
const settledReady = () => {
|
|
884
1299
|
const url = authenticatedRoot();
|
|
@@ -891,8 +1306,12 @@ function apply(ctx) {
|
|
|
891
1306
|
else settled.then(settledReady).catch(() => {});
|
|
892
1307
|
const home = petHome();
|
|
893
1308
|
const pet = installPetService(ctx, home);
|
|
894
|
-
installPetDriver(ctx, home, pet);
|
|
895
|
-
|
|
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");
|
|
896
1315
|
ctx.effect(() => () => {
|
|
897
1316
|
readFile(petLocatorPath(), "utf8").then((raw) => {
|
|
898
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
|
|
@@ -21,8 +21,8 @@ const trace = (line) => {
|
|
|
21
21
|
appendFile(join(petHome(), "whale598", "pet-driver.log"), `${logStamp()} pet-tools ${line}\n`, "utf8").catch(() => {});
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
24
|
-
* Register the pet tools (say/do/digest/interactions/affection/state/
|
|
25
|
-
* cancel_wake) for sessions mounted with the pet preset.
|
|
24
|
+
* Register the pet tools (say/inbox/do/digest/interactions/affection/state/
|
|
25
|
+
* schedule_wake/cancel_wake) for sessions mounted with the pet preset.
|
|
26
26
|
*
|
|
27
27
|
* The service instance is assembled HERE, not read from the main entry (a
|
|
28
28
|
* Cordis service registered on the main row's fiber context is invisible to
|