fix(agents,gateway): three subagent announce delivery failures in loopback token-auth setups#85716
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed July 13, 2026, 2:07 PM ET / 18:07 UTC. Summary PR surface: Source +65, Tests +16. Total +81 across 4 files. Reproducibility: yes. at source level: current main’s auth-none omission, the PR’s unconditional subagent opt-in, and the documented auth-none scope-clearing failure form a high-confidence regression path; a live current-head run is still absent. Review metrics: none identified. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Preserve auth-none device-identity omission, honor the subagent opt-in only when active token/password shared auth requires paired-device scope verification, add auth-none and token-auth regressions, retain the bounded settle/context cleanup, and provide redacted current-head live proof. Do we have a high-confidence way to reproduce the issue? Yes at source level: current main’s auth-none omission, the PR’s unconditional subagent opt-in, and the documented auth-none scope-clearing failure form a high-confidence regression path; a live current-head run is still absent. Is this the best way to solve the issue? No as submitted: the explicit opt-in is narrower than the original scopes-based guard and the settle wait is now bounded, but the opt-in must preserve auth-none behavior before this is the safest fix. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 553c2459619f. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +65, Tests +16. Total +81 across 4 files. View PR surface stats
Security concerns:
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
Review history (1 earlier review cycle)
|
|
ClawSweeper PR egg 🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat. Where did the egg go?
|
martingarramon
left a comment
There was a problem hiding this comment.
CI: 67/70 — 2 real failures from Fix 1
checks-node-agentic-gateway-core and agentic-control-plane-auth-node both fail on the same root cause.
Fix 1 — condition in shouldOmitDeviceIdentityForGatewayCall is too broad
The added guard at call.ts:341-344:
if (requestedScopes.length > 0) {
return false; // keep device identity
}fires before the BACKEND + GATEWAY_CLIENT + loopback + hasSharedAuth block. Any call where the resolved scopes array is non-empty now retains device identity — not just subagent loopback calls.
Two tests in src/gateway/call.test.ts encode the opposite contract:
:427—keeps direct-local backend shared-token auth independent of paired device state:expect(lastClientOptions?.deviceIdentity).toBeNull()— now fails:658—uses backend client metadata for explicit scoped default calls: same assertion
The production bug (subagent completion failing with missing scope: operator.write) is real. The fix path through callSubagentGateway → callGateway → callGatewayWithScopes (explicit-scopes branch) → executeGatewayRequestWithScopes → shouldOmitDeviceIdentityForGatewayCall is correct — that path needs device identity when scopes are present. One narrowing would be an explicit flag threaded from the subagent path (e.g., requireDeviceIdentity?: true); any equivalent that preserves the existing direct-local shared-token contract is acceptable.
Fix 2 — waitForSessionsYieldAbortSettle — direction correct, bounded-wait question
The await params.settlePromise.catch(() => {}) after the timeout log ensures the lock drains before the function returns. The intended direction is right. One question: if the settle promise itself stalls (e.g., a hung fs operation), this second await has no timeout fallback of its own. Is there an upper bound on how long settlePromise can remain pending, or does it need its own guard here?
Fix 3 — LGTM
stripSessionsYieldArtifacts: both the messages loop and fileEntries loop now cover SESSIONS_YIELD_CONTEXT_CUSTOM_TYPE, exactly mirroring the existing pattern for SESSIONS_YIELD_INTERRUPT_CUSTOM_TYPE. Ties the comparison to the shared constant (defined at line 5) rather than a raw string.
Fix 1 still needs narrowing before this is merge-ready.
This comment was marked as spam.
This comment was marked as spam.
|
@martingarramon thanks — you were right, the Fix 1 guard was too broad. Inferring "subagent call" from the presence of Pushed a narrowing using the explicit Re the codegraph cross-PR flag with #85424: both touch |
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
…pback token-auth setups Rebased onto current main. Three narrow fixes for subagent completion announces failing to deliver over loopback gateway-client + token auth: - Fix 1 (gateway/call.ts): add an explicit `requireDeviceIdentity` opt-in so subagent completion calls retain device identity for operator-scope checks, instead of inferring it from the presence of scopes (which also fired for ordinary direct-local shared-token calls). Narrows the guard per review. - Fix 2 (embedded-agent-runner): after the sessions-yield abort-settle soft timeout, keep waiting (bounded by a hard cap) for the session file lock to release so the next turn does not hit a stale lock — without blocking forever if the settle itself stalls. - Fix 3 (embedded-agent-runner): also strip the sessions_yield context marker during abort cleanup so a subsequent announce re-run does not re-yield and get rejected as "did not produce a visible reply". Fixes openclaw#77807. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
895d4d3 to
429d29e
Compare
|
Rebased onto current main and addressed the open items:
Now MERGEABLE. Real-behavior-proof still pending on my side. @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
Heads-up on |
What Problem This Solves
Three related bugs that break subagent completion delivery when running OpenClaw with loopback token-auth (e.g. Google Chat channel,
sessions_spawnorchestration pattern). All three were found and validated on a production Linux/GCP deployment: OpenClaw 2026.5.20, Node 22, systemd user service.Fix 1 —
src/gateway/call.ts— device identity for BACKEND calls with explicit scopesFixes: #77807
shouldOmitDeviceIdentityForGatewayCallunconditionally omitted device identity for allBACKEND + GATEWAY_CLIENT + loopbackcalls. Internal calls such ascallSubagentGatewaycarry explicit operator scopes like["operator.write"]. Without device identity the gateway cannot verify those scopes against the paired device token and rejects with:Fix: return
false(keep device identity) whenparams.scopesis non-empty. Also threadsscopesthroughresolveDeviceIdentityForGatewayCallso the helper can inspect them.Fix 2 —
src/agents/pi-embedded-runner/run/attempt.sessions-yield.ts— await settle after timeoutwaitForSessionsYieldAbortSettleraced the settle promise against a 2 s timeout and returned immediately on timeout, leaving the session transcript file lock held. The next turn on the same persistent session (e.g. a Google Chat DM) failed with"file lock stale", triggering a model fallback and surfacing an internal error message to end users.Fix: after logging the timeout warning,
await params.settlePromise.catch(() => {})so the file lock is always released before the function returns.Fix 3 —
src/agents/pi-embedded-runner/run/attempt.sessions-yield.ts— strip context message before completion announceWhen a new incoming message aborts an active
sessions_yield, theopenclaw.sessions_yieldcontext message (containing[Context: The previous turn ended intentionally via sessions_yield...]) remains in the session transcript. When a subagent completion announce subsequently re-runs the agent to deliver the result, the agent sees this context message and responds viasessions_yieldagain (producing acustom_message). The announce system does not recognise acustom_messageas a visible reply and emits:stripSessionsYieldArtifactsalready strips the interrupt custom type (openclaw.sessions_yield_interrupt) but not the context custom type (openclaw.sessions_yield).Fix: strip both types in the in-memory messages loop and in the
fileEntriesloop.Test plan
missing scope: operator.writeTested on production: Google Chat + Pipedrive sub-agent workflow, 5+ consecutive turns, no errors.
Reported-by: jailbirt [email protected]
🤖 Generated with Claude Code
Evidence
Per @martingarramon's review, Fix 1's guard inferred "subagent call" from the mere presence of
params.scopes, but scopes ride on many ordinary backend calls, so it kept device identity for direct-local shared-token calls too — breakingcall.test.ts:427and:658. Narrowed to an explicitrequireDeviceIdentityopt-in flag set only at the subagent chokepoint (callSubagentGateway).Validation (re-runnable)
Both previously-failing shards now pass. New regression test: a loopback BACKEND scoped call with
requireDeviceIdentity: truekeepsdeviceIdentity(scopes["operator.write"], BACKEND/GATEWAY_CLIENT).Change
src/gateway/call.ts— addrequireDeviceIdentity?: booleantoCallGatewayBaseOptions; inshouldOmitDeviceIdentityForGatewayCallreplaceif (requestedScopes.length > 0) return false;withif (params.opts.requireDeviceIdentity === true) return false;; drop the now-unusedscopesarg fromresolveDeviceIdentityForGatewayCall.src/agents/subagent-spawn.ts—callSubagentGatewaypassesrequireDeviceIdentity: true(the single chokepoint the completion announce /agentcall flows through).src/gateway/call.test.ts— regression test reproducing the original bug.Cross-PR #85424
Both touch
executeGatewayRequestWithScopesbut on non-overlapping regions: #85424 adds event-loop-drift/timeout handling in the body; this PR's only edit there is theresolveDeviceIdentityForGatewayCall(...)argument at the deviceIdentity line. No semantic conflict — suggest #85424 merges first and this rebases on top.