# 0034 — Per-minter op-id namespacing; tracing-only op families auto-retry

Date: 2026-07-04 · Status: accepted (operator-ruled, v0.24.0 bug-batch grill Q4)

## Context

The broker effect journal dedups network ops under one keyspace
`(NET_EFFECT_SESSION, op)`, but op integers are minted by independent
counters that all start at 1 (`cli-ops.json`, `pump-ops.json`,
`epoch.json`, F-030's `psyche-ops`). A CLI op colliding with an
already-journaled daemon op yields the typed "already applied … retry with a
fresh op_id" failure with **no broker restart** — field-hit by `spt endpoint
wake id@node` (WOKE_FAIL). Meanwhile `net/rest.rs` declares rest op-ids
"tracing/correlation only … redelivery needs reporting, not dedup" — the
journal was enforcing exactly-once semantics on ops that never asked for it.
The typed error's advice ("retry with a fresh op_id") is core-internal lingo
no end user can act on.

## Decision

1. **Journal keys carry the minter**: (effect-class, **minter-tag**, op) —
   `cli` / `pump` / `psyche` / `epoch` / `rc`. Distinct minters can never
   collide. Old-shape keys differ structurally from new-shape keys, so
   existing journals need no migration and no collision window exists across
   upgrade.
2. **Tracing-only op families auto-retry once** with a fresh op on the typed
   no-longer-held error: the rest family and rc attach stream-open. The
   sync/update pull families keep their durable `open_op` exactly-once
   semantics — their dedup is load-bearing by design.
3. **The typed op error becomes internal-only.** The retry consumes it; if
   the retry also fails, the user-facing line names the observable situation
   and next action in operator language; internal detail goes to a debug
   surface. (General rule, same grill: public CLI stderr never speaks
   journal/op/brain lingo.)

## Consequences

- The WOKE_FAIL collision class dies everywhere at once (wake, net-dial,
  shell-link) rather than per-verb.
- FAULT-MATRIX row 9's "brain re-dials" promise becomes true via the retry
  instead of being edited down.
- Every new counter file MUST declare its minter tag; sharing another
  minter's counter file is the named anti-pattern (the F-030 W1
  `psyche_ops_epoch_file()` split pre-implemented this rule).
- Rejected: one global counter (cross-process RMW contention + still fragile
  across identity-dir resets); range-partitioned integers (silent exhaustion
  and reset hazards).

## Amendment — 2026-07-05 (doyle ruling, REMOTE-TRUTH A-4 recon)

- **`rc` is a named minter tag.** The rc attach `OpMinter` mints from a random
  63-bit base and is collision-safe against other rc sessions by construction —
  it STILL declares and stamps the `rc` tag. Rationale: the legacy/untagged
  discriminant is reserved for **pre-upgrade journal lines only** and must
  monotonically shrink; a live producer minting untagged would parse as legacy
  forever and poison that discriminant. Uniform rule, no exceptions: every
  minter declares a tag; the random base becomes belt-and-suspenders.
- **One canonical minter enum.** The numeric key discriminant and the journal
  line's minter token derive from a single enum (one source for numeric +
  textual form) — two independent mappings WILL drift.
- **The wire field is additive.** Op-carrying requests gain an optional minter
  field (serde default = absent ⇒ legacy at the broker). The IPC wire is
  serde_json with no `deny_unknown_fields`, so old decoders ignore the new
  field and new decoders default it on old senders — no wire-protocol version
  bump. The two-shape decode compatibility is pinned by test, not assumed.

## Amendment 2 — 2026-07-05 (doyle ruling, A-4a ground-truth recon: the
## Decision's minter list did not match the code)

<!-- [doc->REQ-OPID-MINTER-NAMESPACE] -->
Executor recon verified the actual journaled-op surface against HEAD; the
Decision-1 tag list was wrong in both directions. Corrections, binding:

- **The journal minter enum reflects reality**: `{legacy, cli, pump, rc,
  shell}`. `psyche` and `epoch` are DROPPED from the journal enum — psyche-ops
  mints the notif epoch and `epoch.json` orders registry lease / notif /
  grants (the `EpochSource` domain); neither ever submits to broker
  `apply_once`. They are a **separate counter domain**, not journal minters —
  a journal tag with no stamp site is a doc'd-but-dead knob (the named
  contract-lie class). The W1 counter-file hygiene rule (`psyche_ops_epoch_file()`
  split; never share another minter's counter file) still binds — it is a
  counter-domain rule, not a journal-enum rule.
- **`shell` is the 5th named minter** — `shellchan::deliver_stdin_pending` is
  a live journaled producer keying `(shell_sid, row_id)`. The spool **row id
  stays the seq**: it is the durable identity spool exactly-once dedup is
  built on — it is NEVER re-minted; the `shell` tag is what separates its
  keyspace from an rc operator's ops on the same shell session (a real
  in-scope collision both ways: dropped keystroke or dropped spool row).
- **Every op carries its minter as a TYPE invariant**: a `MintedOp{minter,
  seq}` newtype replaces bare `op_id: u64` through the brain/daemon threading
  paths, so an unstamped op is uncompilable — the minter resolves at the
  genuine minting site and threads by construction, not by discipline. The
  wire keeps the additive optional field (legacy materialized at broker
  decode); the newtype is not forced into wire structs.
- **The journal-line minter token is TEXTUAL** (derived from the canonical
  enum), self-describing in a journal a human reads during recovery.
  Numeric discriminants (in-memory key + wire byte) may be renumbered freely
  ONLY until this ships in a release; after that they are frozen.
- Verified op-submit map at ruling time (all production `Some(op)` journal
  sites): cli = wansend request_rest/fetch/push; rc = rc attach + rc input +
  serve/flush attach legs; pump = pump sync + update pulls; shell =
  `deliver_stdin_pending`. (Superseded by the Addendum's exhaustive table —
  this map missed forward_wake; see below.)

### Addendum — 2026-07-05 (doyle ruling: `wake` is the 6th minter; the tag
### names the MINTING SOURCE, never the subsystem/file)

A-4a's exhaustive producer sweep found a 5th live journaled producer the
ruling-time map missed (2nd map-incompleteness in one wave):
`shellwake::forward_wake` — a presence/wake reconciler journaling rest-family
ops under `NET_EFFECT_SESSION` with `seq = now_ms()` (collision-immune by
magnitude). Rulings:

- **`wake` is its own variant**: `{legacy, cli, pump, rc, shell, wake}`.
  Stamping it `shell` (subsystem-based, "it lives in shellwake.rs") was
  rejected: the minter tag names the **minting source** — forward_wake's
  `now_ms` source is not shellchan's spool-row counter, and the textual
  journal token exists precisely so recovery reads true; a wake op labeled
  `shell` is a semantic lie at the moment of debugging. Subsystem-based
  stamping is the named anti-pattern (it is exactly how both map misses
  happened).
- **`now_ms`-seq producers still declare a tag.** Collision-immunity by
  magnitude satisfies nothing: never-legacy-for-a-live-op binds every
  producer, and two same-class `now_ms` producers sharing a tag WOULD
  collide within one millisecond. One source, one tag — a future `now_ms`
  producer mints under its own variant.
- **Enum churn pre-ship is free** (discriminants unfrozen until first
  release; single-source enum) — churn-avoidance is never a reason to
  mis-tag.
- The A-4a evidence commit carries the exhaustive site→minter→seq-source
  stamp table as the authoritative map, superseding both partial maps above.

### Addendum 2 — 2026-07-05 (doyle ruling, A-4b: the retry discriminates the
### no-longer-held error by a SHARED CONST MARKER, not a typed wire variant)

<!-- [doc->REQ-OPID-TRACING-RETRY] -->
Decision 3's "typed op error" is realized as a single `pub const` marker
phrase, not a new typed variant on the KIND_ERROR wire path:

- **One shared const** sources the broker's no-longer-held message; the retry
  helper substring-matches that const. Single-source kills literal drift; no
  wire-protocol change. (Precedent: F-030's `PSYCHE_SESSION_NOT_FOUND`
  discriminable-marker reseed — same pattern class.)
- **The const is a frozen compat surface.** It preserves the CURRENT wire
  text verbatim: a new CLI must still match an un-upgraded daemon's error
  string across the IPC boundary. Changing the phrase is a wire-compat break
  and is treated like a frozen discriminant (see the freeze rule above).
- **Match discrimination is tested**: a different broker error must NOT
  trigger the retry (rides the durable-family negative control).
- A typed wire variant (rejected for now) is the right move only if a second
  consumer of structured broker errors appears — revisit then, not before.
- Retry placement is structural: wraps `request_rest` +
  `request_attach_endpoint` call sites only (one `with_tracing_retry(mint,
  run)` helper at the 3 mint-reachable sites; wansend's moves up into
  `wan_rest` where the `EpochSource` is in scope). It is NEVER inserted below
  `net_open_stream` — that would leak the retry to the sync/update durable
  families by construction.
