通过ticket系统和--resume机制实现Claude Code与人协作决策,中断后恢复时值自动注入上下文。
当我的编程 Agent 需要我做决策时,它会在工单里提问,然后停止。进程退出。我的回答用 claude -p --resume 重新启动同一个 session id,我选择的值作为它的下一条 prompt 到达。
约束条件:默认情况下,orchestrator 一次只运行一个 agent,且每次运行上限为 240 分钟。如果一个 session 空闲着等我查看通知,这两个都会消耗掉。
我是 Panth,在 Oizom 带领软件团队。这来自我的开源看板 ticket-tracker,其中 headless Claude Code session 与人一起处理工单。Orchestrator 就是 workspaces/ 文件夹:每个看板一个进程,每个工单一个 Claude Code session。
README 里是这么画的:
To do ──orch claims──▶ In progress ──agent reports "review"──▶ Review ──owner──▶ Done
│ ▲
agent asks a │ │ the answer (form or a comment) resumes
question ────────▼ │ the same session
waiting
"waiting" 不是看板上的一个阶段。工单停留在 In progress。这是 orchestrator 在自己的状态文件里维护的一个阶段,和 session id 并排存放。
每个 agent 收到的是同样的简报。第 3 步是这样的:
如果一个真正的决策需要主人来做(范围、设计、任何破坏性或对外的事情),用 blocking: true 和真实的选项调用 ask_question。然后停止工作,用 status: "waiting" 和 question_id 结束本轮。不要轮询答案。Orchestrator 在答案到来时恢复同一个 session。
ask_question 在工单线程里放入一张表单卡片:标题、可选上下文、1 到 10 个字段(单选、多选、文本、数字、是/否、日期)。一个阻塞式问题会在工单卡片上显示一个 "Waiting for you" 徽章,并通知到目标人。提交后,卡片锁定并显示我的答案。
Agent 永远不会自己决定完成了。它的最后一条消息是结构化输出,用 --json-schema 强制执行:
const OUTCOME_SCHEMA = {
type: 'object',
properties: {
status: { type: 'string', enum: ['review', 'waiting', 'blocked'] },
summary: { type: 'string' },
question_id: { type: 'string' },
},
required: ['status', 'summary'],
};
结束一轮的三种方式,每一种对 orchestrator 都有不同的含义。
最后两种之间的区分很重要。waiting 表示需要人介入。blocked 在长工单上几乎总是意味着本轮用尽了空间,所以 orchestrator 会自己恢复它若干次(有限次数),不会问任何人。
每次运行是一个 claude -p 进程。当它退出时,orchestrator 发布一张收据,记录本轮的开销,并标记槽位为空,这样 To do 中的下一个工单可以开始。在 maxAgents: 1 的情况下,一个等待我的 session 会占用唯一的槽位。看板上所有其他工单都会在我的收件箱后面等待。
简报里还有第二个原因。Session 是 headless 的,所以当一轮结束时进程退出,带走所有后台任务。一轮要么完成工作,要么干净地结束。不存在一个半活的进程让我之后去找到。
而且没有任何轮询。Agent 被告知不要轮询。Orchestrator 也不挂在定时器上:它在看板变更时唤醒,答案作为事件到达。
当我提交表单时,tracker 在 agent 的收件箱里放入一个 question_answered 事件。Orchestrator 排空收件箱,看到处于 waiting 状态的工单的事件,用从我答案构建的 prompt 恢复 session:
answered: (t, q) => `The owner answered your question "${q.title}" on ${t.key}.
Values: ${JSON.stringify(q.answer?.values ?? q.values ?? {})}
Comment: …
Carry on with the work.`,
恢复本身就是一个 flag。如果该 id 的 session 文件仍然存在,orchestrator 传入 --resume <id>;如果不存在,就启动一个全新 session 并将成本重置为零。
...(resume ? ['--resume', sessionId] : ['--session-id', sessionId]),
Agent 带着它的全部上下文继续:它读过的文件、它的计划、它提过的问题。它不需要重新读取仓库来搞清楚自己在哪里。
一路上处理了几个边界情况:
用评论代替表单。如果我在线程里回复而不是填写卡片,也会恢复 session,prompt 里包含我的消息。
没有空闲槽位。如果另一个工单正在运行,工单的状态行显示 "Got your reply · queued until an agent is free",所以已回答的问题永远不会被当成忽略。
取消或过期。Prompt 里会说明这一点,并告诉 agent 去读线程,然后用最保守的选项继续,或者重新提问。
事件丢失。每四次循环,一个仍在等待的工单会直接拉取问题,以防事件丢失。
答案就是进展。它将工单的自动续接计数重置为零,我的任何评论也会重置为零。
Claude Code 通过 --resume 累计报告 total_cost_usd。在一个提了两个问题的 session 里,把报告的总成本相加会把第一轮的成本算三次。所以一轮的收据是相对于 session 上次报告总额的差值,session 的运行总额存在于 orchestrator 的状态中。
一个问题在我离开时不消耗计算资源:没有进程需要保持运行。代价是延迟:工单挂起直到我回答,没有任何其他东西来接手。我对此没有意见。Agent 提出的问题正是我说了需要我来做的决策。
对于进一步的操作,你可以考虑屏蔽此人或举报滥用