[v0.1/Proto Review] Decide whether to remove unobservable operation IDs before release #296

Closed
opened 2026-09-18 21:42:30 +09:00 by natsukium · 1 comment
Owner

Related: #24, #12

Why

The release-readiness review found that retarget operation IDs are currently exposed as a public protocol and CLI feature without a user-observable completion path.

OpsSwitched.operation_id is returned by switch and retarget commands, but no CLI verb or bridge operation accepts the token for lookup, waiting, or cancellation. In the daemon, OperationLedger::lookup has no production caller; OperationLedger::land only updates bookkeeping whose result is used for a debug log. Carrier retargets cannot be resolved by the origin daemon by design.

This leaves the release contract paying for OperationId, OperationEcho, several minor-version gates, CLI schema fields, and a bounded ledger without providing a capability that a caller can act on. The reference documentation currently describes pending, landed, and superseded states, but does not expose a way for users to observe them.

Decision required before the first release

Choose one of these deliberately:

  1. Remove operation IDs from the v0.1 contract. Keep queued as the honest result of a switch, remove the operation fields and ledger, and revisit completion tracking only when a real consumer needs it.
  2. Complete the feature. Add a typed operation-status path and an explicit CLI/bridge wait or query contract, including timeout, eviction, daemon restart, and cross-daemon retarget semantics.

The current partial implementation should not remain the unexamined final state.

Acceptance criteria

  • The decision is recorded in the owning IPC explanation and reference documents.
  • The proto, minor ledger, CLI machine schema, bridge schema, and tests match the decision.
  • If operation IDs are removed, the pre-release wire break is acknowledged and queued remains the only switch completion claim.
  • If they are retained, a caller can observe the documented states and golden conversations cover same-daemon, superseded, timeout, eviction, restart, and carrier-target behavior.
  • #24 is updated to point to the final disposition.

Evidence

  • crates/felis-protocol/proto/felis.proto: OperationId, OperationEcho, and OpsSwitched.operation_id
  • crates/felis-daemon/src/operations.rs: bounded ledger and test-only lookup coverage
  • crates/felis-daemon/src/serve.rs: landing result is only logged when it cannot be attributed
  • crates/felis-cli/src/cli_output.rs: operation ID is exposed in machine output
  • docs/reference/ipc.md: documented operation states and minor-8 compatibility behavior
Related: #24, #12 ## Why The release-readiness review found that retarget operation IDs are currently exposed as a public protocol and CLI feature without a user-observable completion path. `OpsSwitched.operation_id` is returned by `switch` and retarget commands, but no CLI verb or bridge operation accepts the token for lookup, waiting, or cancellation. In the daemon, `OperationLedger::lookup` has no production caller; `OperationLedger::land` only updates bookkeeping whose result is used for a debug log. Carrier retargets cannot be resolved by the origin daemon by design. This leaves the release contract paying for `OperationId`, `OperationEcho`, several minor-version gates, CLI schema fields, and a bounded ledger without providing a capability that a caller can act on. The reference documentation currently describes `pending`, `landed`, and `superseded` states, but does not expose a way for users to observe them. ## Decision required before the first release Choose one of these deliberately: 1. **Remove operation IDs from the v0.1 contract.** Keep `queued` as the honest result of a switch, remove the operation fields and ledger, and revisit completion tracking only when a real consumer needs it. 2. **Complete the feature.** Add a typed operation-status path and an explicit CLI/bridge wait or query contract, including timeout, eviction, daemon restart, and cross-daemon retarget semantics. The current partial implementation should not remain the unexamined final state. ## Acceptance criteria - The decision is recorded in the owning IPC explanation and reference documents. - The proto, minor ledger, CLI machine schema, bridge schema, and tests match the decision. - If operation IDs are removed, the pre-release wire break is acknowledged and `queued` remains the only switch completion claim. - If they are retained, a caller can observe the documented states and golden conversations cover same-daemon, superseded, timeout, eviction, restart, and carrier-target behavior. - #24 is updated to point to the final disposition. ## Evidence - `crates/felis-protocol/proto/felis.proto`: `OperationId`, `OperationEcho`, and `OpsSwitched.operation_id` - `crates/felis-daemon/src/operations.rs`: bounded ledger and test-only lookup coverage - `crates/felis-daemon/src/serve.rs`: landing result is only logged when it cannot be attributed - `crates/felis-cli/src/cli_output.rs`: operation ID is exposed in machine output - `docs/reference/ipc.md`: documented operation states and minor-8 compatibility behavior
Author
Owner

