mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 13:55:50 +00:00
refactor: 简化/复用/防御 — 清理 PR #386 审计发现
简化 (S1, S2): - src/cli/print.ts: 抽出 dispatchHeadlessCronCommand 本地 helper,把 cron 三个入口(onFire / onFireTask agent / onFireTask 非-agent)共享的 「dedup-claim → input-close-recheck → onSuccess」管线集中到一处, 避免三个分支在「claim 与 dispatch 之间发生 inputClosed」的处理上漂移。 enqueueAndRun 再抽出来,使两个非-agent 分支共用一个 onSuccess 回调。 约 -55 行重复模板。 - src/utils/autonomyPersistence.ts: 新增 retainActiveFirst<T> 泛型 helper —— active 记录无条件保留(不参与 cap),inactive 按 timestamp desc 填满剩余预算;统一 selectPersistedAutonomyRuns / Flows 的两阶段 排序语义。 - src/utils/autonomyRuns.ts、autonomyFlows.ts: 改用 retainActiveFirst, 删掉重复的内联两阶段排序逻辑。 复用 (R1, review #8): - tests/mocks/file-system.ts: 新增 readTempFile / tempPathExists 两个 Bun.file 包装,补齐 Node fs.readFileSync / existsSync 在测试里的 Bun-only 等价物。 - src/utils/__tests__/autonomyRuns.test.ts: 把全部 Node fs/path 导入 (existsSync, readFileSync, mkdir, writeFile, path.join/resolve)替换为 tests/mocks/file-system 的共享 helper + node:path(带 node: 前缀)。 不再有 6 处 mkdir + writeFile 模板,统一用 writeTempFile(自带 mkdir-p)。 解决 review #8 (Major) 的 Bun-only 运行时契约违反。 防御 (D1, OOM 早期信号): - src/services/compact/postCompactCleanup.ts: 在 void import().then() 末尾 补 .catch(logError)。当前 attributionHooks 是 stub,但当真实现被恢复 且 sweepFileContentCache 抛错时,这个 .catch 阻止它变成 unhandled rejection(函数返回值是 void,调用者无从观察异步失败)。 - src/utils/autonomyRuns.ts: 给 active runs 加 100 条软上限 + 一次性 warn。selectPersistedAutonomyRuns 仍然永不淘汰 active 记录,但跨过 阈值时 logError 一次,作为 finalize-leak 早期信号——避免 active 无限 增长悄悄使 AUTONOMY_RUNS_MAX 失效。
This commit is contained in:
156
src/cli/print.ts
156
src/cli/print.ts
@@ -2819,97 +2819,89 @@ function runHeadlessStreaming(
|
||||
let cronScheduler: import('../utils/cronScheduler.js').CronScheduler | null =
|
||||
null
|
||||
if (cronGate.isKairosCronEnabled()) {
|
||||
// Shared dedup-claim → input-close-recheck → onSuccess pipeline for the
|
||||
// three cron entry points (legacy onFire, onFireTask agent, onFireTask
|
||||
// non-agent). Centralizing the cancel-on-late-shutdown contract here keeps
|
||||
// the three branches from drifting on what happens between claim and
|
||||
// dispatch. onSuccess receives the claimed QueuedCommand and decides
|
||||
// whether to enqueue it (normal path) or mark the run failed (agent path).
|
||||
const dispatchHeadlessCronCommand = (params: {
|
||||
basePrompt: string
|
||||
sourceId: string
|
||||
sourceLabel: string
|
||||
logSuffix: string
|
||||
onSuccess: (command: QueuedCommand) => void | Promise<void>
|
||||
}): void => {
|
||||
if (inputClosed) return
|
||||
void (async () => {
|
||||
const command = await createAutonomyQueuedPromptIfNoActiveSource({
|
||||
basePrompt: params.basePrompt,
|
||||
trigger: 'scheduled-task',
|
||||
currentDir: cwd(),
|
||||
sourceId: params.sourceId,
|
||||
sourceLabel: params.sourceLabel,
|
||||
workload: WORKLOAD_CRON,
|
||||
shouldCreate: () => !inputClosed,
|
||||
})
|
||||
if (!command) return
|
||||
if (inputClosed) {
|
||||
await cancelQueuedAutonomyCommands({ commands: [command] })
|
||||
return
|
||||
}
|
||||
await params.onSuccess(command)
|
||||
})().catch(error => {
|
||||
logError(error)
|
||||
logForDebugging(
|
||||
`[ScheduledTasks] failed to enqueue headless task${params.logSuffix}: ${error}`,
|
||||
{ level: 'error' },
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const enqueueAndRun = (command: QueuedCommand): void => {
|
||||
enqueue({
|
||||
...command,
|
||||
uuid: randomUUID(),
|
||||
})
|
||||
void run()
|
||||
}
|
||||
|
||||
cronScheduler = cronSchedulerModule.createCronScheduler({
|
||||
onFire: prompt => {
|
||||
if (inputClosed) return
|
||||
void (async () => {
|
||||
// Use the prompt itself as the dedup source: legacy KAIROS-style
|
||||
// cron entries fire the same prompt repeatedly, and without a
|
||||
// dedicated task id the prompt text is what uniquely identifies
|
||||
// the entry. Without source-dedup, repeated fires would stack
|
||||
// additional runs while an earlier one is still active. Match the
|
||||
// onFireTask branch below to keep the two paths consistent.
|
||||
const command = await createAutonomyQueuedPromptIfNoActiveSource({
|
||||
basePrompt: prompt,
|
||||
trigger: 'scheduled-task',
|
||||
currentDir: cwd(),
|
||||
sourceId: prompt,
|
||||
sourceLabel: prompt,
|
||||
workload: WORKLOAD_CRON,
|
||||
shouldCreate: () => !inputClosed,
|
||||
})
|
||||
if (!command) return
|
||||
if (inputClosed) {
|
||||
await cancelQueuedAutonomyCommands({ commands: [command] })
|
||||
return
|
||||
}
|
||||
enqueue({
|
||||
...command,
|
||||
uuid: randomUUID(),
|
||||
})
|
||||
void run()
|
||||
})().catch(error => {
|
||||
logError(error)
|
||||
logForDebugging(
|
||||
`[ScheduledTasks] failed to enqueue headless task: ${error}`,
|
||||
{
|
||||
level: 'error',
|
||||
},
|
||||
)
|
||||
// Legacy KAIROS-style entries: the prompt text is what uniquely
|
||||
// identifies the cron entry, so it doubles as both source id and
|
||||
// source label for dedup.
|
||||
dispatchHeadlessCronCommand({
|
||||
basePrompt: prompt,
|
||||
sourceId: prompt,
|
||||
sourceLabel: prompt,
|
||||
logSuffix: '',
|
||||
onSuccess: enqueueAndRun,
|
||||
})
|
||||
},
|
||||
onFireTask: task => {
|
||||
if (inputClosed) return
|
||||
void (async () => {
|
||||
if (task.agentId) {
|
||||
const command = await createAutonomyQueuedPromptIfNoActiveSource({
|
||||
basePrompt: task.prompt,
|
||||
trigger: 'scheduled-task',
|
||||
currentDir: cwd(),
|
||||
sourceId: task.id,
|
||||
sourceLabel: task.prompt,
|
||||
workload: WORKLOAD_CRON,
|
||||
shouldCreate: () => !inputClosed,
|
||||
})
|
||||
if (!command) return
|
||||
if (inputClosed) {
|
||||
await cancelQueuedAutonomyCommands({ commands: [command] })
|
||||
return
|
||||
}
|
||||
await markAutonomyRunFailed(
|
||||
command.autonomy!.runId,
|
||||
`No teammate runtime available for scheduled task owner ${task.agentId} in headless mode.`,
|
||||
command.autonomy!.rootDir,
|
||||
)
|
||||
return
|
||||
}
|
||||
const command = await createAutonomyQueuedPromptIfNoActiveSource({
|
||||
if (task.agentId) {
|
||||
dispatchHeadlessCronCommand({
|
||||
basePrompt: task.prompt,
|
||||
trigger: 'scheduled-task',
|
||||
currentDir: cwd(),
|
||||
sourceId: task.id,
|
||||
sourceLabel: task.prompt,
|
||||
workload: WORKLOAD_CRON,
|
||||
shouldCreate: () => !inputClosed,
|
||||
})
|
||||
if (!command) return
|
||||
if (inputClosed) {
|
||||
await cancelQueuedAutonomyCommands({ commands: [command] })
|
||||
return
|
||||
}
|
||||
enqueue({
|
||||
...command,
|
||||
uuid: randomUUID(),
|
||||
})
|
||||
void run()
|
||||
})().catch(error => {
|
||||
logError(error)
|
||||
logForDebugging(
|
||||
`[ScheduledTasks] failed to enqueue headless task ${task.id}: ${error}`,
|
||||
{
|
||||
level: 'error',
|
||||
logSuffix: ` ${task.id}`,
|
||||
onSuccess: async command => {
|
||||
await markAutonomyRunFailed(
|
||||
command.autonomy!.runId,
|
||||
`No teammate runtime available for scheduled task owner ${task.agentId} in headless mode.`,
|
||||
command.autonomy!.rootDir,
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
return
|
||||
}
|
||||
dispatchHeadlessCronCommand({
|
||||
basePrompt: task.prompt,
|
||||
sourceId: task.id,
|
||||
sourceLabel: task.prompt,
|
||||
logSuffix: ` ${task.id}`,
|
||||
onSuccess: enqueueAndRun,
|
||||
})
|
||||
},
|
||||
isLoading: () => running || inputClosed,
|
||||
|
||||
Reference in New Issue
Block a user