fix(examples): remove legacy retry rejection

This commit is contained in:
Turtle
2026-07-25 16:28:31 +08:00
parent 5345e93ceb
commit fcd355bf45
14 changed files with 4 additions and 113 deletions

View File

@@ -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
2026-07-24-provider-retry-policies.md: cd39717c03f2a7714b2f5e93404efb0d2d17a6da
2026-07-24-provider-retry-policies.zh.md: 1768f67dfb98757d964589693a108f946924749b
2026-07-24-provider-retry-policies.md: ed6b401c792f85eec6ca1c52495733222ea998a1
2026-07-24-provider-retry-policies.zh.md: 2a4853ceec71d0ce78a1225f455575035f0bca13

View File

@@ -56,7 +56,7 @@ Each scheduled retry appends a non-surface `llm/retry` event with the failed pro
## Verification
Adapter tests validate nested policies at provider load, prove registration captures configured and default policies, and retain the serving policy across in-flight route replacement. Unit and plugin-validation tests select policies from the failed request's serving registration, reject top-level `llmRetry` at the spine, CLI, TUI, and ACP schemas, separate different same-mode policies while preserving histories across reordered code sets, exercise always mode beyond the normal budget, pin jitter and delay caps, prove downstream recovery ordering, prove cancellation and disposal drain delegated recovery before reaching quiescence, and prove both abort active backoff waits. Published Loader fixtures reject the invalid app-level key in CLI and ACP and the invalid bundle-level key when loading the spine directly. Request-level coverage compares the complete messages of failed and retried attempts and rejects both provider error text and discarded partial output. A keyless headless `stream-json` snapshot runs failure, retry, and success through the assembled app, pins the complete `llm/retry` record, and rejects any model-message change between attempts. JSONL and SQLite tests round-trip an always event without `Infinity`; invariant tests validate the canonical policy tuple, bind its provider to the request header, bind its failure code and delay to the encoded policy, and bind its retry number to the active provider policy key; TUI tests render finite and infinite limits.
Adapter tests validate nested policies at provider load, prove registration captures configured and default policies, and retain the serving policy across in-flight route replacement. Unit tests select policies from the failed request's serving registration, separate different same-mode policies while preserving histories across reordered code sets, exercise always mode beyond the normal budget, pin jitter and delay caps, prove downstream recovery ordering, prove cancellation and disposal drain delegated recovery before reaching quiescence, and prove both abort active backoff waits. Request-level coverage compares the complete messages of failed and retried attempts and rejects both provider error text and discarded partial output. A keyless headless `stream-json` snapshot runs failure, retry, and success through the assembled app, pins the complete `llm/retry` record, and rejects any model-message change between attempts. JSONL and SQLite tests round-trip an always event without `Infinity`; invariant tests validate the canonical policy tuple, bind its provider to the request header, bind its failure code and delay to the encoded policy, and bind its retry number to the active provider policy key; TUI tests render finite and infinite limits.
## Consequences

View File

@@ -56,7 +56,7 @@ always 模式先请求下游恢复使上下文溢出压缩compaction
## 验证
适配器测试会在提供方加载时校验嵌套策略,证明注册流程会捕获已配置策略和默认策略,并证明请求进行期间替换路由后仍会保留实际提供服务的策略。单元测试与插件校验测试根据失败请求实际使用的注册项选择策略、在主干、CLI、TUI 与 ACP schema 拒绝顶层 `llmRetry`分离模式相同但策略不同的替换路由历史,同时在错误代码集合仅顺序不同时延续历史、验证 always 模式可越过 normal 预算、固定抖动和延迟上限、证明下游恢复顺序、证明取消与 dispose 会先排空已委托的恢复再达到完全停稳,并证明二者都会停止正在进行的退避等待。发布版 Loader fixture测试前置数据会在 CLI 与 ACP 中拒绝无效的应用级配置键,并在直接加载主干时拒绝无效的 bundle 级配置键。请求级覆盖会比较失败尝试与重试尝试的完整消息,并排除提供方错误文本和丢弃的部分输出。一个无密钥 headless `stream-json` 快照会通过组装后的应用执行失败、重试与成功流程,固定完整的 `llm/retry` 记录并拒绝各次尝试之间出现任何模型消息变化。JSONL 与 SQLite 测试会往返读写不含 `Infinity` 的 always 事件不变式测试会校验规范策略元组、将事件中的提供方绑定到请求头、将失败代码与延迟绑定到编码后的策略并将重试编号绑定到活跃的提供方策略键TUI 测试会渲染有限和无限上限。
适配器测试会在提供方加载时校验嵌套策略,证明注册流程会捕获已配置策略和默认策略,并证明请求进行期间替换路由后仍会保留实际提供服务的策略。单元测试根据失败请求实际使用的注册项选择策略、分离模式相同但策略不同的替换路由历史,同时在错误代码集合仅顺序不同时延续历史、验证 always 模式可越过 normal 预算、固定抖动和延迟上限、证明下游恢复顺序、证明取消与 dispose 会先排空已委托的恢复再达到完全停稳,并证明二者都会停止正在进行的退避等待。请求级覆盖会比较失败尝试与重试尝试的完整消息,并排除提供方错误文本和丢弃的部分输出。一个无密钥 headless `stream-json` 快照会通过组装后的应用执行失败、重试与成功流程,固定完整的 `llm/retry` 记录并拒绝各次尝试之间出现任何模型消息变化。JSONL 与 SQLite 测试会往返读写不含 `Infinity` 的 always 事件不变式测试会校验规范策略元组、将事件中的提供方绑定到请求头、将失败代码与延迟绑定到编码后的策略并将重试编号绑定到活跃的提供方策略键TUI 测试会渲染有限和无限上限。
## 后果

