From db1cbf0bcc9b6f98692d2954bf0ebd5959dfaeeb Mon Sep 17 00:00:00 2001 From: Hypatia May Date: Thu, 30 Jul 2026 22:00:34 +0800 Subject: [PATCH] fix(compact): address manual compaction review --- ...6-07-30-queued-manual-compaction.i18n.yaml | 4 +- .../2026-07-30-queued-manual-compaction.md | 6 ++- .../2026-07-30-queued-manual-compaction.zh.md | 6 ++- apps/cli/config/base.cordis.yml | 3 -- docs/cordis-catalog/services.md | 2 +- packages/client/runtime/README.i18n.yaml | 4 +- packages/client/runtime/README.md | 2 +- packages/client/runtime/README.zh.md | 2 +- .../src/client/sessions/request-inspection.ts | 9 ++++ .../runtime/tests/request-inspection.spec.ts | 31 ++++++++++++++ .../compact/command-compact/README.i18n.yaml | 4 +- packages/compact/command-compact/README.md | 2 +- packages/compact/command-compact/README.zh.md | 2 +- packages/compact/command-compact/src/index.ts | 26 +++++++++--- .../tests/command-compact.spec.ts | 41 +++++++++++++++++++ packages/compact/compact/src/index.ts | 10 +++-- 16 files changed, 130 insertions(+), 24 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-07-30-queued-manual-compaction.i18n.yaml b/.agents/notes/implemented/feature/2026-07-30-queued-manual-compaction.i18n.yaml index e4b5e883b1..33740e2b31 100644 --- a/.agents/notes/implemented/feature/2026-07-30-queued-manual-compaction.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-30-queued-manual-compaction.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-queued-manual-compaction.md -2026-07-30-queued-manual-compaction.md: 65cd2041be5caa7b437fc649ba862ec18f0cff9a -2026-07-30-queued-manual-compaction.zh.md: 03d6c4e4cc4da8041238fc7174823349a03cf803 +2026-07-30-queued-manual-compaction.md: 05676ef824bc62ddbdbd8895a325570e91e4bafd +2026-07-30-queued-manual-compaction.zh.md: b2e40a42451570887a194c215df07e98aa1bd864 diff --git a/.agents/notes/implemented/feature/2026-07-30-queued-manual-compaction.md b/.agents/notes/implemented/feature/2026-07-30-queued-manual-compaction.md index 65cd2041be..05676ef824 100644 --- a/.agents/notes/implemented/feature/2026-07-30-queued-manual-compaction.md +++ b/.agents/notes/implemented/feature/2026-07-30-queued-manual-compaction.md @@ -20,6 +20,8 @@ This note extends the [compaction capability seam](2026-06-18-compaction-capabil `@deepseek-ai/dsh-command-compact` registers one argument-free human command through `ctx.commands`. It calls the third abstract `CompactService` operation, `compactNow(agent, signal)`, and maps the closed `ManualCompactionError` taxonomy (`busy | changed | summary | commit | persistence`) to direct UI results. `command/run` and `command/done` preserve the command lifecycle without entering model history or consuming a model-loop turn. +The command plugin tracks each real handler promise independently of the command executor's abort-aware wait. Its composite lifecycle effect unregisters `/compact` before asynchronously draining handlers that already started, so root teardown reaches quiescence only after backend close and flush work settles. + The seam's `ManualCompactAgentContext` adds only `reserveTurnAdmission()` to the session and routing facts compaction already needs. Retention, balancing, summarization, marker ordering, replacement, and durability remain backend responsibilities. ### Idle turn admission is synchronously reservable @@ -67,6 +69,8 @@ Tail scanning finds the current turn, unmatched compaction start, and newest `se The compaction invariant uses the same transition logic during seed replay: `session/end-seed` clears an open historical trace. The boundary need not publish live from the constructor for this case; replay is the load-bearing path. +The client request projection closes an unmatched compaction request as interrupted at the `session/end-seed` time and clears its active index. A later `compact/start` therefore creates an independent request instead of leaving or overwriting a permanently running orphan. + Once a transaction has appended its start, every later failure makes one closing attempt. A failed close leaves the unmatched start deliberately visible and blocking, and no flush is attempted. A closed manual attempt is flushed even when it reports an expected failure. Cancellation retains exact-reason precedence after required close and flush cleanup. ### Reference implementation boundaries @@ -95,7 +99,7 @@ That reference also carried client-side replacement-anchor machinery to preserve Agent-loop tests cover same-tick right of way, preserved IDs and FIFO lifecycle, waking and quiet queued work, idempotent release, `whenIdle()`, cancellation, and teardown. Compact tests cover standalone and numbered invariant ownership, end-seed replay, live versus stale orphans, re-entrant listeners, selected-span drift, commit and close failures, flush ordering, exact cancellation causes, raw output and usage preservation, and automatic/manual mutual exclusion. -The command package pins registration, Loader composition, argument rejection, exact success/failure text, cancellation, and absence from model history. The `queued-manual-compact` terminal snapshot drives real keystrokes through the assembled TUI: `/help` discovers the command, a held summary admits a queued prompt and immediate injection, `turn: null` markers and the flush precede the queued prompt turn, command lifecycle stays log-only, and the derived order is checkpoint → injection → queued prompt. +The command package pins registration, Loader composition, argument rejection, exact success/failure text, cancellation, absence from model history, and disposal waiting across separate close and flush boundaries after an abort stops the executor from awaiting the handler. The client runtime projection test pins end-seed interruption followed by an independent completed attempt. The `queued-manual-compact` terminal snapshot drives real keystrokes through the assembled TUI: `/help` discovers the command, a held summary admits a queued prompt and immediate injection, `turn: null` markers and the flush precede the queued prompt turn, command lifecycle stays log-only, and the derived order is checkpoint → injection → queued prompt. ## Consequences diff --git a/.agents/notes/implemented/feature/2026-07-30-queued-manual-compaction.zh.md b/.agents/notes/implemented/feature/2026-07-30-queued-manual-compaction.zh.md index 03d6c4e4cc..b2e40a4245 100644 --- a/.agents/notes/implemented/feature/2026-07-30-queued-manual-compaction.zh.md +++ b/.agents/notes/implemented/feature/2026-07-30-queued-manual-compaction.zh.md @@ -20,6 +20,8 @@ Status: implemented `@deepseek-ai/dsh-command-compact` 通过 `ctx.commands` 注册一个无参数、面向用户的命令。它调用第三个抽象 `CompactService` 操作 `compactNow(agent, signal)`,并把封闭的 `ManualCompactionError` 分类体系(`busy | changed | summary | commit | persistence`)映射为直接 UI 结果。`command/run` 和 `command/done` 保留命令生命周期,同时不进入模型历史,也不消耗模型循环轮次。 +命令插件会独立跟踪每个实际处理器 promise,不依赖命令执行器的中止感知等待。其复合生命周期 effect 先注销 `/compact`,再异步等待所有已开始的处理器结算,因此根级 teardown 只有在后端的闭合与 flush 工作结算后才会完全停稳。 + 该 seam 的 `ManualCompactAgentContext` 只在压缩已需使用的会话与路由事实之上增加 `reserveTurnAdmission()`。保留、平衡、摘要、标记排序、替换与持久性仍由后端负责。 ### 可以同步预留空闲轮次接纳 @@ -67,6 +69,8 @@ DSH 有意在调用摘要器前记录 `compact/start`。缓慢或崩溃的尝试 压缩不变量在 seed 回放期间使用同一项转换逻辑:`session/end-seed` 会清除开放的历史追踪状态。此场景不要求构造函数实时发布该边界;回放才是承重路径。 +客户端请求投影会在 `session/end-seed` 时刻将未匹配的压缩请求以中断状态结束,并清除其活动索引。因此,后续 `compact/start` 会创建一个独立请求,而不是让该遗留的未匹配请求永久保持运行状态或将其覆盖。 + 事务追加 start 后,每次后续失败都会进行一次闭合尝试。闭合失败会有意留下可见且具有阻塞作用的未匹配 start,并且不尝试 flush。已闭合的手动尝试即使报告预期失败也会 flush。完成必需的闭合与 flush 清理后,取消仍保留原始原因优先级。 ### 参考实现边界 @@ -95,7 +99,7 @@ DSH 有意在调用摘要器前记录 `compact/start`。缓慢或崩溃的尝试 Agent loop 测试覆盖同一 tick 内的优先权、保留 ID 与 FIFO 生命周期、会唤醒和静默的排队工作、幂等释放、`whenIdle()`、取消与 teardown。压缩测试覆盖独立与数字形式的不变量 owner、end-seed 回放、活动与陈旧未匹配标记、listener 重入、所选 span 漂移、commit 与闭合失败、flush 顺序、原始取消原因、raw output 与 usage 保留,以及自动/手动互斥。 -命令包固定注册行为、Loader 组合、参数拒绝、精确的成功/失败文本、取消和不进入模型历史的保证。`queued-manual-compact` 终端快照通过已组装 TUI 驱动真实按键:`/help` 可发现该命令;被暂停的摘要会接纳一个排队提示词和即时注入;`turn: null` 标记与 flush 先于排队提示词轮次;命令生命周期保持纯日志;派生顺序固定为检查点 → 注入 → 排队提示词。 +命令包固定注册行为、Loader 组合、参数拒绝、精确的成功/失败文本、取消、不进入模型历史的保证,以及处置操作在中止使执行器停止等待处理器后,仍会跨越相互独立的闭合与 flush 边界等待该处理器结算。客户端运行时投影测试固定 end-seed 中断,以及随后一次独立尝试的完成。`queued-manual-compact` 终端快照通过已组装 TUI 驱动真实按键:`/help` 可发现该命令;被暂停的摘要会接纳一个排队提示词和即时注入;`turn: null` 标记与 flush 先于排队提示词轮次;命令生命周期保持纯日志;派生顺序固定为检查点 → 注入 → 排队提示词。 ## 后果 diff --git a/apps/cli/config/base.cordis.yml b/apps/cli/config/base.cordis.yml index 5beaa15b5d..0eb98e41bf 100644 --- a/apps/cli/config/base.cordis.yml +++ b/apps/cli/config/base.cordis.yml @@ -156,9 +156,6 @@ - id: command-compact name: '@deepseek-ai/dsh-command-compact' -# Expose fresh-child `spawn` and completed-prefix `fork` through independent -# in-process backends. Each tool instance needs a distinct `toolName`; the registry -# rejects duplicates. These leaves follow the app because it provides `ctx.agents` and `ctx.tools`. - id: subagent name: '@deepseek-ai/dsh-subagent' diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 638b53dcc2..3e5cc60326 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -506,7 +506,7 @@ abstract compactRegion( start: number, end: number, agent: CompactAgentContext, Types: [CompactionResult](../core-data-structures/compaction.md) · [CompactionTrigger](../core-data-structures/compaction.md) -Source: [`packages/compact/compact/src/index.ts:76`](../../packages/compact/compact/src/index.ts) +Source: [`packages/compact/compact/src/index.ts:80`](../../packages/compact/compact/src/index.ts) ## `ctx.directoryPicker` — `DirectoryPicker` (abstract seam) diff --git a/packages/client/runtime/README.i18n.yaml b/packages/client/runtime/README.i18n.yaml index 8f9b6ba17c..dc170526fe 100644 --- a/packages/client/runtime/README.i18n.yaml +++ b/packages/client/runtime/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/runtime/README.md -README.md: fbb979ad410e520e01220519b57dd428bbda14f1 -README.zh.md: 29e3f0ef46e4b016679cf17dc58d8fe1a67fca6c +README.md: 2d0e3228931f9a5dd467c996e69d1ea6a81352cc +README.zh.md: 07c0ac90e82d11c71581f96b2fab64cf686fdd52 diff --git a/packages/client/runtime/README.md b/packages/client/runtime/README.md index fbb979ad41..2d0e322893 100644 --- a/packages/client/runtime/README.md +++ b/packages/client/runtime/README.md @@ -28,7 +28,7 @@ Because the projection is log-ordered, the node array is seq-monotonic by constr ## Request inspection -`SessionHistoryInspection.requests` is one chronological, purpose-discriminated provider-request stream. Assistant requests always carry their numeric `turn` and `step`; compaction requests carry `step: 0` and a `turn` owner that may be `null`. That null owner means a manual compaction ran standalone between turns, not that it belongs to either adjacent turn. +`SessionHistoryInspection.requests` is one chronological, purpose-discriminated provider-request stream. Assistant requests always carry their numeric `turn` and `step`; compaction requests carry `step: 0` and a `turn` owner that may be `null`. That null owner means a manual compaction ran standalone between turns, not that it belongs to either adjacent turn. A `session/end-seed` boundary closes an unmatched compaction request as an error at the boundary time with `Compaction was interrupted before completion.`; a later start projects as an independent request instead of overwriting the orphan. ## Code Mode sub-dispatch index diff --git a/packages/client/runtime/README.zh.md b/packages/client/runtime/README.zh.md index 29e3f0ef46..07c0ac90e8 100644 --- a/packages/client/runtime/README.zh.md +++ b/packages/client/runtime/README.zh.md @@ -28,7 +28,7 @@ SlotsService 分别为 renderer 提供 `useSessions` 与 `useWorkspaces` 的裸 ## 请求检查 -`SessionHistoryInspection.requests` 是一条按时间顺序排列、以用途为判别字段的提供方请求流。助手请求始终携带数值型 `turn` 与 `step`;压缩请求携带 `step: 0`,其 `turn` 所有者可以是 `null`。这个 null 所有者表示手动压缩独立运行在两个轮次之间,并不表示它属于任一相邻轮次。 +`SessionHistoryInspection.requests` 是一条按时间顺序排列、以用途为判别字段的提供方请求流。助手请求始终携带数值型 `turn` 与 `step`;压缩请求携带 `step: 0`,其 `turn` 所有者可以是 `null`。这个 null 所有者表示手动压缩独立运行在两个轮次之间,并不表示它属于任一相邻轮次。`session/end-seed` 边界会在边界时刻将未匹配的压缩请求以错误状态结束,错误固定为 `Compaction was interrupted before completion.`;后续 start 会投影为独立请求,而不会覆盖这项遗留的未匹配请求。 ## Code Mode 子调用索引 diff --git a/packages/client/runtime/src/client/sessions/request-inspection.ts b/packages/client/runtime/src/client/sessions/request-inspection.ts index c74e6aca9d..e57ff23f58 100644 --- a/packages/client/runtime/src/client/sessions/request-inspection.ts +++ b/packages/client/runtime/src/client/sessions/request-inspection.ts @@ -367,6 +367,15 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[] } const type = sourceEvent.type as string + if (type === 'session/end-seed' && activeCompaction !== undefined) { + updateCompaction(activeCompaction, { + completedAt: sourceEvent.time, + status: 'error', + error: 'Compaction was interrupted before completion.', + }) + activeCompaction = undefined + continue + } if (type === 'compact/start') { const event = sourceEvent as unknown as CompactionStartEvent activeCompaction = requests.length diff --git a/packages/client/runtime/tests/request-inspection.spec.ts b/packages/client/runtime/tests/request-inspection.spec.ts index 7ba9b50951..031109ca8c 100644 --- a/packages/client/runtime/tests/request-inspection.spec.ts +++ b/packages/client/runtime/tests/request-inspection.spec.ts @@ -116,6 +116,37 @@ describe('inspectRequests', () => { } }) + it('interrupts an orphaned compaction at end-seed before projecting a new attempt', () => { + const snapshot = inspectRequests(entriesOf([ + at(0, 'compact/start', { turn: null }), + at(1, 'session/end-seed', {}), + at(2, 'compact/start', { turn: null }), + at(3, 'compact/summary', { + summary: [{ type: 'text', text: 'replacement summary' }], + provider: 'fake', + model: 'compact-model', + }), + at(4, 'compact/end', { turn: null }), + ])) + + expect(snapshot.requests).toMatchObject([ + { + purpose: 'compaction', + startSeq: 0, + status: 'error', + completedAt: 1_700_000_000_001, + error: 'Compaction was interrupted before completion.', + }, + { + purpose: 'compaction', + startSeq: 2, + status: 'complete', + completedAt: 1_700_000_000_004, + summary: [{ type: 'text', text: 'replacement summary' }], + }, + ]) + }) + it('captures schemas for nested tool dispatches from the active request header', () => { const snapshot = inspectRequests(entriesOf([ at(0, 'request/header', { diff --git a/packages/compact/command-compact/README.i18n.yaml b/packages/compact/command-compact/README.i18n.yaml index b66b4faa9b..eca38d67da 100644 --- a/packages/compact/command-compact/README.i18n.yaml +++ b/packages/compact/command-compact/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/compact/command-compact/README.md -README.md: 314b259025a2b7a13faf27f9d3935cca370224fe -README.zh.md: 9fb30c6720c3c3d69ed64242e995d953ec8a008f +README.md: 1445e76f8328a9ac1c5f9dd43094f1c1cd5d2ad4 +README.zh.md: 0fb306afb3713e47fb17d2c914a4f691b63b77eb diff --git a/packages/compact/command-compact/README.md b/packages/compact/command-compact/README.md index 314b259025..1445e76f83 100644 --- a/packages/compact/command-compact/README.md +++ b/packages/compact/command-compact/README.md @@ -24,7 +24,7 @@ Expected `ManualCompactionError` codes become stable direct errors: | `commit` | `Compaction did not finish cleanly; some session history may have changed. Inspect the current session state before retrying.` | | `persistence` | `Compaction finished, but the session could not be saved.` | -The busy result is intentionally process-scoped: a live unmatched marker blocks, while a marker older than the newest `session/end-seed` is stale and does not. Unexpected implementation failures reject dispatch. Cancellation remains authoritative; the backend completes its required close/flush cleanup, and the command settles internally as `Compaction cancelled.` while the command executor stops waiting with its cancellation error. +The busy result is intentionally process-scoped: a live unmatched marker blocks, while a marker older than the newest `session/end-seed` is stale and does not. Unexpected implementation failures reject dispatch. Cancellation remains authoritative; the backend completes its required close/flush cleanup, and the command settles internally as `Compaction cancelled.` while the command executor stops waiting with its cancellation error. Plugin disposal first unregisters `/compact`, then drains every handler that already started, so root teardown cannot pass an aborted command's close or flush boundary. Prompts submitted while compaction runs remain accepted in the agent's ordinary FIFO with the same identity and wakeup facts. They start only after the compaction's explicit durability checkpoint and admission release. Idle injected context is not held: it may be logged between `compact/start` and `compact/end`, and positional replacement leaves it visible after the checkpoint. diff --git a/packages/compact/command-compact/README.zh.md b/packages/compact/command-compact/README.zh.md index 9fb30c6720..0fb306afb3 100644 --- a/packages/compact/command-compact/README.zh.md +++ b/packages/compact/command-compact/README.zh.md @@ -24,7 +24,7 @@ | `commit` | `Compaction did not finish cleanly; some session history may have changed. Inspect the current session state before retrying.` | | `persistence` | `Compaction finished, but the session could not be saved.` | -busy 结果有意限定在进程范围内:活动的未匹配标记会阻塞,而早于最新 `session/end-seed` 的标记已陈旧,不会阻塞。意外实现故障会拒绝分发。取消仍具有最终决定权;后端会完成必需的闭合/flush 清理,命令内部以 `Compaction cancelled.` 结算,而命令执行器会因取消错误停止等待。 +busy 结果有意限定在进程范围内:活动的未匹配标记会阻塞,而早于最新 `session/end-seed` 的标记已陈旧,不会阻塞。意外实现故障会拒绝分发。取消仍具有最终决定权;后端会完成必需的闭合/flush 清理,命令内部以 `Compaction cancelled.` 结算,而命令执行器会因取消错误停止等待。插件处置会先注销 `/compact`,再等待所有已开始的处理器结算,因此根级 teardown 不会越过已中止命令的闭合或 flush 边界。 压缩运行期间提交的提示词仍会按 agent 的普通 FIFO 获得接纳,保留相同的身份与唤醒信息。它们仅在压缩的显式持久性检查点和接纳预留释放后启动。空闲注入的上下文不受阻塞:它可以记录在 `compact/start` 与 `compact/end` 之间,位置替换会使其在检查点之后保持可见。 diff --git a/packages/compact/command-compact/src/index.ts b/packages/compact/command-compact/src/index.ts index 782057d8ec..991ad5b43a 100644 --- a/packages/compact/command-compact/src/index.ts +++ b/packages/compact/command-compact/src/index.ts @@ -79,9 +79,25 @@ async function executeCompact( * @param ctx - context carrying the command registry and the compaction seam. */ export function apply(ctx: Context): void { - ctx.commands.register({ - name: 'compact', - description: 'Compact older conversation history', - handler: invocation => executeCompact(ctx, invocation), - }) + const active = new Set>() + const handler = (invocation: CommandInvocation): Promise => { + const operation = executeCompact(ctx, invocation) + active.add(operation) + const retire = (): void => { active.delete(operation) } + // Both branches retire without rethrowing, so the derived observer promise + // cannot become an unhandled mirror of an expected handler rejection. + void operation.then(retire, retire) + return operation + } + + ctx.effect(function* () { + // Yield drain before registration: composite teardown is LIFO, so no new + // invocation can enter while already-started handler promises quiesce. + yield async () => { await Promise.allSettled(active) } + yield ctx.commands.register({ + name: 'compact', + description: 'Compact older conversation history', + handler, + }) + }, 'command-compact lifecycle') } diff --git a/packages/compact/command-compact/tests/command-compact.spec.ts b/packages/compact/command-compact/tests/command-compact.spec.ts index 3a3f8d8bb9..b0406beb47 100644 --- a/packages/compact/command-compact/tests/command-compact.spec.ts +++ b/packages/compact/command-compact/tests/command-compact.spec.ts @@ -204,4 +204,45 @@ describe('/compact human command', () => { await expect(run(unexpected)).rejects.toBe(bug) expectLastLifecycle(unexpected, '', { kind: 'error', text: bug.message }) }) + + it('drains an aborted handler through close and flush before plugin disposal settles', async () => { + const test = await harness() + const controller = new AbortController() + const abort = new Error('operator cancelled') + const started = Promise.withResolvers() + const allowClose = Promise.withResolvers() + const closed = Promise.withResolvers() + const allowFlush = Promise.withResolvers() + const flushed = Promise.withResolvers() + test.compact.operation = async () => { + started.resolve(undefined) + await allowClose.promise + closed.resolve(undefined) + await allowFlush.promise + flushed.resolve(undefined) + throw abort + } + + const execution = run(test, '', controller) + await started.promise + controller.abort(abort) + await expect(execution).rejects.toBe(abort) + + let disposed = false + const disposal = test.plugin.dispose() + void disposal.then(() => { disposed = true }) + await new Promise(resolve => setTimeout(resolve, 0)) + expect(test.ctx.commands.find(test.agent, 'compact')).toBeUndefined() + expect(disposed).toBe(false) + + allowClose.resolve(undefined) + await closed.promise + await new Promise(resolve => setTimeout(resolve, 0)) + expect(disposed).toBe(false) + + allowFlush.resolve(undefined) + await flushed.promise + await disposal + expect(disposed).toBe(true) + }) }) diff --git a/packages/compact/compact/src/index.ts b/packages/compact/compact/src/index.ts index d51df1d778..ecc00597b2 100644 --- a/packages/compact/compact/src/index.ts +++ b/packages/compact/compact/src/index.ts @@ -24,13 +24,17 @@ export type CompactionTrigger = 'pressure' | 'context-overflow' /** Expected failure classes for an explicit idle-session compaction request. */ export type ManualCompactionErrorCode = 'busy' | 'changed' | 'summary' | 'commit' | 'persistence' -/** Expected manual-compaction failure suitable for a direct human-command result. */ +/** + * Expected manual-compaction failure suitable for a direct human-command result. + * Shared durable-lock entry assertions may also throw the `busy` subtype from + * automatic compaction paths. + */ export class ManualCompactionError extends Error { override readonly name = 'ManualCompactionError' /** - * Create one classified manual-compaction failure. - * @param code - stable failure class for a human-command consumer. + * Create one classified compaction failure. + * @param code - stable failure class; `busy` may originate from any compaction entry path. * @param message - backend diagnostic retained as the Error message. * @param options - optional original failure. */