Decision (2026-09-19): remove operation ids from the v0.1 contract (option 1)

Correction to the premise

The current state is not "unexamined". #24's triage plan split the work deliberately: put the id on the wire in v0.1 because "a missing echo field means the verdict can never be attributed" after the tag, and land --wait / OperationStatus post-v0.1. docs/reference/ipc.md "Operation ids" says so plainly: "No message reads a record back." What this issue reopens is that deferral decision itself. Two things changed since #24: the follow-up issue its plan called for ("Move --wait and OperationStatus to a post-v0.1 follow-up issue") was never filed, and the consumer evidence #24 leaned on is gone (next section).

Why remove rather than keep or complete

  1. Principle 1. No caller can act on the token. #24 counted the then-current agent skill's advice ("confirm with sessions info" after a switch) as partial evidence for the revisit trigger; the skill-scope cleanup (5bb91114, 2026-09-05) dropped that workflow, and the shipped skills/felis/SKILL.md now lists sessions switch / ssh / window retarget under "Surfaces this skill does not cover" because a headless agent has no window to move. There is no consumer, not even a polling one.
  2. The "expensive after the tag" argument is weaker than #24 stated. Adding OperationId/OperationEcho later is an additive minor. Its degradation for a peer that predates it (a client that echoes nothing leaves its target pending until eviction) is exactly what the current minor-8 row in ipc.md documents and accepts. A later addition costs unattributable landings in a mixed-version fleet, and that specific cost is already on the books.
  3. Freezing a token format with zero consumers is the worst of both options. operation_id renders as <incarnation:016x>-<sequence> in CLI and bridge output (docs/reference/cli.md "switch"). Once tagged, that shape is contract. A field whose meaning arrives later cannot change shape when it does.
  4. #277 removed fan-out. A successfully resolved SwitchScope selects at most one attachment, so the per-target verdict vector and the reordered-landing attribution from #24 round 3 solve a shape that no longer exists.
  5. Option 2 is L-sized (timeout, eviction, restart, carrier goldens, a --wait verb) for a consumer that does not exist, on a release still blocked by #12 / #31.

Scope of the removal

  • Proto: drop OperationId, OperationEcho, OpsSwitched.operation_id, PushReattach.operation, PushRetargetHost.operation, SessionAttach.operation. Leave the tags free, not reserved (no release baseline carries them), and add a base: entry to crates/felis-protocol/proto/BREAKING.md following the #277 entry's form. The minor-8 row in docs/reference/ipc.md keeps Spawn/Created and the acceptedqueued rename; only the operation-id clauses go. MINOR_OPERATION_IDS goes with them: the constant and its ledger text in felis-protocol/src/preface.rs, the re-export in lib.rs, and the four gated-field entries plus their tests in minor.rs.
  • Daemon: delete crates/felis-daemon/src/operations.rs and its pub mod in lib.rs; drop Operations from the pool (pool.rs); drop the mint at the OpsSwitch handler in serve.rs (operations().mint()), the land call and its debug log at the attach path in serve.rs, and the stamping/recording in serve/session_task.rs; update serve/tests.rs.
  • Protocol crate: lib.rs, messages.rs, messages/{ops,push,session}.rs, convert.rs, convert/ops.rs, regenerated felis.v1.rs.
  • Client side: the echo threading in felis-client-core (connector.rs, dial.rs) and in felis-client (main.rs, event_handler.rs, app_methods.rs).
  • CLI / bridge: remove operation_id from the switch, ssh, window retarget objects and the bridge results (cli_output.rs, cli_sessions.rs, cli_bridge.rs); regenerate crates/felis-cli/schemas/felis-cli-v1.schema.json and felis-bridge-v1.schema.json; update tests/cli_sessions.rs. The bridge goldens hold no successful sessions.switch result today, so the removal of the key from the bridge object needs a success-path test added, not a regeneration.
  • Done when grep -rn 'OperationId\|OperationEcho\|MINOR_OPERATION_IDS\|operation_id' over crates/, docs/, skills/ is empty except CHANGELOG.md and BREAKING.md.
  • Kept: queued as the sole completion claim, not_queued as its error kind.
  • Docs: docs/reference/ipc.md (Switch/Switched, Push, the "Operation ids" section, minor-8 row), docs/reference/cli.md (output tables and the operation_id paragraph), docs/reference/control-surfaces.md if it names the field, CHANGELOG.md (user-affecting: switch/ssh/window retarget machine output and the bridge result lose a key).
  • Explanation: docs/explanation/architecture/ipc.md "What a switch reply can report" is rewritten in the present tense, per doc-cascade §3/§4: a switch reply promises queue admission and nothing else; completion tracking is rejected because no consumer needs it and because the origin daemon cannot observe a carrier landing at all, so any completion contract would either stay partial or make the client a participant in cross-host daemon bookkeeping. The Revisit if stays and names the observable trigger: a consumer that must act on the window's new home. Identifier shape, incarnation keying, and retention bounds are not recorded; they are design for a feature that does not exist, and the consumer that triggers the revisit defines what it needs. The removal itself is recorded in CHANGELOG.md and git, not in the docs.

