fix: address codex review round 1

Two functional gaps in the search tools change:

- Enforce rawOutputMaxBytes on UNTRUNCATED inline stdout too. The cap was
  only checked on the truncated->raw-spill path, so an executor retaining
  more inline than the search cap (or a deployment lowering the cap below
  the bash retention) could smuggle an over-cap parse through, contradicting
  the documented SEARCH_RAW_OUTPUT_OVERFLOW contract. Covered by a new
  over-cap-inline test.

- Load @deepseek-ai/dsh-timeout-policy in the coding-agent tree. The search
  tools declare timeoutMs but nothing in the demo enforced it, so the
  advertised 30s budget silently degraded to the bash executor's 60s
  backstop. The keyless smoke boots the amended tree.
This commit is contained in:
Dudu-0223
2026-07-09 21:12:41 +08:00
parent e0f20088d8
commit e94305d99e
4 changed files with 39 additions and 6 deletions

View File

@@ -45,6 +45,8 @@ flowchart LR
cfg --> plugin_coding_tool_fs
plugin_coding_tool_fs_search["tool-fs-search<br/>@deepseek-ai/dsh-tool-fs-search"]
cfg --> plugin_coding_tool_fs_search
plugin_coding_timeout_policy["timeout-policy<br/>@deepseek-ai/dsh-timeout-policy"]
cfg --> plugin_coding_timeout_policy
plugin_coding_spill_local["spill-local<br/>@deepseek-ai/dsh-spill-local"]
cfg --> plugin_coding_spill_local
plugin_coding_spill_policy["spill-policy<br/>@deepseek-ai/dsh-spill-policy"]
@@ -68,6 +70,7 @@ flowchart LR
| `fs-policy` | `@deepseek-ai/dsh-fs-policy` |
| `tool-fs` | `@deepseek-ai/dsh-tool-fs` |
| `tool-fs-search` | `@deepseek-ai/dsh-tool-fs-search` |
| `timeout-policy` | `@deepseek-ai/dsh-timeout-policy` |
| `spill-local` | `@deepseek-ai/dsh-spill-local` |
| `spill-policy` | `@deepseek-ai/dsh-spill-policy` |

View File

@@ -127,6 +127,13 @@
- id: tool-fs-search
name: '@deepseek-ai/dsh-tool-fs-search'
# The tool-call timeout enforcer: arms each declared ToolDefinition.timeoutMs
# (the search tools above declare 30s) as a deadline on exec.signal. Without
# it a declared budget is advisory and only the bash executor's own timeout
# backstop applies.
- id: timeout-policy
name: '@deepseek-ai/dsh-timeout-policy'
# Tool-output spill stack: a local backend that saves oversized tool text under
# a private session-scoped dir, and the tools/post-execute policy that replaces
# an over-budget plain-text result with a preview + the spill path (the model

View File

@@ -103,15 +103,26 @@ function classifyRunFailure(toolName: string, result: BashRunResult): SearchErro
}
/**
* Acquire the COMPLETE raw stdout of a finished run. Untruncated stdout is used
* as-is; truncated stdout is recovered from the executor's local raw spill file
* only when the complete file fits within `rawOutputMaxBytes`. A missing spill
* path or an over-cap file is a clear failure telling the model to narrow the
* search — never a silently-partial parse.
* Acquire the COMPLETE raw stdout of a finished run, enforcing
* `rawOutputMaxBytes` on BOTH transports: inline executor text (an executor
* retaining more than this package's cap must not smuggle an over-cap parse
* through the untruncated path) and the executor's local raw spill file, read
* only when the complete file fits the cap. A missing spill path or over-cap
* output is a clear failure telling the model to narrow the search — never a
* silently-partial parse.
*/
async function completeStdout(toolName: string, result: BashRunResult, rawOutputMaxBytes: number): Promise<string> {
if (!result.stdout.truncated) return result.stdout.text
const narrow = 'narrow pattern, path, or include and retry'
if (!result.stdout.truncated) {
const inlineBytes = Buffer.byteLength(result.stdout.text, 'utf8')
if (inlineBytes > rawOutputMaxBytes) {
throw new SearchError(
`${toolName} produced ${inlineBytes} bytes of raw output, over the ${rawOutputMaxBytes}-byte cap; ${narrow}`,
'SEARCH_RAW_OUTPUT_OVERFLOW',
)
}
return result.stdout.text
}
const spillPath = result.stdout.spillPath
if (spillPath === undefined) {
throw new SearchError(

View File

@@ -392,6 +392,18 @@ describe('raw output acquisition', () => {
expect(text(result)).toContain('narrow pattern, path, or include')
})
it('fails with SEARCH_RAW_OUTPUT_OVERFLOW when UNTRUNCATED inline stdout exceeds the cap', async () => {
// An executor retaining more inline than this package's cap (or a
// deployment lowering rawOutputMaxBytes below the bash retention) must not
// smuggle an over-cap parse through the untruncated path.
dir = await mkdtemp(join(tmpdir(), 'dsh-search-raw-'))
const { ctx, bash } = await setup({ config: { rawOutputMaxBytes: 16 } })
bash.handler = () => runResult(`${'x'.repeat(64)}\n`)
const result = await call(ctx, 'grep', { pattern: 'x' })
expect(result.error).toMatchObject({ name: 'SearchError', code: 'SEARCH_RAW_OUTPUT_OVERFLOW' })
expect(text(result)).toContain('narrow pattern, path, or include')
})
it('fails with SEARCH_RAW_OUTPUT_OVERFLOW when truncated stdout has no spill path', async () => {
dir = await mkdtemp(join(tmpdir(), 'dsh-search-raw-'))
const { ctx, bash } = await setup()