fix(sdk-message-processor): preserve activeTaskToolIds for a pending sub-agent permission on task_notification (lr-b75006) - #419
Conversation
…sub-agent permission on task_notification (lr-b75006) Third instance of a class fixed twice before (lr-8b2e, lr-f969dc, lr-9d4b). Incomplete application of lr-9d4b own fix: task_notification deleted session.activeTaskToolIds[parentId] unconditionally while a sibling guard protects subagentToolOwners against the identical race (sub-agent nested permission still pending). partitionSubagentOwnedPermissions requires BOTH maps to agree a Task is live; with only one guarded, the pending permission was force-denied and purged by sweepClearedPermissionIndex while the browser card stayed rendered -- every click (Allow/Allow-for-Session/Deny) then found nothing to resolve, matching the reported symptom exactly. Fix: compute the same live-ownership snapshot already used to guard subagentToolOwners, and use it to also defer releasing activeTaskToolIds[parentId] and its activity token while a live permission still references the Task. Deferral is safe: tool_result release, per-turn generation bump, and idle-sweep backstop all still apply once the permission resolves. Regression test drives the real task_notification then result sequence end to end at the message-processor layer. Reproduced failing against pre-fix code (assertion on activeTaskToolIds survival failed as predicted), confirmed passing after the fix, full suite green 1584/1584. TASK: lr-b75006
|
PEACHES — clean (0 findings) Root-Cause VerificationPR claim: partitionSubagentOwnedPermissions (sdk-permission-ownership.js:47) requires BOTH owningTaskId AND activeTaskToolIds[owningTaskId] to be true. Pre-fix, only one half was guarded. VERIFIED TRUE. Pre-fix sdk-message-processor.js line 947 had unconditional delete session.activeTaskToolIds[parentId] with NO guard. Existing lr-9d4b guard protected subagentToolOwners (lines 1015-1021) only. AND-check was failing its second half when task_notification fired before result. Fix CompletenessFix defers activeTaskToolIds deletion by computing liveOwnedToolIds (lines 944-948) and checking parentOwnsLivePermission (950-957), guarding the delete (986-989). Precomputed snapshot is reused by subagentToolOwners guard (1015-1021). SYMMETRIC AND COMPLETE. Both halves of AND-check now defer. All ownership fields accounted for:
Deferral SafetyOperator resolving, still-pending at result, sub-agent errors — all paths eventually clear the deferred entries. Ownership invariant maintained; cleanup paths unbroken. Three Buttons RestoredAll route through permission_response handler reading session.pendingPermissions[requestId]. Pre-fix entry was force-denied and PURGED while card stayed rendered; clicks no-op'd. Post-fix entry survives, Deny works. Test QualityThird instance of this class (lr-8b2e, lr-f969dc, lr-9d4b). New variant risk LOW. This PR adds missing end-to-end test: sdk-message-processor-activetasktoolids-notification-race-lr-b75006.test.js. Exercises REACHABILITY (task_notification → result → permission_response), not plumbing. Key assertion would FAIL on pre-fix code (activeTaskToolIds deletion causes partition to purge entry via sweepClearedPermissionIndex). Addresses tome #845 lesson 1. |
|
BOBBIE -- clean Security audit of PR #419 (lr-b75006), scope b4811a2..68aa87d (lib/sdk-message-processor.js task_notification handler plus new regression test). Focus per assignment: fail-open risk, implicit-authorization window, grant scope, cross-ownership, resource exhaustion.
Scanners: semgrep --config auto against the 2 changed files -- 0 findings (200 rules, clean parse). gitleaks detect over b4811a2..68aa87d -- no leaks found. osv-scanner: not applicable, no dependency manifest or lockfile changed in this diff. No blocking or nit findings. |
|
Merged via clagentic-loadout v0.2.0
|
What changed
A sibling guard in lib/sdk-message-processor.js task_notification handler was incomplete. The handler deleted session.activeTaskToolIds[parentId] unconditionally, while a nearby guard (added by lr-9d4b) protects session.subagentToolOwners against the identical race: task_notification can arrive while a sub-agent own nested permission (e.g. its last Bash call) is still awaiting the operator click.
partitionSubagentOwnedPermissions (lib/sdk-permission-ownership.js), which result and processQueryStream finally rely on to decide whether to preserve a sub-agent-owned pending permission across turn-boundary cleanup, requires BOTH owningTaskId AND activeTaskToolIds[owningTaskId] to hold. Guarding only subagentToolOwners left the other half false, so the entry was force-resolved to deny and purged from session.pendingPermissions and sm.permissionRequestIndex by sweepClearedPermissionIndex -- while the browser permission card was never told to update, so it stayed rendered and clickable. Every subsequent click (Allow Once / Allow for Session / Deny) then hit the same dead pendingPermissions lookup in project-sessions.js permission_response handler and silently no-opped -- matching the reported symptom of none of the three buttons sticking.
Fix: compute the same live-ownership snapshot task_notification already uses to guard subagentToolOwners, and use it to also defer releasing activeTaskToolIds[parentId] and the activity token while a live permission still references this Task. Deferring the activity-token release is safe -- the token own tool_result release path, per-turn generation bump, and idle-sweep backstop all still apply once the permission resolves.
Why (investigation summary)
This is lr-b75006, reported as a P1: Allow, Allow-for-Session, and Deny all failed to stop a re-firing Bash permission prompt during crew-manifest orchestration work. Per the mandated investigation order:
Named predecessor: this is not a fresh independent cause and not a literal repeat of lr-8b2e, lr-f969dc, or lr-9d4b -- it is an incomplete application of lr-9d4b own fix. lr-9d4b added a guard so session.subagentToolOwners survives task_notification while a nested permission is pending, but did not mirror the same guard onto session.activeTaskToolIds, the other half of the AND-check partitionSubagentOwnedPermissions uses to decide preservation.
Verification (demonstrated failure then pass)
Regression coverage: test/sdk-message-processor-activetasktoolids-notification-race-lr-b75006.test.js. The message-processor layer is the right layer because it can directly observe the ordering between task_notification, a still-pending permission, and a later turn-boundary cleanup without a full daemon/WS/SDK harness.
Test status
npm test: 1584/1584 pass, 0 fail (full suite, post-fix).
TASK: lr-b75006