Feat/acp hub delegated sessions#91093
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 11, 2026, 8:55 AM ET / 12:55 UTC. Summary PR surface: Source +1466, Tests +900, Docs +51, Config 0, Other +18. Total +2435 across 56 files. Reproducibility: not applicable. as a bug reproduction; the PR body provides after-patch screenshots for spawn, label follow-up, close, and post-close rejection in a real local gateway setup. Review metrics: 2 noteworthy metrics.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the explicit opt-in delegate feature only after maintainer acceptance of the lifecycle defaults and protocol surface; keep broader threadless Do we have a high-confidence way to reproduce the issue? Not applicable as a bug reproduction; the PR body provides after-patch screenshots for spawn, label follow-up, close, and post-close rejection in a real local gateway setup. Is this the best way to solve the issue? Yes, as an explicit opt-in delegate contract this is a better-scoped path than relaxing every AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 68ec783e74b5. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +1466, Tests +900, Docs +51, Config 0, Other +18. Total +2435 across 56 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
@clawsweeper re-review Updated Real behavior proof on the latest head:
Video: https://github.com/user-attachments/assets/226ddef2-22d1-44b2-9709-4cb2c44deb1f Please re-check real behavior proof and the P1/P2 review items against current head. |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
@clawsweeper re-review Latest head
Please re-review proof + merge risk on current head. |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
@clawsweeper re-review Updates on current head
Please re-review scope wording, proof, and merge readiness on this head. |
…gent registry Validate hub-delegated lineage before persisting session-store mutations so rejected patches cannot leave mismatched spawnedBy rows on disk. Skip registerSubagentRun for delegate ACP spawns that already create a silent task. Co-authored-by: Cursor <cursoragent@cursor.com>
Align selectAcpSessionRowForEntry with sessionId-only entry picks, skip deleted-agent rejection for hub-delegated workers, and pass ACP metadata scope through label resolve. Co-authored-by: Cursor <cursoragent@cursor.com>
Share delegate/store fixtures, table-drive acp-core pure helpers, split lifecycle tests by close vs maintenance, and move sessions_send boundaries into a focused suite while removing cross-layer label/owner/expiry duplicates. Co-authored-by: Cursor <cursoragent@cursor.com>
Drop cross-layer duplicates for label reuse, resolve wiring, activity projection, and concurrent auto-label suffixes; consolidate lifecycle into one file with only close/lock/repair and a single maintenance scan test. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
love the demo/tutorial, very nice :D |
Summary
mode: "session"plusthread: true, which is unavailable or awkward on many hub channels.sessions_spawn({ runtime: "acp", delegate: true, label?, task }), follow up withsessions_send(label=...), list owned workers viasessions_list({ delegated: true }), and operators inspect or close workers with/acp delegate list|status|close <label>. Persistent harness teardown for hub-delegated workers is operator-facing (/acp delegate close) plus maintenance idle/max-age sweeps — the same lifecycle model documented indocs/tools/acp-agents.md.hubDelegatedmetadata and label follow-up, A2A skip conditions for parent-owned delegates, lifecycle maintenance/close ordering (acp.delegate.*), fail-closed ACP metadata/session-id/repair boundaries, owner-scoped label uniqueness (spawn pre-check andsessions.patch), and JSON-store vs sqlite discovery parity for operator/acp delegatevs maintenance.ACP spawn shapes (scope boundary)
Three
sessions_spawn({ runtime: "acp", ... })paths — this PR delivers row 3 only:mode: "run"sessions_sendmode: "session"+thread: true/acp closedelegate: truesessions_send(label=...)from owning hub session/acp delegate close+acp.delegate.*maintenanceThis PR delivers: hub-delegated persistent workers (row 3) for WebChat/WeChat and other non-thread hub surfaces.
Does not change: row 1 (
mode: "run") and row 2 (thread-bound persistent ACP). Broader decoupled-session / global session-management work stays out of scope — see Linked context.Full product docs:
docs/tools/acp-agents.md(Hub-delegated persistent workers).Design rationale: why
delegateinstead of relaxingmode: "session"?Short answer: persistence alone is under-specified.
mode: "session"today means thread-visible persistent ACP (follow-up in the bound thread, close via/acp close). Hub orchestration needs a different contract: parent-owned background worker, label-routed follow-up viasessions_send, skip A2A for owner sends, and operator/maintenance close — not “same persistent session, minus thread.”Why not
{ mode: "session" }withoutthread: true?label), visibility (channel thread vs internal worker), close owner (/acp closevs/acp delegate close+acp.delegate.*), or label rules (global vs owner-scoped).mode: "session"withoutthreadfail-closes on purpose; silently allowing it would create persistent workers without an explicit product contract.What
delegate: truebuys:hubDelegatedownership, owner-scoped labels, parent relay semantics,/acp delegateoperator surface, and maintenance policy — without changing row 1 (run) or row 2 (thread session) behavior.Relationship to #53548: this PR delivers the hub/orchestrator slice only; it does not generalize decoupled
mode: "session"for all channels or requesters.Architecture (hub → ACP worker)
Hub-delegated workers are parent-owned background ACP sessions. The hub session stays the only user-visible chat surface; the harness worker stays alive off-channel and is addressed by label, not by opening a Discord/Telegram thread.
Component map
flowchart LR subgraph Visible["User-visible hub surface"] U[User] CH["Channel adapter WebChat WeChat etc"] HUB["Hub session agent main webchat main"] end subgraph Worker["Parent-owned ACP worker background"] ACP["Persistent ACP session agent codex acp"] HX["External harness Codex Claude Code etc"] end subgraph Meta["Ownership and naming"] HD["hubDelegated ownerSessionKey plus label per owner"] end subgraph Lifecycle["Operator and maintenance"] OPS["acp delegate list status close"] SWP["Idle and max-age maintenance"] end U --> CH --> HUB HUB -->|sessions_spawn delegate true| ACP HUB -->|sessions_send by label| ACP ACP --> HX HUB --- HD ACP --- HD OPS --> HD SWP --> HD HUB -->|summarize or relay| CH --> UKey invariant: the worker does not bind a channel thread/topic. Follow-ups reuse the same harness session via
label; the hub agent decides what (if anything) to publish back to the visible channel.1) Initial spawn (first task)
Notes for reviewers:
thread: trueand no channel binding record is required.delegate: trueimplies persistentmode: "session"; one-shotmode: "run"is rejected.delegate-YYYYMMDD-HHMMSS(UTC) and returns it in the spawn result.2) Follow-up turns (send by label)
Notes for reviewers:
sessions.resolveuses owner-scoped label matching for hub-delegated workers (not global label uniqueness across owners).sessions_sendinto its own hub-delegated worker skips the A2A announce loop because the hub already owns the visible reply channel.sessions_sendreads the worker reply from canonicalchat.history; hub-delegated turns stay off visible delivery surfaces but still persist assistant/user turns to the session transcript./acp delegate close <label>(operator commands) and maintenance idle/max-age policy; thread-bound ACP sessions continue to use/acp close.3) How this differs from thread-bound persistent ACP
delegate: true)thread: true,mode: "session")sessions_send(label=…)from hub/acp delegate close <label>Also bundled from live dogfood on this branch:
agent.waitrace returnsnull, abort both waiters immediately sosessions_sendhonorstimeoutSeconds(default 30s) instead of blocking for minutes.Linked context
Which issues, PRs, or discussions are related?
delegate: true+sessions_send(label=…)). Does not close or replace the full decoupled-session ask.Does not address:
thread: true,/acp close) — unchanged.mode: "session"withoutdelegate: true.sessions_closefor hub-delegated workers (close is/acp delegate close+ maintenance by design).Was this requested by a maintainer or owner?
Local maintainer dogfood for hub → ACP delegation from WebChat/stable gateway (port 18789).
Real behavior proof (required for external PRs)
/acp delegate.~/.openclaw), WeChat hub session + WebChat delegated ACP worker session, Codex ACP harness via acpx. Close-path proof recorded against current head after the delegate label-routing close fix.sessions_spawn({ runtime: "acp", delegate: true, agentId: "codex", task: "..." })(with and without explicitlabel).sessions_send({ label: "<label>", message: "..." }).sessions_list({ delegated: true })./acp delegate list,/acp delegate status <label>./acp delegate close <label>, then/acp delegate list(expect empty), thensessions_send({ label: "<label>", message: "..." })(expect rejected / no resolve).sessions_sendtimeout honoring (~30s default).Screenshot 1 — hub-delegated ACP subagent spawn (
sessions_spawn({ runtime: "acp", delegate: true, ... }); persistentmode: "session"worker created withoutthread: true):Screenshot 2 — follow-up via
sessions_send(hub session sends to the spawned worker by label):Screenshot 3 —
/acp delegate listempty after close (after/acp delegate close <label>on current head):Screenshot 4 —
sessions_sendrejected after close (follow-up with the same closed delegate label):Observed result after fix:
Hub-delegated spawn accepted without
thread: true.Follow-ups by
labelwork from the same hub session.sessions_list({ delegated: true })returns owned workers.A2A follow-up skipped for parent-owned hub-delegated targets (
delivery.status="skipped"path).Idle stale-handle reconnect and
sessions_sendtimeout fixes verified after long-idle and short-wait scenarios.Spawn/follow-up (screenshots 1–2): hub-delegated spawn succeeds without
thread: true;sessions_send({ label: "<label>", ... })reaches the persistent worker and returns a reply.Close path (screenshots 3–4):
/acp delegate close <label>succeeds;/acp delegate listandsessions_list({ delegated: true })are empty afterward; follow-upsessions_send({ label: "<label>", ... })is rejected and does not reopen the closed worker.What was not tested:
timeoutSeconds: 0fire-and-forget on hub-delegated follow-up.Remote Crabbox/Testbox or GitHub Actions CI (local CI only; see Tests section).
Proof limitations or environment constraints: Proof is from a maintainer local environment, not Crabbox. Four screenshots above cover spawn,
sessions_sendfollow-up, close, and post-close rejection; list/status and edge cases remain covered by unit tests. Duplicate-label conflict, owner-scoped label resolve, expired-delegate reactivation, and close-ordering edge cases remain covered by unit tests.Before evidence (optional but encouraged): Prior dogfood without P0/P1 fixes showed long idle turns hanging until ~900s and
sessions_sendwaits exceeding configured timeout when race waiters stacked.Tests and validation
Which commands did you run?
What regression coverage was added or updated?
packages/acp-core/src/hub-delegated.test.ts— policy, ownership, expiry, auto-label generation/conflict suffix.packages/acp-core/src/session-interaction-mode.test.ts— hub-delegated parent-owned background classification.src/acp/hub-delegated-lifecycle.test.ts— JSON-store sweep without sqlite metadata, close ordering, expired-candidate resolution.src/commands/agent.acp.test.ts— hub-delegated ACP turns persist to canonical transcript for waitedsessions_send.src/agents/acp-spawn.test.ts— delegate spawn, auto-label, delegate/thread conflict, duplicate label across ACP harness stores; concurrent auto-label suffix allocation under label lock.src/agents/openclaw-tools.sessions.test.ts—sessions_sendskip-A2A for hub-delegated targets; owner-scoped label resolve viaspawnedBy.src/acp/control-plane/manager.repair-missing-metadata.test.ts— hub-delegated persistent repair, one-shot repair, thread-bound binding-key repair, fail-closed with no evidence.src/acp/runtime/session-meta.test.ts— fail-closed session_id drift; explicit rebind only.src/tasks/task-registry.maintenance.hub-delegated.test.ts— expired delegate maintenance via JSON-store sweep and marker-first close.src/config/schema.test.ts—acp.delegate.*accepted by strict schema.docs/tools/acp-agents.md,docs/concepts/session-tool.md,docs/gateway/configuration-reference.md.What failed before this fix, if known?
check:changedinitially failed onsrc/agents/tools/sessions-spawn-tool.tsTS5076 (??/||mixing); fixed with parentheses before CI green.sessions_sendcould block far longer thantimeoutSecondswhenagent.waitrace returnednullbut left a waiter alive.If no test was added, why not?
Targeted unit/integration coverage was added for the main contracts above. Full channel-matrix live proof remains manual dogfood.
Risk checklist
Did user-visible behavior change? (
Yes)Yes. New hub-delegated ACP worker shape, new
/acp delegateoperator commands, optional auto-generated delegate labels, owner-scoped label follow-up, and user-facing docs for the flow. Existing thread-bound and one-shot ACP paths are unchanged unless the caller opts intodelegate: true.Did config, environment, or migration behavior change? (
Yes)Yes, additive only:
acp.delegate.idleHours(default72) andacp.delegate.maxAgeHours(default168). Keys are now schema-valid with label/help metadata. No doctor migration required; defaults apply to new hub-delegated workers.Did security, auth, secrets, network, or tool execution behavior change? (
No)No new auth surfaces. Hub-delegated workers remain parent-owned with internal delivery/control-ui session effects and silent notify policy; waited
sessions_sendstill reads canonical session transcript viachat.history. Visibility stays scoped to the owning requester for list/close/status and label-based follow-up.What is the highest-risk area?
Parent/child session ownership and
sessions_senddelivery semantics (skipping A2A while still allowing intentional parent follow-up), plus ACP runtime handle reuse after idle (P0 touch) and lifecycle closure/repair boundaries for expired or partially lost delegates.How is that risk mitigated?
hubDelegated.ownerSessionKeystored in session metadata; list/close/status filter by owner.spawnedBy/hubDelegated.ownerSessionKey).hubDelegatedbefore runtime teardown so missing-metadata repair cannot resurrect closed workers.hubDelegatedrows even when sqlite metadata is absent or drifted.hubDelegated, binding key shape, or active bindings); session_id drift is fail-closed until explicit metadata rebind.