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: 3 additions & 0 deletions .claude/board/AGENT_LOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 2026-06-21 (cont.³²) — S4 delivery edge SHIPPED — S4 mechanism now COMPLETE end-to-end

**Main thread (Opus), self-directed (da-capo).** Completed S4 on the same light crate: `deliver_kanban_step("kanban.<mailbox>.<phase>")` in `lance-graph-supervisor::kanban_actor` — `parse_kanban_step` (snake_case phase vocab) → `ractor::registry::where_is(mailbox)` → `cast(KanbanMsg::Advance{to})` → relays the owner's `try_advance_phase` result. Address source = the step's existing string + the actor system's OWN registry (NOT a bespoke bridge registry, NOT a `UnifiedStep` field — exactly the codex-#574-corrected design). `KanbanRouteError`: `BadStepType` / `NoMailbox` (routing miss, NOT a no-owner case — a live mailbox is always owned) / `Illegal` (relayed RubiconTransitionError) / `Rpc`. **+2 tests (4 total green):** `parse_kanban_step_shapes`, `delivery_edge_resolves_via_registry_then_advances` (legal advance via where_is; unknown mailbox → graceful NoMailbox; illegal edge → Illegal; malformed → BadStepType). clippy clean (fixed an `unnecessary_to_owned` on the where_is arg) + fmt; light build, no disk/symbiont gate. **S4 mechanism is now COMPLETE end-to-end** (owner-advance #576 + delivery edge); only the S2/S3 *drivers* that SEND Advance remain, composing on top. Plan S4 status → "COMPLETE". Rides a PR on jirak.
## 2026-06-21 (cont.³¹) — S4 owner-advance SHIPPED: real ractor KanbanActor (first true OUT-leg wire)

**Main thread (Opus), self-directed ("go").** First REAL OUT-leg code (not docs/stubs), enabled by the cont.³⁰ unblock. New `lance-graph-supervisor::kanban_actor` (gated `supervisor`): `KanbanActor<O: MailboxSoaOwner>` — a ractor actor whose `State` IS the owner (mailbox-as-owner, per operator "every SoA is ractor-owned"). On `KanbanMsg::Advance { to, reply }` the owner advances ITSELF via the contract's checked `try_advance_phase` (single-writer by construction — ractor serializes one message at a time, so `&mut state` can't alias = the E-CE64-MB-4 no-race guarantee, realized by `&mut` + mailbox, no lock); illegal Rubicon edge → typed `RubiconTransitionError`, no mutation. `KanbanMsg::Phase` reads back. **2 tests green** (`actor_advances_its_own_phase_on_message`, `illegal_edge_is_a_typed_error_no_mutation`) under `--features supervisor`; clippy clean, fmt clean; light build (no lance/datafusion, no disk/symbiont gate). This is the owner-advance HALF of S4 (the mechanism the operator named). **Remaining S4:** the delivery edge (`kanban.*` → `where_is` → `cast(Advance)`) + the S2/S3 drivers that send `Advance` (those compose on top). Plan S4 row annotated "owner-advance HALF shipped". Rides a PR on jirak.
Expand Down
24 changes: 15 additions & 9 deletions .claude/plans/capstone-out-leg-wiring-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,21 @@ is consumption only.)

## S4 — envelope route gets a `Kanban` handler (resolve-then-reject → accept)

