Subagent policy v3 - #222
Conversation
Add unified Delegation record/zod schema and a thin file-IO DelegationStore. No RPC wiring yet; RequestStore and ChatSettings.subagents remain authoritative.
Add singleton DelegationManager with full state-machine transitions (create/approve/reject/markResolved/list/get/delete/wipeAll) and the new DAEMON_EVENT_DELEGATION_RESOLVED event. Wait/unsubscribe/sendToSubagent remain stubs (filled in by later tickets). Daemon boot now also wipes the unified delegations tree, additive to legacy cleanup paths.
Policy-request creation, approval, rejection, and /pending all flow through delegationManager. Resolved records are retained on disk (under .clawmini/tmp/delegations/<chatId>/<id>.json) instead of being deleted on resolve. RequestStore + PolicyRequestService class removed; the service file is now a thin executor.
Subagent spawn/send/stop/list/tail/wait now route through delegationManager. Subagent IDs are 3-char alphanum (UUIDs gone), terminal state writes via markResolved (stop settles at failed, not completed). assertVisibleTo/update added to the manager; single-id sync wait implemented. ChatSettings.subagents is deprecated (kept one release) and no longer written.
Add the `subagents` rule list in policies.json with built-in $self → $self auto-approve. createSubagent and sendToSubagent evaluate rules (exact / prefix / * / $self, first-match-wins) and hold non-approved edges as pending delegations with a chat preview. /approve and /reject now dispatch by kind to start subagents on the subagent code path.
DelegationManager.wait now supports sync + subscribe and any + all modes. ObserverRegistry indexes waiters/subs per chat and runs the suppression invariant: per-id <notification> messages are swallowed for delivery:'notify' delegations covered by an observer; the covering observer owns the wakeup. Subscriptions capture originSessionId so wakeups survive /new.
Add delegationList/Show/Delete endpoints and the kind-agnostic `delegations` CLI group (list/wait/notify-when/unsubscribe/show/delete). Lite client's polling loop in subagents spawn/send replaced with a single delegationWait call. subagents wait/list/delete are gone. delegationDelete refuses while a covering subscription exists.
Surface delivery on subagents spawn/send and request <cmd>. Defaults: notify for root callers, manual for subagent callers (depth ≥ 1). --async kept as deprecated alias with stderr warning; --delivery wins when both are passed. Manual-mode invocations print a one-line hint pointing to delegations wait / notify-when.
Add clawmini-delegations skill; rewrite clawmini-subagents and clawmini-requests SKILL.md for --delivery + delegations group + approval gating. Remove deprecated surfaces: --async flag, subagentWait tRPC wrapper, ChatSettings.subagents, SubagentTracker, PolicyRequest/RequestState types. delegationManager.wipeAll() is now the only daemon-start wipe.
5fe2e3b to
3be3720
Compare
…, observer race fixes - approve/reject/markResolved now take chatId and load directly instead of findById's cross-chat scan, which could transition the wrong chat's record on 3-char id collisions (or identical --id values) and corrupt both chats. - runApprovedPolicy is the single execute-on-approve path (auto-approve and /approve); any throw marks the record failed instead of stranding it in 'running' with no way to retry, reject, or wake waiters. - /pending lists pending subagent approvals, not just policy requests. - subagentSpawn/subagentSend increment the parent turn counter synchronously before any await again (restores the deleted guard; a sibling completion could otherwise fire turnEnded mid-spawn), and share one gateAndExecute tail. - Policy/subagent reject honor wasCovered + delivery like approve does (no more double-fire); subagent rejection now notifies the requesting agent. - ObserverRegistry: subscriptions register before hydration (a resolution landing mid-registration was lost, wedging mode:'all' forever); fire() deletes the subscription file even if the notification append throws; unsubscribe(chatId) removes the on-disk file when memory misses. - sendToSubagent refuses to flip a running record to pending (the in-flight run's terminal transition was silently skipped) and persists delivery. - DelegationStore: atomic writes via shared writeJsonFile; corrupt files are warned+skipped instead of wedging every list; subscription IO deduped via readValidated; parallel list loads; wipeAll renames + background-deletes. - Restore the 100-pending-per-chat cap the old PolicyRequestService enforced. - Delete dead code: findById, update(), waitForSingleId, isCoveredByObserver. - Single source for terminal states + kind/state enums in shared/delegations; base schema defined once via .extend; shared isENOENT helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VfmL3y2DPt7x5jDi5EC8aH
…, web approval rendering - delegationWait/Show/Delete enforce the parent-child visibility rule the legacy subagent endpoints had (FORBIDDEN on non-children); wait also rejects unknown ids instead of hanging to timeout. Subscription reads go through the manager's store instead of per-request DelegationStore instances. - createPolicyRequest returns the resolved delivery; the request CLI now prints delivery-correct guidance — a manual-delivery subagent was told 'the result will arrive as a new user message; do not poll' when no message is ever pushed for manual delivery, stranding it forever. It now points at delegations wait/notify-when/show. - subagents spawn/send: report approval-gated results instead of silently returning (spawn) or blocking 60s on a delegation that isn't running (send). The no-flag legacy sync-send path is kept (e2e-tested) and documented. - role:'policy' messages are discriminated by kind everywhere (new isSubagentApprovalMessage guard; writers stamp kind:'policy'), replacing 'commandName' in msg sniffing in turn-log, messages, subagents tail. - web: chat page rendered msg.commandName/msg.args for every policy-role message, so a subagent approval preview crashed the message list (msg.args.join on undefined) exactly when the user needed to approve. It now renders both shapes, with working approve/reject buttons. - delegations CLI reuses the shared handleError helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VfmL3y2DPt7x5jDi5EC8aH
…server race, corrupt-file tolerance - delegation-manager tests updated for chatId-scoped approve/reject/ markResolved; update() test removed with the method. - New coverage: cross-chat id collision isolation, per-chat pending cap, sendToSubagent running-state guard + persisted delivery, unsubscribe with chatId removing phantom on-disk files, and a deterministic regression test for the subscription registration race. - New delegations-router test file: FORBIDDEN on non-child show/delete/ wait, unknown-id wait rejection, parent-child visibility, and unsubscribe cleaning up on-disk files without an in-memory observer. - slash-policies tests updated for the shared runApprovedPolicy path; new cases for kind-agnostic /pending, execution-failure reply, and approve/reject notification suppression (wasCovered / manual delivery). - delegation-store tests: corrupt files warn-and-skip instead of throwing; list() skips corrupt entries; atomic write leaves no tmp files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VfmL3y2DPt7x5jDi5EC8aH
|
Pushed a review-fix series on top of the branch (also merged latest Correctness fixes
Cleanups / structure
Tests addedCross-chat scoping, pending cap, endpoint authorization (FORBIDDEN on non-children, unknown-id waits), the subscription registration race (deterministic), phantom-subscription cleanup, corrupt-file tolerance, atomic writes, kind-agnostic One deliberate behavior note: 🤖 Generated with Claude Code https://claude.ai/code/session_01VfmL3y2DPt7x5jDi5EC8aH Generated by Claude Code |
No description provided.