Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/rules/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ Demux the single stream: responses by request `id`, notifications by `threadId`,

## Primitives (typed; Pydantic wire models)

`initialize` → `thread_start/resume/list/read/archive` → `turn_start/steer/interrupt` → `inject_items` → approval responder. Verified gotchas to encode:
`initialize` → `thread_start/resume/list/read/archive/unarchive/name-set/search` → `turn_start/steer/interrupt` → `inject_items` → approval responder. Verified gotchas to encode:

- `thread/start.sandbox` is a **string** enum (`read-only`/`workspace-write`/`danger-full-access`); `turn/start.sandboxPolicy` is an **object** (`{type:"readOnly", ...}`). Different encodings — model both.
- `turn/steer` requires `expectedTurnId` (from `turn/started`).
- `thread/list` results are under `result.data` (not `result.threads`); `useStateDbOnly:true` reads the persisted store.
- `thread/search` is experimental; enable the experimental API capability before using it and keep the wrapper thin.
- `thread/resume` of a *persisted* thread yields live event fan-out; pre-persistence it errors `no rollout found`.
- Approvals are server→client requests: lane emits `thread/status/changed` `activeFlags:["waitingOnApproval"]`; reply `{id, result:{decision}}` (`accept`/`acceptForSession`/`decline`/`cancel`); server emits `serverRequest/resolved`. File-change approvals carry **no diff** — correlate by `itemId` to the `fileChange` item.
- Threads persist by default (`ephemeral:false`). Pass `ephemeral:true` for throwaway/test lanes.
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ uv run dispatch daemon log --limit 10
uv run dispatch down
```

Use owned lanes for writes. Existing desktop Codex threads can be attached, but v0 treats
attached lanes as observe-only: mutating commands such as `send`, `stop`, `lane archive`,
`goal set`, `goal clear`, `lane fork`, `lane rollback`, and `lane compact` are blocked by
ADR-0005. Attach is metadata-only by default; use `dispatch lane sync <lane>` when you want
dispatch to refresh its local indexed view of an attached thread.
Use owned lanes for turn-writing work. Existing desktop Codex threads can be attached as
managed lanes, but ADR-0005 still blocks turn-writing and history-mutating commands such
as `send`, `stop`, `goal set`, `goal clear`, `lane fork`, `lane rollback`, and
`lane compact` on attached lanes. Metadata/lifecycle actions (`rename`, `archive`,
`restore`) can target managed lanes or raw unmanaged Codex thread ids, and `search` can
span both. Attach is metadata-only by default; use `dispatch lane sync <lane>` when you
want dispatch to refresh its local indexed view of an attached thread.

For the operator guide, CLI/MCP examples, triggers, and plugin setup, start at
[`docs/usage/README.md`](docs/usage/README.md).
Expand Down
8 changes: 4 additions & 4 deletions docs/adrs/0005-lane-authority-capability-ladder.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ owners: ['[galligan](https://github.com/galligan)']

# ADR-0005: Lane Authority Capability Ladder

> Accepted (2026-06-03) after the Phase-1 cross-process spike. The spike did **not** clear attached-lane writes — it confirmed the observe-only default and revealed that cross-process observation is not even live. See "Phase-1 spike outcome" below. The gated write rungs remain locked for v0.
> Accepted (2026-06-03) after the Phase-1 cross-process spike. The spike did **not** clear attached-lane turn writes — it confirmed the write-locked default and revealed that cross-process observation is not live. See "Phase-1 spike outcome" below. The gated turn-write rungs remain locked for v0.

## Context

Expand All @@ -21,7 +21,7 @@ dispatch drives both lanes it **owns** (created via `open`) and lanes it **attac
Authority over a lane is a ladder, not a flag:

- **Owned lanes** (dispatch created them): full read/write, always.
- **Attached lanes** (existing desktop threads): **observe-only by default** — read metadata, sync a local index, read history; no `send`/`steer`/`brief`/`interrupt`.
- **Attached lanes** (existing desktop threads): turn-writing and history-mutating ops are blocked by default — read metadata, sync a local index, read history, and allow explicit metadata/lifecycle actions (`rename`, `archive`, `restore`); no `send`/`steer`/`brief`/`interrupt`, `goal set/clear`, `fork`, `rollback`, or `compact`.
- **idle-only-write** and **full-write** on attached lanes are unlocked only when **(a)** the slice-0 cross-process spike shows it is safe, **and (b)** the user explicitly opts in (per-lane or global).

## Assumptions (must hold; verify before relying)
Expand All @@ -44,7 +44,7 @@ Two `codex app-server` processes shared one isolated `CODEX_HOME` (modelling our
- **Live fan-out does NOT cross processes:** while A ran a turn, B (resumed) received **zero** live events. Live event fan-out is intra-process only (one app-server process). The spike-04 "resume = live co-presence" finding holds only for multiple connections to the *same* server process — which is exactly dispatch's own topology (ADR-0002), not the desktop-vs-daemon case.
- **Concurrent turns are uncoordinated:** A and B each ran a turn on the shared thread with no error returned, but there is no cross-process interlock (dispatch's advisory lock is dispatch-local and cannot gate the desktop app), so "no error" is not "safe."

**Decision:** keep attached lanes **observe-only** for v0. Observation is limited to metadata reads, explicit sync, history read, and periodic re-read (no live cross-process stream). ADR-0017 makes default attach metadata-only instead of `thread/resume`-based. The idle-only-write and full-write rungs stay locked; unlocking them needs a real cross-process interlock, which Codex does not expose today. This is the safe default the ladder already proposed — the spike confirms rather than relaxes it.
**Decision:** keep attached lanes locked for turn-writing and history-mutating ops in v0. Observation is limited to metadata reads, explicit sync, history read, and periodic re-read (no live cross-process stream). ADR-0018 permits explicit metadata/lifecycle actions (`rename`, `archive`, `restore`) because they do not start turns, steer turns, or mutate turn history. ADR-0017 makes default attach metadata-only instead of `thread/resume`-based. The idle-only-write and full-write rungs stay locked; unlocking them needs a real cross-process interlock, which Codex does not expose today. This is the safe default the ladder already proposed — the spike confirms rather than relaxes it.

## Alternatives considered

Expand All @@ -53,4 +53,4 @@ Two `codex app-server` processes shared one isolated `CODEX_HOME` (modelling our

## References

- ADR-0002 (Single Daemon over One App Server); ADR-0017 (Progressive Thread Sync Index); `docs/research/app-server-verification.md` (resume fan-out, cross-process untested); `PLAN.md` Phase-1 slice-0 spike.
- ADR-0002 (Single Daemon over One App Server); ADR-0017 (Progressive Thread Sync Index); ADR-0018 (Top-Level Thread Actions and Search); `docs/research/app-server-verification.md` (resume fan-out, cross-process untested); `PLAN.md` Phase-1 slice-0 spike.
2 changes: 1 addition & 1 deletion docs/adrs/0011-codex-session-registration-is-explicit.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ owners: ['[galligan](https://github.com/galligan)']

## Context

dispatch can discover persisted Codex sessions and attach them as lanes, but ADR-0005 keeps attached lanes observe-only by default because desktop Codex and dispatch run separate app-server processes over shared state. Automatically "picking up" every new Codex session would surprise users who do not want all agents visible to dispatch, a mesh peer, an MCP client, or automation triggers.
dispatch can discover persisted Codex sessions and attach them as lanes, but ADR-0005 keeps attached lanes blocked for turn-writing by default because desktop Codex and dispatch run separate app-server processes over shared state. Automatically "picking up" every new Codex session would surprise users who do not want all agents visible to dispatch, a mesh peer, an MCP client, or automation triggers.

Some users will want the opposite: a smooth path where sessions created in Codex become known to dispatch without manual `attach`. Codex hooks on session/thread start could provide that path by registering a session intentionally at creation time.

Expand Down
12 changes: 7 additions & 5 deletions docs/adrs/0016-history-goals-and-bounded-watch.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,16 @@ Treat `watch` as a bounded sample, not a streaming subscription. A durable live
belongs in a later protocol change that can push events over the control socket.

Keep mutating history/goal operations locked to owned lanes. Attached lanes remain
observe-only until cross-process semantics are verified.
turn-write locked until cross-process semantics are verified.

## Consequences

### Positive

- Agents can harvest history, inspect goals, and control long-running lanes without
leaving the contract-derived CLI/MCP architecture.
- The implementation uses stable App Server methods and avoids experimental
`thread/turns/list` or `thread/search`.
- The implementation uses stable App Server methods for transcript, goal, and history
controls. ADR-0018 separately allows broad search on experimental `thread/search`.
- The watch surface is honest about current transport limits.

### Negative
Expand All @@ -65,5 +65,7 @@ observe-only until cross-process semantics are verified.
and existing operator docs made `show` the summary command.
- **Expose a fake infinite `tail` over request/response JSONL** — rejected: it would
be misleading and fragile.
- **Use experimental `thread/turns/list` and `thread/search` now** — rejected: stable
`thread/read(includeTurns:true)` is enough for the first history surface.
- **Use experimental `thread/turns/list` for transcript now** — rejected: stable
`thread/read(includeTurns:true)` is enough for the first history surface. ADR-0018 later
accepts experimental `thread/search` only for broad search, where stable App Server
primitives do not provide an equivalent.
2 changes: 1 addition & 1 deletion docs/adrs/0017-progressive-thread-sync-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Attach is metadata-only by default:

- `dispatch lane attach <thread-id>` verifies the id with
`thread/read(includeTurns:false)`.
- It registers an observe-only attached lane and stores metadata sync state.
- It registers a turn-write locked attached lane and stores metadata sync state.
- It does not call `thread/resume`, load turn history, or grant write authority.

Progressive sync is explicit:
Expand Down
97 changes: 97 additions & 0 deletions docs/adrs/0018-top-level-thread-actions-and-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
id: 0018
slug: top-level-thread-actions-and-search
title: Top-Level Thread Actions and Search
status: accepted
created: 2026-06-05
updated: 2026-06-05
owners: ['[galligan](https://github.com/galligan)']
---

# ADR-0018: Top-Level Thread Actions and Search

## Context

dispatch originally required a Codex thread to be registered as a lane before most
operator workflows felt natural. That made the first-run experience clunky: a user could
see existing Codex sessions through `lane list --unmanaged`, but still had to attach before
basic lifecycle cleanup or targeted inspection.

At the same time, ADR-0005 correctly blocks turn-writing and history-mutating operations
on attached lanes because dispatch and the desktop app do not share a cross-process write
interlock. We need better thread ergonomics without weakening that authority boundary.

The App Server exposes stable metadata/lifecycle methods (`thread/name/set`,
`thread/archive`, `thread/unarchive`) and an experimental broad search method
(`thread/search`). The experimental search method can search persisted threads, but it
does not provide all dispatch-facing filters directly.

## Decision

Use three explicit states:

- **Managed**: a thread registered in dispatch's registry, either owned or attached.
- **Unmanaged**: a persisted Codex thread visible to App Server but not registered in
dispatch.
- **Synced**: a managed lane whose local dispatch index has been refreshed. Sync is
separate from management and separate from App Server thread lifecycle.

Expose `rename`, `archive`, `restore`, and `search` at the top level, while keeping lane
group variants for lane-shaped workflows:

- `dispatch rename <target> <new>` and `dispatch lane rename <old> <new>`
- `dispatch archive <target>` and `dispatch lane archive <target>`
- `dispatch restore <target>` and `dispatch lane restore <target>`
- `dispatch search <query>` and `dispatch lane search <lane> <query>`

Targets may be a managed lane id, a managed `@handle`, or a raw unmanaged Codex thread id.
An unresolved `@handle` is a missing lane, not a raw thread id fallback. Raw thread ids keep
the first-run path available without silently reinterpreting human handles.

`restore` only calls `thread/unarchive`; it must not resume the thread, start a turn, or
drain queued work.

Broad `search` uses experimental App Server `thread/search`, then applies dispatch-side
filters for managed/unmanaged state, repo/directory containment, and date ranges. Focused
lane search uses `thread/read(includeTurns:true)` and a local substring scan because the
App Server search schema does not expose a thread-id filter.

## Consequences

### Positive

- Users can clean up and inspect existing Codex threads before deciding whether to attach.
- The managed/unmanaged/synced vocabulary matches the real state model and avoids implying
that sync grants authority.
- Attached lanes remain protected from turn-writing and history-mutating operations while
still supporting explicit metadata/lifecycle actions.
- CLI and MCP continue to derive from the same ops; top-level commands are ergonomic routes,
not separate behavior.

### Negative

- Broad search depends on an experimental App Server method. It must stay documented as
experimental and covered by schema/client tests.
- Repo, directory, date, and managed/unmanaged filters are dispatch-side filters, so
`--max-scan` can bound results before every possible match is examined.
- Unmanaged raw-id actions rely on App Server errors for nonexistent raw ids.

## Alternatives Considered

- **Require attach before rename/archive/restore/search** — rejected: it preserves the old
friction and makes first-run cleanup unnecessarily indirect.
- **Treat sync as attach/hydrate/management** — rejected: sync is only an index refresh and
should not imply write authority or ownership.
- **Use `thread/search` for lane-focused search too** — rejected: the current experimental
schema does not provide a thread-id filter, so local transcript scan is more precise.
- **Build a full local transcript database first** — rejected for this slice: progressive
sync already captures compact local facts, and broad search can start from App Server
search without an ingestion-heavy first-run path.

## References

- ADR-0005 (Lane Authority Capability Ladder)
- ADR-0016 (History, Goals, and Bounded Watch)
- ADR-0017 (Progressive Thread Sync Index)
- `docs/research/app-server-verification.md` (`thread/name/set`, `thread/archive`,
`thread/unarchive`, experimental `thread/search`)
3 changes: 2 additions & 1 deletion docs/adrs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Files are `NNNN-slug.md`. Copy [`template.md`](template.md) to start one. Keep t
| [0002](0002-single-daemon-over-one-app-server.md) | Single Daemon over One App Server | Accepted |
| [0003](0003-own-scheduler-not-codex-automations.md) | Own Scheduler, Not Codex Automations | Accepted |
| 0004 | Single-Sourced Agent Docs (`.claude/rules` ↔ `AGENTS.md` symlinks) | Accepted — see [`.claude/rules/agent-docs.md`](../../.claude/rules/agent-docs.md) |
| [0005](0005-lane-authority-capability-ladder.md) | Lane Authority Capability Ladder | Accepted — Phase-1 spike confirmed attached=observe-only |
| [0005](0005-lane-authority-capability-ladder.md) | Lane Authority Capability Ladder | Accepted — Phase-1 spike keeps attached lanes turn-write locked |
| [0006](0006-handler-context-and-di.md) | Handler Context and Dependency Injection | Accepted |
| [0007](0007-normalized-internal-lane-events.md) | Normalized Internal LaneEvent Vocabulary | Accepted |
| [0008](0008-control-socket-protocol.md) | Control-Socket Protocol — JSON-RPC-lite over JSONL | Accepted |
Expand All @@ -26,3 +26,4 @@ Files are `NNNN-slug.md`. Copy [`template.md`](template.md) to start one. Keep t
| [0015](0015-new-command-config-presets-and-name-prefixes.md) | New Command, Config Presets, and Name Prefixes | Proposed |
| [0016](0016-history-goals-and-bounded-watch.md) | History, Goals, and Bounded Watch | Accepted |
| [0017](0017-progressive-thread-sync-index.md) | Progressive Thread Sync Index | Accepted |
| [0018](0018-top-level-thread-actions-and-search.md) | Top-Level Thread Actions and Search | Accepted |
Loading