From 8d34e78577640b5723ae0834343171b7f028f463 Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Thu, 13 Aug 2026 07:17:40 -0500 Subject: [PATCH] fix(activity): spawn's docs named a recipe purpose that resolves to NOTHING (#274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `activity/spawn` told every caller — human, citizen, and schema consumer — that the recipe argument takes `benchmark`. No such purpose exists. The authored recipe's purpose is `benchmark/hard-rs`, and `RecipeExperienceSource` keys on the exact string, with `unknown_purpose_yields_none` pinning that an unknown purpose resolves to `None`. So a caller following the documented affordance got a room bound to a purpose nothing can project. Per the binding comment in spawn's own `run`, that room then "will project as a plain chat room". A benchmark run silently becoming a chat room is the academy failure in miniature — and academy is exactly what we have: 131 cards accumulated in ONE room, 130,954 work events against 910 messages, 36 cards frozen under claims heartbeating unbroken since 2026-08-07. The stale list appeared TWICE — the command DESCRIPTION (what a citizen reads when choosing a tool) and the `recipe` param doc (what schema consumers read). Both fixed. Deliberately NOT replaced with a corrected list. Recipes are DATA, overlaid from disk by `builtins_with_overlay`, so any enumeration in a comment is stale the moment someone authors a recipe — re-hardcoding exactly what the loader exists to keep dynamic. Both docs now point at the catalogue and state the rule that matters: the purpose is EXACT, family names do not resolve. NOT FIXED, and it is the bigger half: spawn does not VALIDATE that the recipe resolves. A typo still mints a chat room silently — a fallback, in a subsystem whose own loader cites [[fallbacks-are-illegal-fail-loud]]. The fix needs the experience source threaded into ActivitySpawn so it can refuse with the live `purposes()` list, which already exists. Tracked on #274. Docs only; no behaviour change beyond what callers are told. No generated TS binding exists for ActivitySpawnParams, so no binding drift. Verified: rustfmt --check clean on the changed file, cargo check -p continuum-core --lib --features metal,accelerate clean. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo --- core/continuum-core/src/modules/activity.rs | 48 ++++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/core/continuum-core/src/modules/activity.rs b/core/continuum-core/src/modules/activity.rs index 5b643bf5a..bfb1fba27 100644 --- a/core/continuum-core/src/modules/activity.rs +++ b/core/continuum-core/src/modules/activity.rs @@ -105,10 +105,21 @@ pub struct ActivitySpawnParams { /// the room everyone reuses forever. pub name: String, - /// Which recipe to build from — the `purpose` key of an authored recipe - /// (`chat`, `benchmark`, `video-chat`, `profile`, or anything dropped into the - /// recipes directory). The recipe decides the room's regions, verbs and layout; - /// this command only decides that a room exists and which recipe it follows. + /// Which recipe to build from — the EXACT `purpose` key of an authored recipe. + /// + /// Exact, because [`crate::experience::RecipeExperienceSource`] keys on the + /// literal string and an unknown purpose resolves to `None`. Family names do + /// not work: the authored benchmark recipe's purpose is `benchmark/hard-rs`, + /// so `benchmark` matches nothing and the room falls through to rendering as + /// plain chat. This doc used to list `chat, benchmark, video-chat, profile` + /// and that middle one was never real. + /// + /// Not enumerated here on purpose: recipes are DATA, overlaid from disk by + /// `builtins_with_overlay`, so any list in this comment is stale the moment + /// someone authors a new one. Read the catalogue instead. + /// + /// The recipe decides the room's regions, verbs and layout; this command only + /// decides that a room exists and which recipe it follows. pub recipe: String, /// Optional parent activity — activities spawn activities, and the graph is @@ -169,11 +180,34 @@ impl ActionCommand for ActivitySpawn { /// idea. It takes nothing from anyone — a new room is additive, and every /// destructive verb in this module is gated separately. const ACCESS: AccessLevel = AccessLevel::AiSafe; + // The recipe names used to be listed inline here as "(chat, benchmark, …)". + // Two things were wrong with that. It NAMED A PURPOSE THAT DOES NOT EXIST — + // the authored recipe's purpose is `benchmark/hard-rs`, never `benchmark` — + // and `RecipeExperienceSource` keys on the exact string, with + // `unknown_purpose_yields_none` pinning that an unknown purpose resolves to + // None. So a caller following this description got a room bound to a purpose + // nothing can project, which (per the binding comment in `run`) then renders + // as a plain chat room. A benchmark run that silently becomes a chat room is + // precisely the academy failure. + // + // And a hardcoded list goes stale by construction: recipes are DATA, overlaid + // from disk by `builtins_with_overlay`, so the catalogue grows without + // touching this file. Naming members here re-hardcodes what the recipe loader + // exists to keep dynamic. + // + // So this points at the live catalogue instead of enumerating it. The real + // fix is one layer deeper and is NOT done: spawn does not VALIDATE that the + // recipe resolves, so any typo still mints a chat room silently — a fallback, + // in a subsystem whose own loader cites [[fallbacks-are-illegal-fail-loud]]. + // That needs the experience source threaded to this command so it can refuse + // with the live `purposes()` list. Tracked on #274. const DESCRIPTION: &'static str = "Create a new room from a recipe. `name` is what people call this instance; \ - `recipe` is which template to build from (chat, benchmark, …). Everything is \ - a room — a chat, a benchmark run, a doc, a settings pane — so spawn one \ - whenever an idea needs its own shared space. Returns the room_id."; + `recipe` is the `purpose` of an authored recipe — use the exact purpose \ + string from the recipe catalogue (e.g. `benchmark/hard-rs`), not a family \ + name. Everything is a room — a chat, a benchmark run, a doc, a settings \ + pane — so spawn one whenever an idea needs its own shared space. \ + Returns the room_id."; type Params = ActivitySpawnParams; type Output = ActivitySpawnResult;