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:
1 parent
f9b30b7e72
commit
5c6f483420
8 files changed
+382
-28
No files matched your search
@@ -0,0 +1,63 @@
|
||||
import type { Kysely } from "kysely";
|
||||
import { dropTables, entityTable, execDdl, oneOf } from "../src/migration-helpers.ts";
|
||||
|
||||
// C1-B Agent 与账户遗留收尾:
|
||||
// - agent_run.await_wake:父会话挂起等待子会话(agent.await / agent.spawn{await:true})时登记的唤醒信号
|
||||
// {engineRunId, fact};子会话落定时按它写 fact + 发引擎信号,父会话恢复后清空。
|
||||
// - admin_recovery_code:TOTP 备用恢复码(一次性,argon2id 哈希存库,used_at 非空即已用)。
|
||||
// - admin_passkey:WebAuthn Passkey(COSE 公钥、签名计数器、传输方式、名称),可多把。
|
||||
// - webauthn_challenge:注册 / 登录挑战(一次性、5 分钟有效;条件更新消费,防重放)。
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(
|
||||
db,
|
||||
`
|
||||
alter table agent_run add column await_wake jsonb;
|
||||
|
||||
create table admin_recovery_code (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
admin_user_id uuid not null references admin_user (id) on delete cascade,
|
||||
code_hash text not null,
|
||||
used_at timestamptz,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
create index admin_recovery_code_unused_idx on admin_recovery_code (admin_user_id) where used_at is null;
|
||||
|
||||
${entityTable(
|
||||
"admin_passkey",
|
||||
`
|
||||
admin_user_id uuid not null references admin_user (id) on delete cascade,
|
||||
credential_id text not null,
|
||||
public_key bytea not null,
|
||||
counter bigint not null default 0 check (counter >= 0),
|
||||
transports jsonb not null default '[]'::jsonb,
|
||||
device_type text,
|
||||
backed_up boolean not null default false,
|
||||
name text not null,
|
||||
last_used_at timestamptz,
|
||||
constraint admin_passkey_credential_key unique (credential_id)`,
|
||||
)}
|
||||
|
||||
create table webauthn_challenge (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
purpose text not null ${oneOf("purpose", ["register", "login"])},
|
||||
challenge text not null,
|
||||
admin_user_id uuid references admin_user (id) on delete cascade,
|
||||
expires_at timestamptz not null,
|
||||
used_at timestamptz,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
create index webauthn_challenge_expires_idx on webauthn_challenge (expires_at);
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(
|
||||
db,
|
||||
`${dropTables(["webauthn_challenge", "admin_passkey", "admin_recovery_code"])}
|
||||
alter table agent_run drop column if exists await_wake;`,
|
||||
);
|
||||
}
|
||||
@@ -246,6 +246,7 @@ export type TaskStatus = "pending" | "running" | "waiting" | "succeeded" | "fail
|
||||
export type TasksOpLedgerStatus = "started" | "succeeded";
|
||||
export type TitleAliasKind = "original" | "translated" | "short" | "site_title" | "romanized" | "other";
|
||||
export type TitleAliasSource = "tmdb" | "user" | "site" | "agent" | "nfo" | "import";
|
||||
export type WebauthnChallengePurpose = "register" | "login";
|
||||
|
||||
export interface AcquisitionPlanTable {
|
||||
id: Generated<string>;
|
||||
@@ -296,6 +297,29 @@ export interface AcquisitionSlotTable {
|
||||
position: Generated<number>;
|
||||
}
|
||||
|
||||
export interface AdminPasskeyTable {
|
||||
id: Generated<string>;
|
||||
admin_user_id: string;
|
||||
credential_id: string;
|
||||
public_key: Buffer;
|
||||
counter: Generated<number>;
|
||||
transports: Generated<Json>;
|
||||
device_type: string | null;
|
||||
backed_up: Generated<boolean>;
|
||||
name: string;
|
||||
last_used_at: Timestamptz | null;
|
||||
created_at: GeneratedTimestamptz;
|
||||
updated_at: GeneratedTimestamptz;
|
||||
}
|
||||
|
||||
export interface AdminRecoveryCodeTable {
|
||||
id: Generated<string>;
|
||||
admin_user_id: string;
|
||||
code_hash: string;
|
||||
used_at: Timestamptz | null;
|
||||
created_at: GeneratedTimestamptz;
|
||||
}
|
||||
|
||||
export interface AdminSessionTable {
|
||||
id: Generated<string>;
|
||||
admin_user_id: string;
|
||||
@@ -355,6 +379,7 @@ export interface AgentRunTable {
|
||||
upstream_id: string | null;
|
||||
depth: Generated<number>;
|
||||
spawn_reason: string | null;
|
||||
await_wake: Json | null;
|
||||
}
|
||||
|
||||
export interface AgentRunEventTable {
|
||||
@@ -1552,9 +1577,21 @@ export interface TrashItemTable {
|
||||
updated_at: GeneratedTimestamptz;
|
||||
}
|
||||
|
||||
export interface WebauthnChallengeTable {
|
||||
id: Generated<string>;
|
||||
purpose: WebauthnChallengePurpose;
|
||||
challenge: string;
|
||||
admin_user_id: string | null;
|
||||
expires_at: Timestamptz;
|
||||
used_at: Timestamptz | null;
|
||||
created_at: GeneratedTimestamptz;
|
||||
}
|
||||
|
||||
export interface DB {
|
||||
acquisition_plan: AcquisitionPlanTable;
|
||||
acquisition_slot: AcquisitionSlotTable;
|
||||
admin_passkey: AdminPasskeyTable;
|
||||
admin_recovery_code: AdminRecoveryCodeTable;
|
||||
admin_session: AdminSessionTable;
|
||||
admin_user: AdminUserTable;
|
||||
agent_run: AgentRunTable;
|
||||
@@ -1627,6 +1664,7 @@ export interface DB {
|
||||
tasks_run_meta: TasksRunMetaTable;
|
||||
title_alias: TitleAliasTable;
|
||||
trash_item: TrashItemTable;
|
||||
webauthn_challenge: WebauthnChallengeTable;
|
||||
}
|
||||
|
||||
export type AcquisitionPlanRow = Selectable<AcquisitionPlanTable>;
|
||||
@@ -1635,6 +1673,12 @@ export type AcquisitionPlanUpdate = Updateable<AcquisitionPlanTable>;
|
||||
export type AcquisitionSlotRow = Selectable<AcquisitionSlotTable>;
|
||||
export type NewAcquisitionSlot = Insertable<AcquisitionSlotTable>;
|
||||
export type AcquisitionSlotUpdate = Updateable<AcquisitionSlotTable>;
|
||||
export type AdminPasskeyRow = Selectable<AdminPasskeyTable>;
|
||||
export type NewAdminPasskey = Insertable<AdminPasskeyTable>;
|
||||
export type AdminPasskeyUpdate = Updateable<AdminPasskeyTable>;
|
||||
export type AdminRecoveryCodeRow = Selectable<AdminRecoveryCodeTable>;
|
||||
export type NewAdminRecoveryCode = Insertable<AdminRecoveryCodeTable>;
|
||||
export type AdminRecoveryCodeUpdate = Updateable<AdminRecoveryCodeTable>;
|
||||
export type AdminSessionRow = Selectable<AdminSessionTable>;
|
||||
export type NewAdminSession = Insertable<AdminSessionTable>;
|
||||
export type AdminSessionUpdate = Updateable<AdminSessionTable>;
|
||||
@@ -1851,3 +1895,6 @@ export type TitleAliasUpdate = Updateable<TitleAliasTable>;
|
||||
export type TrashItemRow = Selectable<TrashItemTable>;
|
||||
export type NewTrashItem = Insertable<TrashItemTable>;
|
||||
export type TrashItemUpdate = Updateable<TrashItemTable>;
|
||||
export type WebauthnChallengeRow = Selectable<WebauthnChallengeTable>;
|
||||
export type NewWebauthnChallenge = Insertable<WebauthnChallengeTable>;
|
||||
export type WebauthnChallengeUpdate = Updateable<WebauthnChallengeTable>;
|
||||
Reference in new issue
Block a user