@menghuan1918/dsh-apollo 0.5.1 → 0.5.3

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.md CHANGED
@@ -86,13 +86,13 @@ flowchart TB
86
86
 
87
87
  ```sh
88
88
  # npm 安装(发布后)
89
- dsh plugin --profile <profile> add @menghuan1918/dsh-apollo@0.5.1
89
+ dsh plugin --profile <profile> add @menghuan1918/dsh-apollo@0.5.2
90
90
 
91
91
  # 或从 GitHub main 源码安装(备选)
92
92
  dsh plugin --profile <profile> add github:Menghuan1918/dsh-apollo
93
93
  ```
94
94
 
95
- > **发布状态(诚实说明)**:Apollo `0.5.0` 已发布,`dsh-better-subagent@0.1.0` / `dsh-better-compact@0.1.0` 作为其精确依赖已可从 npm 取得。当前版本 `0.5.1` 是旧 profile `name` guard 的迁移修复版;本仓库 0.5.1 的验证使用本地 tarball `npm install --no-save`,提交的 manifest 中**没有** `file:` / `link:` / Git 说明符。
95
+ > **发布状态(诚实说明)**:Apollo `0.5.0` 已发布,`dsh-better-subagent@0.1.0` / `dsh-better-compact@0.1.1` 作为其精确依赖已可从 npm 取得。当前版本 `0.5.2` 是 compact 0.1.1 的设置表单 schema 修复版;本仓库 0.5.1 的验证使用本地 tarball `npm install --no-save`,提交的 manifest 中**没有** `file:` / `link:` / Git 说明符。
96
96
 
97
97
  安装、升级或换包都会改变运行的代码,必须重启对应 profile 的 DSH 进程;只刷新网页不够。目标行、子代理行与压缩行由 Apollo 的 bundle patch 插入,不需要再单独 `add` 三个子包。
98
98
 
@@ -102,14 +102,14 @@ dsh plugin --profile <profile> add github:Menghuan1918/dsh-apollo
102
102
 
103
103
  ```sh
104
104
  dsh plugin --profile <profile> remove dsh-apollo
105
- dsh plugin --profile <profile> add @menghuan1918/dsh-apollo@0.5.1
105
+ dsh plugin --profile <profile> add @menghuan1918/dsh-apollo@0.5.2
106
106
  ```
107
107
 
108
108
  > `dsh-apollo` 只是旧入口名;unscoped 的 `dsh-apollo` 已被其他项目占用,不要从 npm 安装它。
109
109
 
110
110
  ## 从 0.4.x 升级
111
111
 
112
- 0.5.0 把子代理与压缩能力移出 Apollo,0.5.1 修复旧 profile 行的 `name` guard 迁移缺口。旧 profile 不需要手工重填配置:Apollo 启动后在 settings/loader 可用时运行一次**幂等迁移**([src/migrate.js](src/migrate.js))。
112
+ 0.5.0 把子代理与压缩能力移出 Apollo,0.5.1 修复旧 profile 行的 `name` guard 迁移缺口;0.5.2 依赖 compact 0.1.1,修复 Web 设置表单的 schema rehydrate。旧 profile 不需要手工重填配置:Apollo 启动后在 settings/loader 可用时运行一次**幂等迁移**([src/migrate.js](src/migrate.js))。
113
113
 
114
114
  > **0.5.1 迁移修复**:如果旧 profile patch 的 `apollo-skills` 覆盖还带历史名称 `name: dsh-apollo`,DSH 会把 `name` 当 guard 并跳过该覆盖,导致有效配置看起来仍是默认值。0.5.1 从 `configEditor.configuration().override` 恢复这些显式值(含 `false` / `0` / `""` / `[]`),再合并到迁移来源;被跳过的旧预设覆盖(`preset-eng` / `preset-eng-ptc`)同样处理。无需手工修改旧行;升级后重启 profile 即可。
115
115
 
@@ -171,7 +171,7 @@ flowchart LR
171
171
  dsh plugin --profile <profile> add @menghuan1918/dsh-better-subagent@0.1.0
172
172
 
173
173
  # 只装压缩
174
- dsh plugin --profile <profile> add dsh-better-compact@0.1.0
174
+ dsh plugin --profile <profile> add dsh-better-compact@0.1.1
175
175
  ```
176
176
 
177
177
  独立安装时的行、页面和提示词与 Apollo 集成完全相同;行 id/name 也是同一组稳定值。区别只在**由谁插入这些行**:Apollo 的 bundle patch 自己携带同样的行,因为 DSH 不会自动应用传递依赖的 bundle patch。
@@ -226,7 +226,7 @@ dsh plugin --profile <profile> remove dsh-better-compact
226
226
  - `package.json` 的 `files` 只列入 `docs/DEVELOPMENT.md`,并额外用 [.npmignore](.npmignore) 排除 `docs/specs/`;
227
227
  - `tests/packaging.test.mjs` 直接运行 `npm pack --dry-run --json`,断言发布清单没有 Specs、测试、脚本、快照或 tarball。
228
228
 
229
- 发布顺序:**先发布并验证两个独立包(`@menghuan1918/dsh-better-subagent@0.1.0`、`dsh-better-compact@0.1.0`),再发布 Apollo 0.5.1**。两个 0.1.0 已随 0.5.0 发布;0.5.1 可只发布本包。Apollo 的精确依赖指向尚不存在的版本时,`npm install` 会失败;不要用开发 `file:`/`link:` 依赖发布。两个子仓各自有 trusted publishing workflow;Apollo 的 [release workflow](.github/workflows/release.yml) 只发布本包。
229
+ 发布顺序:**先发布并验证两个独立包(`@menghuan1918/dsh-better-subagent@0.1.0`、`dsh-better-compact@0.1.1`),再发布 Apollo 0.5.2**。两个 0.1.0 已随 0.5.0 发布;0.5.1 可只发布本包。Apollo 的精确依赖指向尚不存在的版本时,`npm install` 会失败;不要用开发 `file:`/`link:` 依赖发布。两个子仓各自有 trusted publishing workflow;Apollo 的 [release workflow](.github/workflows/release.yml) 只发布本包。
230
230
 
231
231
  ## 验证状态(诚实记录)
232
232
 
@@ -239,7 +239,7 @@ dsh plugin --profile <profile> remove dsh-better-compact
239
239
 
240
240
  **未验证 / 明确限制**:
241
241
 
242
- - 两个子包的 `0.1.0` 尚未发布;本轮只用本地 tarball 验证,未验证 npm registry 安装路径。
242
+ - 子包 `@menghuan1918/dsh-better-subagent@0.1.0` 与 `dsh-better-compact@0.1.1` 已可从 npm 取得。
243
243
  - 未修改或重启用户当前运行的 profile,未验证当前 GUI(`http://127.0.0.1:3384`)的设置页/卡片;S3 视觉设置页的 GUI 验收归 S3/最终宿主验收。
244
244
  - 未运行真实 Codex/Claude CLI 的真实模型回合(那属于两个子包自己的验证;Apollo 侧本轮只用脚本化适配器)。
245
245
  - 真实模型摘要保真、缓存命中率与跨版本预设漂移仍需按实际环境复验。
@@ -2,7 +2,7 @@
2
2
 
3
3
  面向维护者、集成者和需要排查装配问题的用户。用户安装与配置请看 [README](../README.md);本文保留 DSH 插件的实现细节、旧配置迁移、组合安装边界与验证方法。
4
4
 
5
- 当前实现版本:`0.5.1`;npm 包名是 `@menghuan1918/dsh-apollo`。纯 ESM,无构建步骤;宿主浏览器半边是直接加载的闭包工厂。
5
+ 当前实现版本:`0.5.3`;npm 包名是 `@menghuan1918/dsh-apollo`。纯 ESM,无构建步骤;宿主浏览器半边是直接加载的闭包工厂。
6
6
 
7
7
  ## 目录
8
8
 
@@ -34,16 +34,15 @@
34
34
  | 依赖 | 版本 | 所有权 |
35
35
  |---|---|---|
36
36
  | `dsh-better-goal` | `0.2.0` | 目标域 |
