@khorsheed/dsh-ankh-guard 0.3.0 → 0.3.2
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/CHANGELOG.md +16 -0
- package/README.en.md +8 -3
- package/README.i18n.yaml +2 -2
- package/README.md +8 -3
- package/lib/index.js +65 -6
- package/lib/preflight-runner.js +39 -4
- package/lib/types/browser-handoff.d.ts +6 -0
- package/lib/types/browser-handoff.js +39 -0
- package/lib/types/index.d.ts +34 -8
- package/lib/types/index.js +32 -10
- package/lib/types/preflight-runner.d.ts +21 -1
- package/lib/types/preflight-runner.js +100 -9
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +13 -19
- package/skills/dsh-self-restart-guard/SKILL.md +2 -0
- package/lib/restart-context-DUyExi9O.js +0 -1245
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.3.2(2026-09-27)
|
|
4
|
+
|
|
5
|
+
适配宿主 0.1.7-rc.2 线:verifiedHost 前移至 0.1.7-rc.2(3080 生产实证线随宿主基线切到 rc.2);rc.1→rc.2 对本包无破坏性变更(逐类清点见 [Agent Note](../../.agents/notes/implemented/architecture/2026-09-27-host-017-rc2-breaking-changes.md)),全量构建+测试双绿。
|
|
6
|
+
|
|
7
|
+
- **修复 tarball profile 上 preflight 误报 FAIL**:runner 把 compose 算出的 runtime resolution(0.1.7 的 `resolution` / 0.1.6 的 `generation`)算完即丢,boot prepare 从不挂载——tarball profile 的 node_modules 没有任何 `@deepseek-ai/*` 条目,原生解析全灭(实测 177 个条目 failed to import,真实启动完全干净)。现在 composePreflightPatches 保留并按两线键名返回 pluginPackagesConfig,prepare 按 runProfile 顺序挂载(profileContext → 启动环境 → PluginPackages → provideCmdline);提供 profileContext 的线追加 dry-run 覆写 `{ id: 'hmr', disabled: true }` 守住 no-HMR 契约。判定契约(0/1/3)与诊断强度不变
|
|
8
|
+
- **skill 防踩坑**:`dsh-self-restart-guard` 的取证步骤写明「证据命令作用域到改动所在仓库」——凭证绑定 harness 检出的 git HEAD;家族外改动(tarball 进 profile 的插件)由部署驱动器(`pnpm deploy:3080`)在自己的绿色门禁里记录。对 harness 全量套件手跑 `record --run` 会先清空既有有效凭证再撞上本机无关红(~11 分钟、561 个与本改动无关的失败),跑完门禁零证据
|
|
9
|
+
|
|
10
|
+
## 0.3.1(2026-09-26)
|
|
11
|
+
|
|
12
|
+
适配宿主 rc.1 线并实证 0.1.5/0.1.7 双线可用(0.1.5-rc.1 全量 boot 实证,2026-09-25;0.1.7-rc.1 为 3080 生产验证线)。
|
|
13
|
+
|
|
14
|
+
- **修复 0.1.5 上 boot 即死**:preset 派生对宿主 preset-registry 包的导入从顶层静态导入改为运行期双名探测(0.1.7-rc.1 把官方包从 `dsh-agent-presets` 改名为 `dsh-agent-preset-registry`,0.1.5 宿主只装旧名,静态导入在模块解析期就炸穿整个 loader 树);两个名字都装不上时按既定「无 preset 回落部署默认预设」降级,不再抛错
|
|
15
|
+
- rc.1 适配波:中断续跑消息的 source 改为 V4 生产者归属 kind(退役的 `plugin` 外壳在 rc.1 持久层写入即抛);preflight 执行器按宿主代际探测组合解析面(三代际→四代际);`agent/created` 监听在 0.1.6 串行模式下保持 fire-and-forget
|
|
16
|
+
- 插件清单展示元数据(`locale/*.json`):rc.1 宿主插件页的卡面标题/描述中文化
|
|
17
|
+
- 修复 boot 代际刷新过早:代际通道此前只凭「boot id 变了」就回 `ready/reload`,而 Web-server 座位与兄弟行共用——端口刚监听时 `/api` 路由 owner 可能还没挂上,被刷新的页面于是向半挂载的宿主发出第一次、也是唯一一次会话列表拉取,左侧列表只剩重启时被召回的那个会话(重新手动刷新才恢复)。现在代际回答先等新进程的 Loader 树结算(与 web-app 放行浏览器用的是同一信号);就绪前的 poll 回 `waiting` 且**不下发 boot id**,标签页保持陈旧 id 持续重询,就绪即刷;无 Loader 的裸组合仍视为就绪。宿主侧新增「就绪前不回 reload、就绪后仍一次性刷新」单测,客户端侧新增「held 响应不消费代际判定」单测
|
|
18
|
+
|
|
3
19
|
## 0.3.0(2026-09-11)
|
|
4
20
|
|
|
5
21
|
适配宿主 0.1.5 线。
|
package/README.en.md
CHANGED
|
@@ -184,7 +184,7 @@ dsh-ankh-guard restart \
|
|
|
184
184
|
|
|
185
185
|
Mounted as a cordis plugin (base bundle), the same surface is available as the `selfRestartGuard` service for in-app gates. Config: `maxAgeMinutes` (default 10), `stateDir`, `repoDir`, `reportRestartContext` (default `followup`), `fallbackGraceMs` (default 300000).
|
|
186
186
|
|
|
187
|
-
Beyond the verify/record/canary gates, the service exposes `requestRestart({ start, profile, initiator })` — the in-process restart trigger for UI-grade callers (e.g. the mode switcher): `initiator` is required (the requesting session's real id) and the port comes from the launch record. With no live watchdog it drives `restart`; supervised instances drive the `reconfigure` transactional cutover (the only safe way to change the launch command under a watchdog). The credential/preflight/marker/lock gate chain is the CLI's own, refusals return a structured `{ accepted, stage, reason }` and never stop the instance. On the browser side, beside the cutover receipt channel there is a boot-generation channel: after an ordinary restart or a crash respawn, open tabs notice the process's boot id changed over the handoff long-poll and reload once to pick up the new bundle; visible sustained failures (~5 s) show a non-blocking connection banner with Retry; only confirmed cutovers use the restart overlay. Background/offline time does not count as failure. Returning to the foreground or online immediately resets backoff and restarts polling; stalled requests time out after 35 s and stale responses cannot trigger a reload.
|
|
187
|
+
Beyond the verify/record/canary gates, the service exposes `requestRestart({ start, profile, initiator })` — the in-process restart trigger for UI-grade callers (e.g. the mode switcher): `initiator` is required (the requesting session's real id) and the port comes from the launch record. With no live watchdog it drives `restart`; supervised instances drive the `reconfigure` transactional cutover (the only safe way to change the launch command under a watchdog). The credential/preflight/marker/lock gate chain is the CLI's own, refusals return a structured `{ accepted, stage, reason }` and never stop the instance. On the browser side, beside the cutover receipt channel there is a boot-generation channel: after an ordinary restart or a crash respawn, open tabs notice the process's boot id changed over the handoff long-poll and reload once to pick up the new bundle — before reloading it waits for the successor's composition to settle (the Web-server seat is shared, so `/api`'s route owner can still be mounting while the port already answers; until the Loader settles the poll is answered `waiting` with no boot id, the tab keeps asking with its stale id and reloads the moment the tree is up); visible sustained failures (~5 s) show a non-blocking connection banner with Retry; only confirmed cutovers use the restart overlay. Background/offline time does not count as failure. Returning to the foreground or online immediately resets backoff and restarts polling; stalled requests time out after 35 s and stale responses cannot trigger a reload.
|
|
188
188
|
|
|
189
189
|
## Model Experience
|
|
190
190
|
|
|
@@ -196,9 +196,10 @@ None.
|
|
|
196
196
|
|
|
197
197
|
## Compatibility
|
|
198
198
|
|
|
199
|
-
- npm release line (`@deepseek-ai/dsh@0.1.5-rc.1`):
|
|
199
|
+
- npm release line (`@deepseek-ai/dsh@0.1.5-rc.1`): supported — 0.1.5-rc.1 full-line boot-verified (42 packages including capture, 2026-09-25) — with two designed degrades. The composition-preflight gate runs through the standalone `preflight-runner` (composing through the published `@deepseek-ai/dsh-app-boot` primitives with a drift tripwire, since 0.1.5-rc.1 still does not export `composeProfile`) wherever a dsh app layout resolves — `--harness-root`, the durable launch spec, `DSH_HARNESS`, or the default checkout. On a pure npm deployment with no harness checkout the gate reports a notice and proceeds instead. The original-tab bridge feature-probes the optional WebServer/connection authentication seams; hosts without token auth naturally take the existing-cookie path. Cold reads (the parked probe and the preset derivation) ride the 0.1.5 handle-based sessionPersistence (`open(id, 'read')` → `read` → `close`; the one-shot `inspect` is gone). Every other capability is intact on the npm line. minHost stays 0.1.5-rc.1 — older hosts stay on the previous release line.
|
|
200
200
|
- Historical verification: a live npm-host 0.1.1-rc.2 → 0.1.2-alpha.4 isolated cutover passed (transition preflight removed a v3 whole-unit projection cache with an old-schema record from a home copy, live apply quarantined the old file, and target completed Token URL → 303 → cookie 200, the ownership stability window, and canary at zero retries; the old file remained byte-exact in the cutover directory. An untransitioned control over the same home failed on the missing Alpha.4 record fields, demonstrating that acceptance covered the real schema break).
|
|
201
|
-
- source line (deepseek-harness master, fork or upstream): ✅ (verifiedHost: 0.1.
|
|
201
|
+
- source line (deepseek-harness master, fork or upstream): ✅ (verifiedHost: 0.1.7-rc.2) — the gate runs through the standalone `preflight-runner` (resolves the published `@deepseek-ai/dsh-app-boot` etc. from the live checkout), so no fork patch is required.
|
|
202
|
+
- Dual-line evidence: the 0.1.5 boot passes end-to-end through three compat layers — the [preset-registry dual-name probe](../../.agents/notes/implemented/bug-fix/2026-09-25-preset-registry-dual-name-probe.md), [dual-shape typert codecs](../../.agents/notes/implemented/bug-fix/2026-09-25-typert-codec-dual-shape.md), and [typert faces carrying zod@4](../../.agents/notes/implemented/bug-fix/2026-09-25-typert-faces-carry-zod-v4.md).
|
|
202
203
|
|
|
203
204
|
**Version line mapping**: the first release after 0.2.0 supports host `0.1.5-rc.1` and later; hosts on `0.1.2-rc.1` stay on `0.2.0`, hosts on `0.1.0-rc.6` ~ `0.1.1-rc.2` stay on the 0.1.x release line (last release `0.1.1`).
|
|
204
205
|
|
|
@@ -214,6 +215,10 @@ None.
|
|
|
214
215
|
- **`restart`/`supervise` discover the listener via `lsof`** (macOS/Linux with lsof); the guard prefers absolute system paths, while other platforms need `--pid`.
|
|
215
216
|
- **Kills are per-pid identity with a descendant sweep, never per process group** — the instance is not setsid'd, so `restart`, `schedule-exit`'s exit agent, and watchdog cleanup target recorded child/listener identities. Cutover forced paths `SIGSTOP` first, then revalidate a Linux boot/start-tick token or macOS `proc_pidinfo` microsecond start time; a mismatch is only resumed and refused. Descendants are likewise frozen and their parentage rechecked while walking `pgrep -P`. Ordinary non-cutover port recovery retains a bounded listener-cleanup escape hatch; cutovers never select or kill a process by port.
|
|
216
217
|
|
|
218
|
+
## Test concurrency
|
|
219
|
+
|
|
220
|
+
Process integration tests run up to 4 shards concurrently by default; the unit lane caps at 2. On constrained machines, use `DSH_TEST_MAX_WORKERS=1 pnpm --filter @khorsheed/dsh-ankh-guard test` to schedule every shard serially. The same variable applies to the repository-wide `pnpm gate`. This only reduces scheduling concurrency: it does not omit tests, extend assertion deadlines, or change production restart behavior. Invalid values retain the defaults.
|
|
221
|
+
|
|
217
222
|
## Changelog
|
|
218
223
|
|
|
219
224
|
See [CHANGELOG.md](CHANGELOG.md).
|
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# last confirmed-consistent state. Both languages carry equal authority; after
|
|
3
3
|
# editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/ankh-guard/README.en.md
|
|
5
|
-
packages/ankh-guard/README.en.md:
|
|
6
|
-
packages/ankh-guard/README.md:
|
|
5
|
+
packages/ankh-guard/README.en.md: 1503f247dbba22fb64d0d5988225a06ee2e49c9d
|
|
6
|
+
packages/ankh-guard/README.md: 90693789e64935abff5fb1d7fbbf5d4807c19a99
|
package/README.md
CHANGED
|
@@ -183,7 +183,7 @@ dsh-ankh-guard restart \
|
|
|
183
183
|
|
|
184
184
|
以 cordis 插件挂载(base bundle)后,同一套能力以 `selfRestartGuard` 服务的形式供应用内闸门使用。配置:`maxAgeMinutes`(默认 10)、`stateDir`、`repoDir`、`reportRestartContext`(默认 `followup`)、`fallbackGraceMs`(默认 300000)。
|
|
185
185
|
|
|
186
|
-
除 verify/record/canary 等闸门外,服务还暴露 `requestRestart({ start, profile, initiator })`——UI 级调用方(如 mode-switcher)的进程内重启触发缝:`initiator` 必填(发起会话的真实 id),端口从 launch 记录自知;无活 watchdog 走 `restart`,受监督走 `reconfigure` 事务 cutover(受监督下唯一安全的换命令通道),凭证/preflight/marker/lock 全套闸门与 CLI 同源,拒绝返回结构化 `{ accepted, stage, reason }` 且绝不停机。浏览器侧,除 cutover receipt 通道外还有 boot 代际通道:普通重启或崩溃救回后,打开的标签页经 handoff 长轮询发现进程 boot id 已变,自动全页刷新一次拿到新 bundle
|
|
186
|
+
除 verify/record/canary 等闸门外,服务还暴露 `requestRestart({ start, profile, initiator })`——UI 级调用方(如 mode-switcher)的进程内重启触发缝:`initiator` 必填(发起会话的真实 id),端口从 launch 记录自知;无活 watchdog 走 `restart`,受监督走 `reconfigure` 事务 cutover(受监督下唯一安全的换命令通道),凭证/preflight/marker/lock 全套闸门与 CLI 同源,拒绝返回结构化 `{ accepted, stage, reason }` 且绝不停机。浏览器侧,除 cutover receipt 通道外还有 boot 代际通道:普通重启或崩溃救回后,打开的标签页经 handoff 长轮询发现进程 boot id 已变,自动全页刷新一次拿到新 bundle;刷新前会等新进程的组合挂载完成(Web-server 座位与兄弟行共用,端口刚应答时 `/api` 路由 owner 可能还没挂上;Loader 结算前一律回 `waiting` 且不下发 boot id,标签页保持陈旧 id 继续问,就绪即刷);仅前台持续失败约 5 秒后显示带重试按钮的轻量连接提示;只有确认的 cutover 使用重启遮罩。后台或离线时间不计入失败时长;回前台或恢复联网立即清除退避并重新轮询;挂起的请求在 35 秒后超时,过期响应不能触发刷新。
|
|
187
187
|
|
|
188
188
|
## Model Experience
|
|
189
189
|
|
|
@@ -195,9 +195,10 @@ dsh-ankh-guard restart \
|
|
|
195
195
|
|
|
196
196
|
## Compatibility
|
|
197
197
|
|
|
198
|
-
- npm 发布线(`@deepseek-ai/dsh@0.1.5-rc.1
|
|
198
|
+
- npm 发布线(`@deepseek-ai/dsh@0.1.5-rc.1`):支持——0.1.5-rc.1 全量 boot 实证通过(42 包含 capture,2026-09-25)——带两处设计内降级。composition-preflight 门禁通过独立的 `preflight-runner` 运行(0.1.5-rc.1 仍未导出 `composeProfile`,runner 改经已发布的 `@deepseek-ai/dsh-app-boot` 原语组装,带漂移绊线测试),只要能解析到 dsh app 布局——`--harness-root`、耐久 launch spec、`DSH_HARNESS` 或默认检出路径——就完整运行。没有 harness 检出的纯 npm 部署下门禁退化为提示后放行。原标签页桥会探测可选 WebServer/connection 认证 seam,不使用 token 认证的宿主自然走现有 Cookie 路径;冷读(停靠探测与 preset 推导)走 0.1.5 的 handle 制 sessionPersistence(`open(id, 'read')` → `read` → `close`,一次性 `inspect` 已移除)。其余能力在 npm 线上完整。minHost 保持 0.1.5-rc.1,旧宿主请停留在旧发布线。
|
|
199
199
|
- 历史验证:npm host 的 0.1.1-rc.2 → 0.1.2-alpha.4 隔离切换已通过(transition preflight 在 home 副本上移开带旧 schema record 的 v3 whole-unit projection cache,live apply 隔离旧文件,target 以零重试完成 Token URL → 303 → Cookie 200、ownership 稳定窗口与 canary;旧文件逐字节保留在 cutover 目录。相同 home 的无 transition 对照因缺少 Alpha.4 record 字段而拒绝,证明验收覆盖了真实 schema 断裂面)。
|
|
200
|
-
- 源码线(deepseek-harness master,fork 或上游):✅(verifiedHost: 0.1.
|
|
200
|
+
- 源码线(deepseek-harness master,fork 或上游):✅(verifiedHost: 0.1.7-rc.2)——门禁通过独立的 `preflight-runner` 运行(从在线 checkout 解析已发布的 `@deepseek-ai/dsh-app-boot` 等),不再需要 fork 补丁。
|
|
201
|
+
- 双线证据:0.1.5 的 boot 经三层兼容修复端到端通过——[preset-registry 双名探测](../../.agents/notes/implemented/bug-fix/2026-09-25-preset-registry-dual-name-probe.md)、[typert codec 双形状](../../.agents/notes/implemented/bug-fix/2026-09-25-typert-codec-dual-shape.md)、[face 自带 zod@4](../../.agents/notes/implemented/bug-fix/2026-09-25-typert-faces-carry-zod-v4.md)。
|
|
201
202
|
|
|
202
203
|
**版本线对照**:0.2.0 之后的首个发布起支持宿主 `0.1.5-rc.1` 及以后;宿主 `0.1.2-rc.1` 请停留在 `0.2.0`,宿主 `0.1.0-rc.6` ~ `0.1.1-rc.2` 请停留在 0.1.x 发布线(末版 `0.1.1`)。
|
|
203
204
|
|
|
@@ -213,6 +214,10 @@ dsh-ankh-guard restart \
|
|
|
213
214
|
- **`restart`/`supervise` 通过 `lsof` 发现监听者**(macOS / 带 lsof 的 Linux);guard 优先使用系统绝对路径,其他平台需用 `--pid`。
|
|
214
215
|
- **杀进程一律按单 pid identity + 后代回收,从不按进程组**——实例不是 setsid 的,所以 `restart`、`schedule-exit` 的退出代理和 watchdog 清理都针对已记录的 child/listener;cutover 强制路径先 `SIGSTOP`,再用 Linux boot/start-tick 或 macOS `proc_pidinfo` 微秒启动时间复核 identity,不匹配就只 `SIGCONT` 并拒绝,然后才沿 `pgrep -P` 冻结、复核亲缘并回收后代。普通非 cutover 端口恢复仍有受限的 listener 清理兜底;cutover 禁止凭端口选择或杀进程。
|
|
215
216
|
|
|
217
|
+
## 测试并发
|
|
218
|
+
|
|
219
|
+
进程集成测试默认并行运行 4 个分片,unit lane 上限为 2。资源紧张时可用 `DSH_TEST_MAX_WORKERS=1 pnpm --filter @khorsheed/dsh-ankh-guard test` 串行调度全部分片;全仓 `pnpm gate` 也可使用这个环境变量。只降低调度并发,不减少测试、延长断言期限或改变生产重启行为;无效值保留默认值。
|
|
220
|
+
|
|
216
221
|
## 变更记录
|
|
217
222
|
|
|
218
223
|
见 [CHANGELOG.md](CHANGELOG.md)。
|
package/lib/index.js
CHANGED
|
@@ -11,7 +11,6 @@ import { dirname, join, resolve, sep } from "node:path";
|
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import z from "@deepseek-ai/schemastery";
|
|
13
13
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
14
|
-
import * as agentPresetsHost from "@deepseek-ai/dsh-agent-presets";
|
|
15
14
|
//#region lib/types/browser-handoff.js
|
|
16
15
|
/**
|
|
17
16
|
* Browser handoff bridge for launch cutovers.
|
|
@@ -31,6 +30,14 @@ import * as agentPresetsHost from "@deepseek-ai/dsh-agent-presets";
|
|
|
31
30
|
* means the process it knew is gone — answer ready/reload. A cutover receipt
|
|
32
31
|
* always wins over the generation check: during a cutover the old tab's known
|
|
33
32
|
* id is already stale, and the receipt protocol owns the pacing.
|
|
33
|
+
*
|
|
34
|
+
* The generation answer waits for the successor's composition to settle. The
|
|
35
|
+
* shared Web-server seat answers as soon as it listens — well before sibling
|
|
36
|
+
* rows mount — and a page that loads then calls into a half-mounted /api
|
|
37
|
+
* surface (the Web app gates its own browser handoff on the same Loader
|
|
38
|
+
* settlement for that reason). Until then the poll is answered `waiting`
|
|
39
|
+
* WITHOUT the boot id: teaching the tab the successor's id would consume its
|
|
40
|
+
* one-shot generation check and strand it on an unusable page.
|
|
34
41
|
*/
|
|
35
42
|
const BROWSER_HANDOFF_ROUTE = "/_ankh-guard/browser-handoff";
|
|
36
43
|
const CAPABILITY_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
@@ -278,6 +285,10 @@ function createBrowserHandoffHandler(dependencies) {
|
|
|
278
285
|
});
|
|
279
286
|
};
|
|
280
287
|
if (bootId !== void 0 && message.knownBootId !== void 0 && message.knownBootId !== bootId && activeCutover(dependencies.stateDir) === null) {
|
|
288
|
+
if (dependencies.applicationReady?.() === false) {
|
|
289
|
+
json(res, 200, { state: "waiting" });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
281
292
|
respond(200, {
|
|
282
293
|
state: "ready",
|
|
283
294
|
action: "reload",
|
|
@@ -448,7 +459,8 @@ function registerBrowserHandoff(ctx, stateDir) {
|
|
|
448
459
|
if (webServer === void 0) return;
|
|
449
460
|
const handler = createBrowserHandoffHandler({
|
|
450
461
|
stateDir,
|
|
451
|
-
connectionProvider: () => webCtx.get("connection")
|
|
462
|
+
connectionProvider: () => webCtx.get("connection"),
|
|
463
|
+
applicationReady: applicationReadyProbe(webCtx)
|
|
452
464
|
});
|
|
453
465
|
webCtx.effect(() => webServer.register({
|
|
454
466
|
kind: "exact",
|
|
@@ -457,6 +469,30 @@ function registerBrowserHandoff(ctx, stateDir) {
|
|
|
457
469
|
}), "ankh-guard: browser handoff route");
|
|
458
470
|
});
|
|
459
471
|
}
|
|
472
|
+
/**
|
|
473
|
+
* Build the generation channel's readiness probe from the Loader tree.
|
|
474
|
+
*
|
|
475
|
+
* The Web-server seat is shared: it answers as soon as it listens, while
|
|
476
|
+
* sibling rows — including the /api route owner a reloaded page immediately
|
|
477
|
+
* calls into — are still mounting. The Web app gates its own URL line and
|
|
478
|
+
* browser handoff on this exact settlement, so the guard's automatic reload
|
|
479
|
+
* must wait for it too. A composition without a Loader is already complete.
|
|
480
|
+
*
|
|
481
|
+
* Only settlement counts as ready: a failed tree leaves the tab on its old
|
|
482
|
+
* page (the watchdog's crash recovery owns that outcome), and the next healthy
|
|
483
|
+
* process answers the still-stale poll.
|
|
484
|
+
* @param ctx - context owning the Loader service.
|
|
485
|
+
* @returns probe consulted by every boot-generation poll.
|
|
486
|
+
*/
|
|
487
|
+
function applicationReadyProbe(ctx) {
|
|
488
|
+
const settled = ctx.get("loader")?.await();
|
|
489
|
+
if (settled === void 0) return () => true;
|
|
490
|
+
let ready = false;
|
|
491
|
+
settled.then(() => {
|
|
492
|
+
ready = true;
|
|
493
|
+
}, () => {});
|
|
494
|
+
return () => ready;
|
|
495
|
+
}
|
|
460
496
|
//#endregion
|
|
461
497
|
//#region lib/types/restart-request.js
|
|
462
498
|
/**
|
|
@@ -628,6 +664,29 @@ async function requestRestart(request, context) {
|
|
|
628
664
|
}
|
|
629
665
|
//#endregion
|
|
630
666
|
//#region lib/types/index.js
|
|
667
|
+
let presetRegistryProbe;
|
|
668
|
+
/**
|
|
669
|
+
* Resolve the preset-registry module by name. The 0.1.7-rc.1 host renamed
|
|
670
|
+
* `@deepseek-ai/dsh-agent-presets` to `@deepseek-ai/dsh-agent-preset-registry`
|
|
671
|
+
* (the resolve/mount/livePresetMounts/agentPresetProjectionDefinition API
|
|
672
|
+
* carried over); 0.1.5/0.1.6 hosts install only the old name, and a host
|
|
673
|
+
* without preset composition installs neither — `null`, and the derivation
|
|
674
|
+
* then receives `{}`, the established no-preset degrade (the resume falls
|
|
675
|
+
* back to the deployment's default preset). A STATIC import of either name is
|
|
676
|
+
* a module-resolution failure at load on hosts installing only the other —
|
|
677
|
+
* pack-dist strips `dependencies`, so the specifier resolves against the
|
|
678
|
+
* host's install tree. Both specifiers stay static string literals so tsdown
|
|
679
|
+
* externalizes them and tsc type-checks them.
|
|
680
|
+
* @returns the module namespace, or null when neither name resolves.
|
|
681
|
+
*/
|
|
682
|
+
function loadPresetRegistry() {
|
|
683
|
+
presetRegistryProbe ??= import("@deepseek-ai/dsh-agent-preset-registry").catch(() => import("@deepseek-ai/dsh-agent-presets")).catch(() => null);
|
|
684
|
+
return presetRegistryProbe;
|
|
685
|
+
}
|
|
686
|
+
/** Test seam: drop the cached probe so a mocked resolution path re-runs. */
|
|
687
|
+
function resetPresetRegistryProbeForTest() {
|
|
688
|
+
presetRegistryProbe = void 0;
|
|
689
|
+
}
|
|
631
690
|
/**
|
|
632
691
|
* Which preset a session actually runs, newest selection winning. The rc host
|
|
633
692
|
* exports `resolveSessionPreset` for exactly this fold; the 0.1.2 host
|
|
@@ -788,7 +847,7 @@ function apply(ctx, config) {
|
|
|
788
847
|
text
|
|
789
848
|
}],
|
|
790
849
|
source: {
|
|
791
|
-
kind: "
|
|
850
|
+
kind: "ankh-guard",
|
|
792
851
|
plugin: name,
|
|
793
852
|
form: "snapshot",
|
|
794
853
|
sections: [{
|
|
@@ -902,7 +961,7 @@ function apply(ctx, config) {
|
|
|
902
961
|
const persistence = probeColdReader(ctx);
|
|
903
962
|
if (presets !== void 0 && persistence !== void 0) {
|
|
904
963
|
const inspected = await readColdLog(persistence, id);
|
|
905
|
-
const presetId = deriveSessionPreset(
|
|
964
|
+
const presetId = deriveSessionPreset(await loadPresetRegistry() ?? {}, {
|
|
906
965
|
header: inspected.meta,
|
|
907
966
|
events: inspected.events
|
|
908
967
|
});
|
|
@@ -988,7 +1047,7 @@ function apply(ctx, config) {
|
|
|
988
1047
|
text
|
|
989
1048
|
}],
|
|
990
1049
|
source: {
|
|
991
|
-
kind: "
|
|
1050
|
+
kind: "ankh-guard",
|
|
992
1051
|
plugin: name,
|
|
993
1052
|
form: "snapshot",
|
|
994
1053
|
sections: [{
|
|
@@ -1066,4 +1125,4 @@ function apply(ctx, config) {
|
|
|
1066
1125
|
})();
|
|
1067
1126
|
}
|
|
1068
1127
|
//#endregion
|
|
1069
|
-
export { Config, apply, deriveSessionPreset, inject, name };
|
|
1128
|
+
export { Config, apply, deriveSessionPreset, inject, loadPresetRegistry, name, resetPresetRegistryProbeForTest };
|
package/lib/preflight-runner.js
CHANGED
|
@@ -121,7 +121,7 @@ var PreflightInfraError = class extends Error {};
|
|
|
121
121
|
* bundle layers in `dsh.profile.bundles` order, the profile user layer, the
|
|
122
122
|
* home-level user layer, `--patch` overlays, the agent-presets roots overlay
|
|
123
123
|
* (rc host line only; the 0.1.2 line's preset package self-ships its root),
|
|
124
|
-
* then the telemetry switch.
|
|
124
|
+
* then the telemetry switch. Three host API generations are mirrored and
|
|
125
125
|
* feature-detected per run — see the `hostLine` branch below. Exported so
|
|
126
126
|
* the drift tripwire can compare this assembly against the launcher's own
|
|
127
127
|
* dump without booting anything.
|
|
@@ -147,23 +147,46 @@ async function composePreflightPatches(profile, patchFiles, root, home, binding
|
|
|
147
147
|
}
|
|
148
148
|
const composeEntries = appBoot.composeEntries;
|
|
149
149
|
const healProfilesModuleFallback = appBoot.healProfilesModuleFallback;
|
|
150
|
+
const createProfileResolutionGeneration = appBoot.createProfileResolutionGeneration;
|
|
151
|
+
const createRuntimeResolution = appBoot.createRuntimeResolution;
|
|
150
152
|
const loadOptionalPatches = appBoot.loadOptionalPatches;
|
|
151
153
|
const loadOverlayPatches = appBoot.loadOverlayPatches;
|
|
152
154
|
const loadProfile = appBoot.loadProfile;
|
|
153
155
|
const resolveDshHome = homePaths.resolveDshHome;
|
|
154
156
|
const anchor = binding.installAnchor;
|
|
155
157
|
const resolvedHome = resolveDshHome(home);
|
|
156
|
-
const hostLine = "DEFAULT_PROFILE_PATCH_RELOAD" in appBoot ? "0.1.2" : "rc";
|
|
158
|
+
const hostLine = "createRuntimeResolution" in appBoot ? "0.1.7" : "createProfileResolutionGeneration" in appBoot ? "0.1.6" : "DEFAULT_PROFILE_PATCH_RELOAD" in appBoot ? "0.1.2" : "rc";
|
|
157
159
|
if (hostLine === "rc") healProfilesModuleFallback(anchor, resolvedHome);
|
|
158
160
|
const composed = loadProfile(NAME, profile, anchor, resolvedHome, { userLayer: true });
|
|
159
161
|
writeFileSync(join(composed.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG);
|
|
162
|
+
let pluginPackagesConfig;
|
|
160
163
|
if (hostLine === "0.1.2") await healProfilesModuleFallback({
|
|
161
164
|
installAnchor: anchor,
|
|
162
165
|
profile: composed,
|
|
163
166
|
home: resolvedHome
|
|
164
167
|
});
|
|
168
|
+
else if (hostLine === "0.1.6") pluginPackagesConfig = { generation: await createProfileResolutionGeneration({
|
|
169
|
+
installAnchor: anchor,
|
|
170
|
+
profile: composed,
|
|
171
|
+
home: resolvedHome
|
|
172
|
+
}) };
|
|
173
|
+
else if (hostLine === "0.1.7") pluginPackagesConfig = { resolution: await createRuntimeResolution({
|
|
174
|
+
installAnchor: anchor,
|
|
175
|
+
profile: composed
|
|
176
|
+
}) };
|
|
165
177
|
const homePatches = loadOptionalPatches(NAME, join(resolvedHome, HOME_PATCH_FILENAME)) ?? [];
|
|
166
178
|
const overlays = patchFiles.flatMap((file) => loadOverlayPatches(NAME, resolve(file)));
|
|
179
|
+
const profileContext = hostLine === "0.1.6" || hostLine === "0.1.7" ? {
|
|
180
|
+
name: profile,
|
|
181
|
+
dir: composed.dir,
|
|
182
|
+
patchPath: composed.patchPath,
|
|
183
|
+
installAnchor: anchor,
|
|
184
|
+
startedBundles: composed.layers.map((layer) => layer.packageName),
|
|
185
|
+
cwd: process.cwd(),
|
|
186
|
+
home: resolvedHome,
|
|
187
|
+
overlays,
|
|
188
|
+
telemetryDisabledEnv: process.env.DSH_TELEMETRY_DISABLED
|
|
189
|
+
} : void 0;
|
|
167
190
|
const bundlePatches = composed.layers.flatMap((layer) => layer.patches);
|
|
168
191
|
const patches = [
|
|
169
192
|
...bundlePatches,
|
|
@@ -197,6 +220,10 @@ async function composePreflightPatches(profile, patchFiles, root, home, binding
|
|
|
197
220
|
openBrowser: false
|
|
198
221
|
}
|
|
199
222
|
});
|
|
223
|
+
if (profileContext !== void 0 && rows.has("hmr")) composedOverlays.push({
|
|
224
|
+
id: "hmr",
|
|
225
|
+
disabled: true
|
|
226
|
+
});
|
|
200
227
|
if (rows.has("ankh-guard")) composedOverlays.push({
|
|
201
228
|
id: "ankh-guard",
|
|
202
229
|
config: {
|
|
@@ -213,7 +240,9 @@ async function composePreflightPatches(profile, patchFiles, root, home, binding
|
|
|
213
240
|
return {
|
|
214
241
|
patches,
|
|
215
242
|
rows,
|
|
216
|
-
profileDir: composed.dir
|
|
243
|
+
profileDir: composed.dir,
|
|
244
|
+
...profileContext === void 0 ? {} : { profileContext },
|
|
245
|
+
...pluginPackagesConfig === void 0 ? {} : { pluginPackagesConfig }
|
|
217
246
|
};
|
|
218
247
|
}
|
|
219
248
|
/**
|
|
@@ -291,6 +320,7 @@ async function runPreflight(profile, patchFiles = [], root = resolveHarnessRoot(
|
|
|
291
320
|
const loadLayeredEnv = appBoot.loadLayeredEnv;
|
|
292
321
|
const launchEnvironmentKey = launchEnvironment.DSH_LAUNCH_ENVIRONMENT_KEY;
|
|
293
322
|
const provideCmdline = cmdline.provideCmdline;
|
|
323
|
+
const PluginPackages = appBoot.PluginPackages;
|
|
294
324
|
let environment;
|
|
295
325
|
try {
|
|
296
326
|
environment = loadLayeredEnv(NAME);
|
|
@@ -311,8 +341,13 @@ async function runPreflight(profile, patchFiles = [], root = resolveHarnessRoot(
|
|
|
311
341
|
});
|
|
312
342
|
const rootConfig = join(composed.profileDir, PROFILE_ROOT_FILENAME);
|
|
313
343
|
const appReady = createAppReadyStub();
|
|
314
|
-
const ctx = await boot(NAME, rootConfig, structuredClone(patches), (hostCtx) => {
|
|
344
|
+
const ctx = await boot(NAME, rootConfig, structuredClone(patches), async (hostCtx) => {
|
|
345
|
+
if (composed.profileContext !== void 0) hostCtx.provide?.("profileContext", composed.profileContext);
|
|
315
346
|
hostCtx.provide?.(launchEnvironmentKey, environment);
|
|
347
|
+
if (composed.pluginPackagesConfig !== void 0) {
|
|
348
|
+
if (PluginPackages === void 0 || hostCtx.plugin === void 0) throw new Error("host line requires a PluginPackages mount but the loaded app-boot does not export PluginPackages");
|
|
349
|
+
await hostCtx.plugin(PluginPackages, composed.pluginPackagesConfig);
|
|
350
|
+
}
|
|
316
351
|
provideCmdline(hostCtx, {
|
|
317
352
|
args: [],
|
|
318
353
|
exit: () => {},
|
|
@@ -41,6 +41,12 @@ interface BrowserHandoffDependencies {
|
|
|
41
41
|
connection?: ConnectionSlice;
|
|
42
42
|
/** Resolve lazily because WebServer can activate before the optional connection service. */
|
|
43
43
|
connectionProvider?: () => ConnectionSlice | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Whether the serving composition has finished mounting, consulted only by
|
|
46
|
+
* the boot-generation channel. Omitted means ready — a hand-built tree
|
|
47
|
+
* without a Loader is already complete.
|
|
48
|
+
*/
|
|
49
|
+
applicationReady?: () => boolean;
|
|
44
50
|
/** Test seams; production defaults turn idle polling into a held request. */
|
|
45
51
|
longPollMs?: number;
|
|
46
52
|
longPollIntervalMs?: number;
|
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
* means the process it knew is gone — answer ready/reload. A cutover receipt
|
|
17
17
|
* always wins over the generation check: during a cutover the old tab's known
|
|
18
18
|
* id is already stale, and the receipt protocol owns the pacing.
|
|
19
|
+
*
|
|
20
|
+
* The generation answer waits for the successor's composition to settle. The
|
|
21
|
+
* shared Web-server seat answers as soon as it listens — well before sibling
|
|
22
|
+
* rows mount — and a page that loads then calls into a half-mounted /api
|
|
23
|
+
* surface (the Web app gates its own browser handoff on the same Loader
|
|
24
|
+
* settlement for that reason). Until then the poll is answered `waiting`
|
|
25
|
+
* WITHOUT the boot id: teaching the tab the successor's id would consume its
|
|
26
|
+
* one-shot generation check and strand it on an unusable page.
|
|
19
27
|
*/
|
|
20
28
|
import { createHash, randomBytes } from 'node:crypto';
|
|
21
29
|
import { chmodSync, linkSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
@@ -313,6 +321,13 @@ export function createBrowserHandoffHandler(dependencies) {
|
|
|
313
321
|
// would race the registration/ack dance.
|
|
314
322
|
if (bootId !== undefined && message.knownBootId !== undefined && message.knownBootId !== bootId
|
|
315
323
|
&& activeCutover(dependencies.stateDir) === null) {
|
|
324
|
+
if (dependencies.applicationReady?.() === false) {
|
|
325
|
+
// The successor's composition is still mounting. Answer WITHOUT the
|
|
326
|
+
// boot id (not through `respond`): the tab must keep its stale id so
|
|
327
|
+
// this one-shot generation check survives until readiness lands.
|
|
328
|
+
json(res, 200, { state: 'waiting' });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
316
331
|
respond(200, { state: 'ready', action: 'reload', authentication: 'existing-cookie' });
|
|
317
332
|
return;
|
|
318
333
|
}
|
|
@@ -483,7 +498,31 @@ export function registerBrowserHandoff(ctx, stateDir) {
|
|
|
483
498
|
const handler = createBrowserHandoffHandler({
|
|
484
499
|
stateDir,
|
|
485
500
|
connectionProvider: () => webCtx.get('connection'),
|
|
501
|
+
applicationReady: applicationReadyProbe(webCtx),
|
|
486
502
|
});
|
|
487
503
|
webCtx.effect(() => webServer.register({ kind: 'exact', path: BROWSER_HANDOFF_ROUTE, handler }), 'ankh-guard: browser handoff route');
|
|
488
504
|
});
|
|
489
505
|
}
|
|
506
|
+
/**
|
|
507
|
+
* Build the generation channel's readiness probe from the Loader tree.
|
|
508
|
+
*
|
|
509
|
+
* The Web-server seat is shared: it answers as soon as it listens, while
|
|
510
|
+
* sibling rows — including the /api route owner a reloaded page immediately
|
|
511
|
+
* calls into — are still mounting. The Web app gates its own URL line and
|
|
512
|
+
* browser handoff on this exact settlement, so the guard's automatic reload
|
|
513
|
+
* must wait for it too. A composition without a Loader is already complete.
|
|
514
|
+
*
|
|
515
|
+
* Only settlement counts as ready: a failed tree leaves the tab on its old
|
|
516
|
+
* page (the watchdog's crash recovery owns that outcome), and the next healthy
|
|
517
|
+
* process answers the still-stale poll.
|
|
518
|
+
* @param ctx - context owning the Loader service.
|
|
519
|
+
* @returns probe consulted by every boot-generation poll.
|
|
520
|
+
*/
|
|
521
|
+
function applicationReadyProbe(ctx) {
|
|
522
|
+
const settled = ctx.get('loader')?.await();
|
|
523
|
+
if (settled === undefined)
|
|
524
|
+
return () => true;
|
|
525
|
+
let ready = false;
|
|
526
|
+
void settled.then(() => { ready = true; }, () => { });
|
|
527
|
+
return () => ready;
|
|
528
|
+
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -18,6 +18,15 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import type { Context } from '@deepseek-ai/cordis';
|
|
20
20
|
import z from '@deepseek-ai/schemastery';
|
|
21
|
+
import type { ContextFormed } from '@deepseek-ai/dsh-llm';
|
|
22
|
+
declare module '@deepseek-ai/dsh-llm' {
|
|
23
|
+
interface MessageSourceMap {
|
|
24
|
+
'ankh-guard': {
|
|
25
|
+
kind: 'ankh-guard';
|
|
26
|
+
plugin: string;
|
|
27
|
+
} & ContextFormed;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
21
30
|
import { type RestartRequest, type RestartRequestResult } from './restart-request.ts';
|
|
22
31
|
import { type GuardState, type VerifyResult } from './state.ts';
|
|
23
32
|
/**
|
|
@@ -52,6 +61,23 @@ export interface PresetDerivationSurface {
|
|
|
52
61
|
};
|
|
53
62
|
resolveSessionPreset?: (session: PersistedPresetSource) => string | undefined;
|
|
54
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the preset-registry module by name. The 0.1.7-rc.1 host renamed
|
|
66
|
+
* `@deepseek-ai/dsh-agent-presets` to `@deepseek-ai/dsh-agent-preset-registry`
|
|
67
|
+
* (the resolve/mount/livePresetMounts/agentPresetProjectionDefinition API
|
|
68
|
+
* carried over); 0.1.5/0.1.6 hosts install only the old name, and a host
|
|
69
|
+
* without preset composition installs neither — `null`, and the derivation
|
|
70
|
+
* then receives `{}`, the established no-preset degrade (the resume falls
|
|
71
|
+
* back to the deployment's default preset). A STATIC import of either name is
|
|
72
|
+
* a module-resolution failure at load on hosts installing only the other —
|
|
73
|
+
* pack-dist strips `dependencies`, so the specifier resolves against the
|
|
74
|
+
* host's install tree. Both specifiers stay static string literals so tsdown
|
|
75
|
+
* externalizes them and tsc type-checks them.
|
|
76
|
+
* @returns the module namespace, or null when neither name resolves.
|
|
77
|
+
*/
|
|
78
|
+
export declare function loadPresetRegistry(): Promise<unknown>;
|
|
79
|
+
/** Test seam: drop the cached probe so a mocked resolution path re-runs. */
|
|
80
|
+
export declare function resetPresetRegistryProbeForTest(): void;
|
|
55
81
|
/**
|
|
56
82
|
* Which preset a session actually runs, newest selection winning. The rc host
|
|
57
83
|
* exports `resolveSessionPreset` for exactly this fold; the 0.1.2 host
|
|
@@ -68,11 +94,11 @@ export declare function deriveSessionPreset(host: PresetDerivationSurface, sessi
|
|
|
68
94
|
/** Plugin configuration. */
|
|
69
95
|
export interface SelfRestartGuardConfig {
|
|
70
96
|
/** Credential freshness window in minutes (default 10). */
|
|
71
|
-
maxAgeMinutes?: number;
|
|
97
|
+
maxAgeMinutes?: number | undefined;
|
|
72
98
|
/** State directory; defaults to $DSH_HOME/state, else `<cwd>/.dsh-guard-state`. */
|
|
73
|
-
stateDir?: string;
|
|
99
|
+
stateDir?: string | undefined;
|
|
74
100
|
/** Repository the credential binds to; defaults to the process cwd. */
|
|
75
|
-
repoDir?: string;
|
|
101
|
+
repoDir?: string | undefined;
|
|
76
102
|
/**
|
|
77
103
|
* How to surface a scheduled restart's record to the agent (default
|
|
78
104
|
* `followup` — fully autonomous: the plugin queues the report as the next
|
|
@@ -80,7 +106,7 @@ export interface SelfRestartGuardConfig {
|
|
|
80
106
|
* system uses for reminders, so the agent reports without any user message).
|
|
81
107
|
* `step` rides the first step of whatever turn comes next; `off` disables.
|
|
82
108
|
*/
|
|
83
|
-
reportRestartContext?: 'followup' | 'step' | 'off';
|
|
109
|
+
reportRestartContext?: 'followup' | 'step' | 'off' | undefined;
|
|
84
110
|
/**
|
|
85
111
|
* Resume the sessions a restart interrupted (default true). At SIGTERM the
|
|
86
112
|
* plugin snapshots which root sessions had a live turn (plus the restart's
|
|
@@ -90,21 +116,21 @@ export interface SelfRestartGuardConfig {
|
|
|
90
116
|
* The pass only runs on a restart boot (restart marker or pending restart
|
|
91
117
|
* record present); a cold start drops the snapshot without acting.
|
|
92
118
|
*/
|
|
93
|
-
resumeInterrupted?: boolean;
|
|
119
|
+
resumeInterrupted?: boolean | undefined;
|
|
94
120
|
/**
|
|
95
121
|
* Delay before the interrupted-session resume pass runs after plugin load
|
|
96
122
|
* (default 5000 ms), so the pass starts turns only after the app's services
|
|
97
123
|
* are up.
|
|
98
124
|
*/
|
|
99
|
-
resumeDelayMs?: number;
|
|
125
|
+
resumeDelayMs?: number | undefined;
|
|
100
126
|
/**
|
|
101
127
|
* Maximum age of the interrupted-session snapshot the resume pass honors
|
|
102
128
|
* (default 600000 ms, ten minutes). A snapshot older than that comes from a
|
|
103
129
|
* manual stop/start, not a restart, and is dropped without acting.
|
|
104
130
|
*/
|
|
105
|
-
resumeMaxSnapshotAgeMs?: number;
|
|
131
|
+
resumeMaxSnapshotAgeMs?: number | undefined;
|
|
106
132
|
}
|
|
107
|
-
export declare const Config: z
|
|
133
|
+
export declare const Config: z;
|
|
108
134
|
/** One canary check line. */
|
|
109
135
|
export interface CanaryCheck {
|
|
110
136
|
name: string;
|
package/lib/types/index.js
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import z from '@deepseek-ai/schemastery';
|
|
2
2
|
import { connect } from 'node:net';
|
|
3
3
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
4
|
-
// Namespace handle for runtime feature detection: the 0.1.2 host replaced
|
|
5
|
-
// the `resolveSessionPreset` free function (and the `PresetBearingSession`
|
|
6
|
-
// type) with the `agentPresetProjectionDefinition` unit, and a STATIC named
|
|
7
|
-
// import of a removed export is a SyntaxError at module load — exactly the
|
|
8
|
-
// failure this dual-host probing exists to survive. The derivation below
|
|
9
|
-
// reads both surfaces through one structural cast.
|
|
10
|
-
import * as agentPresetsHost from '@deepseek-ai/dsh-agent-presets';
|
|
11
4
|
import { existsSync, readFileSync, unlinkSync } from 'node:fs';
|
|
12
5
|
import { dirname, join } from 'node:path';
|
|
13
6
|
import { fileURLToPath } from 'node:url';
|
|
@@ -20,6 +13,31 @@ import { registerBrowserHandoff } from "./browser-handoff.js";
|
|
|
20
13
|
import { requestRestart } from "./restart-request.js";
|
|
21
14
|
import { acknowledgeRestartRecord, buildLaunchCommand, continueAndReportText, continueInterruptedText, interruptedSnapshotFile, isParkedOnUserInput, writeInstanceLaunch, writeSkillRegistration, pendingRestartRecord, readInterruptedSnapshot, restartContextText, writeInterruptedSnapshot, } from "./restart-context.js";
|
|
22
15
|
import { clearCredential, loadState, recordCredential, setCheckpoint, verifyCredential, } from "./state.js";
|
|
16
|
+
let presetRegistryProbe;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the preset-registry module by name. The 0.1.7-rc.1 host renamed
|
|
19
|
+
* `@deepseek-ai/dsh-agent-presets` to `@deepseek-ai/dsh-agent-preset-registry`
|
|
20
|
+
* (the resolve/mount/livePresetMounts/agentPresetProjectionDefinition API
|
|
21
|
+
* carried over); 0.1.5/0.1.6 hosts install only the old name, and a host
|
|
22
|
+
* without preset composition installs neither — `null`, and the derivation
|
|
23
|
+
* then receives `{}`, the established no-preset degrade (the resume falls
|
|
24
|
+
* back to the deployment's default preset). A STATIC import of either name is
|
|
25
|
+
* a module-resolution failure at load on hosts installing only the other —
|
|
26
|
+
* pack-dist strips `dependencies`, so the specifier resolves against the
|
|
27
|
+
* host's install tree. Both specifiers stay static string literals so tsdown
|
|
28
|
+
* externalizes them and tsc type-checks them.
|
|
29
|
+
* @returns the module namespace, or null when neither name resolves.
|
|
30
|
+
*/
|
|
31
|
+
export function loadPresetRegistry() {
|
|
32
|
+
presetRegistryProbe ??= import('@deepseek-ai/dsh-agent-preset-registry')
|
|
33
|
+
.catch(() => import('@deepseek-ai/dsh-agent-presets'))
|
|
34
|
+
.catch(() => null);
|
|
35
|
+
return presetRegistryProbe;
|
|
36
|
+
}
|
|
37
|
+
/** Test seam: drop the cached probe so a mocked resolution path re-runs. */
|
|
38
|
+
export function resetPresetRegistryProbeForTest() {
|
|
39
|
+
presetRegistryProbe = undefined;
|
|
40
|
+
}
|
|
23
41
|
/**
|
|
24
42
|
* Which preset a session actually runs, newest selection winning. The rc host
|
|
25
43
|
* exports `resolveSessionPreset` for exactly this fold; the 0.1.2 host
|
|
@@ -178,7 +196,7 @@ export function apply(ctx, config) {
|
|
|
178
196
|
if (followupReport || resumeInterrupted) {
|
|
179
197
|
const pluginMessage = (text) => createUserMessage({
|
|
180
198
|
content: [{ type: 'text', text }],
|
|
181
|
-
source: { kind: '
|
|
199
|
+
source: { kind: 'ankh-guard', plugin: name, form: 'snapshot', sections: [{ name: 'restart', text }] },
|
|
182
200
|
});
|
|
183
201
|
// Interrupted sessions awaiting their `agent/created` to receive the
|
|
184
202
|
// "continue" followup: session id → exitAt of the interrupting exit.
|
|
@@ -359,7 +377,8 @@ export function apply(ctx, config) {
|
|
|
359
377
|
const persistence = probeColdReader(ctx);
|
|
360
378
|
if (presets !== undefined && persistence !== undefined) {
|
|
361
379
|
const inspected = await readColdLog(persistence, id);
|
|
362
|
-
const
|
|
380
|
+
const host = await loadPresetRegistry();
|
|
381
|
+
const presetId = deriveSessionPreset((host ?? {}), {
|
|
363
382
|
header: inspected.meta,
|
|
364
383
|
events: inspected.events,
|
|
365
384
|
});
|
|
@@ -452,6 +471,9 @@ export function apply(ctx, config) {
|
|
|
452
471
|
}, 250);
|
|
453
472
|
ctx.effect(() => () => { clearInterval(releaseTimer); });
|
|
454
473
|
}
|
|
474
|
+
// agent/created went @mode serial in 0.1.6: the creation transaction
|
|
475
|
+
// awaits whatever a listener returns, so delivery stays fire-and-forget
|
|
476
|
+
// here — return nothing, and never await agent.whenIdle in this listener.
|
|
455
477
|
ctx.on('agent/created', ({ agent }) => {
|
|
456
478
|
if (!ctx.agents.roots().includes(agent))
|
|
457
479
|
return;
|
|
@@ -482,7 +504,7 @@ export function apply(ctx, config) {
|
|
|
482
504
|
createUserMessage({
|
|
483
505
|
content: [{ type: 'text', text }],
|
|
484
506
|
source: {
|
|
485
|
-
kind: '
|
|
507
|
+
kind: 'ankh-guard', plugin: name, form: 'snapshot',
|
|
486
508
|
sections: [{ name: 'restart', text }],
|
|
487
509
|
},
|
|
488
510
|
}),
|
|
@@ -52,13 +52,33 @@ export interface PreflightComposition {
|
|
|
52
52
|
}>;
|
|
53
53
|
/** The profile directory (the include root's anchor). */
|
|
54
54
|
profileDir: string;
|
|
55
|
+
/**
|
|
56
|
+
* The launcher-owned profileContext service value (0.1.6+ lines), built
|
|
57
|
+
* field-for-field as runProfile builds it. Undefined on the heal-based
|
|
58
|
+
* lines, whose launcher provides no such service. The boot prepare must
|
|
59
|
+
* provide it before the tree mounts: the 0.1.7 settings service injects
|
|
60
|
+
* profileContext, so without it every settings-dependent apply never runs.
|
|
61
|
+
*/
|
|
62
|
+
profileContext?: Record<string, unknown>;
|
|
63
|
+
/**
|
|
64
|
+
* The PluginPackages mount config the boot prepare must apply, mirroring
|
|
65
|
+
* the launcher: `{ generation }` on the 0.1.6 line, `{ resolution }` on the
|
|
66
|
+
* 0.1.7 line. Undefined on the heal-based lines (rc, 0.1.2), where the heal
|
|
67
|
+
* materializes real fallback links and native Node resolution carries the
|
|
68
|
+
* tree — the launcher mounts no PluginPackages there either. Without this
|
|
69
|
+
* mount the in-memory resolution is computed and discarded, so on a tarball
|
|
70
|
+
* profile (whose own node_modules holds no `@deepseek-ai/*` entries) every
|
|
71
|
+
* entry import fails natively while the real boot of the same profile is
|
|
72
|
+
* clean.
|
|
73
|
+
*/
|
|
74
|
+
pluginPackagesConfig?: Record<string, unknown>;
|
|
55
75
|
}
|
|
56
76
|
/**
|
|
57
77
|
* Compose one profile's full patch stack through the launcher's layering —
|
|
58
78
|
* bundle layers in `dsh.profile.bundles` order, the profile user layer, the
|
|
59
79
|
* home-level user layer, `--patch` overlays, the agent-presets roots overlay
|
|
60
80
|
* (rc host line only; the 0.1.2 line's preset package self-ships its root),
|
|
61
|
-
* then the telemetry switch.
|
|
81
|
+
* then the telemetry switch. Three host API generations are mirrored and
|
|
62
82
|
* feature-detected per run — see the `hostLine` branch below. Exported so
|
|
63
83
|
* the drift tripwire can compare this assembly against the launcher's own
|
|
64
84
|
* dump without booting anything.
|