Skip to content

Subagent policy v3 - #222

Open
tbuckley wants to merge 15 commits into
mainfrom
subagent-policy-v3
Open

Subagent policy v3#222
tbuckley wants to merge 15 commits into
mainfrom
subagent-policy-v3

Conversation

@tbuckley

Copy link
Copy Markdown
Owner

No description provided.

tbuckley added 11 commits May 9, 2026 17:56
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.
@tbuckley
tbuckley force-pushed the subagent-policy-v3 branch from 5fe2e3b to 3be3720 Compare July 3, 2026 02:44
claude added 4 commits July 3, 2026 13:15
…, 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

tbuckley commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

Pushed a review-fix series on top of the branch (also merged latest main, so the PR is no longer behind). npm run validate passes: format, lint, typecheck, 966 tests including the full e2e suite, plus the web workspace tests.

Correctness fixes

  • Cross-chat id collisions: approve/reject/markResolved now take chatId and load directly. The old findById walked every chat dir and could transition the wrong chat's record when two chats shared an id (trivially reachable with --id), corrupting both chats' state.
  • Missing authorization: delegationWait/delegationShow/delegationDelete now enforce the same parent→child visibility rule the legacy subagent endpoints had — a nested subagent could previously read any delegation's executionResult/prompt or stop and delete a sibling's running session. delegationWait also rejects unknown ids instead of hanging to timeout.
  • Stranded running records: execution-on-approve is now one shared path (runApprovedPolicy) used by both auto-approve and /approve; any throw marks the record failed instead of leaving it stuck in running with no way to retry, reject, or wake waiters.
  • Manual-delivery deadlock: the request CLI told subagents "the result will arrive as a new user message; do not poll" even though manual delivery never pushes a message. The endpoint now returns the resolved delivery and the CLI prints mode-correct guidance (delegations wait/notify-when/show).
  • Web crash on subagent approvals: the chat page rendered msg.commandName/msg.args.join(...) for every role: 'policy' message, so a subagent approval preview threw exactly when the user opened the chat to approve. Both shapes render now (with working approve/reject buttons), and role: 'policy' messages are properly discriminated by kind everywhere instead of 'commandName' in msg sniffing.
  • /pending hid subagent approvals: it filtered kind: 'policy' while /approve//reject handle both kinds; pending spawns/sends are now listed.
  • Turn-counter race reintroduced: spawn/send increment the parent turn's subagent counter synchronously before any await again (the deleted guard's comment described exactly this race — a sibling completion firing turnEnded mid-spawn).
  • Observer races: subscriptions register in the live registry before hydrating from disk (a resolution landing mid-registration was silently lost, wedging mode: 'all' subscriptions forever); fire() deletes the subscription file even if the notification append throws; unsubscribe removes the on-disk file when no in-memory observer matches (phantom files permanently blocked delegations delete).
  • Reject parity: policy/subagent rejection honors wasCovered + delivery like approve (no more double-fire), and subagent rejection now notifies the requesting agent instead of leaving it blocked.
  • State clobbering: an approval-gated subagents send to a running subagent flipped the record back to pending, so the in-flight run's completion was silently dropped; it now returns CONFLICT. Send-time delivery is persisted on the record and executeSubagent reads delivery from the record instead of a threaded isAsync flag.
  • CLI approval feedback: subagents spawn/send report approval-gated results instead of silently returning or blocking 60s on a delegation that isn't running.
  • Restored the 100-pending-per-chat cap the old PolicyRequestService enforced (with its test).
  • Store robustness: writes are atomic (shared writeJsonFile tmp+rename); corrupt files warn-and-skip instead of one torn write breaking /pending, delegations list, subagentList, and stop for the whole chat.

Cleanups / structure

  • Dead code deleted: findById, update() (bypassed the state machine), waitForSingleId, isCoveredByObserver/isCovered.
  • Single source of truth for terminal states + kind/state enums in shared/delegations.ts (base schema defined once via .extend, so a field can't be silently stripped from one kind on load); shared isENOENT replaces 8+ inline copies; DelegationStore's record/subscription IO deduped through one readValidated helper.
  • Spawn/send share one gateAndExecute tail; the delegations router reuses the manager's store instead of constructing its own.
  • Perf: parallel loads in list()/hydration/wait paths; startup wipe renames the delegations tree aside and deletes in the background so daemon readiness doesn't scale with accumulated history.

Tests added

Cross-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 /pending, execution-failure handling on /approve, and approve/reject notification suppression.

One deliberate behavior note: subagents send with no --delivery flag keeps the legacy synchronous block-and-print behavior (there's an e2e test codifying it); explicit --delivery notify returns immediately.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VfmL3y2DPt7x5jDi5EC8aH


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants