Files
deepseek-harness/docs/defensive-patterns.md
T
Ziya ec47f2e40f docs(i18n): core-docs batch — five bilingual pairs via the committed pipeline
architecture / cordis-primer / defensive-patterns / glossary / testing
五篇核心文档配对,译文由进仓流水线产出(committed renderer + 金标
few-shot + 严格 XML 协议),并经二遍校验(逐句对照原文复述核查 +
注入 architecture/glossary 仓库上下文的一致性修复)。五篇生成文档
(agent-lifecycle、capability-seams、event-producer-consumer、
graph-atlas、tool-execution-pipeline 均为 gen-doc-graphs 产物)列入
排除清单——手写译文会在再生成时失效,与既有生成目录排除策略一致。
2026-07-15 23:03:13 -07:00

3.0 KiB

Defensive patterns

English | 中文

Hard-won bug-class rules: each pattern below is a class of defect that actually shipped or nearly shipped here, stated as the rule that prevents its recurrence. Read this before writing lifecycle, concurrency, subprocess, or teardown code. Test-tier counterparts (real entry path, world-verification, resource ownership) are in testing.md.

Report orthogonal outcomes independently

A result can be several things at once — a process can time out AND exit 0 because it trapped the signal. Surface each independent fact (timedOut, signal, exitCode) on its own; never nest one flag's report inside another's branch, or a caller reads a cut-short run as a clean success.

Honor cross-seam contracts on BOTH sides

When an interface documents two valid ways to signal something — an adapter may report failure by THROWING from stream() or by ending the stream with a finish {kind:'error'|'aborted'} chunk — the consumer handles both, not just the one the first implementation used. A library-backed adapter that can't throw mid-stream relies on the in-band path; a loop that only catches throws turns a provider 401 into a normal completed turn. Document the contract where the type is defined; exercise every branch through the real consumer.

Async state is not synchronous state

agent.send() does not flip status before returning; a background task's completion races turn boundaries; reader.close() fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (agent/status, task.done), and observe the transition (saw running THEN idle) rather than counting actions you assume map 1:1 to turns (the loop batches queued messages). The guard cuts both ways: if the awaited transition can never occur (EOF with no work submitted → never running), the wait hangs — handle the "nothing to wait for" branch explicitly.

Dispose must reach quiescence, not just request it

A teardown that issues kills/aborts but returns before the work stops leaves orphans. Make cleanup async and await the children's exit (kill → await done), and close listener/notification registries BEFORE killing so late completions stay silent. Tests prove disposal waited (pid gone right after await fiber.dispose()), not merely that the process eventually dies.

Contain callback exceptions at the boundary

A user-supplied listener that throws must not reject the promise it runs inside or starve the listeners after it. Wrap the dispatch loop in try/catch and log; one bad subscriber never breaks core lifecycle.

Never hand untrusted output the ambient environment or predictable paths

Spawned commands get a scrubbed env (drop *KEY*/*SECRET*/*TOKEN*) so harness credentials cannot leak into output, env, or spill files. Temp/spill files use a private (0700) dir, random names, and exclusive owner-only opens ('wx', 0o600) — predictable world-readable paths invite symlink races and disclosure.