**Status (2026-06-21): owner-advance HALF shipped.** `lance-graph-supervisor::
kanban_actor::KanbanActor<O: MailboxSoaOwner>` is a real ractor actor whose
`State` IS the owner; on `KanbanMsg::Advance` it calls `try_advance_phase`
(owner advances itself; illegal edge → typed `RubiconTransitionError`, no
mutation). 2 tests green under `--features supervisor` (light build, no
disk/symbiont gate). **Remaining:** the *delivery edge* — `kanban.*` step →
mailbox id → `ractor::registry::where_is` → `cast(Advance)` — plus the S2/S3
drivers that send `Advance`. The mechanism the operator described ("every SoA
is ractor-owned, the owner advances itself") is now real code.
**Status (2026-06-21): S4 mechanism COMPLETE (owner-advance + delivery edge).**
`lance-graph-supervisor::kanban_actor` (feature `supervisor`):
- `KanbanActor<O: MailboxSoaOwner>` — ractor actor whose `State` IS the owner;
`KanbanMsg::Advance` → `try_advance_phase` (owner advances itself; illegal
edge → typed `RubiconTransitionError`, no mutation).
- `deliver_kanban_step("kanban.<mailbox>.<phase>")` — the delivery edge:
`parse_kanban_step` → `ractor::registry::where_is(mailbox)` → `cast(Advance)`;
unknown mailbox → graceful `KanbanRouteError::NoMailbox`, illegal edge →
`Illegal`, malformed → `BadStepType`. Address source = the step string + the
actor system's OWN registry (no bespoke registry, no `UnifiedStep` field).
4 tests green under `--features supervisor`; clippy + fmt clean; light build (no
disk/symbiont gate). **Remaining:** only the S2/S3 *drivers* that SEND
`KanbanMsg::Advance` (the MUL gate and the live version tick) — both compose ON
TOP of this complete mechanism. The operator's model ("every SoA is
ractor-owned, the owner advances itself") is now real, tested code end-to-end.

**Census state (original):** GAP. `kanban.*` resolves to `StepDomain::Kanban` then every
bridge impl returns `DomainUnavailable` (`orchestration_impl.rs:55-57`,
Expand Down
125 changes: 125 additions & 0 deletions crates/lance-graph-supervisor/src/kanban_actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,78 @@ where
}
}

// ─── S4 delivery edge: `kanban.<mailbox>.<phase>` → where_is → cast(Advance) ───

/// Error from delivering a `kanban.*` step to its owning actor.
#[derive(Debug, thiserror::Error)]
pub enum KanbanRouteError {
/// `step_type` was not a well-formed `kanban.<mailbox>.<phase>`.
#[error("malformed kanban step_type: {0}")]
BadStepType(String),
/// No live actor is registered under `<mailbox>`. A routing MISS, distinct
/// from the (impossible) "no owner" case: a live mailbox is always owned by
/// its actor — this means the *named* mailbox isn't registered/live.
#[error("no live mailbox registered as `{0}`")]
NoMailbox(String),
/// The owner rejected the transition (illegal Rubicon edge; no mutation).
#[error("illegal transition {from:?} -> {to:?}")]
Illegal {
from: KanbanColumn,
to: KanbanColumn,
},
/// The actor RPC failed (mailbox closed, timeout, …).
#[error("kanban rpc failed: {0}")]
Rpc(String),
}

/// Parse a `kanban.<mailbox>.<phase>` step type into `(mailbox, target_phase)`,
/// where `<phase>` is the snake_case [`KanbanColumn`] name (e.g. `cognitive_work`).
/// Returns `None` for anything that isn't a well-formed kanban step.
pub fn parse_kanban_step(step_type: &str) -> Option<(&str, KanbanColumn)> {
let mut it = step_type.splitn(3, '.');
match (it.next(), it.next(), it.next()) {
(Some("kanban"), Some(mailbox), Some(phase)) if !mailbox.is_empty() => {
phase_from_name(phase).map(|p| (mailbox, p))
}
_ => None,
}
}

/// Snake_case phase name → [`KanbanColumn`] (the `kanban.*` routing vocabulary;
/// inverse of the canonical column names).
fn phase_from_name(name: &str) -> Option<KanbanColumn> {
Some(match name {
"planning" => KanbanColumn::Planning,
"cognitive_work" => KanbanColumn::CognitiveWork,
"evaluation" => KanbanColumn::Evaluation,
"commit" => KanbanColumn::Commit,
"plan" => KanbanColumn::Plan,
"prune" => KanbanColumn::Prune,
_ => return None,
})
}

