@menghuan1918/dsh-apollo 0.5.2 → 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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  面向维护者、集成者和需要排查装配问题的用户。用户安装与配置请看 [README](../README.md);本文保留 DSH 插件的实现细节、旧配置迁移、组合安装边界与验证方法。
4
4
 
5
- 当前实现版本:`0.5.2`;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.1` | 压缩 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、写入拒绝、幂等、重读验证、有界等待。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@menghuan1918/dsh-apollo",
3
- "version": "0.5.2",
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.1",
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
  }