feat(agent): agent.spawn{await:true} suspends too; children wake the parent via fact

agent.await and agent.spawn{await:true} now return immediately when
children are still running: the session closes, runAgent registers a
wake signal in agent_run.await_wake and waits with waitForFact without
holding an io/agent worker slot. A child that settles (terminal or
waiting) signals the parent's registered fact, so the parent resumes at
once instead of polling; the suspended call replays and the child
report becomes the tool result. agent.spawn is registered replay-safe;
a suspended spawn is recorded as failed in the ledger so the replay
re-runs it against the same child.

Migration 0037 also adds the admin_recovery_code, admin_passkey and
webauthn_challenge tables used by the account work.

E11 asserts the parent is suspended with no run held by any worker
while it waits, and covers spawn{await:true} end to end.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 20:21:52 +08:00
1 parent f9b30b7e72
commit 5c6f483420
8 files changed
+382 -28

No files matched your search

+6 -3
View File
@@ -27,7 +27,7 @@ import { type RunUpstream, resolveRunUpstream } from "../pi/index.ts";
import type { ServerContext } from "../types.ts";
import { DEMO_SCRIPTS, parseFakeScripts } from "./fake-scripts.ts";
import { buildGoalReport } from "./report.ts";
import { type AwaitSuspension, attachLiveRun, type LiveRun, subagentSettings } from "./subagents.ts";
import { type AwaitSuspension, attachLiveRun, type LiveRun, subagentSettings, wakeParent } from "./subagents.ts";
// runAgent 任务的执行体:从设置装配 Pi(或 fake)运行时 → 事件先写 agent_run_event 再推 SSE →
// 崩溃恢复(pendingOperations>0 时先按幂等键对账未知结果再 resume)→ 消费追加消息 → 汇报以业务表为准。
@@ -502,11 +502,14 @@ export async function drive(
export async function goalSettled(env: AgentEnv, runId: string, modelText: string): Promise<AgentGoalReport | null> {
const r = await buildGoalReport(env.ctx.db, runId, modelText);
if (r.state !== "waiting") return r;
await env.ctx.db
const u = await env.ctx.db
.updateTable("agent_run")
.set({ status: "waiting", summary: r.summary })
.where("id", "=", runId)
.where("status", "in", ["running", "waiting"])
.execute();
.returning(["parent_run_id", sql<boolean>`(select status = 'running' from agent_run o where o.id = ${runId})`.as("was_running")])
.executeTakeFirst();
// 子会话刚转 waiting(对父会话来说已有结果):唤醒挂起等待的父会话
if (u?.parent_run_id && u.was_running) await wakeParent(env.ctx, runId);
return null;
}