Codex 教程
按顺序阅读,循序渐进掌握相关 AI 工具与实战方法。
有一类 bug“平时测不出来、一上线就出事”。dsh 官方文档把它们归纳成“来之不易的缺陷类别规则”,每条都来自真实发布或差点发布的缺陷。本章讲五个最重要的模式:结果正交上报、dispose 完全停稳、凭据擦除、链接安全删除、回调异常隔离——共同目标是不让一个简单的边界情况把整个 Agent 搞挂。
一次运行可以同时具有多种性质:进程可能已超时,却仍捕获终止信号以退出码 0 结束。timedOut、signal、exitCode 是三个独立事实,必须各自上报,绝不能把一个标志嵌套在另一个的分支里,否则提前终止会被误判为正常成功:
export interface RunResult {
timedOut: boolean // 独立事实 1:是否超时
signal: NodeJS.Signals | null // 独立事实 2:是否被信号终止
exitCode: number | null // 独立事实 3:退出码
stdout: string
stderr: string
}
let timedOut = false
const timer = setTimeout(() => {
timedOut = true
child.kill('SIGTERM')
}, timeoutMs)
child.on('close', (code, signal) => {
clearTimeout(timer)
// 三个字段各自独立返回:timedOut=true 且 exitCode=0 是合法组合
resolve({ timedOut, signal, exitCode: code, stdout, stderr })
})
清理流程若只发出终止信号就返回,而不等工作真正停止,就会留下孤儿进程。正确做法是异步等待子进程退出,并在终止前先移除监听器,让迟到的完成事件保持静默:
export async function disposeQuiescent(child: ChildProcess): Promise<void> {
child.removeAllListeners() // 1. 先移除监听器
child.kill('SIGTERM') // 2. 请求停止
// 3. 等待真正退出;超时仍不退则升级 SIGKILL,Promise.race 防止被永远挂起
const forceKill = new Promise<void>((resolve) => {
setTimeout(() => { child.kill('SIGKILL'); resolve() }, 5_000)
})
await Promise.race([once(child, 'exit').then(() => undefined), forceKill])
}
启动命令必须使用清理过的环境变量,凡名称匹配 *KEY*、*SECRET*、*TOKEN*、*PASSWORD* 的项一律移除,否则 harness 凭证可能通过命令输出、env 或 spill 文件泄漏。临时文件与 spill 文件应放在权限 0700 的私有目录中,用随机文件名并以 'wx'、0o600 独占方式打开:
const SENSITIVE_NAME = /.*(?:KEY|SECRET|TOKEN|PASSWORD).*/i
export function scrubEnv(env: NodeJS.ProcessEnv): Record<string, string> {
const clean: Record<string, string> = {}
for (const [key, value] of Object.entries(env)) {
if (SENSITIVE_NAME.test(key)) continue // 敏感键直接跳过
if (value !== undefined) clean[key] = value
}
return clean
}
// 用法:spawn(argv[0], argv.slice(1), { env: scrubEnv(process.env), ... })
可能是符号链接或 Windows junction 的路径,先用 lstatSync().isSymbolicLink() 判断,再用 unlinkSync 删除——unlink 只删链接本身并拒绝真实目录,绝不跟随链接进入目标;Windows 上对 junction 调 rmSync(link) 会抛 ERR_FS_EISDIR。只有确认是真实目录后,才使用带 recursive 的 rmSync。
用户提供的监听器抛异常时,不得让它所在的 promise 被 reject,也不得饿死排在后面的监听器。用 try/catch 包裹分发循环并记录日志——一个行为不当的订阅者绝不能破坏核心生命周期:
export function dispatch(listeners: ReadonlyArray<() => void>): void {
for (const listener of listeners) {
try {
listener()
} catch (err) {
console.error('[dispatch] a listener failed:', err) // 记录并继续
}
}
}
dsh 内部随处可见此模式:session/event 的观察者失败会被记录并隔离,而不会让已提交的 append 失败。
另有两条值得记住:公共约定两侧都要遵守——同一结果的多种表示应在通过公共 API 返回前规范化;异步状态不是同步状态——不要把 agent/status 或 whenIdle() 当作某次 followup() 的结果,拥有一次运行的调用方必须显式定义区间。