37
- | `@menghuan1918/dsh-better-subagent` | `0.1.0` | 子代理 CLI/等待/child guard/分档/设置页 |
38
- | `dsh-better-compact` | `0.1.0` | 压缩 companion + engine + 设置页/诊断 |
37
+ | `@menghuan1918/dsh-better-subagent` | `0.1.1` | 子代理 CLI/等待/child guard/分档/设置页 |
38
+ | `dsh-better-compact` | `0.1.2` | 压缩 companion + engine + 设置页/诊断 |
39
39
  | `@deepseek-ai/schemastery` | `~3.18.4` | Apollo 行 Config |
40
40
  | `react` | peer | 宿主提供 |
41
41
 
42
- 两个 0.1.0 版本在实现完成时尚未发布到 npm;验证使用本地 tarball:
42
+ 正式安装从 npm registry 获取上述精确版本,不需要本地 tarball 或 link:
43
43
 
44
44
  ```sh
45
45
  npm install
46
- npm install --no-save /path/to/menghuan1918-dsh-better-subagent-0.1.0.tgz /path/to/dsh-better-compact-0.1.0.tgz
47
46
  ```
48
47
 
49
48
  发布顺序必须是先两个子包、后 Apollo(见[发布](#实现入口打包与发布))。
@@ -192,7 +191,7 @@ npm install --no-save /path/to/menghuan1918-dsh-better-subagent-0.1.0.tgz /path/
192
191
  1. 有 Loader 时等待装配完成(`loader.await()`,有界);
193
192
  2. 等待 `legacyImport`(旧 eng.json)完成;
194
193
  3. 有界轮询 `settings.describe()`,直到 `dsh-better-subagent` 与 `dsh-better-compact` 两个命名空间都出现(默认 5s,50ms 间隔);
195
- 4. 子代理迁移 → 压缩迁移,各自写入后重读验证;
194
+ 4. 子代理迁移 → 恢复 Apollo job 开关 → 压缩迁移,各自写入后重读验证;
196
195
  5. 返回状态并记录日志(complete / pending / blocked / error)。
197
196
 
198
197
  若目标命名空间一直不可读/不可写(例如双显式 bundle 导致写入被拒),日志明确要求重载 profile,迁移保持幂等,下次启动会再试;不宣告成功。迁移不删除旧值,也不创建迁移 sidecar。
@@ -205,6 +204,8 @@ npm install --no-save /path/to/menghuan1918-dsh-better-subagent-0.1.0.tgz /path/
205
204
 
206
205
  **0.5.1 修复**:DSH 把 profile 覆盖行的 `name` 当 guard。旧 profile 若保留历史 `name: dsh-apollo`,Loader 会跳过该覆盖,`describe().value` 只剩默认值(例如用户显式 `subagents.eng: false` 会被看成默认 `true`)。迁移现在额外读取 `configEditor.configuration()` 中该行的 `override`,递归合并到有效配置之上:普通对象逐键合并,数组与所有标量(含 `false`/`0`/`""`/`[]`)整体覆盖。这样被跳过的显式值仍会迁移。`preset-eng` / `preset-eng-ptc` 的预设覆盖也按同样方式合并(`plugins` 数组整体替换),再读取原生 `compaction-basic`。
207
206
 
207
+ 历史 name guard 同样会让仍活跃的 Apollo job `exitGuard` 静默回退到默认 false。现在迁移还会只把这个开关经 `settings.update` 恢复到 Apollo,并重读验证;不会激活其他已弃用字段。宿主 `describe().user` 会投影被跳过的旧覆盖,因此优先检查活动 Apollo entry 的 raw config:它已声明 `exitGuard` 时(尤其当前 scoped 行显式 false),不覆盖。child 目标仍按自己的 `user.childExitGuard` 存在性判断,不让两个开关互相覆盖。旧 unscoped 行保留,重复执行不重写。仅在子代理迁移完成/无需写入后恢复 job 开关,避免新 scoped 行遮蔽尚未导入的旧来源;写入拒绝为 pending,重读不一致为 error,均纳入顶层状态。
208
+
208
209
  目标:`dsh-better-subagent` 的 `settings.describe()`:
209
210
 
210
211
  - `target.user` 的存在性表示显式值(含 false/0/""/[]);
@@ -218,7 +219,7 @@ npm install --no-save /path/to/menghuan1918-dsh-better-subagent-0.1.0.tgz /path/
218
219
  | `tiers.fast` / `tiers.quality` | 同名 | 目标该档已有任一显式字段 → 整档不混写;否则来源档与目标默认不同才写 |
219
220
  | `subagents.eng` / `global` | 同名 | 目标缺字段且值不同才写;显式 false 是值 |
220
221
  | `subagents.codex/claude.*` | 同名 | 逐叶补齐:enabled、executable、profile(仅 codex)、permissionMode、defaultModel、models(含顺序/effort 元数据) |
221
- | `exitGuard` | `childExitGuard` | 只补目标缺失字段;Apollo 自己的 `exitGuard` 保留给 job 守卫 |
222
+ | `exitGuard` | Apollo `exitGuard` + `childExitGuard` | 两份相同初值,随后独立配置;当前目标显式值(含 false)各自优先 |
222
223
  | `trustedHosts` | `trustedHosts` | 非 volatile;目标 raw config 未声明且来源非空时用 `configEditor.edit` 写 |
223
224
 
224
225
  写入:volatile 用 `settings.update(ns, patch, revision)`;非 volatile 用 target entry 的 `configEditor.edit`。`settings.update` 会触发 reconcile,可能替换目标 entry 对象,因此写 `trustedHosts` 前重新从 `entries()` 取目标 entry。写后重新 `describe()`/读 raw config 校验,任何字段不一致按 `error` 报告。
@@ -240,6 +241,10 @@ npm install --no-save /path/to/menghuan1918-dsh-better-subagent-0.1.0.tgz /path/
240
241
 
241
242
  原生默认值不会变成目标显式值:只有来源行里显式存在的键才会被携带。压缩配置是 volatile 保存、引擎载入时冻结快照;迁移后的设置需要重载 profile 才会被运行中引擎采用(inspect 的 `requiresReload` 可观察)。
242
243
 
244
+ Apollo 在迁移挂载时先登记启动门禁:验证完成前,`agent/pre-step` 阻止任务步骤,`llm/stream` 同时阻止不经过 pre-step 的手动摘要调用。迁移 pending/blocked/error 不得按默认配置继续执行;应检查日志后修复来源或目标并重载。若本次迁移改变 compact 的载入快照,持久化验证完成后仍要求重载,重载后确认无差异才放行。普通后续设置保存不关闭已通过的启动门禁,仍按原有“当前快照继续、重载后应用新值”语义处理。
245
+
246
+ 宿主行明确 `disabled: true` 时,跳过该增强的迁移,不将其缺少 settings namespace 误判为全宿主故障;活动行缺失 namespace 仍报 pending。子代理增强被禁用时保留尚未迁出的旧来源,并暂缓会遮蔽该来源的旧 job 开关恢复,待重新启用后再迁移;已生效的 Apollo job 配置不变。停用 compact 增强时仍需按卸载说明启用原生 `compaction-basic`,以恢复 `/compact`;启动门禁不替用户改写禁用选择。
247
+
243
248
  ### 迁移测试
244
249
 
245
250
  - [tests/migrate.test.mjs](../tests/migrate.test.mjs):纯函数与 fake host 编排。覆盖显式 false/空值、目标优先(分档整档、models 空列表、保留方式二选一)、缺失来源、ambiguity、trustedHosts、写入拒绝、幂等、重读验证、有界等待。
@@ -308,10 +313,10 @@ DSH_PACKAGE_ROOT="/…/node_modules/@deepseek-ai/dsh" npm run prompts
308
313
  ### npm 安装
309
314
 
310
315
  ```sh
311
- dsh plugin --profile <profile> add @menghuan1918/dsh-apollo@0.5.1
316
+ dsh plugin --profile <profile> add @menghuan1918/dsh-apollo@0.5.2
312
317
  ```
313
318
 
314
- 包发布后即可用此命令安装,并带入三个精确依赖、应用三份 bundle patch。安装后需要重启对应 profile 的 DSH 进程。0.5.0 已发布;0.5.1 是迁移修复版本,可只发布本包。
319
+ 包发布后即可用此命令安装,并带入三个精确依赖、应用三份 bundle patch。安装后需要重启对应 profile 的 DSH 进程。0.5.0 已发布;0.5.1 是迁移修复版本;0.5.2 依赖 compact 0.1.1,修复 Web 设置表单的 schema rehydrate。
315
320
 
316
321
  ### GitHub 源码安装
317
322
 
@@ -319,7 +324,7 @@ dsh plugin --profile <profile> add @menghuan1918/dsh-apollo@0.5.1
319
324
  dsh plugin --profile <profile> add github:Menghuan1918/dsh-apollo
320
325
  ```
321
326
 
322
- GitHub main 仍是源码安装选项。它同样按 `package.json` 解析精确依赖;两个子包 0.1.0 在 registry 可取得前,源码安装会因依赖 404 失败。
327
+ GitHub main 仍是源码安装选项。它同样按 `package.json` 解析精确依赖;子包 0.1.0/0.1.1 在 registry 可取得前,源码安装会因依赖 404 失败。
323
328
 
324
329
  ### 本地 link 安装
325
330
 
@@ -363,9 +368,9 @@ dsh plugin --profile <profile> remove @menghuan1918/dsh-apollo
363
368
 
364
369
  两个子仓和 Apollo 都用 GitHub Release 触发 `.github/workflows/release.yml`,通过 npm trusted publishing(OIDC)发布,仓库里不存 npm token。tag 必须是 `v<package.json 的 version>`,CI 会先校验再跑测试和 `npm publish --provenance`。
365
370
 
366
- 1. 先发布 `@menghuan1918/dsh-better-subagent@0.1.0` 与 `dsh-better-compact@0.1.0`(各自仓库的 Release);
371
+ 1. 先发布 `@menghuan1918/dsh-better-subagent@0.1.0` 与 `dsh-better-compact@0.1.1`(各自仓库的 Release);
367
372
  2. 在官方 registry(`https://registry.npmjs.org/`)确认两个版本可取得、可安装;
368
- 3. 再把 Apollo 的精确依赖指向已发布版本,推 main 并发布 Apollo Release(0.5.0 已发布;本轮为 `v0.5.1`);
373
+ 3. 再把 Apollo 的精确依赖指向已发布版本,推 main 并发布 Apollo Release(0.5.0 已发布;本轮为 `v0.5.2`);
369
374
  4. 子包版本可取得前,不要把指向它们的精确依赖推到 Apollo main,否则 `npm install` / GitHub 源码安装会 404。
370
375
 
371
376
  ## 测试与冒烟
@@ -424,7 +429,7 @@ DSH_PACKAGE_ROOT=/path/to/node_modules/@deepseek-ai/dsh node scripts/runtime-smo
424
429
  - 本仓库不运行真实 Codex/Claude CLI;两个子包各自的 `cli-smoke` / runtime smoke 负责真实 CLI 生命周期。
425
430
  - 真实模型摘要保真、缓存命中率、S3 可视化页面的 GUI 验收不在 Apollo 侧冒烟内。
426
431
  - 未修改用户当前运行的 profile,未验证当前 GUI;当前 URL 以执行时 runtime context 为准。
427
- - 单元测试通过不等于 registry 发布/安装路径通过;两个 0.1.0 未发布前,`npm install` 的正式路径仍未验证。
432
+ - 单元测试通过不等于 registry 发布/安装路径通过;子包 0.1.0/0.1.1 已发布;registry 安装路径已用 fresh cache 验证。
428
433
 
429
434
  ## 历史兼容
430
435
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@menghuan1918/dsh-apollo",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "DSH plugin bundle: workflow skills, engineering presets, independent better-goal / better-subagent / better-compact integration, and a minimal job exit guard. Pure ESM, no build step.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -54,8 +54,8 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@deepseek-ai/schemastery": "~3.18.4",
57
- "@menghuan1918/dsh-better-subagent": "0.1.0",
58
- "dsh-better-compact": "0.1.0",
57
+ "@menghuan1918/dsh-better-subagent": "0.1.1",
58
+ "dsh-better-compact": "0.1.2",
59
59
  "dsh-better-goal": "0.2.0"
60
60
  },
61
61
  "peerDependencies": {
@@ -0,0 +1,36 @@
1
+ import { isDeepStrictEqual } from 'node:util'
2
+
3
+ /** Do not run a turn or manual summary using pre-migration defaults. */
4
+ export function mountMigrationAdmission(ctx) {
5
+ let report = { status: 'pending' }
6
+ let requiresReload = false
7
+ const assertReady = () => {
8
+ if (report.status !== 'complete') {
9
+ const detail = report.error ?? report.compact?.error ?? report.compact?.reason ?? report.subagent?.error
10
+ const error = new Error('Apollo 配置迁移尚未完成(' + report.status + ')。请检查迁移诊断,修复配置后重载 profile。' + (detail ? ' ' + detail : ''))
11
+ error.code = 'APOLLO_MIGRATION_PENDING'
12
+ throw error
13
+ }
14
+ if (requiresReload) {
15
+ const error = new Error('Apollo 已迁移压缩配置,但当前引擎仍使用旧快照。请重载 profile 后继续;刷新网页不会应用压缩配置。')
16
+ error.code = 'APOLLO_MIGRATION_RELOAD_REQUIRED'
17
+ throw error
18
+ }
19
+ }
20
+ ctx.on('agent/pre-step', (_payload, next) => {
21
+ assertReady()
22
+ return next()
23
+ }, { prepend: true })
24
+ // Manual /compact does not enter pre-step. Guard its provider boundary too,
25
+ // without replacing the compaction service or changing any request messages.
26
+ ctx.on('llm/stream', async function* (_options, next) {
27
+ assertReady()
28
+ yield* next()
29
+ }, { prepend: true })
30
+ return (result) => {
31
+ report = result
32
+ const compact = ctx.get?.('betterCompactConfig')
33
+ requiresReload = result.status === 'complete' && result.compact?.fields?.length > 0
34
+ && compact !== undefined && !isDeepStrictEqual(compact.snapshot(), compact.current())
35
+ }
36
+ }
package/src/index.js CHANGED
@@ -14,6 +14,7 @@ import { readFile, readdir } from 'node:fs/promises'
14
14
  import { dirname, join } from 'node:path'
15
15
  import { fileURLToPath } from 'node:url'
16
16
  import { mountMigration } from './migrate.js'
17
+ import { mountMigrationAdmission } from './admission.js'
17
18
  import { mountSettings } from './settings.js'
18
19
  export { Config } from './config.js'
19
20
 
@@ -33,8 +34,9 @@ export const inject = ['skills']
33
34
  export function apply(ctx, config) {
34
35
  ctx.skills.registerProvider(() => createProvider(ctx))
35
36
  if (typeof ctx.inject === 'function') {
37
+ const onResult = typeof ctx.on === 'function' ? mountMigrationAdmission(ctx) : undefined
36
38
  const settings = mountSettings(ctx, config)
37
- mountMigration(ctx, config, { legacyImport: settings.legacyImport })
39
+ mountMigration(ctx, config, { legacyImport: settings.legacyImport, onResult })
38
40
  }
39
41
  }
40
42
 
package/src/migrate.js CHANGED
@@ -7,8 +7,8 @@
7
7
  *
8
8
  * - `apollo-skills` 的 `tiers.fast/quality`、`subagents.{eng,global}`、
9
9
  * `subagents.{codex,claude}.*`、`trustedHosts` → `dsh-better-subagent`
10
- * 相同路径;`exitGuard` → `childExitGuard`。旧 `exitGuard` 仍留在 Apollo
11
- * 作为 job/通用守卫开关,不被删除。
10
+ * 相同路径;`exitGuard` → `childExitGuard`。旧 `exitGuard` 同时保留/恢复到
11
+ * Apollo 作为 job 守卫开关;两边目标显式配置(含 false)各自优先。
12
12
  * - 旧 `eng` / `eng-ptc` 预设里原生 `compaction-basic` 的显式配置 →
13
13
  * `dsh-better-compact` 同名原生键;目标缺失的 `mode` 采用默认 `goal`。
14
14
  *
@@ -524,6 +524,28 @@ export async function migrateSubagent({ settings, configEditor, descriptors, sou
524
524
  return { status: 'complete', fields }
525
525
  }
526
526
 
527
+ /** Restore only the job switch lost to a historical Apollo name guard. */
528
+ async function migrateJobExitGuard({ settings, descriptors, source, rawSource }) {
529
+ const target = descriptors.find((row) => row?.ns === APOLLO_NS)
530
+ // user also projects skipped overrides, so the active raw config is the
531
+ // authority for an already configured job switch (including explicit false).
532
+ if (hasOwn(rawSource, 'exitGuard')
533
+ || (hasOwn(target?.user, 'exitGuard') && target.user.exitGuard !== source.exitGuard)
534
+ || target?.value?.exitGuard === source.exitGuard) {
535
+ return { status: 'no-change' }
536
+ }
537
+ if (target === undefined) return { status: 'pending', error: 'Apollo namespace is not readable' }
538
+ try {
539
+ await settings.update(APOLLO_NS, { exitGuard: source.exitGuard }, target.revision)
540
+ } catch (error) {
541
+ return { status: 'pending', error: errorMessage(error) }
542
+ }
543
+ const after = (await settings.describe()).find((row) => row?.ns === APOLLO_NS)
544
+ return after?.value?.exitGuard === source.exitGuard
545
+ ? { status: 'complete' }
546
+ : { status: 'error', error: 'unverified: exitGuard' }
547
+ }
548
+
527
549
  /**
528
550
  * Apply the compaction migration.
529
551
  *
@@ -570,7 +592,8 @@ function errorMessage(error) {
570
592
  * Run the one-time (idempotent) migration.
571
593
  *
572
594
  * Order: Loader settlement → legacy `eng.json` import → bounded wait for both
573
- * target namespaces → subagent plan/write/verify → compact plan/write/verify.
595
+ * target namespaces → subagent plan/write/verify → job switch recovery →
596
+ * compact plan/write/verify.
574
597
  * A missing target namespace yields `pending`; errors are reported, never
575
598
  * silently claimed as success, and no source value is deleted.
576
599
  *
@@ -588,7 +611,12 @@ export async function runMigration(ctx, options = {}) {
588
611
  const loader = loaderOf(ctx)
589
612
  if (loader !== undefined) await bounded(Promise.resolve(loader.await()).catch(() => undefined), timeoutMs)
590
613
  if (options.legacyImport !== undefined) await bounded(options.legacyImport, timeoutMs)
591
- const descriptors = await waitForDescriptors(settings, [SUBAGENT_NS, COMPACT_NS], {
614
+ const configEditor = options.configEditor ?? serviceOf(ctx, 'configEditor')
615
+ // A deliberately disabled row is not an unloaded/failed active plugin. Keep
616
+ // its legacy source intact for a later re-enable instead of blocking native work.
617
+ const disabled = new Set([SUBAGENT_NS, COMPACT_NS].filter((ns) => targetEntryOf(configEditor, ns)?.options?.disabled === true))
618
+ const targets = [SUBAGENT_NS, COMPACT_NS].filter((ns) => !disabled.has(ns))
619
+ const descriptors = await waitForDescriptors(settings, targets, {
592
620
  timeoutMs,
593
621
  intervalMs: options.intervalMs,
594
622
  })
@@ -599,7 +627,6 @@ export async function runMigration(ctx, options = {}) {
599
627
  return { status: 'pending', error: message }
600
628
  }
601
629
  const sourceDescriptor = descriptors.find((row) => row?.ns === APOLLO_NS)
602
- const configEditor = options.configEditor ?? serviceOf(ctx, 'configEditor')
603
630
  const agentPresets = options.agentPresets ?? serviceOf(ctx, 'agentPresets')
604
631
  // describe().value only projects the *volatile* form, so read the Apollo
605
632
  // row's raw config for non-volatile fields (trustedHosts) and overlay the
@@ -617,23 +644,34 @@ export async function runMigration(ctx, options = {}) {
617
644
  effectiveSource,
618
645
  configurationOverrideOf(configEditor, APOLLO_NS),
619
646
  ))
620
- const subagent = await migrateSubagent({ settings, configEditor, descriptors, source, log })
621
- const compact = await migrateCompact({ settings, configEditor, descriptors, agentPresets, log })
622
- const status = [subagent.status, compact.status].some((value) => value === 'error')
647
+ const subagent = disabled.has(SUBAGENT_NS)
648
+ ? { status: 'disabled', fields: [] }
649
+ : await migrateSubagent({ settings, configEditor, descriptors, source, log })
650
+ // A scoped job write would become the last override and hide the skipped
651
+ // source from configuration().override. Wait until its child fields are safe.
652
+ const job = subagent.status === 'disabled'
653
+ ? { status: 'deferred', reason: 'subagent is disabled; preserve the legacy source until it is re-enabled' }
654
+ : ['complete', 'no-change'].includes(subagent.status)
655
+ ? await migrateJobExitGuard({ settings, descriptors, source, rawSource })
656
+ : { status: 'pending', error: 'waiting for the subagent migration before restoring the job switch' }
657
+ const compact = disabled.has(COMPACT_NS)
658
+ ? { status: 'disabled', fields: [] }
659
+ : await migrateCompact({ settings, configEditor, descriptors, agentPresets, log })
660
+ const status = [job.status, subagent.status, compact.status].some((value) => value === 'error')
623
661
  ? 'error'
624
- : [subagent.status, compact.status].some((value) => value === 'pending')
662
+ : [job.status, subagent.status, compact.status].some((value) => value === 'pending')
625
663
  ? 'pending'
626
664
  : ['ambiguous', 'invalid', 'source-missing'].includes(compact.status) ? 'blocked'
627
665
  : 'complete'
628
- for (const [name, section] of [['subagent', subagent], ['compact', compact]]) {
666
+ for (const [name, section] of [['job', job], ['subagent', subagent], ['compact', compact]]) {
629
667
  if (section.error !== undefined) {
630
668
  log?.warn?.(`apollo-migration: ${name} is ${section.status}: ${section.error}`)
631
669
  }
632
670
  }
633
671
  log?.[status === 'complete' ? 'info' : 'warn']?.(
634
- `apollo-migration: ${status} (subagent=${subagent.status}, compact=${compact.status})`,
672
+ `apollo-migration: ${status} (job=${job.status}, subagent=${subagent.status}, compact=${compact.status})`,
635
673
  )
636
- return { status, source: sourceDescriptor === undefined ? 'apply-config' : 'settings', subagent, compact }
674
+ return { status, source: sourceDescriptor === undefined ? 'apply-config' : 'settings', job, subagent, compact }
637
675
  }
638
676
 
639
677
  /**
@@ -641,7 +679,8 @@ export async function runMigration(ctx, options = {}) {
641
679
  *
642
680
  * @param ctx - plugin context.
643
681
  * @param config - Apollo row config (fallback source when describe() is not ready).
644
- * @param options - `{ legacyImport, env, timeoutMs, intervalMs }`.
682
+ * @param options - `{ legacyImport, env, timeoutMs, intervalMs, onResult }`.
683
+ * `onResult` receives the settled report without blocking the Loader callback.
645
684
  */
646
685
  export function mountMigration(ctx, config, options = {}) {
647
686
  let started = false
@@ -656,7 +695,8 @@ export function mountMigration(ctx, config, options = {}) {
656
695
  log: settingsCtx.logger ?? ctx.logger,
657
696
  }).catch((error) => {
658
697
  settingsCtx.logger?.warn?.(`apollo-migration: skipped: ${errorMessage(error)}`)
659
- })
698
+ return { status: 'error', error: errorMessage(error) }
699
+ }).then((result) => options.onResult?.(result))
660
700
  }
661
701
  ctx.inject(['settings'], start)
662
702
  }