dsh-agentic-pet 0.4.6 → 0.4.8
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/README.zh.md +7 -1
- package/lib/index.js +166 -28
- package/lib/{service-R8qwDx1Q.js → service-Df8olC1E.js} +128 -134
- package/lib/tools.js +1 -1
- package/lib/types/registry.d.ts +29 -20
- package/lib/types/service.d.ts +15 -13
- package/package.json +1 -1
package/README.zh.md
CHANGED
|
@@ -9,10 +9,16 @@ kind: "package-bundle"
|
|
|
9
9
|
|
|
10
10
|
## 简介
|
|
11
11
|
|
|
12
|
-
`dsh-agentic-pet` 是 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 生态的桌面宠物插件:宠物以透明置顶窗常驻桌面,跟随 agent 会话状态(idle / thinking / working / error),与 web 界面竞速应答审批与提问(先答者赢),维护每宠独立的触摸好感账本,并向模型暴露表达工具(`pet_say`、`pet_do`、`pet_digest`、`pet_interactions`、`pet_state
|
|
12
|
+
`dsh-agentic-pet` 是 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 生态的桌面宠物插件:宠物以透明置顶窗常驻桌面,跟随 agent 会话状态(idle / thinking / working / error),与 web 界面竞速应答审批与提问(先答者赢),维护每宠独立的触摸好感账本,并向模型暴露表达工具(`pet_say`、`pet_do`、`pet_digest`、`pet_interactions`、`pet_state`、`pet_schedule_wake`、`pet_cancel_wake`)——模型可以借宠物说话、做动作、感知被触摸、给自己定作息。说话内容可通过 [BreezeBlue](https://breezeblue.ai) TTS 朗读——carrier 侧合成每条 `/say` 文本,语音就绪后与气泡同时下发;合成失败或超时静默降级为只显示(桌面设置页可配)。多只宠物实现可以并存:目录注册表(`~/.dsh/whale598/pet-http.d/`)发现全部在线宠物,状态、交互、账本按宠路由。
|
|
13
13
|
|
|
14
14
|
宠物本身是零 DSH 知识的纯显示端。理解运行时的一切——事件收敛、交互瀑布、多宠路由、模型工具——都在插件的 driver 里,宠物只实现一个小型本地 HTTP 控制接口。任何讲同一 HTTP 契约的宠物实现都能被采纳,插件零改动。
|
|
15
15
|
|
|
16
|
+
### 多宠会话路由(session ↔ 宠物)
|
|
17
|
+
|
|
18
|
+
- **宠自己的内心戏会话**(每宠一个:boot greeting 自动创建,或设置页「指定会话」手动绑定):driver 用 `voiceOwners` 映射表记录会话→宠物,模型在会话里的 `pet_say`/`pet_do` 不点名也自动落回这只宠;这些回合不画状态灯(呼吸不是工作状态)。
|
|
19
|
+
- **工作会话**:有绑定宠时状态灯只亮在它身上(宠物在交互竞速里答过题即赢得绑定);无绑定或绑定宠离线时,状态广播到全部在线宠。审批/问答永远全员广播。工作会话没有宠物工具,不涉及说话/动作的分发。
|
|
20
|
+
- **手动在侧栏开的宠物模式会话**:driver 不认识它(不在映射表里),按普通工作会话对待——状态灯上身,表演类工具(`pet_say`/`pet_do`/定撤闹钟)直接拒答、不分发。这不是预期用法:想跟宠物说话,请用宠物表面的输入卡,直达她自己的会话。
|
|
21
|
+
|
|
16
22
|
## 仓库结构
|
|
17
23
|
|
|
18
24
|
| 路径 | 内容 |
|
package/lib/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { c as globalFilePath, d as readJsonl, f as readPetName, h as writePetName, i as installPetDriver, l as readAffectionRaw, m as writeGlobalJson, n as installPetService, p as writeAffectionRaw, r as petHome, s as eventsFile, u as readGlobalJson } from "./service-
|
|
1
|
+
import { c as globalFilePath, d as readJsonl, f as readPetName, h as writePetName, i as installPetDriver, l as readAffectionRaw, m as writeGlobalJson, n as installPetService, p as writeAffectionRaw, r as petHome, s as eventsFile, u as readGlobalJson } from "./service-Df8olC1E.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 } from "node:fs";
|
|
5
|
+
import { appendFileSync, existsSync, readFileSync, 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";
|
|
@@ -656,46 +656,169 @@ function apply(ctx) {
|
|
|
656
656
|
return binary;
|
|
657
657
|
};
|
|
658
658
|
/**
|
|
659
|
-
*
|
|
660
|
-
* (`electron
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
* explicit env override, a binary staged beside this package, then the
|
|
664
|
-
* `electron` npm dependency (its module resolves to the binary path).
|
|
665
|
-
* The carrier's single-instance lock collapses duplicates.
|
|
659
|
+
* Locate the npm `electron` package WITHOUT loading it. Loading the module
|
|
660
|
+
* (`require('electron')`) runs its lazy installer synchronously — a ~250 MB
|
|
661
|
+
* download inside the host's event loop — so only the manifest is resolved
|
|
662
|
+
* here; installing is a separate, background step.
|
|
666
663
|
*/
|
|
667
|
-
const
|
|
668
|
-
const override = process.env.DSH_PET_ELECTRON;
|
|
669
|
-
if (override !== void 0 && override !== "") {
|
|
670
|
-
if (existsSync(override)) return override;
|
|
671
|
-
logCarrier(`DSH_PET_ELECTRON is set but missing, falling back: ${override}`);
|
|
672
|
-
}
|
|
673
|
-
const pkgRoot = resolve(join(dirname(fileURLToPath(import.meta.url)), ".."));
|
|
674
|
-
const staged = join(pkgRoot, "electron-bin", process.platform === "win32" ? "electron.exe" : process.platform === "darwin" ? join("Electron.app", "Contents", "MacOS", "Electron") : "electron");
|
|
675
|
-
if (existsSync(staged)) return staged;
|
|
664
|
+
const findNpmElectronPackage = () => {
|
|
676
665
|
try {
|
|
677
666
|
const require = createRequire(import.meta.url);
|
|
678
|
-
const
|
|
679
|
-
if (existsSync(
|
|
667
|
+
const pkgDir = dirname(require.resolve("electron/package.json"));
|
|
668
|
+
if (existsSync(join(pkgDir, "package.json"))) return pkgDir;
|
|
680
669
|
} catch {}
|
|
681
670
|
try {
|
|
682
671
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
683
672
|
for (let i = 0; i < 8; i += 1) {
|
|
684
673
|
const pkgDir = join(dir, "node_modules", "electron");
|
|
685
|
-
|
|
686
|
-
const binary = join(pkgDir, "dist", exe);
|
|
687
|
-
if (existsSync(binary)) return ensureElectronUsable(binary);
|
|
674
|
+
if (existsSync(join(pkgDir, "package.json"))) return pkgDir;
|
|
688
675
|
const parent = dirname(dir);
|
|
689
676
|
if (parent === dir) break;
|
|
690
677
|
dir = parent;
|
|
691
678
|
}
|
|
692
679
|
} catch {}
|
|
693
|
-
return process.versions.electron !== void 0 ? process.execPath : void 0;
|
|
694
680
|
};
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
681
|
+
const npmElectronBinary = (pkgDir) => {
|
|
682
|
+
try {
|
|
683
|
+
const exe = readFileSync(join(pkgDir, "path.txt"), "utf8").trim();
|
|
684
|
+
const binary = join(pkgDir, "dist", exe);
|
|
685
|
+
return existsSync(binary) ? binary : void 0;
|
|
686
|
+
} catch {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
const INSTALL_LOCK_STALE_MS = 18e5;
|
|
691
|
+
const installLockPath = (pkgDir) => join(pkgDir, ".dsh-pet-install.lock");
|
|
692
|
+
const acquireInstallLock = (pkgDir) => {
|
|
693
|
+
const lock = installLockPath(pkgDir);
|
|
694
|
+
const write = () => {
|
|
695
|
+
writeFileSync(lock, JSON.stringify({
|
|
696
|
+
pid: process.pid,
|
|
697
|
+
at: Date.now()
|
|
698
|
+
}), { flag: "wx" });
|
|
699
|
+
};
|
|
700
|
+
try {
|
|
701
|
+
write();
|
|
702
|
+
return true;
|
|
703
|
+
} catch {
|
|
704
|
+
try {
|
|
705
|
+
if (Date.now() - statSync(lock).mtimeMs > INSTALL_LOCK_STALE_MS) {
|
|
706
|
+
unlinkSync(lock);
|
|
707
|
+
write();
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
} catch {}
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
const releaseInstallLock = (pkgDir) => {
|
|
715
|
+
try {
|
|
716
|
+
unlinkSync(installLockPath(pkgDir));
|
|
717
|
+
} catch {}
|
|
718
|
+
};
|
|
719
|
+
/**
|
|
720
|
+
* Install the npm electron binary off the activation path. The package's
|
|
721
|
+
* lazy installer is a ~250 MB download; running it synchronously (which
|
|
722
|
+
* `require('electron')` does) freezes the host, so it runs as a child with
|
|
723
|
+
* progress in the spawn log and a mirror hint on failure.
|
|
724
|
+
*/
|
|
725
|
+
const installNpmElectron = async (pkgDir) => {
|
|
726
|
+
const installer = join(pkgDir, "install.js");
|
|
727
|
+
if (!existsSync(installer)) {
|
|
728
|
+
logCarrier(`electron binary missing and install.js not found: ${pkgDir}`);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (!acquireInstallLock(pkgDir)) {
|
|
732
|
+
logCarrier(`electron install already in progress (lock present): ${pkgDir}`);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
logCarrier(`electron binary missing; installing in background: ${pkgDir}`);
|
|
736
|
+
return await new Promise((resolveInstall) => {
|
|
737
|
+
let settled = false;
|
|
738
|
+
const finish = (value) => {
|
|
739
|
+
if (settled) return;
|
|
740
|
+
settled = true;
|
|
741
|
+
releaseInstallLock(pkgDir);
|
|
742
|
+
resolveInstall(value);
|
|
743
|
+
};
|
|
744
|
+
let stderr = "";
|
|
745
|
+
let stdout = "";
|
|
746
|
+
let lastProgress = 0;
|
|
747
|
+
const child = spawn(process.execPath, [installer], {
|
|
748
|
+
cwd: pkgDir,
|
|
749
|
+
env: {
|
|
750
|
+
...process.env,
|
|
751
|
+
ELECTRON_RUN_AS_NODE: "1"
|
|
752
|
+
},
|
|
753
|
+
stdio: [
|
|
754
|
+
"ignore",
|
|
755
|
+
"pipe",
|
|
756
|
+
"pipe"
|
|
757
|
+
],
|
|
758
|
+
windowsHide: true
|
|
759
|
+
});
|
|
760
|
+
child.stdout?.setEncoding("utf8");
|
|
761
|
+
child.stdout?.on("data", (chunk) => {
|
|
762
|
+
stdout = `${stdout}${chunk}`.slice(-2e3);
|
|
763
|
+
const now = Date.now();
|
|
764
|
+
if (now - lastProgress > 5e3) {
|
|
765
|
+
lastProgress = now;
|
|
766
|
+
const line = stdout.split(/\r?\n/).map((part) => part.trim()).filter(Boolean).pop();
|
|
767
|
+
if (line !== void 0) logCarrier(`electron install: ${line}`);
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
child.stderr?.setEncoding("utf8");
|
|
771
|
+
child.stderr?.on("data", (chunk) => {
|
|
772
|
+
stderr = `${stderr}${chunk}`.slice(-2e3);
|
|
773
|
+
});
|
|
774
|
+
child.on("error", (error) => {
|
|
775
|
+
logCarrier(`electron install failed to start: ${error.message}`);
|
|
776
|
+
finish(void 0);
|
|
777
|
+
});
|
|
778
|
+
child.on("exit", (code) => {
|
|
779
|
+
if (code !== 0) {
|
|
780
|
+
logCarrier(`electron install failed (exit ${String(code)}): ${stderr.trim() || "no stderr"}. Set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ (or electron_mirror in .npmrc) and restart DSH; or run "node ${installer}" manually.`);
|
|
781
|
+
finish(void 0);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const binary = npmElectronBinary(pkgDir);
|
|
785
|
+
if (binary === void 0) {
|
|
786
|
+
logCarrier(`electron install exited 0 but the binary is still missing: ${pkgDir}`);
|
|
787
|
+
finish(void 0);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
logCarrier(`electron installed: ${binary}`);
|
|
791
|
+
finish(ensureElectronUsable(binary));
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
};
|
|
795
|
+
/**
|
|
796
|
+
* Auto-start the pet window. A bare Electron binary runs any app directory
|
|
797
|
+
* (`electron.exe <dir>`), but a PACKAGED one (the installed desktop) loads
|
|
798
|
+
* its own app and dies on its single-instance lock instead — so the
|
|
799
|
+
* resolution order is an explicit env override, a binary staged beside this
|
|
800
|
+
* package, then the `electron` npm dependency. The carrier's single-instance
|
|
801
|
+
* lock collapses duplicates.
|
|
802
|
+
*/
|
|
803
|
+
const resolveElectron = () => {
|
|
804
|
+
const override = process.env.DSH_PET_ELECTRON;
|
|
805
|
+
if (override !== void 0 && override !== "") {
|
|
806
|
+
if (existsSync(override)) return { binary: override };
|
|
807
|
+
logCarrier(`DSH_PET_ELECTRON is set but missing, falling back: ${override}`);
|
|
808
|
+
}
|
|
809
|
+
const pkgRoot = resolve(join(dirname(fileURLToPath(import.meta.url)), ".."));
|
|
810
|
+
const staged = join(pkgRoot, "electron-bin", process.platform === "win32" ? "electron.exe" : process.platform === "darwin" ? join("Electron.app", "Contents", "MacOS", "Electron") : "electron");
|
|
811
|
+
if (existsSync(staged)) return { binary: staged };
|
|
812
|
+
const npmPackageDir = findNpmElectronPackage();
|
|
813
|
+
if (npmPackageDir !== void 0) {
|
|
814
|
+
const binary = npmElectronBinary(npmPackageDir);
|
|
815
|
+
if (binary !== void 0) return { binary: ensureElectronUsable(binary) };
|
|
816
|
+
return { npmPackageDir };
|
|
817
|
+
}
|
|
818
|
+
logCarrier("no electron binary: no staged electron-bin, no npm electron dependency, and DSH_PET_ELECTRON is unset");
|
|
819
|
+
return {};
|
|
820
|
+
};
|
|
821
|
+
const launchCarrier = (url, carrierDir, electronBinary) => {
|
|
699
822
|
const launchArgs = [carrierDir];
|
|
700
823
|
if (process.env.DSH_PET_NO_SANDBOX === "1") launchArgs.push("--no-sandbox");
|
|
701
824
|
logCarrier(`spawn binary=${electronBinary} args=${JSON.stringify(launchArgs)} pid=${String(process.pid)}`);
|
|
@@ -742,6 +865,21 @@ function apply(ctx) {
|
|
|
742
865
|
child.kill();
|
|
743
866
|
}, "pet-surface: carrier shutdown");
|
|
744
867
|
};
|
|
868
|
+
const spawnPetCarrier = (url) => {
|
|
869
|
+
const carrierDir = resolve(join(dirname(fileURLToPath(import.meta.url)), "..", "carrier"));
|
|
870
|
+
if (process.env.DSH_PET_NO_SPAWN === "1" || !existsSync(join(carrierDir, "package.json"))) return;
|
|
871
|
+
const resolution = resolveElectron();
|
|
872
|
+
if (resolution.binary !== void 0) {
|
|
873
|
+
launchCarrier(url, carrierDir, resolution.binary);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if (resolution.npmPackageDir === void 0) return;
|
|
877
|
+
installNpmElectron(resolution.npmPackageDir).then((binary) => {
|
|
878
|
+
if (binary !== void 0) launchCarrier(url, carrierDir, binary);
|
|
879
|
+
}).catch((error) => {
|
|
880
|
+
logCarrier(`electron install failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
881
|
+
});
|
|
882
|
+
};
|
|
745
883
|
const settledReady = () => {
|
|
746
884
|
const url = authenticatedRoot();
|
|
747
885
|
if (url === void 0) return;
|
|
@@ -5395,19 +5395,16 @@ function buildChatPrompt(text, mergedPlan) {
|
|
|
5395
5395
|
}
|
|
5396
5396
|
// lib/types/registry.js
|
|
5397
5397
|
/**
|
|
5398
|
-
* The pet registry: a directory of per-pet control manifests
|
|
5399
|
-
*
|
|
5400
|
-
*
|
|
5401
|
-
*
|
|
5402
|
-
* pet
|
|
5403
|
-
*
|
|
5404
|
-
*
|
|
5405
|
-
* session's bound pet if alive, else the default pet (first alive entry),
|
|
5406
|
-
* plus an all-notify broadcast mode for interactions.
|
|
5398
|
+
* The pet registry: a directory of per-pet control manifests. Each pet
|
|
5399
|
+
* implementation owns its own file under whale598/pet-http.d/ (named by its
|
|
5400
|
+
* pet id) and re-asserts it on a cadence; entries whose publisher pid is dead
|
|
5401
|
+
* expire by omission. The driver reads the whole table and routes per the
|
|
5402
|
+
* multi-pet design: a session's state rides its bound pet when that pet is
|
|
5403
|
+
* alive and broadcasts to every live pet otherwise; interactions broadcast to
|
|
5404
|
+
* all; the default pet (earliest-booted) resolves unnamed expression targets.
|
|
5407
5405
|
* @module dsh-agentic-pet/registry
|
|
5408
5406
|
*/
|
|
5409
5407
|
const REGISTRY_DIR = `${WHALE_DIR}/pet-http.d`;
|
|
5410
|
-
const LEGACY_REGISTRY_DIR = "pet-http.d";
|
|
5411
5408
|
/**
|
|
5412
5409
|
* A live pet is one whose control plane ANSWERS, not merely one whose pid
|
|
5413
5410
|
* exists — and not merely one whose port accepts TCP: a frozen pet (stuck
|
|
@@ -5438,94 +5435,65 @@ async function probeControlPlane(port) {
|
|
|
5438
5435
|
return false;
|
|
5439
5436
|
}
|
|
5440
5437
|
}
|
|
5441
|
-
/**
|
|
5442
|
-
*
|
|
5443
|
-
*
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
const live = [];
|
|
5459
|
-
for (const [id, file] of candidates) try {
|
|
5460
|
-
const raw = JSON.parse(await readFile(file, "utf8"));
|
|
5461
|
-
const port = typeof raw.port === "number" ? raw.port : NaN;
|
|
5462
|
-
const pid = typeof raw.pid === "number" ? raw.pid : NaN;
|
|
5463
|
-
if (!Number.isFinite(port) || !Number.isFinite(pid)) continue;
|
|
5464
|
-
try {
|
|
5465
|
-
process.kill(pid, 0);
|
|
5466
|
-
} catch {
|
|
5467
|
-
continue;
|
|
5468
|
-
}
|
|
5469
|
-
if (!await probeControlPlane(port)) continue;
|
|
5470
|
-
live.push({
|
|
5471
|
-
id,
|
|
5472
|
-
port,
|
|
5473
|
-
pid,
|
|
5474
|
-
since: typeof raw.since === "number" ? raw.since : 0
|
|
5475
|
-
});
|
|
5476
|
-
} catch {}
|
|
5477
|
-
return live;
|
|
5478
|
-
}
|
|
5479
|
-
/** Legacy single-file manifests folded into the registry: the electron
|
|
5480
|
-
* carrier's dedicated file and the pure pet's shared file, identity-validated
|
|
5481
|
-
* and probe-validated, so pre-registry publishers stay drivable. */
|
|
5482
|
-
const LEGACY_SOURCES = [{
|
|
5483
|
-
id: "electron",
|
|
5484
|
-
file: "pet-http-electron.json",
|
|
5485
|
-
identity: "electron"
|
|
5486
|
-
}, {
|
|
5487
|
-
id: "pure",
|
|
5488
|
-
file: "pet-http.json",
|
|
5489
|
-
identity: "python"
|
|
5490
|
-
}];
|
|
5491
|
-
/** Every live pet: registry entries plus legacy single-file manifests folded
|
|
5492
|
-
* under their pet ids (registry entries win on id collision). */
|
|
5438
|
+
/**
|
|
5439
|
+
* The default-pet order: earliest booted first (`since` ascending). A missing
|
|
5440
|
+
* or mistyped `since` (0) reads as "unknown boot time" and sorts LAST — it
|
|
5441
|
+
* must never outrank a publisher that stated its boot time. This turns the
|
|
5442
|
+
* multi-pet design's de-facto rule ("the default pet is the first STARTED")
|
|
5443
|
+
* into an actual guarantee: discovery output IS boot order, so every
|
|
5444
|
+
* `pets[0]` consumer (defaultPet, pet_state's defaultPet) reads the
|
|
5445
|
+
* same pet. Stable sort: equal `since` values keep registry listing order.
|
|
5446
|
+
*/
|
|
5447
|
+
function bootOrder(pets) {
|
|
5448
|
+
return [...pets].sort((a, b) => (a.since > 0 ? a.since : Number.POSITIVE_INFINITY) - (b.since > 0 ? b.since : Number.POSITIVE_INFINITY));
|
|
5449
|
+
}
|
|
5450
|
+
/** Every live pet in boot order. Dead-pid entries are skipped — they expire
|
|
5451
|
+
* by omission the next time their file is rewritten — and so are silent
|
|
5452
|
+
* entries (pid alive, control plane unresponsive). Probes run CONCURRENTLY:
|
|
5453
|
+
* wall time per pass is the slowest pet (≤800ms frozen budget), not the
|
|
5454
|
+
* serial sum. */
|
|
5493
5455
|
async function readAllPets(home) {
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5456
|
+
let names = [];
|
|
5457
|
+
try {
|
|
5458
|
+
names = (await readdir(join(home, REGISTRY_DIR))).filter((name) => name.endsWith(".json"));
|
|
5459
|
+
} catch {
|
|
5460
|
+
return [];
|
|
5461
|
+
}
|
|
5462
|
+
return bootOrder((await Promise.all(names.map(async (name) => {
|
|
5463
|
+
const id = name.slice(0, -5);
|
|
5464
|
+
if (id === "") return null;
|
|
5497
5465
|
try {
|
|
5498
|
-
const
|
|
5499
|
-
|
|
5500
|
-
|
|
5466
|
+
const raw = JSON.parse(await readFile(join(home, REGISTRY_DIR, name), "utf8"));
|
|
5467
|
+
const port = typeof raw.port === "number" ? raw.port : NaN;
|
|
5468
|
+
const pid = typeof raw.pid === "number" ? raw.pid : NaN;
|
|
5469
|
+
if (!Number.isFinite(port) || !Number.isFinite(pid)) return null;
|
|
5501
5470
|
try {
|
|
5502
|
-
process.kill(
|
|
5471
|
+
process.kill(pid, 0);
|
|
5503
5472
|
} catch {
|
|
5504
|
-
|
|
5473
|
+
return null;
|
|
5505
5474
|
}
|
|
5506
|
-
if (!await probeControlPlane(
|
|
5507
|
-
|
|
5475
|
+
if (!await probeControlPlane(port)) return null;
|
|
5476
|
+
return {
|
|
5508
5477
|
id,
|
|
5509
|
-
port
|
|
5510
|
-
pid
|
|
5511
|
-
since: 0
|
|
5512
|
-
}
|
|
5513
|
-
} catch {
|
|
5514
|
-
|
|
5515
|
-
|
|
5478
|
+
port,
|
|
5479
|
+
pid,
|
|
5480
|
+
since: typeof raw.since === "number" ? raw.since : 0
|
|
5481
|
+
};
|
|
5482
|
+
} catch {
|
|
5483
|
+
return null;
|
|
5484
|
+
}
|
|
5485
|
+
}))).filter((entry) => entry !== null));
|
|
5516
5486
|
}
|
|
5517
5487
|
/**
|
|
5518
|
-
*
|
|
5519
|
-
*
|
|
5520
|
-
*
|
|
5521
|
-
*
|
|
5488
|
+
* The default pet for UNNAMED targets: the earliest-booted live entry
|
|
5489
|
+
* (`pets[0]` — discovery's bootOrder guarantees start order, not listing
|
|
5490
|
+
* accident). State routing does NOT go through here anymore: an unbound
|
|
5491
|
+
* (or bound-but-offline) session broadcasts its state to every live pet —
|
|
5492
|
+
* the driver computes that audience itself. Used by the tools'
|
|
5493
|
+
* resolveTarget when neither a named pet nor the owning session resolves.
|
|
5522
5494
|
* @returns the entry, or undefined when no pet is alive.
|
|
5523
5495
|
*/
|
|
5524
|
-
function
|
|
5525
|
-
if (boundPetId !== void 0) {
|
|
5526
|
-
const bound = pets.find((pet) => pet.id === boundPetId);
|
|
5527
|
-
if (bound !== void 0) return bound;
|
|
5528
|
-
}
|
|
5496
|
+
function defaultPet(pets) {
|
|
5529
5497
|
return pets[0];
|
|
5530
5498
|
}
|
|
5531
5499
|
/** The pets that receive an INTERACTION (all-notify mode): every live pet,
|
|
@@ -5834,34 +5802,39 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5834
5802
|
};
|
|
5835
5803
|
const pushState = async (state) => {
|
|
5836
5804
|
currentState = state;
|
|
5837
|
-
const
|
|
5838
|
-
|
|
5805
|
+
const pets = await pet.list();
|
|
5806
|
+
const bound = pets.filter((pet) => pet.id === boundPetId);
|
|
5807
|
+
const audience = bound.length > 0 ? bound : pets;
|
|
5808
|
+
if (audience.length === 0) return;
|
|
5839
5809
|
const autonomy = await readAutonomy();
|
|
5840
5810
|
const autonomyKey = JSON.stringify(autonomy);
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
try {
|
|
5849
|
-
const response = await fetch(`http://127.0.0.1:${String(target.port)}/state`, {
|
|
5850
|
-
method: "POST",
|
|
5851
|
-
headers: { "content-type": "application/json" },
|
|
5852
|
-
body: JSON.stringify({
|
|
5853
|
-
state,
|
|
5854
|
-
autonomy
|
|
5855
|
-
})
|
|
5811
|
+
await Promise.all(audience.map(async (target) => {
|
|
5812
|
+
const last = lastPushedByPet.get(target.id);
|
|
5813
|
+
if (last !== void 0 && last.state === state && last.since === target.since && last.autonomy === autonomyKey) return;
|
|
5814
|
+
lastPushedByPet.set(target.id, {
|
|
5815
|
+
state,
|
|
5816
|
+
since: target.since,
|
|
5817
|
+
autonomy: autonomyKey
|
|
5856
5818
|
});
|
|
5857
|
-
|
|
5858
|
-
|
|
5819
|
+
try {
|
|
5820
|
+
const response = await fetch(`http://127.0.0.1:${String(target.port)}/state`, {
|
|
5821
|
+
method: "POST",
|
|
5822
|
+
headers: { "content-type": "application/json" },
|
|
5823
|
+
body: JSON.stringify({
|
|
5824
|
+
state,
|
|
5825
|
+
autonomy
|
|
5826
|
+
}),
|
|
5827
|
+
signal: AbortSignal.timeout(5e3)
|
|
5828
|
+
});
|
|
5829
|
+
if (!response.ok) {
|
|
5830
|
+
console.error(`pet-surface driver: /state answered HTTP ${String(response.status)}`);
|
|
5831
|
+
lastPushedByPet.delete(target.id);
|
|
5832
|
+
} else if (state === "error") lastPushedByPet.delete(target.id);
|
|
5833
|
+
} catch {
|
|
5834
|
+
console.error(`pet-surface driver: /state to ${target.id} failed`);
|
|
5859
5835
|
lastPushedByPet.delete(target.id);
|
|
5860
|
-
}
|
|
5861
|
-
}
|
|
5862
|
-
console.error(`pet-surface driver: /state to ${target.id} failed`);
|
|
5863
|
-
lastPushedByPet.delete(target.id);
|
|
5864
|
-
}
|
|
5836
|
+
}
|
|
5837
|
+
}));
|
|
5865
5838
|
};
|
|
5866
5839
|
events.on("api-session/status", (sessionId, running) => {
|
|
5867
5840
|
if (isVoiceSession(sessionId)) return;
|
|
@@ -6471,7 +6444,8 @@ ${text}`;
|
|
|
6471
6444
|
if (target !== void 0) fetch(`http://127.0.0.1:${String(target.port)}/action`, {
|
|
6472
6445
|
method: "POST",
|
|
6473
6446
|
headers: { "content-type": "application/json" },
|
|
6474
|
-
body: JSON.stringify({ action: "celebrate" })
|
|
6447
|
+
body: JSON.stringify({ action: "celebrate" }),
|
|
6448
|
+
signal: AbortSignal.timeout(5e3)
|
|
6475
6449
|
}).catch(() => {});
|
|
6476
6450
|
}
|
|
6477
6451
|
triggerTouchReply(petId, part);
|
|
@@ -6582,10 +6556,14 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6582
6556
|
}
|
|
6583
6557
|
}
|
|
6584
6558
|
const focus = await readFocus();
|
|
6585
|
-
const target = (focus !== void 0 ? pets.find((entry) => entry.id === focus) : void 0) ??
|
|
6559
|
+
const target = (focus !== void 0 ? pets.find((entry) => entry.id === focus) : void 0) ?? defaultPet(pets);
|
|
6586
6560
|
if (target === void 0) throw new Error("no pet is running");
|
|
6587
6561
|
return target;
|
|
6588
6562
|
};
|
|
6563
|
+
const requireOwnedSession = (exec) => {
|
|
6564
|
+
const sessionId = exec?.agent?.id;
|
|
6565
|
+
if (typeof sessionId === "string" && !voiceOwners.has(sessionId)) throw new Error("本会话没有绑定宠物(不是宠物自己的内心戏会话),pet 工具不分发。想和宠物说话,请用宠物表面的输入卡。");
|
|
6566
|
+
};
|
|
6589
6567
|
const classify = (error) => {
|
|
6590
6568
|
if (error instanceof Error) {
|
|
6591
6569
|
if (/fetch failed|ECONNREFUSED|ENOTFOUND/i.test(error.message)) return /* @__PURE__ */ new Error(`offline: the pet is not reachable (${error.message})`);
|
|
@@ -6616,6 +6594,7 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6616
6594
|
}]
|
|
6617
6595
|
},
|
|
6618
6596
|
async execute(args, exec) {
|
|
6597
|
+
requireOwnedSession(exec);
|
|
6619
6598
|
const text = String(args.text).slice(0, 200);
|
|
6620
6599
|
console.log(`pet-surface driver: pet_say ${JSON.stringify(text).slice(0, 60)}`);
|
|
6621
6600
|
try {
|
|
@@ -6661,6 +6640,7 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6661
6640
|
}]
|
|
6662
6641
|
},
|
|
6663
6642
|
async execute(args, exec) {
|
|
6643
|
+
requireOwnedSession(exec);
|
|
6664
6644
|
try {
|
|
6665
6645
|
const entry = await resolveTarget(args.pet, exec?.agent?.id);
|
|
6666
6646
|
const requested = String(args.action);
|
|
@@ -6973,6 +6953,7 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6973
6953
|
}]
|
|
6974
6954
|
},
|
|
6975
6955
|
async execute(args, exec) {
|
|
6956
|
+
requireOwnedSession(exec);
|
|
6976
6957
|
try {
|
|
6977
6958
|
const entry = await resolveTarget(args.pet, exec?.agent?.id);
|
|
6978
6959
|
const id = typeof args.id === "string" ? args.id.trim().slice(0, WAKE_PLAN_ID_MAX) : "";
|
|
@@ -7057,6 +7038,7 @@ function registerPetTools(ctx, home, pet) {
|
|
|
7057
7038
|
}]
|
|
7058
7039
|
},
|
|
7059
7040
|
async execute(args, exec) {
|
|
7041
|
+
requireOwnedSession(exec);
|
|
7060
7042
|
try {
|
|
7061
7043
|
const entry = await resolveTarget(args.pet, exec?.agent?.id);
|
|
7062
7044
|
const list = voiceWakePlans.get(entry.id) ?? [];
|
|
@@ -7168,8 +7150,8 @@ function registerPetTools(ctx, home, pet) {
|
|
|
7168
7150
|
* The `ctx.petService` capability seam — the dsh three-roles pattern applied
|
|
7169
7151
|
* inside the process boundary. Pets are separate processes by design, so this
|
|
7170
7152
|
* service is NOT the pet itself: it is the typed in-process seam over the
|
|
7171
|
-
* out-of-process HTTP control plane. Discovery (pet-http.d/
|
|
7172
|
-
*
|
|
7153
|
+
* out-of-process HTTP control plane. Discovery (the whale598/pet-http.d/
|
|
7154
|
+
* registry), liveness (pid + two-layer probe), retry policy, and the
|
|
7173
7155
|
* /status field matrix (per-implementation extras) are normalized HERE —
|
|
7174
7156
|
* consumers (the driver, the model tools, the settings page, the upcoming
|
|
7175
7157
|
* proactivity scheduler) never see registry files, ports, or raw payloads.
|
|
@@ -7189,11 +7171,12 @@ function petHome() {
|
|
|
7189
7171
|
*
|
|
7190
7172
|
* Implementations must honor these semantics:
|
|
7191
7173
|
* - {@link list} returns only pets whose control plane ANSWERS: a dead pid
|
|
7192
|
-
* and a frozen publisher (listening socket, silent HTTP) are both skipped
|
|
7193
|
-
*
|
|
7194
|
-
*
|
|
7195
|
-
*
|
|
7196
|
-
*
|
|
7174
|
+
* and a frozen publisher (listening socket, silent HTTP) are both skipped,
|
|
7175
|
+
* in boot order (earliest `since` first).
|
|
7176
|
+
* - Interaction BROADCAST is not a route: see `notifyPets` in the registry
|
|
7177
|
+
* module for the all-notify view, and `defaultPet` for unnamed targets.
|
|
7178
|
+
* State audiences are computed by the driver (bound pet alive → that pet,
|
|
7179
|
+
* otherwise every live pet).
|
|
7197
7180
|
* - {@link snapshot} never throws on an unreachable pet — it resolves with
|
|
7198
7181
|
* `online: false`. Field-matrix differences stay here.
|
|
7199
7182
|
* - {@link call} retries once through a fresh discovery pass, so a pet
|
|
@@ -7291,7 +7274,7 @@ function normalizeLastAction(raw) {
|
|
|
7291
7274
|
}
|
|
7292
7275
|
/**
|
|
7293
7276
|
* HTTP control-plane provider: talks to pet processes over the frozen wire
|
|
7294
|
-
* contract,
|
|
7277
|
+
* contract, discovering pets through the whale598/pet-http.d/ registry. A new
|
|
7295
7278
|
* pet implementation is adopted by publishing the same contract — this
|
|
7296
7279
|
* class does not change for it.
|
|
7297
7280
|
*/
|
|
@@ -7299,15 +7282,23 @@ var PetHttpService = class PetHttpService extends PetService {
|
|
|
7299
7282
|
home;
|
|
7300
7283
|
capsCache = /* @__PURE__ */ new Map();
|
|
7301
7284
|
static CAPS_TTL_MS = 3e4;
|
|
7285
|
+
listCache;
|
|
7286
|
+
static LIST_TTL_MS = 1e3;
|
|
7302
7287
|
constructor(ctx, home = petHome()) {
|
|
7303
7288
|
super(ctx);
|
|
7304
7289
|
this.home = home;
|
|
7305
7290
|
}
|
|
7291
|
+
/** Every live pet in boot order, served from the 1s single-flight cache:
|
|
7292
|
+
* concurrent callers share the running probe pass. */
|
|
7306
7293
|
list() {
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7294
|
+
const hit = this.listCache;
|
|
7295
|
+
if (hit !== void 0 && Date.now() - hit.at < PetHttpService.LIST_TTL_MS) return hit.flight;
|
|
7296
|
+
const flight = readAllPets(this.home);
|
|
7297
|
+
this.listCache = {
|
|
7298
|
+
at: Date.now(),
|
|
7299
|
+
flight
|
|
7300
|
+
};
|
|
7301
|
+
return flight;
|
|
7311
7302
|
}
|
|
7312
7303
|
async snapshot(entry) {
|
|
7313
7304
|
try {
|
|
@@ -7348,25 +7339,27 @@ var PetHttpService = class PetHttpService extends PetService {
|
|
|
7348
7339
|
async call(entry, path, init) {
|
|
7349
7340
|
const { timeoutMs, retry } = callBudget(path);
|
|
7350
7341
|
const attempts = retry ? 2 : 1;
|
|
7342
|
+
const callerSignal = init?.signal ?? void 0;
|
|
7351
7343
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
7352
7344
|
const current = attempt === 0 ? entry : (await readAllPets(this.home)).find((pet) => pet.id === entry.id);
|
|
7353
7345
|
if (current === void 0) throw new Error(`pet "${entry.id}" is no longer running`);
|
|
7346
|
+
let response;
|
|
7354
7347
|
try {
|
|
7355
7348
|
const timeout = AbortSignal.timeout(timeoutMs);
|
|
7356
|
-
const callerSignal = init?.signal ?? void 0;
|
|
7357
7349
|
const signal = callerSignal !== void 0 ? AbortSignal.any([callerSignal, timeout]) : timeout;
|
|
7358
|
-
|
|
7350
|
+
response = await fetch(`http://127.0.0.1:${String(current.port)}${path}`, {
|
|
7359
7351
|
...init,
|
|
7360
7352
|
signal
|
|
7361
7353
|
});
|
|
7362
|
-
if (!response.ok) {
|
|
7363
|
-
const detail = await response.text().catch(() => "");
|
|
7364
|
-
throw new Error(`pet control ${path} answered HTTP ${String(response.status)}${detail ? `: ${detail.slice(0, 300)}` : ""}`);
|
|
7365
|
-
}
|
|
7366
|
-
return await response.json();
|
|
7367
7354
|
} catch (error) {
|
|
7368
|
-
if (attempt === attempts - 1) throw error;
|
|
7355
|
+
if (attempt === attempts - 1 || callerSignal?.aborted === true) throw error;
|
|
7356
|
+
continue;
|
|
7357
|
+
}
|
|
7358
|
+
if (!response.ok) {
|
|
7359
|
+
const detail = await response.text().catch(() => "");
|
|
7360
|
+
throw new Error(`pet control ${path} answered HTTP ${String(response.status)}${detail ? `: ${detail.slice(0, 300)}` : ""}`);
|
|
7369
7361
|
}
|
|
7362
|
+
return await response.json();
|
|
7370
7363
|
}
|
|
7371
7364
|
throw new Error("unreachable");
|
|
7372
7365
|
}
|
|
@@ -7411,6 +7404,7 @@ function createUnregisteredPetHttp(home = petHome()) {
|
|
|
7411
7404
|
const instance = Object.create(PetHttpService.prototype);
|
|
7412
7405
|
instance.home = home;
|
|
7413
7406
|
instance.capsCache = /* @__PURE__ */ new Map();
|
|
7407
|
+
instance.listCache = void 0;
|
|
7414
7408
|
return instance;
|
|
7415
7409
|
}
|
|
7416
7410
|
/**
|
package/lib/tools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as logStamp, o as registerPetTools, r as petHome, t as createUnregisteredPetHttp } from "./service-
|
|
1
|
+
import { a as logStamp, o as registerPetTools, r as petHome, t as createUnregisteredPetHttp } from "./service-Df8olC1E.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/registry.d.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The pet registry: a directory of per-pet control manifests
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* pet
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* session's bound pet if alive, else the default pet (first alive entry),
|
|
10
|
-
* plus an all-notify broadcast mode for interactions.
|
|
2
|
+
* The pet registry: a directory of per-pet control manifests. Each pet
|
|
3
|
+
* implementation owns its own file under whale598/pet-http.d/ (named by its
|
|
4
|
+
* pet id) and re-asserts it on a cadence; entries whose publisher pid is dead
|
|
5
|
+
* expire by omission. The driver reads the whole table and routes per the
|
|
6
|
+
* multi-pet design: a session's state rides its bound pet when that pet is
|
|
7
|
+
* alive and broadcasts to every live pet otherwise; interactions broadcast to
|
|
8
|
+
* all; the default pet (earliest-booted) resolves unnamed expression targets.
|
|
11
9
|
* @module dsh-agentic-pet/registry
|
|
12
10
|
*/
|
|
13
11
|
/** One pet's published control facts. */
|
|
@@ -30,21 +28,32 @@ export interface PetEntry {
|
|
|
30
28
|
* HTTP probe catches the frozen-but-listening zombies.
|
|
31
29
|
*/
|
|
32
30
|
export declare function probeControlPlane(port: number): Promise<boolean>;
|
|
33
|
-
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
31
|
+
/**
|
|
32
|
+
* The default-pet order: earliest booted first (`since` ascending). A missing
|
|
33
|
+
* or mistyped `since` (0) reads as "unknown boot time" and sorts LAST — it
|
|
34
|
+
* must never outrank a publisher that stated its boot time. This turns the
|
|
35
|
+
* multi-pet design's de-facto rule ("the default pet is the first STARTED")
|
|
36
|
+
* into an actual guarantee: discovery output IS boot order, so every
|
|
37
|
+
* `pets[0]` consumer (defaultPet, pet_state's defaultPet) reads the
|
|
38
|
+
* same pet. Stable sort: equal `since` values keep registry listing order.
|
|
39
|
+
*/
|
|
40
|
+
export declare function bootOrder(pets: PetEntry[]): PetEntry[];
|
|
41
|
+
/** Every live pet in boot order. Dead-pid entries are skipped — they expire
|
|
42
|
+
* by omission the next time their file is rewritten — and so are silent
|
|
43
|
+
* entries (pid alive, control plane unresponsive). Probes run CONCURRENTLY:
|
|
44
|
+
* wall time per pass is the slowest pet (≤800ms frozen budget), not the
|
|
45
|
+
* serial sum. */
|
|
39
46
|
export declare function readAllPets(home: string): Promise<PetEntry[]>;
|
|
40
47
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
48
|
+
* The default pet for UNNAMED targets: the earliest-booted live entry
|
|
49
|
+
* (`pets[0]` — discovery's bootOrder guarantees start order, not listing
|
|
50
|
+
* accident). State routing does NOT go through here anymore: an unbound
|
|
51
|
+
* (or bound-but-offline) session broadcasts its state to every live pet —
|
|
52
|
+
* the driver computes that audience itself. Used by the tools'
|
|
53
|
+
* resolveTarget when neither a named pet nor the owning session resolves.
|
|
45
54
|
* @returns the entry, or undefined when no pet is alive.
|
|
46
55
|
*/
|
|
47
|
-
export declare function
|
|
56
|
+
export declare function defaultPet(pets: PetEntry[]): PetEntry | undefined;
|
|
48
57
|
/** The pets that receive an INTERACTION (all-notify mode): every live pet,
|
|
49
58
|
* the session-bound one included. The binding routes STATE — it must never
|
|
50
59
|
* mute interactions: excluding the bound pet would silence the only body
|
package/lib/types/service.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* The `ctx.petService` capability seam — the dsh three-roles pattern applied
|
|
3
3
|
* inside the process boundary. Pets are separate processes by design, so this
|
|
4
4
|
* service is NOT the pet itself: it is the typed in-process seam over the
|
|
5
|
-
* out-of-process HTTP control plane. Discovery (pet-http.d/
|
|
6
|
-
*
|
|
5
|
+
* out-of-process HTTP control plane. Discovery (the whale598/pet-http.d/
|
|
6
|
+
* registry), liveness (pid + two-layer probe), retry policy, and the
|
|
7
7
|
* /status field matrix (per-implementation extras) are normalized HERE —
|
|
8
8
|
* consumers (the driver, the model tools, the settings page, the upcoming
|
|
9
9
|
* proactivity scheduler) never see registry files, ports, or raw payloads.
|
|
@@ -77,11 +77,12 @@ export interface PetLastAction {
|
|
|
77
77
|
*
|
|
78
78
|
* Implementations must honor these semantics:
|
|
79
79
|
* - {@link list} returns only pets whose control plane ANSWERS: a dead pid
|
|
80
|
-
* and a frozen publisher (listening socket, silent HTTP) are both skipped
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
80
|
+
* and a frozen publisher (listening socket, silent HTTP) are both skipped,
|
|
81
|
+
* in boot order (earliest `since` first).
|
|
82
|
+
* - Interaction BROADCAST is not a route: see `notifyPets` in the registry
|
|
83
|
+
* module for the all-notify view, and `defaultPet` for unnamed targets.
|
|
84
|
+
* State audiences are computed by the driver (bound pet alive → that pet,
|
|
85
|
+
* otherwise every live pet).
|
|
85
86
|
* - {@link snapshot} never throws on an unreachable pet — it resolves with
|
|
86
87
|
* `online: false`. Field-matrix differences stay here.
|
|
87
88
|
* - {@link call} retries once through a fresh discovery pass, so a pet
|
|
@@ -90,11 +91,9 @@ export interface PetLastAction {
|
|
|
90
91
|
*/
|
|
91
92
|
export declare abstract class PetService extends Service {
|
|
92
93
|
constructor(ctx: Context);
|
|
93
|
-
/** Every live pet (
|
|
94
|
-
*
|
|
94
|
+
/** Every live pet in boot order (earliest `since` first — the default-pet
|
|
95
|
+
* rule is a discovery guarantee, not a listing accident). */
|
|
95
96
|
abstract list(): Promise<PetEntry[]>;
|
|
96
|
-
/** The STATE carrier: the bound pet when alive, else the default pet. */
|
|
97
|
-
abstract route(boundPetId?: string): Promise<PetEntry | undefined>;
|
|
98
97
|
/** The normalized mirror of one pet, or `online: false` when unreachable. */
|
|
99
98
|
abstract snapshot(entry: PetEntry): Promise<PetSnapshot>;
|
|
100
99
|
/** One JSON call to a pet endpoint, with a single fresh-discovery retry. */
|
|
@@ -112,7 +111,7 @@ export declare abstract class PetService extends Service {
|
|
|
112
111
|
export declare function normalizeStatus(id: string, raw: Record<string, unknown>, online: boolean): PetSnapshot;
|
|
113
112
|
/**
|
|
114
113
|
* HTTP control-plane provider: talks to pet processes over the frozen wire
|
|
115
|
-
* contract,
|
|
114
|
+
* contract, discovering pets through the whale598/pet-http.d/ registry. A new
|
|
116
115
|
* pet implementation is adopted by publishing the same contract — this
|
|
117
116
|
* class does not change for it.
|
|
118
117
|
*/
|
|
@@ -120,9 +119,12 @@ export declare class PetHttpService extends PetService {
|
|
|
120
119
|
readonly home: string;
|
|
121
120
|
private readonly capsCache;
|
|
122
121
|
private static readonly CAPS_TTL_MS;
|
|
122
|
+
private listCache;
|
|
123
|
+
private static readonly LIST_TTL_MS;
|
|
123
124
|
constructor(ctx: Context, home?: string);
|
|
125
|
+
/** Every live pet in boot order, served from the 1s single-flight cache:
|
|
126
|
+
* concurrent callers share the running probe pass. */
|
|
124
127
|
list(): Promise<PetEntry[]>;
|
|
125
|
-
route(boundPetId?: string): Promise<PetEntry | undefined>;
|
|
126
128
|
snapshot(entry: PetEntry): Promise<PetSnapshot>;
|
|
127
129
|
capabilities(entry: PetEntry): Promise<PetCapabilities>;
|
|
128
130
|
call(entry: PetEntry, path: string, init?: RequestInit): Promise<unknown>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agentic-pet",
|
|
3
3
|
"description": "The pet-surface bundle: serves the desktop pet renderer over the web composition, turning dsh into a surface the desktop pet attaches to as an authenticated client",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.8",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|