View File

@@ -73,8 +73,6 @@ export interface Config {
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
/** Persisted same-session goals; owner defaults enable them, or false disables the stack and tools. */
goals?: agentCore.GoalConfig | false
/** Invalid at app level; configure `retryPolicy` under each provider. */
llmRetry?: never
}
```
@@ -162,8 +160,6 @@ export interface Config {
invariants?: InvariantConfig
/** Opt-in persisted same-session goal stack; set false or omit to leave it unmounted. */
goals?: GoalConfig | false
/** Invalid at bundle level; configure `retryPolicy` under each provider. */
llmRetry?: never
}
/** Skill bundle config forwarded to the registry, local provider, and model-facing consumer. */
@@ -265,8 +261,6 @@ export interface Config {
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
/** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
workspaceContext: agentCore.Config['workspaceContext']
/** Invalid at app level; configure `retryPolicy` under each provider. */
llmRetry?: never
}
```
@@ -1804,8 +1798,6 @@ export interface Config {
resumeSessionId?: string
/** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
workspaceContext: agentCore.Config['workspaceContext']
/** Invalid at app level; configure `retryPolicy` under each provider. */
llmRetry?: never
}
```

View File

@@ -67,8 +67,6 @@ export interface Config {
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
/** Persisted same-session goals; owner defaults enable them, or false disables the stack and tools. */
goals?: agentCore.GoalConfig | false
/** Invalid at app level; configure `retryPolicy` under each provider. */
llmRetry?: never
}
// Each front door owns a complete, directly readable config schema; extracting
@@ -94,8 +92,6 @@ export const Config: z<Config> = z.object({
toolBash: agentCore.ToolBashConfigSchema,
toolTasks: z.union([z.const(false), agentCore.ToolTasksConfigSchema]),
goals: z.union([z.const(false), agentCore.GoalConfigSchema]),
// Provider retryPolicy makes a top-level llmRetry invalid.
llmRetry: z.never(),
})
/* jscpd:ignore-end */

View File

@@ -76,16 +76,6 @@ async function withIsolatedSkillHomes<T>(run: () => Promise<T>): Promise<T> {
}
describe('dsh-acp-demo composition', () => {
it('rejects app-level llmRetry config through plugin validation', async () => {
const ctx = new Context()
await expect(ctx.plugin(acpAgent, {
provider: 'mock',
model: 'mock',
workspaceContext: false,
llmRetry: { maxTransientRetries: 2 },
} as never)).rejects.toThrow(/llmRetry/)
})
it('brings up the spine + persistence + the ACP bridge', async () => {
const ctx = await mount({
provider: 'mock',

View File

@@ -208,19 +208,6 @@ describe.skipIf(!existsSync(acpBin))('dsh-acp-demo BUILT bin (node lib/bin.js, n
expect(stderr).toContain('config file not found')
}, 30_000)
it('rejects legacy app-level llmRetry through the published Loader path', async () => {
consumer = await makeConsumer()
const configPath = join(consumer, 'cordis.yml')
const config = await readFile(configPath, 'utf8')
await writeFile(configPath, config.replace(
' workspaceContext: false',
' workspaceContext: false\n llmRetry:\n maxTransientRetries: 2',
))
const { code, stderr } = await runBinExpectingExit('./cordis.yml', consumer)
expect(code).not.toBe(0)
expect(stderr).toContain('llmRetry')
}, 30_000)
})
/** Spawn the built acp bin against `configArg` and resolve with its exit code + stderr. */

View File

@@ -112,8 +112,6 @@ export interface Config {
invariants?: InvariantConfig
/** Opt-in persisted same-session goal stack; set false or omit to leave it unmounted. */
goals?: GoalConfig | false
/** Invalid at bundle level; configure `retryPolicy` under each provider. */
llmRetry?: never
}
/** The skill config schema exported for app packages that forward `skills`. */
@@ -154,9 +152,6 @@ export const Config = z.intersect([
toolTasks: z.union([z.const(false), ToolTasksConfigSchema]),
invariants: InvariantService.Config,
goals: z.union([z.const(false), GoalConfigSchema]),
// Schemastery preserves unknown object properties. A top-level llmRetry is
// known-but-impossible because provider retryPolicy owns this configuration.
llmRetry: z.never(),
}) as unknown as z<Pick<Config, 'tools' | 'dshHome' | 'sessionTitle' | 'skills' | 'workspaceContext' | 'toolBash' | 'toolTasks' | 'invariants' | 'goals'>>,
]) as unknown as z<Config>

View File

@@ -594,14 +594,6 @@ describe('dsh-agent-spine-demo bundle', () => {
expect(agentCore.name).toBe('agent-spine-demo')
})
it('rejects bundle-level llmRetry config through plugin validation', async () => {
const ctx = new Context()
await expect(ctx.plugin(agentCore, {
workspaceContext: false,
llmRetry: { maxTransientRetries: 2 },
} as never)).rejects.toThrow(/llmRetry/)
})
it('has the namespace-plugin export shape (no stray default) so the Loader keeps name/Config/apply', () => {
// A default export would make `unwrapExports` collapse this inject-less namespace and silently
// drop `name`/`Config`. Apps import the bundle directly, so this is its Loader-shape guard.

View File

@@ -52,8 +52,6 @@ export interface Config {
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
/** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
workspaceContext: agentCore.Config['workspaceContext']
/** Invalid at app level; configure `retryPolicy` under each provider. */
llmRetry?: never
}
// Each front door keeps a complete Loader schema so its deployment contract is
@@ -75,8 +73,6 @@ export const Config: z<Config> = z.object({
toolBash: agentCore.ToolBashConfigSchema,
toolTasks: z.union([z.const(false), agentCore.ToolTasksConfigSchema]),
workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
// Provider retryPolicy makes a top-level llmRetry invalid.
llmRetry: z.never(),
})
/* jscpd:ignore-end */

View File

@@ -192,39 +192,6 @@ describe.skipIf(!existsSync(cliBin))('dsh-cli-demo BUILT bin', () => {
}
}, 30_000)
it('rejects legacy app-level llmRetry through the published Loader path', async () => {
consumer = await makeConsumer()
const configPath = join(consumer, 'cordis.yml')
const config = await readFile(configPath, 'utf8')
await writeFile(configPath, config.replace(
' workspaceContext: false',
' workspaceContext: false\n llmRetry:\n maxTransientRetries: 2',
))
const result = await runBuiltBin(consumer, ['--config', './cordis.yml', 'task'])
expect(result.code).not.toBe(0)
expect(result.stdout).toBe('')
expect(result.stderr).toContain('llmRetry')
}, 30_000)
it('rejects legacy bundle-level llmRetry when the published spine is loaded directly', async () => {
consumer = await makeConsumer()
await writeFile(join(consumer, 'cordis.yml'), [
'- id: spine',
" name: '@deepseek-ai/dsh-agent-spine-demo'",
' config:',
' workspaceContext: false',
' llmRetry:',
' maxTransientRetries: 2',
'',
].join('\n'))
const result = await runBuiltBin(consumer, ['--config', './cordis.yml', 'task'])
expect(result.code).not.toBe(0)
expect(result.stdout).toBe('')
expect(result.stderr).toContain('llmRetry')
}, 30_000)
describe.skipIf(process.platform === 'win32')('POSIX signal delivery', () => {
it.each([
['SIGINT', 130],

View File

@@ -173,16 +173,6 @@ afterEach(async () => {
})
describe('parseCliArgs', () => {
it('rejects app-level llmRetry config through plugin validation', async () => {
const ctx = new Context()
await expect(ctx.plugin(cliDemo, {
provider: 'mock',
model: 'mock',
workspaceContext: false,
llmRetry: { maxTransientRetries: 2 },
} as never)).rejects.toThrow(/llmRetry/)
})
it('parses defaults, explicit options, spaces, and an option-like task after --', () => {
expect(parseCliArgs(['task with spaces'])).toEqual({
kind: 'run', configPath: './cordis.yml', outputFormat: 'text', task: 'task with spaces',

View File

@@ -82,8 +82,6 @@ export interface Config {
resumeSessionId?: string
/** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
workspaceContext: agentCore.Config['workspaceContext']
/** Invalid at app level; configure `retryPolicy` under each provider. */
llmRetry?: never
}
export const Config: z<Config> = z.object({
@@ -108,8 +106,6 @@ export const Config: z<Config> = z.object({
goals: z.union([z.const(false), agentCore.GoalConfigSchema]),
resumeSessionId: z.string(),
workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
// Provider retryPolicy makes a top-level llmRetry invalid.
llmRetry: z.never(),
})
/* jscpd:ignore-end */

View File

@@ -21,16 +21,6 @@ function recordingContext(): { readonly ctx: Context; readonly calls: PluginCall
}
describe('dsh-tui-demo app', () => {
it('rejects app-level llmRetry config through plugin validation', async () => {
const ctx = new Context()
await expect(ctx.plugin(tuiAgent, {
provider: 'mock',
model: 'mock',
workspaceContext: false,
llmRetry: { maxTransientRetries: 2 },
} as never)).rejects.toThrow(/llmRetry/)
})
it('composes the TUI cluster around one fresh exact session identity', () => {
const { ctx, calls } = recordingContext()
tuiAgent.composeTuiApp(ctx, {