Acceptance mapping

  • Decision recorded in the IPC explanation and reference: the rewritten explanation section; the reference drops the "Operation ids" section.
  • Proto, minor ledger, CLI schema, bridge schema, tests match: the scope list above, verified by just check, the schema regeneration, and the identifier sweep.
  • Pre-release wire break acknowledged: the BREAKING.md entry.
  • queued remains the only switch completion claim.
  • #24: add a closing comment pointing here; its "Track retarget completion" scope is deferred with no open issue until a consumer appears.

Effort: S–M, one PR. Labels stay priority/P1, release/v0.1.0; this is a freeze-list change and goes before the tag.

## Decision (2026-09-19): remove operation ids from the v0.1 contract (option 1) ### Correction to the premise The current state is not "unexamined". #24's triage plan split the work deliberately: put the id on the wire in v0.1 because "a missing echo field means the verdict can never be attributed" after the tag, and land `--wait` / `OperationStatus` post-v0.1. `docs/reference/ipc.md` "Operation ids" says so plainly: "No message reads a record back." What this issue reopens is that deferral decision itself. Two things changed since #24: the follow-up issue its plan called for ("Move `--wait` and `OperationStatus` to a post-v0.1 follow-up issue") was never filed, and the consumer evidence #24 leaned on is gone (next section). ### Why remove rather than keep or complete 1. **Principle 1.** No caller can act on the token. #24 counted the then-current agent skill's advice ("confirm with `sessions info`" after a switch) as partial evidence for the revisit trigger; the skill-scope cleanup (`5bb91114`, 2026-09-05) dropped that workflow, and the shipped `skills/felis/SKILL.md` now lists `sessions switch` / `ssh` / `window retarget` under "Surfaces this skill does not cover" because a headless agent has no window to move. There is no consumer, not even a polling one. 2. **The "expensive after the tag" argument is weaker than #24 stated.** Adding `OperationId`/`OperationEcho` later is an additive minor. Its degradation for a peer that predates it (a client that echoes nothing leaves its target `pending` until eviction) is exactly what the current minor-8 row in `ipc.md` documents and accepts. A later addition costs unattributable landings in a mixed-version fleet, and that specific cost is already on the books. 3. **Freezing a token format with zero consumers is the worst of both options.** `operation_id` renders as `<incarnation:016x>-<sequence>` in CLI and bridge output (`docs/reference/cli.md` "switch"). Once tagged, that shape is contract. A field whose meaning arrives later cannot change shape when it does. 4. **#277 removed fan-out.** A successfully resolved `SwitchScope` selects at most one attachment, so the per-target verdict vector and the reordered-landing attribution from #24 round 3 solve a shape that no longer exists. 5. **Option 2 is L-sized** (timeout, eviction, restart, carrier goldens, a `--wait` verb) for a consumer that does not exist, on a release still blocked by #12 / #31. ### Scope of the removal - **Proto:** drop `OperationId`, `OperationEcho`, `OpsSwitched.operation_id`, `PushReattach.operation`, `PushRetargetHost.operation`, `SessionAttach.operation`. Leave the tags free, not reserved (no release baseline carries them), and add a `base:` entry to `crates/felis-protocol/proto/BREAKING.md` following the #277 entry's form. The minor-8 row in `docs/reference/ipc.md` keeps `Spawn`/`Created` and the `accepted` → `queued` rename; only the operation-id clauses go. `MINOR_OPERATION_IDS` goes with them: the constant and its ledger text in `felis-protocol/src/preface.rs`, the re-export in `lib.rs`, and the four gated-field entries plus their tests in `minor.rs`. - **Daemon:** delete `crates/felis-daemon/src/operations.rs` and its `pub mod` in `lib.rs`; drop `Operations` from the pool (`pool.rs`); drop the mint at the `OpsSwitch` handler in `serve.rs` (`operations().mint()`), the `land` call and its debug log at the attach path in `serve.rs`, and the stamping/recording in `serve/session_task.rs`; update `serve/tests.rs`. - **Protocol crate:** `lib.rs`, `messages.rs`, `messages/{ops,push,session}.rs`, `convert.rs`, `convert/ops.rs`, regenerated `felis.v1.rs`. - **Client side:** the echo threading in `felis-client-core` (`connector.rs`, `dial.rs`) and in `felis-client` (`main.rs`, `event_handler.rs`, `app_methods.rs`). - **CLI / bridge:** remove `operation_id` from the `switch`, `ssh`, `window retarget` objects and the bridge results (`cli_output.rs`, `cli_sessions.rs`, `cli_bridge.rs`); regenerate `crates/felis-cli/schemas/felis-cli-v1.schema.json` and `felis-bridge-v1.schema.json`; update `tests/cli_sessions.rs`. The bridge goldens hold no successful `sessions.switch` result today, so the removal of the key from the bridge object needs a success-path test added, not a regeneration. - **Done when** `grep -rn 'OperationId\|OperationEcho\|MINOR_OPERATION_IDS\|operation_id'` over `crates/`, `docs/`, `skills/` is empty except `CHANGELOG.md` and `BREAKING.md`. - **Kept:** `queued` as the sole completion claim, `not_queued` as its error kind. - **Docs:** `docs/reference/ipc.md` (Switch/Switched, Push, the "Operation ids" section, minor-8 row), `docs/reference/cli.md` (output tables and the `operation_id` paragraph), `docs/reference/control-surfaces.md` if it names the field, `CHANGELOG.md` (user-affecting: `switch`/`ssh`/`window retarget` machine output and the bridge result lose a key). - **Explanation:** `docs/explanation/architecture/ipc.md` "What a switch reply can report" is rewritten in the present tense, per doc-cascade §3/§4: a switch reply promises queue admission and nothing else; completion tracking is rejected because no consumer needs it and because the origin daemon cannot observe a carrier landing at all, so any completion contract would either stay partial or make the client a participant in cross-host daemon bookkeeping. The _Revisit if_ stays and names the observable trigger: a consumer that must act on the window's new home. Identifier shape, incarnation keying, and retention bounds are not recorded; they are design for a feature that does not exist, and the consumer that triggers the revisit defines what it needs. The removal itself is recorded in `CHANGELOG.md` and git, not in the docs. ### Acceptance mapping - Decision recorded in the IPC explanation and reference: the rewritten explanation section; the reference drops the "Operation ids" section. - Proto, minor ledger, CLI schema, bridge schema, tests match: the scope list above, verified by `just check`, the schema regeneration, and the identifier sweep. - Pre-release wire break acknowledged: the `BREAKING.md` entry. - `queued` remains the only switch completion claim. - #24: add a closing comment pointing here; its "Track retarget completion" scope is deferred with no open issue until a consumer appears. Effort: S–M, one PR. Labels stay `priority/P1`, `release/v0.1.0`; this is a freeze-list change and goes before the tag.
Sign in to join this conversation.
No description provided.