fix: address codex review round 1

- spill-policy validates maxInlineBytes as a non-negative integer at LOAD, so a
  bad config fails the deployment instead of letting a negative value reach
  TextRetainer and turn every oversized-result call into an isError.
- Document the spill seam vocabulary in docs/core-data-structures/spill.md
  (SaveTextSpill/SpillOwner/SpillSource/SpillRef/SpillPath, verbatim + type-equiv
  gated) and index it from core.md, matching the other capability seams.
This commit is contained in:
Dudu-0223
2026-07-08 22:54:26 +08:00
parent 463b72ce96
commit d0c2f0916d
6 changed files with 80 additions and 2 deletions

View File

@@ -8,7 +8,7 @@ This plugin registers **no service** and owns no storage or preview mechanics: p
| Key | Default | Meaning |
|---|---|---|
| `maxInlineBytes` | *(omitted)* | Model-facing context cap for a plain-text result, in UTF-8 bytes. **Omitted disables the policy entirely** (the plugin registers nothing). When set, a larger result is spilled and replaced with a preview derived from the same budget (head/tail split). |
| `maxInlineBytes` | *(omitted)* | Model-facing context cap for a plain-text result, in UTF-8 bytes (a non-negative integer; validated at load). **Omitted disables the policy entirely** (the plugin registers nothing). When set, a larger result is spilled and replaced with a preview derived from the same budget (head/tail split). |
## Behavior

View File

@@ -101,6 +101,12 @@ export function apply(ctx: Context, config: Config): void {
const maxInlineBytes = config.maxInlineBytes
// Omitted ⇒ no automatic spill policy: register nothing at all.
if (maxInlineBytes === undefined) return
// Validate at LOAD, not per call: a negative/fractional cap would reach
// TextRetainer's assertBudget and throw, turning every oversized-result call
// into an isError. A bad config must fail the deployment, not the tool.
if (!Number.isInteger(maxInlineBytes) || maxInlineBytes < 0) {
throw new Error(`spill-policy: maxInlineBytes must be a non-negative integer (got ${maxInlineBytes})`)
}
ctx.on('tools/post-execute', async (exec, result, next): Promise<PostToolDecision> => {
// Delegate first so a downstream listener (e.g. a hook) settles the result;

View File

@@ -82,6 +82,16 @@ describe('disabled mode', () => {
})
})
describe('config validation', () => {
it('rejects a negative maxInlineBytes at load', async () => {
await expect(setup({ maxInlineBytes: -1 })).rejects.toThrow(/non-negative integer/)
})
it('rejects a fractional maxInlineBytes at load', async () => {
await expect(setup({ maxInlineBytes: 1.5 })).rejects.toThrow(/non-negative integer/)
})
})
describe('oversized plain-text replacement', () => {
it('spills the full text and replaces the result with a preview + path', async () => {
const { ctx, spill } = await setup({ maxInlineBytes: 20 })