/// The S4 **delivery edge**: resolve a `kanban.<mailbox>.<phase>` step to its
/// owning ractor actor via the actor system's OWN name registry
/// ([`ractor::registry::where_is`]) and RPC it [`KanbanMsg::Advance`]. The owner
/// advances ITSELF; this only delivers. No bridge-held owner, no `UnifiedStep`
/// field — the target is recovered from the step string + the registry
/// (mailbox-as-owner addressing). Multi-mailbox resolves because `where_is`
/// looks up any registered mailbox by name.
pub async fn deliver_kanban_step(step_type: &str) -> Result<KanbanMove, KanbanRouteError> {
let (mailbox, to) = parse_kanban_step(step_type)
.ok_or_else(|| KanbanRouteError::BadStepType(step_type.to_string()))?;
let cell = ractor::registry::where_is(mailbox)
.ok_or_else(|| KanbanRouteError::NoMailbox(mailbox.to_string()))?;
let actor: ActorRef<KanbanMsg> = cell.into();
let inner = ractor::call!(actor, |reply| KanbanMsg::Advance { to, reply })
Comment on lines +160 to +163

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate registry hits before sending KanbanMsg

Because the ractor name registry is untyped, this lookup can return any actor registered under the supplied string, not necessarily a KanbanActor. In this crate the supervisor already registers non-kanban actors under names like consumer_g_{g}, so a step such as kanban.consumer_g_2.cognitive_work would coerce that actor cell to ActorRef<KanbanMsg> and send it a message of the wrong type, which ractor treats as a runtime message-type failure instead of the NoMailbox/Rpc miss this API promises. Use a kanban-specific namespace or a typed kanban registry before sending the RPC.

Useful? React with 👍 / 👎.

.map_err(|e| KanbanRouteError::Rpc(e.to_string()))?;
inner.map_err(|e| KanbanRouteError::Illegal {
from: e.from,
to: e.to,
})
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -220,4 +292,57 @@ mod tests {
actor.stop(None);
handle.await.expect("actor join");
}

#[test]
fn parse_kanban_step_shapes() {
assert_eq!(
parse_kanban_step("kanban.mb42.cognitive_work"),
Some(("mb42", KanbanColumn::CognitiveWork))
);
assert_eq!(parse_kanban_step("lg.foo"), None); // wrong domain
assert_eq!(parse_kanban_step("kanban.mb42"), None); // no phase
assert_eq!(parse_kanban_step("kanban..commit"), None); // empty mailbox
assert_eq!(parse_kanban_step("kanban.mb42.bogus"), None); // unknown phase
}

#[tokio::test]
async fn delivery_edge_resolves_via_registry_then_advances() {
// Register the owning actor under a name — `where_is` is the actor
// system's own registry, the S4 addressing source (no bespoke registry).
let name = "mb-kanban-route-test";
let (actor, handle) = Actor::spawn(
Some(name.to_string()),
KanbanActor::<TestBoard>::default(),
board(KanbanColumn::Planning),
)
.await
.expect("spawn named");

// Legal: kanban.<mailbox>.cognitive_work → resolves → owner advances.
let mv = deliver_kanban_step(&format!("kanban.{name}.cognitive_work"))
.await
.expect("delivered + advanced");
assert_eq!(mv.to, KanbanColumn::CognitiveWork);

// Unknown mailbox → graceful routing miss (NOT a panic, NOT a no-owner).
assert!(matches!(
deliver_kanban_step("kanban.no-such-mailbox.cognitive_work").await,
Err(KanbanRouteError::NoMailbox(_))
));

// Illegal Rubicon edge → typed Illegal, relayed from the owner.
assert!(matches!(
deliver_kanban_step(&format!("kanban.{name}.commit")).await,
Err(KanbanRouteError::Illegal { .. })
));

// Malformed step type → BadStepType.
assert!(matches!(
deliver_kanban_step("lg.noop").await,
Err(KanbanRouteError::BadStepType(_))
));

actor.stop(None);
handle.await.expect("actor join");
}
}
Loading