Skip to content

refactor(intents): make hook ordering explicit and run hooks concurrently behind experimental.concurrent-hooks - #734

Merged
stefanhoelzl merged 3 commits into
mainfrom
concurrent-intents
Oct 2, 2026
Merged

stefanhoelzl merged 3 commits into
mainfrom
concurrent-intents

Conversation

@stefanhoelzl

Copy link
Copy Markdown
Owner
  • Explicit ordering instead of registration order. Implicit dependencies between handlers on one hook point are now requires/provides capabilities or hook points:
    • delete shutdown: the agents require agent-stopped (CAPABILITY_AGENT_STOPPED), so they stop the agent only after the api-server has closed its terminal
    • delete/hibernate shutdown: the lifecycle claim provides workspace-claimed; the agent resolver and the api-server's terminal close require it
    • vscode:modal-changed: terminal-focus provides modal-recorded, which the agents require before re-reporting status
    • app:shutdown gains a handoff hook point between stop and quit for the update installer (quitAndInstall must start after teardown and before app.quit())
  • Single-writer result folds. results[] carries no order: lastDefined becomes onlyDefined (throws when two handlers define a field), mergeRecords merges env/metadata maps with a per-key clash check, and the hand-written last-write-wins folds (resolve/open project, resolve workspace identity and state, get-status, switch, delete) use them. detect's blocking processes are concatenated.
  • experimental.concurrent-hooks (default false, applies live): the dispatcher starts every handler whose requires are met at once and re-checks the waiting ones as each finishes; events take the same path. A hook point with a single handler keeps the sequential path, so its timing is unchanged. Sequential mode is tick-for-tick identical to before.
  • e2e runs with the flag on (e2e/fixtures.ts), so an order-only dependency fails in e2e before the flag becomes the default. Locally: 5 repeats of the warm suite (600 spec runs) with the flag on, one failure — a pre-existing startup race (ch ws create right after ch project open, its nested project:open blocked by idempotency while app:ready restores the same project), being investigated separately.
  • Docs: docs/INTENTS.md (ordering contract, fold policy, the flag, app-shutdown hook points), docs/TESTING.md, docs/USER_GUIDE.md (settings), CLAUDE.md.

🤖 Generated with Claude Code

stefanhoelzl and others added 3 commits October 2, 2026 20:24
…e-writer

Prepares the dispatcher for running a hook point's handlers concurrently:
nothing may depend on the order modules are registered in, or on the order
`collect()` returns results in.

Implicit order dependencies, now capabilities or hook points:
- delete "shutdown": the agents require `agent-stopped`, so they stop the
  agent only after the api-server has closed its terminal
- delete/hibernate "shutdown": the lifecycle claim provides
  `workspace-claimed`; the agent resolver and the api-server's terminal close
  require it
- vscode:modal-changed: terminal-focus provides `modal-recorded`, which the
  agents require before re-reporting status
- app:shutdown gains a "handoff" hook point between "stop" and "quit" for the
  update installer, which must start after teardown and before app.quit()

Result folds:
- `lastDefined` becomes `onlyDefined`, which throws when two handlers define
  the same field; the hand-written last-write-wins folds use it too
- `mergeRecords` merges env/metadata maps and throws on a key clash
- `detect`'s blocking processes are concatenated rather than picked

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mental.concurrent-hooks

With the flag on, the dispatcher starts every handler whose `requires` are met
at once and re-checks the waiting ones as each finishes, instead of running
them one at a time. Events take the same path. A handler sees the capabilities
provided before it started; every handler still runs, errors are collected per
handler, and unsatisfiable handlers are still skipped. A hook point with a
single handler keeps the sequential path, so its timing is the same either way.

The flag defaults to false and is read once per hook point, so it applies live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every launch passes --experimental.concurrent-hooks=true, so a hook dependency
that only holds by registration order fails in e2e before the flag becomes the
default. Unit and integration tests keep the dispatcher's sequential default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@stefanhoelzl
stefanhoelzl merged commit eae2b13 into main Oct 2, 2026
16 checks passed
@stefanhoelzl
stefanhoelzl deleted the concurrent-intents branch October 2, 2026 18:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

internal no user facing changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant