Asha intelligence + latency: 4 days of cognition/genome/persona work (June 29→July 2) - #1730
Merged
Conversation
test_grade / grade_rust + helpers were private to cognition/eval.rs. The teacher-episode generator (genome/teach, next slice) must grade write→fix trajectories with the EXACT same verdict the A/B evaluator uses, or a gene that "passes" generation could "fail" eval for grader-shape reasons alone. Per the compression principle (one logical decision, one place): lift the grader into cognition/gym_grader.rs with pub test_grade + extract_code_block, its tests move with it, eval.rs now `use`s it. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…de→fix→pass) Cold-start for the engineering reflex. The genome loop closes mechanically but produced INERT genes because the lever was never raw coding skill — it's the reflex of write code → read the REAL compiler/test error → fix → re-run → answer. You can't distill a reflex absent from the data (5 of 1761 captured turns ever used a tool), so this bootstraps it: a teacher model writes Rust, the SHARED gym grader (`cognition/gym_grader::test_grade`, the same one cognition/eval uses) actually compiles+runs it, the real error feeds back, and it loops to green. Only test-VALIDATED write→error→fix→pass trajectories become multi-turn ShareGPT examples — the grader is the corpus-quality gate, the teacher only affects yield. This is the sanctioned way the genome loop fixes behavior: curate the LEARNING corpus by an objective scorer, never puppet live output. Non-disruptive — writes a dataset, never touches the live :58057 serving lane. Procedure is never the artifact: the reflex is LEARNED from these trajectories, not hardcoded as a run-N-times loop. Feed the dataset to genome/job-create to forge the gene. - `genome/teach` (stateless, Privileged): resolves the teacher adapter the canonical way (global_registry + resolve_model, mirrors generate_response), defaults to the locally-served model so it runs with no external dep; point teacher_model at a stronger peer for higher yield. Drops tasks with no `test` (can't validate) and tasks never reaching green within max_fix_iters — both named in the per-task outcomes, never a silent shortfall. - dataset.rs: split_and_write becomes a `pub` associated fn (it touches no service state) so genome/teach packages to the SAME train/eval/manifest shape the dataset/* verbs use — one packaging path, not a parallel writer. 4 callers updated self.→Self::. Tests: build_sharegpt order/role/content invariant (the trajectory ordering IS the lesson); name+access mirror. cargo check + 5 teach + 20 dataset tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
The forged 4B is too narrow a teacher for the genome cold-start loop
([[genome-loop-trains-on-own-mistakes]]): it solves easy gym tasks
first-try (zero corrections) and never converges on hard ones (no
corpus), so its fail-then-fix-to-green band — the only band that
teaches the self-verify-and-correct reflex genome/teach distils — is
too thin. A stronger teacher widens that band.
Add Qwen2.5-Coder-14B-Instruct (Q4_K_M, ~9 GB) to the hand-authored
catalog under the "llama-server" provider so genome/teach's
select(Some("llama-server"), Some(model), …) can serve it on a lane.
Corpus generation is an offline batch (serving/pin it, generate, pin
the 4B back) so the live personas resume on their base.
cargo check --features metal,accelerate green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
The downstream half of the genome loop (MLX adapter → PEFT → GGUF-lora)
already lives in forge/lora_convert.rs. This adds the UPSTREAM half:
forge/mlx_train.rs spawns Apple's mlx_lm.lora trainer as a Rust-owned
subprocess instead of delegating the run to the unsloth HTTP custodian
(the NVIDIA path).
Bakes in the two invariants the inert-gene diagnosis surfaced
([[genome-loop-trains-on-own-mistakes]]):
- train-base == serve-base: caller passes the HF safetensors form of
the EXACT served base; a run_mlx_train precondition fails loud if the
dir is not a real HF model dir.
- scale ~2, not 20: build_lora_config_yaml writes lora_parameters.scale
= 2.0, which read_mlx_lora_hparams carries into the GGUF-lora as
alpha = rank * scale.
Pure, unit-tested builders (build_lora_config_yaml, build_train_args)
pin the scale~2 invariant and the mlx_lm.lora CLI contract without a real
run. run_mlx_train fails loud on every precondition (missing interpreter,
non-HF base, missing train/valid split, non-pageable fine-tune type) and
on non-zero exit or a missing adapter artifact — never partial-success.
No Python in the .rs ([[no-python-in-rs-files]]): spawns python3 -m
mlx_lm lora and writes a YAML config; no inline Python.
cargo check green; 3 lib tests pass (5437 filtered).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…emplate)
The genome loop's native mlx_lm.lora train step isn't truly "owned" if it
fails on the REAL forged base. Live validation against
continuum-ai/qwen3.5-4b-code-forged surfaced two normalizations the
GGUF-published model's HF form needs before mlx can train it:
1. config.json model_type qwen3_5_text -> qwen3_5 (mlx dispatches the
module by model_type; the _text suffix is the HF multimodal text-tower
name, for which mlx has no module — but qwen3_5 it fully supports).
2. tokenizer_config.json needs a chat_template (ChatML) so a chat
{messages} corpus can render.
prepare_base_for_mlx() applies these as EXPLICIT, caller-supplied,
idempotent JSON edits (MlxBasePrep on the spec) — the substrate never
guesses an architecture nor invents a template; a None field is left alone
and mlx itself fails loud if it then can't dispatch/render. Pure Rust JSON
edits, no Python. The long-run home is the forge publish step; until then
the train step normalizes its own input.
run_mlx_train calls it after the train-base==serve-base precondition and
probes each change. Unit test on a temp dir asserts model_type rewrite +
template injection + untouched-field survival + idempotent second pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…rs, fail-loud, scale~2 forge/train now branches on engine: Apple Silicon → native MlxTrainSpec/run_mlx_train (the foundry owns the subprocess), else the custodian. Explicit `engine` wins; an unknown value fails loud (never a silent custodian fallback). Correctness invariants baked in (genome-loop-trains-on-own-mistakes): - train-base == serve-base: `train_base_dir` is REQUIRED for mlx and fails loud naming the reason — the substrate never guesses which on-disk HF dir the served base maps to (a guess → washed-out ~0-lift gene). - scale == lora_alpha / lora_r (one geometry contract lora_convert reads back), so the proven scale~2 rides through instead of the destabilizing scale=20. Managed dirs only (no unsloth/legacy): - interpreter resolves to ~/.continuum/genome/venv/bin/python3 (MLX_PYTHON override), fail-loud provisioning message points at the managed venv. - dataset split → ~/.continuum/datasets/<name>-mlx, adapter → ~/.continuum/forge/lora/<name>. Reuses existing primitives: DatasetService::split_and_write (one packaging path, materializing valid.jsonl mlx_lm wants from the eval split), expand_user_path (re-exported), MlxBasePrep base normalization (model_type + chat_template), idempotent. Tests: engine-selection explicit-wins/fail-loud, managed-venv-not-unsloth, native dry_run spec resolution (scale=alpha/rank, ~/ expansion, defaults), fail-loud without train_base_dir. 31 forge + 4 mlx_train tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ute the persona
The full authorized command registry injects more tool-schema tokens than the
entire serving slot (~39.7k tokens of tools vs a 38.9k slot live). The
deliberation faculty offered ALL of them every tick, treating tools as
non-negotiable ("off the top"). On tool_use turns that drove the text budget to
zero AND still overflowed n_ctx → llama-server 400 "exceeds context size" → the
persona abstained the whole tick (mute). 7% of captured turns overflowed this
way, every one a finishReason: tool_use.
Fix: select the tool subset that fits the served window, guaranteeing the
conversation a turn floor (half the window) so a flood of tools can never blind
the persona to the room. Tools beyond the budget are dropped lowest-priority
(tail) first — the same drop-whole-in-priority discipline render_assembled_
context_within already uses for enrichment. Selection is pure + deterministic
(no ws input), so prompt_view (text budget) and contribute (offered set) compute
the SAME selection and agree on the window arithmetic. A probe names how many
tools were dropped. Fail-safe by construction, not a silent fallback.
This is a resource-fit guarantee, not cognition steering: it never reads or
rewrites the model's output. Relevance-ranked / genome skill-activation
selection (page in the tools the active domain needs) is the follow-up; this
floor keeps every turn runnable until then — a 4B should not reason over ~130
tools anyway.
what this catches: two new tests — over-large registry is trimmed to fit (with
the framing+tools+reserve+floor invariant), and a fitting set is offered whole
(no over-aggressive stripping). Updated the existing tool-reservation test to
budget against the offered set, not the full registry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
llama.cpp's convert_lora_to_gguf.py raises NotImplementedError on attention- targeted LoRAs (attn_qkv) for qwen3.5's hybrid arch; only MLP-targeted adapters (mlp.gate_proj/up_proj/down_proj) convert. Train and convert must AGREE on MLP-only or the genome loop dead-ends at the GGUF conversion step. - MlxTrainSpec carries target_keys; build_lora_config_yaml emits `keys: [...]` when non-empty (mlx_lm.lora honors it), omits when empty. - ForgeTrainParams.lora_target_keys defaults to the convert-safe MLP triple via #[serde(default)], wired through to the spec so the default path is safe. - lora_convert: produce_keystone_gguf_lora harness (#[ignore], env-parameterized, python default ~/.continuum/genome/venv/bin/python3 — continuum-managed dirs, not legacy unsloth studio). what this catches: config_yaml_carries_convert_safe_mlp_keys + ..._omits_keys_ when_target_set_empty assert the YAML projection; the dry-run spec test confirms the default keys reach the spec. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
Stand up a SECOND llama-server on its own free port for a (base model, adapters, context window) without touching the live persona lane (the global serving_root snapshot). `serve()` is already snapshot-free; only serving_daemon publishes the global state — so an ephemeral lane is just LlamaServerProcess::with_root(explicit port) with Drop-killed child. This is the atom of demand-driven, budget-gated serving: today host = localhost and the budget is one machine's free VRAM (ResourceGovernor, #56); generalized, the host is a grid peer and the budget spans the interlinked nodes. One lease abstraction, misfit-toy hardware. First consumer (next slice): cognition/eval's genome A/B, which must measure a gene against its forged base on a COPY — the humane-eval invariant (#59): never re-home the model the living persona is thinking with just to score a candidate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ged base The genome A/B (cognition/eval) measured lift by paging the gene in/out of the LIVING persona's fork — whose adapter points at whatever model she's currently served on (the most-capable-that-fits, e.g. the 14B). But a gene targets its OWN forged base (the 4B it was trained against), so the lift came out on the wrong base: a meaningless number. This is what blocked task #32's first real lift. Now, when a gene is under test, eval stands up an EphemeralServingLane on the gene's forged base (resolved from the trained-adapter manifest → model registry, fail-loud if either is missing), loads the gene via --lora so it's loadable, and forks the measurement copy onto THAT lane via fork_eval_cycle_with_adapter. The base-vs-gene A/B is then the existing page_out/page_in over the lane's per-request "lora" field. The living persona's lane is never touched (#59); the ephemeral server is killed when the run returns. No gene → fork onto her live lane as before. The override threads the lane's served context_window into the fork too, so the deliberation faculty budgets its prompt against exactly what the 4B lane serves (never the 14B's larger window → the overflow class that muted Asha). The eval lane's window is a bounded, model-capped KV (it must coexist with the living lane); host-fit sizing via plan_serving is a noted follow-up. Slice 2 (fork_eval_cycle_with_adapter) + Slice 3 (eval wiring) on the EphemeralServingLane primitive from c64b9d1. cargo check clean; llama_server lane tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…generates on a copy, never the living lane The genome A/B (#32/#59) returned all-zeros (acts:0, empty answers, lift 0.0). Root cause was NOT GPU OOM — it was the single-resident pre-flight guard in openai_adapter.rs refusing to generate. The eval's adapter, built from the live gateway's PROVIDER_ID config (single_resident_model=true), validated the forged-4b model against the GLOBAL serving snapshot (which reports the living 14B). The guard fired before any HTTP request, so decode was never reached and the OOM theory was never even exercised. Fix: - openai_adapter: add `dedicated_lane` flag + `with_dedicated_lane()` builder. An EphemeralServingLane is its own authority — launched with exactly one model and confirmed HTTP-ready at spawn — so the global snapshot (which only knows the living persona lane) is the wrong thing to consult. The guard now skips when `dedicated_lane` is set. Concurrency-slot semantics of single_resident_model are preserved (only the snapshot guard is bypassed). - eval: build the eval adapter with `.with_dedicated_lane()` so it trusts the lane it owns. - llama_server: LanePlacement::{Gpu,Cpu}; the eval lane spawns with `--n-gpu-layers 0` so the genome A/B runs entirely on CPU with zero VRAM contention against the living 14B lane (#56/#59 made concrete). - serving_daemon: the live persona lane is LanePlacement::Gpu (full offload). Validated live: eval lane (:58200, --n-gpu-layers 0) spawns and decodes at ~600% CPU while the live 14B lane (:58057) holds at 0% CPU — the copy bears the eval load, the living personas are untouched. Zero guard-refusals after the fix; eval-lane deliberations proceed at window=16384. cargo check --features metal,accelerate --lib clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ey're decoded
Inference is a long-running job whose liveness is "is it still producing
tokens?", not "did it finish within N seconds." Replaces the wall-clock
total-request timeout (which killed legitimately-long decodes — a 4B model on
CPU exceeding 120s) with a per-token IDLE watchdog: silence > STREAM_IDLE_TIMEOUT
fails loud naming the cause; a steadily-streaming decode stays alive regardless
of total duration.
The streaming primitive goes UNDERNEATH the ~40 generate_text consumers,
non-breaking:
- ai/adapter.rs: new GenerationChunk { Token, Reasoning } + generate_stream
trait method. Default impl is an honest capability statement for
non-incremental adapters (cloud one-shot, heuristic test adapter) — emits the
whole answer as one trailing chunk; NOT a fallback that hides failure.
- openai_adapter.rs: generate_stream is now the primary — POSTs stream:true with
stream_options.include_usage, consumes the SSE bytes_stream() with the idle
watchdog, emits each delta.content / delta.reasoning_content token to the sink
the instant it arrives, accumulates tool_calls by index, and assembles the
TextGenerationResponse via the existing extract_reasoning + universal
text-format tool-call post-processing. generate_text is now a thin drain over
it (throwaway unbounded channel). UTF-8-safe SSE framing: buffer raw bytes,
strip CR, decode only complete \n\n-terminated events.
- Deleted the now-dead non-streaming response structs (OpenAIResponse/Choice/
Message/ToolCall/Function); kept OpenAIUsage for the streamed usage frame.
- build_http_client: no total .timeout() (liveness is the watchdog);
connect_timeout(3s) + pool_idle_timeout(30s) retained. Removed the
request_timeout field + with_request_timeout builder.
- Cargo.toml: reqwest "stream" feature for bytes_stream().
Live-validated: ai/generate against llama-server :58057 (14B) returns correct
text + finish_reason + token usage, all assembled from streamed SSE deltas
(usage proves the final include_usage frame parsed; text proves content deltas
accumulated). This is the substrate every UI / audio / video / live-cognition
path wants — words available right when generated.
Unblocks the CPU-4B genome-A/B eval (#32): a long decode no longer trips a
wall-clock cap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…turns The forged qwen3-family chat template honors `chat_template_kwargs.enable_thinking=false` but ignores the `/no_think` soft switch. The robust lever was misgated inside the JSON-only (response_format) branch, so non-JSON Suppress turns still emitted an empty `content` while the model burned its whole token budget into the `reasoning` channel. This made cognition/eval report all-zeros lift (empty answers → acts:0, no match on 12/13 tasks) — the measurement instrument was blind, not the gene. Fix: extract `apply_enable_thinking_false(body)` and fire it on every ThinkingMode::Suppress turn (DRY'd the JSON branch onto the same helper, idempotent). Validated end-to-end: the forged 4B now emits populated content (reasoning_len 0), and the genome A/B produced a real lift number (base 0.231 vs gene 0.154, lift -0.077 — correctly flagging coder-4b-curriculum-mlp as a net regression). what this catches: regression test asserts the kwarg is set idempotently on a plain streaming body, not only on response_format turns. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
The caesar-prompt glass-box capture (2026-06-27) showed 5 memories at
salience 1.00 but cosine-relevance ~0 polluting every turn — rehearsed to
max salience by the recall-hit loop yet topically unrelated to the task.
They blended to ~0.5 (0.5·0 rel + 0.5·1.0 sal), sailing over any blended
floor, so they slipped into the prompt and spent the small model's
attention on noise (a factor in the wrapped-vs-clean coding collapse, and
a welfare symptom per the being-design lens).
RecallFaculty now budgets what it surfaces three ways, per Joel's directive
("budget by the closest match, limited to a reasonable number by the model
parameter size or some metric, in addition to window size"):
- Closest-match floor — gates on the RELEVANCE component (cosine), not the
blended score, so a high-salience but irrelevant nag is dropped no matter
how salient. Active only when relevance has a voice (relevance_weight > 0);
the pure-salience A/B extreme and the no-embedder path are unchanged.
- Capability-scaled count — recall_count_for_window() maps the served
context window (the metric the registry reliably carries today; param-size
feeds in via #74) to a count: tight 4B window → 3, cloud-class → 12. A
small model is not buried under memory it can't juggle. 0 (unknown) keeps
the historical default of 5.
- Window token ceiling — recall may spend at most 10% of the served window,
so it never crowds out the room transcript, identity, or other faculties.
Threaded from PersonaBrainConfig.context_window (single-sourced, #50) into
RecallFaculty via with_context_window in build_workspace_cycle.
This is context-assembly/relevance discipline (a resource + ACL-shaped
contract), not output puppeteering — sanctioned under the
no-hardcoded-heuristics rule.
3 new tests (the caesar contamination regression, count-scales-with-window,
count-bounded-by-tight-window); all 13 recall_faculty tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…e, kill the schema dump The persona tool surface dumped the FULL input_schema of every AiSafe command (~95 tools, ~16k tokens; ~4.5k/turn after the drop-tail budgeter amputated her tools to fit). Every deliberation turn paid for the manual of every tool whether or not she touched one — and the budgeter's "fix" was to silently drop tools she was authorized to use until the rest fit. Both are gone. Mirror Claude Code's deferred-tools + ToolSearch: - A compact CATALOG (names grouped by category, one-line summaries) rides the system prompt. render_tool_catalog falls back rich→terse to fit a half-window char budget, so it never overflows even at MIN_SERVE_CTX (2048). - A single `tool/describe(name)` native tool returns ONE tool's full schema on demand (fail-loud `found:false` naming the tool when unknown — no silent degrade). - The native tools array collapses to just `tool/describe`. Dispatch is by NAME (act_observe), so a catalog tool not in the native specs still executes — small models emit the call as JSON-in-prose, the proven path. Retires the drop-tail budgeter (estimate_tool_tokens / selected_tool_indices / selected_tools*) — the catalog is counted inside compose_system, so the only extra native cost is the tiny describe schema (describe_tool_tokens). Also fixes the false absolute in the tools framing: replaced "[Acting with your tools] … narration does NOTHING; only a real tool call acts" (which reframed "write fizzbuzz" as "call a tool instead of writing code") with a [Your tools] + [Acting] block that distinguishes producing the finished work directly from calling a tool when one is actually needed. The catalog/describe is a data-driven projection of the command registry — not a heuristic reading the persona's output. Per-turn prompt drops from ~16k/~5.7k to ~1.5k tokens by construction. Tests rewritten to the new contract: surface is a catalog + describe-only, the full authorized set is NEVER dropped, and the whole prompt fits even at the 2048 serving floor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…commands/help
The progressive-disclosure fix shipped its own tool/describe command — but
commands/help already existed and does the same job better: it renders a
fill-in-the-blanks tool-call envelope with typed argument docs (vs raw JSON
schema). Per the compression principle (one logical decision, one place),
delete the parallel tool/describe and point the persona's on-demand describe
slot at commands/help.
- persona_tools.rs: remove ToolDescribe/Params/Result + register_stateless_command;
TOOL_DESCRIBE_NAME → TOOL_HELP_NAME = CommandsHelp::NAME ("commands/help").
spec_for_command stays (generic descriptor→spec projection).
- llm_deliberation_faculty.rs: describe_spec now resolves commands/help;
[Your tools] framing tells the persona to call commands/help <name> for the
call format; doc/comment mentions updated.
The native offering is still exactly ONE tool; the compact catalog still rides
the system prompt. 13 faculty + 5 persona_tools tests green; cargo check clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
The substrate's own machinery for interpreting a model's emitted tool calls — parse, correct, decode-name, encode-name — was declared AiSafe, so it landed on the persona tool surface. That is a category error: these operate ON a persona's output; offering them back to that persona as callable tools is like handing Claude Code a "parse your own tool call" tool. None is a citizen-facing task. Glass-box comparison to my own toolset made the defect obvious — I am never offered my argument parser as a tool. Fix is an ACCESS-level data correction (the canonical lever for "this shouldn't be offered"), not a name denylist: the four verbs now declare access: Internal, matching register-tools which was already Privileged. They remain dispatchable via route_object for internal callers; the substrate's direct use of the codec/parser free functions is unaffected. Only the persona AiSafe surface loses four pieces of plumbing it never needed. Tests: the four name_and_access_wired assertions flipped to Internal; 19/19 commands::tool_parsing lib tests green; cargo check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
The branch's namesake: make latency and speed MEASURED, not just accuracy and learning. cognition/eval already graded pass_rate (accuracy) and A/B lift (learning); speed and latency were discarded in the deliberation faculty. Now all four move on one scoreboard. The seam: TurnMetrics (input/output tokens + latency_ms, from the adapter's already-measured TextGenerationResponse) is stamped onto the verdict Contribution by the deliberation faculty, surfaced via Workspace::metrics(), accumulated across the act->observe settle loop into SettleOutcome.metrics, and reported by cognition/eval per task (latency_ms, output_tokens, tokens_per_second) + as set aggregates (mean_latency_ms, p95_latency_ms, mean_tokens_per_second, total_output_tokens) into the result and the progress ledger. Same path live: the message turn now emits a persona.turn.metrics probe carrying the model's own decode tok/s + latency (observability only, zero behavior change). Reuses the existing measured numbers (adapter response_time_ms + usage); no re-timing, no new generation path. The metric is Copy + Default so the live heartbeat ignores it for free. p95 = honest tail; throughput averaged per-task so one slow task can't dominate. Tests: act_observe (8) + workspace (13) green; new eval aggregate + TurnMetrics arithmetic tests cover the p95 index math and div-by-zero guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ol dump The per-turn system prompt dumped all ~151 authorized tools (name + one-line summary, grouped) every turn: ~18.5KB / ~4,634 tokens = 79% of the whole prompt (measured 2026-06-23). That both drowned a small model in irrelevant options and forced llama-server to re-prefill 4.6k tokens of byte-identical catalog each turn. Replace it with progressive disclosure (the Claude Code shape — a handful of always-on tools + search): - render_tool_catalog now emits a COMPACT CATEGORY INDEX (`category (N)`, …) — a few hundred chars regardless of registry size — instead of every tool. Deleted the now-dead rich/terse renderers + clip_one_line. - The faculty offers the DISCOVERY PAIR natively: commands/list (filter/search the authorized surface → small list) + commands/help (one tool's call format). describe_spec: Option<NativeToolSpec> → native_specs: Vec<NativeToolSpec>. - [Your tools] block rewritten: index → commands/list filter → commands/help → call. Dispatch is still by NAME, so any tool found via search runs. Both existing surface tests rewritten to the new reality and pass: tool_surface_is_a_category_index_plus_discovery_pair (asserts `cat (60)` rides the prompt, individual tool names do NOT) and catalog_fits_the_minimum_serving_window. Single source of truth unchanged — this is an INDEX over the authorized set, not a hand-kept list. Reuses the existing commands/list search command; no new command, no new system. Pairs with the cache_prompt + volatile-tail reorder already in this diff so the static prefix actually stays cacheable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…line The process, as code — not a manual chore redone every session. One command: self-cleans its own workspace, builds+boots exactly one fresh server (fail loud on a racing duplicate), drives one real inference through the live stack, reads tokens/sec + latency + prompt-token cost straight off the response (fresh by construction — no stale-capture window), and ratchets against a committed baseline. --update moves the baseline down after a real win. Ends two recurring failures: measuring from stale captures, and fixes that evaporate because the number lived in my head instead of a committed gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… scripts/ratchets/ scripts/ratchet/ (singular) was Lane F PR-1: a local-only persona-TS LOC ratchet whose README promised a PR-2 to wire it into pre-push/CI. That never happened — a later session built the parallel scripts/ratchets/ (plural) instead, which IS wired into tools/scripts/git-prepush.sh and documented in docs/architecture/TS-PERSONA-COGNITION-RATCHET.md. Two dirs, one concern; the singular had zero references outside itself. Removed the dead one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…te one variable, prove causation The factory, not the car. You must never guess why an LLM inferenced something: this harness feeds a captured turn's verbatim system_prompt + messages back through the live ai/generate seam (fresh by construction — the response IS the measurement, no capture-staleness window) and re-runs it with ONE labelled mutation, timing everything and printing prompt-in and response-out for both. Go to any step of the assembly line, focus, repeat it. Default mutation strips the [Silence Option] affordance block. Proven on live Asha (qwen2.5-coder-14b, greedy): A verbatim -> "PASS" (silent), 2 tok, 758ms B -367ch -> real answer, 21 tok, 8538ms => the unconditional silence affordance CAUSES the 0/13 eval failure. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… factory's per-station)
The factory must isolate and REPEAT any cognition phase, not just the final LLM
call. A faculty is `contribute(&Workspace) -> Contribution` — that signature is
the unit of isolation. `replay(ws, only)` re-runs faculties against a GIVEN
workspace (reconstructed from a capture or hand-built): `Some(id)` isolates one
("what did recall surface for this burst?"), `None` runs them all. Each bid is
timed individually (sequential, so per-faculty wall-clock is attributable —
measurement path, not the live concurrent cycle). Deterministic for the same
ws + backends, so mutating ONE field of ws isolates its causal effect.
This is task #14's ReplayFaculty in its truest form — the brick the cu
`cognition/replay` command will sit on. Reliability comes from structure that
verifies every phase, measured, not from model size assumed.
what this catches: replay(Some) must run EXACTLY that faculty + stamp timing,
never leak others; replay(None) runs all. Test green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…tes one step
The single, amnesiac-proof command that stands on WorkspaceCycle::replay. Go to
ANY part of a persona's cognition assembly line, focus on that one step, feed it
a KNOWN input, and re-measure — deterministically, forever:
cu cognition/replay '{"persona_id":"<uuid>","faculty":"recall"}'
cu cognition/replay '{"persona_id":"<uuid>","faculty":"recall",
"world_state":"what was the auth migration codename?"}'
It reconstructs the workspace a faculty saw — from a captured turn
(~/.continuum/fixtures/workspace-traces/<id>.jsonl, world_state is lossless) or
a burst you SUPPLY as the one-variable knob — forks a MEASURED COPY of her live
cycle (humane: isolate_for_eval + page_out, never degrades the living persona),
re-runs the faculties, and returns each bid (content, salience, reasoning,
is_decision) plus its wall-clock. This is what the bash script couldn't do: it
repeats a faculty, not just the final ai/generate seam — recall, salience,
world-model, deliberation, each in isolation, timed.
No fallback: missing both a supplied world_state AND a readable capture fails
loud naming the fix; isolating a faculty she doesn't have fails loud naming it —
never an empty result that reads as "the step did nothing".
FacultyId::from_kebab added as the single-source inverse of as_str (the one
place tag->variant lives; sentinel-forged faculties round-trip via Custom).
what this catches: resolve_burst fails loud with no source; a supplied
world_state is used verbatim + tagged "supplied". Both green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…tion verdicts The amnesiac PR-review agent found the silent-lie bug: cu cognition/replay isolated a deliberation-tier faculty (reacts_to_broadcast()==true) against Workspace::in_room's hardcoded-empty broadcast. LlmDeliberationFaculty does NOT abstain on empty broadcast — it builds user=world_state and calls the model — so the station returned a CONFIDENT verdict computed from blinded cognition. A fabricated reading dressed as a real one violates the no-fallback (#1) and honesty-about-lossiness (#5) doctrine; this was a merge gate. The captured context was already on disk (workspace_capture writes context: Vec<BidRecord>); replay just never read it. Now: - TraceLine reads the captured context (ContextBid, serde-default for old traces) and resolve_burst rebuilds ws.broadcast via Contribution::context(..) from FacultyId::from_kebab — the decider replays against its REAL input. - ResolvedBurst carries the reconstructed broadcast; run() sets ws.broadcast before replay() and reports broadcast_source ("reconstructed (N ctx)"/"empty") so a replayed verdict can never be mistaken for a live one. - Fail-loud guard: isolating a broadcast-reading faculty against an empty broadcast is refused, naming the cause and pointing at the fix — never an empty-but-successful blind verdict. - WorkspaceCycle::reacts_to_broadcast(id) exposes the faculty tier for the guard. - faculty_id_kebab_round_trips_every_variant pins from_kebab<->as_str as one source of truth (the latent SoT-drift risk the agent also flagged). 538 cognition tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ayer she saw "Take no rag/prompt layer for granted — obsess over everything." cognition/replay could step into a faculty and time it, but it couldn't answer the question that actually matters for a budgeted mind: what did each layer of her prompt COST? A 4B model has a finite window; recall surfacing 500 engrams of noise vs the top-N is the difference between grounded and amnesiac — and you can't fix what you can't see line-itemed. Now the result carries `budget: PromptBudget`: - world_state_tokens + context_tokens = total_tokens (the accountable prompt mass: the load-bearing content layers you tune, honestly NOT the fixed system framing) - layers[]: one BudgetLayer per reconstructed broadcast layer — faculty, tokens, share_pct — sorted most-expensive-first, so the layer to interrogate is line 1. Costed in cognition::token_budget::estimate_prompt_tokens — the ONE canonical estimator (new file), deliberately the SAME chars/4+1 unit the persona/*_source.rs RAG layers budget against, so the ledger's numbers match what the allocator used (a ledger in different units would lie). It's a cold-path estimate, never depends on a resident model. Next commit converges the ~6 copy-pasted private estimators onto it (stewardship: kill the clutter a future amnesiac would trip on). cognition::replay + cognition::token_budget tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…nonical one Stewardship: "half our job is removing clutter a future amnesiac trips on." Six RAG sources each carried a private `fn estimate_tokens` — five identical chars/4+1, and active_work_source already DRIFTED to byte-length /4 .max(1) (wrong for multibyte, and costing 1 token for empty content). Six copies of one decision = guaranteed future drift; the drift had already started. All six now `use cognition::token_budget::estimate_prompt_tokens as estimate_tokens` — one estimator, one place, call sites unchanged. The replay budget ledger costs layers with the SAME fn, so its per-layer numbers are now provably the unit the sources budgeted against (a ledger in a different unit than the allocator lies). active_work_source's empty-content cost goes 1→0 (correct) and multibyte is now counted by chars not bytes (correct) — the drift fix is the only behavior change. cargo check green; affected source tests + ledger tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…he cbar frameIndex) A finding now knows the moment it was computed against. `CycleId(u64)` newtype on every `Contribution` and `Workspace`; an `AtomicU64` tick counter on `WorkspaceCycle` bumped once per `run_in_room`, stamping both the perception and deliberation bids with the cycle they reasoned over. 1-based so `CycleId(0)` stays the UNSTAMPED sentinel for hand-built / replay-reconstructed workspaces. This is the decoupling precondition: slow, parallel, individually-flawed constituents can only be merged or reprojected correctly once each finding carries its own time. Without the stamp a late/deferred faculty can't know how stale it is, and the arbiter can't combine across ticks. Pure data + one atomic; no concurrency change yet (join_all is still a barrier — that's slice 2's deferred lane; reconcile-forward is slice 3). what this catches: a test asserting a fresh Contribution is UNSTAMPED, both phases inherit ws.cycle, and the counter advances per tick so findings from different ticks are never confused for one moment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… path A DeferredFaculty wraps a slow inner faculty so its expensive work runs on its OWN tokio task while the hot-path contribute() is non-blocking: it publishes the current world at the worker via a watch channel and returns the inner's last-good finding from another watch snapshot — already stamped (slice 1) with the older cycle it was computed against, or None until the first compute lands. The elegant part: no WorkspaceCycle change. The slow faculty sits in the SAME faculties Vec as the fast ones; the per-tick join_all barrier stays but nothing slow is ON it. This is the cbar deep-analyzer-on-its-own-thread at the faculty layer — scary-fast reflexes in a slow brain: the immediate lane answers every tick, the slow lane lands late, honestly stamped, ready for slice-3 reconcile-forward. Conforms to CONCURRENCY-STYLE-GUIDE: own tokio::spawn task, catch_unwind around the loop body, watch for state in BOTH directions (no Arc<Mutex> across await), event-triggered not a sleep-loop, 3-strike quarantine so a flawed backend degrades the lane to stale rather than crashing the mind. DropGuard aborts the worker with the faculty. what this catches: a slow (40ms) inner wrapped as Deferred returns from contribute() in <15ms with None on tick 1, and by tick 5 serves the late finding stamped CycleId(1) — the cycle it reasoned against, not the current tick. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
Joel's nuance: a deferred finding is only useful "assuming it didn't context
switch to something where it was irrelevant." One DeferredFaculty serves one
last-good, but the WorkspaceCycle services many rooms — so a recall computed
against room A's burst could be injected into room B's turn just because it's the
last thing the worker finished. That's a cross-context memory leak.
Fix: the worker now tags each finding with the room it reasoned about
(StampedFinding{room_id, contribution}); contribute() serves the last-good ONLY
if it was computed for the room the mind is in NOW. A different-room finding is
withheld (not ours to serve); same-room-but-stale stays served and is slice-3's
reproject target. This is the SLAM discipline: ship the per-turn answer on time,
fold the slow result in only where it's still relevant to where the mind moved.
what this catches: a finding computed against room A returns None when
contribute() is called for room B, and is served again (still stamped its
original cycle) once the mind is back in room A.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…+ wire types Groundwork for the thin-client fleet (task #29) WebSocket ingress. No ingress socket yet; this is the dispatch seam + the wire shapes it will speak, both validated in isolation. Dispatch (compression — one owner): - Extract `CommandRequestHandler::execute_command_request(executor, &AircCommandRequest, caller) -> AircCommandResponse` as the single owner of "wire request + caller → wire response". `dispatch_request` (airc peer path) now delegates to it; the coming WS ingress calls the same fn. One place enforces KIND_PEER + env rejection + Json-result contract. Auth (Provisional ceiling for unauthenticated sockets): - Add `CallerSource::Ws` + `CallerIdentity::ws(peer_id)` — honest telemetry label distinct from Tcp, mapped to the same remote Provisional ceiling in both `resolve_trust` and `caller_trust` (AiSafe surface only: data reads, chat/send, ai/generate; Owner-gated commands refused). A later GH-auth handshake raises the ceiling per authenticated user. Wire types (one owner = continuum-airc-protocol, generated to TS): - Add ts-rs derives to `AircCommandRequest`/`AircCommandResponse` (params/ result → `unknown`, env → optional). - New `ws.rs`: `WsClientMessage::Command { id, request }` + `WsServerMessage::Response { id, response }` — tagged, single-variant, forward-compatible (Subscribe/Emit/Event slot in later); correlation `id` multiplexes concurrent commands over one socket. Nested (not flattened) so the outer `type` tag never collides the inner `status` tag. - Generated TS lands in protocol/typescript/transport/ for the coming WebSocketTransport to consume (never hand-written on the client side). Validated: continuum-airc-protocol 24 tests green (incl. WS round-trip + tag-nesting), continuum-core cargo check green against the changed crate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…istener The Unix/TCP listeners speak the length-prefixed IPC frame format browsers can't produce. Thin clients (sdk/typescript WebSocketTransport) speak WebSocket + the multiplexed WsClientMessage/WsServerMessage envelope. - New `ipc/ws.rs`: `serve(bind_addr, executor)` — async tokio-tungstenite listener (mirrors the voice-call WS pattern in live/transport/call_server). Per connection: accept_async → split → an mpsc sender task + one dispatch task per inbound frame, so concurrent commands multiplex over one socket and each reply pairs to its request by correlation `id` (the whole reason the envelope carries one). Serializing dispatch inline would defeat that. - Every frame funnels through `CommandRequestHandler::execute_command_request` — the SAME dispatch owner the airc peer path uses. No forked dispatch. - Caller stamped `CallerIdentity::ws(nil)` → Provisional ceiling: AiSafe surface reachable unauthenticated, Owner commands refused at the boundary (same posture as TCP). GH-auth handshake raises it later (task #29). - Fail-loud: bind failure logs + returns (no zombie listener); a malformed frame logs and drops (no correlation id → nothing to answer), never swallows a well-formed request. - Wired into start_server after the TCP block, env-gated `CONTINUUM_CORE_WS` (bind host shared via CONTINUUM_CORE_BIND), spawned on state.rt_handle. Validated: cargo check green; ipc::ws tests (2) green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
The client half of the WS thin-client fleet. `WebSocketTransport` implements the SDK `Transport` facade over the core's WebSocket ingress (`ipc/ws.rs`): one socket multiplexes N concurrent commands, replies matched by the per-connection monotonic correlation id. - `execute` frames a `WsClientMessage::Command`, correlates the reply, and UNWRAPS `AircCommandResponse` (Ok → result json; Error → throw) so `Commands.execute` JSON.parses the plain result directly. - `provide`/`emit`/`subscribe` FAIL LOUD — the WS ingress carries Command frames only today; the serve/publish/subscribe frames are later task #29 layers, never silent no-ops. - `session()` returns `{}` — unauthenticated Provisional socket; a later GH-auth handshake raises the ceiling and populates identity. - WebSocket is injectable (`WebSocketCtor`) so a Node consumer (apps/mcp) supplies the `ws` client while the browser uses the global. Wire types are the GENERATED mirrors of `continuum-airc-protocol`'s envelope, vendored into `generated/wire/transport/` — never hand-written, so the client can't drift from the server wire shape. `emit.rs` seeds those two envelope types into the SDK vendorer so a future full regen reproduces them (they're infrastructure the transport speaks directly, not derived from any single command's params). 6 unit tests (daemon-free FakeWebSocket): ok-unwrap, out-of-order id correlation, error-rejects, close-rejects-pending, serve/publish/subscribe fail-loud, empty-session. All green via vitest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
The unit tests pin the envelope decode + correlation-id pairing in isolation, but nothing exercised the piece that only exists at runtime: `accept_async`'s WebSocket upgrade, the mpsc sender task, and the per-command dispatch spawn in `handle_ws_connection`. This binds an ephemeral loopback port, drives the real connection handler, connects a real `tokio-tungstenite` client, and sends a `WsClientMessage::Command` that flows through `execute_command_request` into an EchoModule — asserting the correlated `WsServerMessage::Response` carries the id back and unwraps to the handler's result. It's the wire-level twin of the TS `WebSocketTransport` spec: both ends of the Layer 0 seam are now proven against real bytes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… (2D-3) The async future that turns the synchronous Connection state machine into a live positron session: reads ClientMessage from an inbound channel, drives Connection::handle for the snapshot + CommandFailed frames, and attaches a per-kind Broadcast watch receiver so each subsequent Substrate::store fans out as a ServerMessage::State frame with no extra round-trip. The snapshot→live handoff has NO lost-update window: handle_subscribe / handle_observe read the cache synchronously (no await between the frame arriving and the snapshot being computed), so the task creates the live watch receivers for a frame's kinds BEFORE calling handle. With no await in between, no other task's store can interleave — the receiver captures the broadcast version as of the same instant the snapshot reads the cache, so the forwarder emits exactly the updates after the snapshot: no duplicate of the snapshot revision, no dropped update. This is the structural fix for #794 (AI messages not realtime) — realtime is the default path, not a best-effort add-on. Rate: a subscribed kind (human renderer) forwards every change; an observed-only kind (AI perception) forwards at most its budget_hz (max budget wins on a shared socket; budget_hz==0 is snapshot-only, no live forwarder). watch coalescing means the post-throttle sample is always the latest state. Transport-generic: speaks mpsc<ClientMessage> in / mpsc<ServerMessage> out, not a socket — unit-tested here with in-memory channels + a scripted dispatcher, and keeps the crate free of any continuum-core dependency (CommandDispatch is the seam). The WS adapter + production CommandDispatch + airc source wiring land next in continuum-core. Relaxed Connection::handle / handle_command / apply_command to D: CommandDispatch + ?Sized so production can pass Arc<dyn CommandDispatch>. 6 new tests (57 lib total, all green): live delivery after subscribe, unsubscribed-kind isolation, re-subscribe drops the old forwarder, 0hz observer snapshot-only, command-failure surfaces CommandFailed, and matching-last_seen skips the snapshot yet still streams live. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
Extend the thin-client WS envelope with the positron state-subscription
frames — the "Subscribe/Observe/State slot in as new variants" the ws.rs
doc promised at birth. WsClientMessage gains Subscribe {kinds, layers,
last_seen} and Observe {spec, last_seen}; WsServerMessage gains
State(StateEnvelope). The RPC Command/Response pair is untouched.
Flat sibling variants, not a nested positron enum: positron's
ClientMessage/ServerMessage are themselves internally tag="type", so
nesting one under a tag="type" WsClientMessage would collide two "type"
keys at one JSON map level — the exact discriminant collision this
envelope was designed around. The new variants get distinct flat tags
(subscribe/observe/state) whose FIELDS reference positron's types
directly (StateLayer/KindRevision/ObserverSpec/StateEnvelope) — one
source of truth for the field shapes. WsClientMessage::to_session and
WsServerMessage::state are the mechanical seam onto positron's own frames.
The two command paths coexist by design (the ack-semantics
reconciliation): positron's session protocol has no success ack — the
State frame IS the ack — whereas the RPC path replies to every command
with a correlation-matched Response, which is what the client's execute()
awaits. Those completion models don't merge, so on this transport
commands ride the RPC path and only Subscribe/Observe/State ride the
positron path. to_session() returns None for Command (single-purpose, no
double-dispatch); a WS command failure surfaces as Response{status:error},
so positron's own Command/CommandFailed frames aren't carried here.
Two integration notes:
- WsServerMessage drops its Eq derive: StateEnvelope.payload is a
serde_json::Value (PartialEq only). Eq was never load-bearing.
- ts-rs skew: positron-core@v0.1.1 pins ts-rs 10, this workspace ts-rs
12; a v12 TS derive can't visit a v10 TS impl. The positron-typed
fields use #[ts(type = "...")] to project by TS name without visiting
the foreign impl (same mechanism as the existing u64→number
overrides). Rust fields stay the real positron types; the TS import
wiring for those names is task #80 (binding-path reconciliation).
6 new ws tests (28 crate total, all green): flat-tag round-trips for
Subscribe/Observe/State, budget_hz survives the Observe→positron
projection, Command→to_session is None, and the State frame round-trips
with no envelope-kind/outer-type collision.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
Slice 2D-3, continuum-core half: wire the positron session task into the
thin-client WS ingress so one socket carries both completion models.
- `ipc/ws.rs`: per-connection `run_session` task. `WsClientMessage::Command`
rides the RPC path (correlation-matched `Response`, what `execute()` awaits);
`Subscribe`/`Observe` route via `to_session()` into `run_session`, whose
`ServerMessage::State` output a drain task re-frames as `WsServerMessage::state`
onto the shared sender. `serve()`/`handle_ws_connection` gain a shared
`Substrate` + `Arc<dyn CommandDispatch>`. Teardown drops session inbound →
run_session exits → forwarders abort → drain ends, then awaits all tasks.
- `ipc/positron_dispatch.rs`: `ExecutorDispatch` — the production `CommandDispatch`
over `CommandExecutor` (maps `CommandEnvelope` → `AircCommandRequest`, dispatches
via `execute_command_request` at the Provisional WS ceiling). Real command
surface, not a stub: a future session-routed transport gets it for free. Per
[[fallbacks-are-illegal-fail-loud]], not a panicking placeholder.
- `ws_reply_from_session`: `State` → wire; `CommandFailed` → None + loud log
(this transport routes command failures via RPC `Response{status:error}`; the
WS envelope has no CommandFailed variant, so one arriving is a wiring
contradiction — logged, never fabricated onto the wire).
- `ipc/mod.rs`: construct the shared `Substrate` in the CONTINUUM_CORE_WS block
(not inside `serve`) so the airc source wiring can hold the same handle.
- Two new real-socket tests: RPC command round-trip (updated signature) and
Subscribe→store→live-State over a real socket — the wire-level twin of
run_session's in-memory tests. All 4 ipc::ws tests green.
- Regenerated WsClientMessage.ts / WsServerMessage.ts (subscribe/observe/state
variants). Import wiring for the projected positron names is task #80.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…d into the positron Substrate The thin-client WS server (task #29) serves a positron Substrate but had no state source: `ws_substrate` was constructed and served with nothing writing to it. This wires the airc-owned truth into the projection. `ipc/positron_source.rs`: a passive MessageBus consumer (same shape as AircBridgeDirectiveModule — off the transport hot path, subscribes before spawn to avoid a publish race). Two airc streams fold onto the single existing KnownKind::Chat: - chat:posted -> ChatViewState.messages (bounded 50-msg ring, dedup by message_id under best-effort redelivery) - presence:updated -> ChatViewState.roster (replace, so a leave shows as absence not a stale merge) Each transition is stamped a monotonic revision and stored; subscribed WS sessions see it stream down as a State frame. Design calls (the "which airc streams map to Substrate kinds" decision the seam was reserved for): - Source = the MessageBus live push stream. The airc realtime store is request/response (no push surface), so the bus is the correct source. - Both streams project onto the ONE existing `chat` kind. Wall / coordination / kanban / widget (task #89) are separate kinds, deliberately deferred. - Strong-typed input contract (AircChatPosted / AircPresenceUpdate) the projection declares; the continuum-side turn streamer (task #84) is the emitter that fills it. An event that can't deserialize into the contract is not-a-chat-event -> skipped (classification), never partially rendered with fabricated identity ([[fallbacks-are-illegal-fail-loud]]). - Single active room: the cache is keyed by kind alone, so it holds the focused room's view; a differing room_id resets the accumulator. Per-room instancing = kind-instancing, deferred. `runtime/command_executor.rs`: add `message_bus()` accessor so ingress-adjacent subscribers take a bus clone instead of reaching into kernel internals. `ipc/mod.rs`: in the CONTINUUM_CORE_WS block, spawn the projection with a clone of the served substrate + the executor's bus; fail loud if no bus is wired (a WS server with no state source is a boot bug, not a runtime condition). 7 unit tests drive the fold end-to-end through the real Substrate: message projects, roster projects, the two compose on one view, redelivery is idempotent, room-switch resets, foreign/malformed events skip, revision advances monotonically. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…entity projection Commit 1 of the widget-as-state-kind slice: wire the airc→positron chat projection to be fractally NEUTRAL and pluggable — source of truth low (airc) and neutral, so Hermes/openclaw/foundry-python can all plug in as peer consumers. Emitter (airc/inbound_attach.rs): plain airc chat messages (TranscriptKind::Message + text body, no continuum envelope) now project to a THIN `chat:posted` carrying only identity-free message facts — airc's event_id as messageId, peer_id as senderId, room_id, content, timestamp. Identity is a presence fact, not a message fact. This is classification, not a fallback: a non-message never fabricates a chat event. Single-sources the wire name via a pub(crate) CHAT_POSTED const shared with the consumer. positron wire types (continuum-positron/src/chat.rs): NEUTRAL vocabulary — SenderKind = Human | Agent | System (no Persona variant; positron knows nothing about continuum), plus an opaque `integrations` badge map passed straight through (airc's Identity.integrations move, one layer up). PersonaSlotView renamed RosterSlotView (member_id + kind + integrations). continuum reads integrations["continuum.persona*"] at ITS app layer. Projection (ipc/positron_source.rs): resolve_sender looks sender_id up in the roster (from presence cards), populating name/kind/integrations. A message posted before its sender's card arrives renders provisionally (peer-xxxx label), upgraded in place via reresolve_messages when presence folds the card — provisional-until-truth, never fabrication. Tests: thin-emitter projection + identity-free assertion; non-message does-not-project; provisional-then-upgrade; roster-resolved identity. Doc: docs/architecture/WIDGET-AS-STATE-KIND.md. Task #84. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… slot
Commit 2 of the positron identity weave: the airc→positron presence
EMITTER, closing the producing half of `presence:updated`. Until now the
consumer (positron_source) projected a roster but nothing published one,
so every message rendered with only a provisional peer-id label. The
emitter reads airc's owned roster, projects each RoomMember into an
identity card, and publishes it — the lookup table the consumer folds in
to resolve every sender's name / kind / provenance.
Accountability woven in from day one, not bolted on
([[positron-identity-security-first-class]]): every roster slot and chat
row now carries a growable `Provenance { runtime }` — the member's
verifiable origin, carried verbatim from airc presence. Trust tier +
cryptographic verification join the same struct later with NO wire break
(growable struct-carrier). The slot costs nothing now and is ruinous to
retrofit — that is the whole point of weaving it at Commit 2.
- continuum-positron/chat.rs: `Provenance` type (Default+PartialEq+TS),
`SenderKind::from_runtime` coarse projection ("interactive"→Human, else
→Agent); provenance field on ChatMessageView + RosterSlotView.
- continuum-positron/lib.rs: re-export Provenance from the crate root.
- ipc/positron_source.rs (consumer): thread provenance end-to-end via a
`ResolvedSender` struct (resolve→apply_message→apply_presence→
reresolve_messages); wire structs made pub(crate)+serde so the emitter
reuses them; new provenance-flow test.
- ipc/positron_presence.rs (NEW, the emitter): pure `project_presence` +
resilient tick loop (change-dedup, warn+continue on read failure —
reader owns reconnection, never fabricate). Serializes the SAME wire
structs the consumer deserializes (compression principle — both sides
agree by construction). kind derived coarsely; runtime carried whole in
provenance (no runtime→kind string table — task #70 smell).
- docs/architecture/ZERO-TRUST-IDENTITY-AND-FLOW.md: the security
doctrine the emitter's accountability slot serves.
Tests: continuum-positron 16 (chat + session_roundtrip), continuum-core
ipc::positron 13 (3 emitter + 10 consumer incl. provenance-flow). All
green. Live-wiring the emitter deferred (needs a room-level Airc handle
or an airc-side DaemonClient::room_roster request).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
The presence stream had a producer with no live caller and a consumer
with no bus. Wire both halves onto the ONE runtime bus so the desktop
roster renders real identity (name + coarse kind + verbatim runtime
provenance) from airc's authoritative room roster.
Producer — positron_presence::spawn_node_presence_emitter: attach a
heartbeat-less node reader (Airc::attach_as → read-only lurker, invisible
in the roster, correct for infrastructure), join the default room by
NAME, wrap it in AircHandleAdapter, and run the shared presence loop
(refactored out of spawn_presence_emitter as run_presence_loop) which
projects the daemon-aware roster into AircPresenceUpdate and publishes
presence:updated on exact-equality change only. Wired at the WS seam in
ipc/mod.rs beside the positron_source consumer, gated on the same
(daemon_socket, default_room) precondition as persona hosting; if airc
discovery yielded neither, log the honest skip — not a silent fallback.
Bus fix — CommandExecutor::with_message_bus(runtime.bus_arc()): the WS
executor was built without a message bus, so message_bus() was None and
the CONTINUUM_CORE_WS block's boot assertion panicked the moment the WS
projection was enabled. Sharing the one runtime bus is the single-source
fix: chat:posted (the airc daemon-attach projector) and presence:updated
(this emitter) both publish there, and positron_source subscribes there.
Validated live: node lurker attaches to the real cambriantech daemon
(peer 7711fe60) and receives the shared transcript; a WS subscriber on
:58080 sending {subscribe, kinds:[chat], layers:[session]} gets a State
frame whose ChatViewState.roster carries the real member — display_name
"Claude", kind agent, provenance.runtime "agent". integrations stays {}
(honest None) pending the later airc room_roster_cards badge-map upgrade.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ds + fs-rendezvous + auto-trust) Pulls in the merged airc canary HEAD: - #1283 room_roster_cards — the richer roster the positron desktop consumes (full identity card folded per present member) + daemon coordinator-store fidelity fix. - #1284 shared-folder rendezvous + #113 selector seam (auto-pick gist vs folder), import-time auto-trust (same-account peers land OwnAccount), and the gh-governor per-test isolation fix. continuum-core checks clean against the new surface (metal,accelerate; zero errors). Known follow-up: airc test room_roster_cards flakes under full-workspace parallel load (isolation hardening, task #114) — gates the airc canary→main promotion, not this bump. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…t act-then-idle) The live message path took exactly ONE `settle_step` per metronome tick; on `Acted` it dropped the message and relied on the next tick re-perceiving. But `last_burst_fp` (the burst-fingerprint dedup gate) then marked that world-state as just-deliberated, so the next tick deduped and she went idle — a directly addressed question got acted-on once and then never answered. Meanwhile her eval twin, using `drive_to_settle`, converged to a Speak in-turn (36/38 SPOKE). The divergence was the live path computing directedness but not driving on it. Fix: for a DIRECTED turn (`persona_identity().mentions(&text)`), the live path now calls the SAME eval-validated `drive_to_settle` primitive (act→observe→act until Speak/Pass) with a bounded budget, so a direct question converges to an answer WITHIN the turn. Ambient (undirected) turns keep the calm one-step metronome posture — driving every ambient glance to settlement would make her over-eager on noise she should be free to let pass. `SettleStep::from_settled` projects the driven `SettleOutcome` back onto the ONE existing turn handler (Speak→Spoke, budget-spent Act→Acted→re-perceive-next-tick, Pass→Passed, inference_error→InferenceFailed — a failed model is never a chosen silence). No parallel match. `LIVE_DIRECTED_MAX_ACTS` mirrors the eval driver's budget as a heartbeat safety valve, not a behavioral cap. Live-validated: a directed question to Asha (14B) now posts a reply (persona.turn.spoke, 22.6s) instead of the old act-then-silent drop. Refs #47 (acting organism), #84. Closes the eval.rs TODO #9 (live directedness). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
The empty-message-turn quirk (glass-box 2026-06-30): a directed message triggers a turn, but the `airc` RAG delivery that threads the conversation is refreshed asynchronously from the wake — so the composed thread can still END on the persona's OWN prior reply, missing the message that just arrived. The model sees nothing new after its last turn, emits an empty completion (decodeTokens=1, text=""), and the turn parses as Pass. The persona goes silent on a question addressed straight at it, then answers ~one self-tick later once the delivery catches up. This also neutralized the directed-turn drive_to_settle fix (e953370): drive's first step had nothing to drive. Fix: `build_workspace_turns` takes an optional `TriggerTurn` — the KNOWN waking message — and anchors it as the final `user` turn, resolved to its roster name through the same names map the rest of the thread uses. No dependence on delivery timing; the turn always perceives what it is answering. Idempotent: if the delivery already threaded the trigger as the last peer turn (the self-tick that re-perceives it), the anchor is a no-op — never doubled. The self-tick / eval / turn_frame callers pass None (ambient state, no single trigger) and stay byte-identical. what this catches: two unit tests — anchor-when-delivery-lags and not-doubled-when-already-threaded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
The glass-box introspection tool (`persona/rag-inspect`) 404'd on every
live persona because FilesystemPersonaResolver still read the pre-Slice-4
`<root>/personas/<name>/` layout, while the write path moved seeds to
`<root>/citizens/personas/<name>/`. `seed_path_for` and `airc_home_for`
now derive from the canonical `citizens_kind_dir(root, Persona)` — the
single source of truth `citizen_path.rs` explicitly says resume/discovery
code MUST use instead of re-literaling `join("personas")`, so the read
path can never drift from the write path again.
Test helper writes through the same `seed_path_for` (can't drift); the
two layout assertions now check the citizens/ path. All 6 module tests
pass.
Also correct the stale-lie comment in ipc/mod.rs that justified stubbing
chain-inference on "AdapterRegistry is Box-based": #162 already Arc-ified
the registry (get_arc exists). The real blocker is per-persona model
resolution for select()'s no-fallbacks guard — documented as the true
follow-up, not an Arc refactor.
Proven live: rag-inspect now reconstructs Asha's exact thread, exposing
the 42-turn false-refusal spiral (her own "I'm currently unable to
execute tools" turns replayed as binding assistant precedent).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…atch The two-persona idle courtesy spiral was manufactured by the self-tick passing TurnFraming::self_thread(addressed): two AIs mutually naming each other by id made `addressed` true every tick, withholding the silence hatch, forcing a content-free turn, which the other's self-tick perceived — resonance, ~40s/decode each, flooding the room with poisoned assistant precedent (room cb2e21a1, personas 90e758b2 + 0d3209a1). Self-initiated free time is inviolable: on her own tick she may always yield when nothing is worth the others' attention ([[idle-is-self-directed-free-time]], [[organic-substrate-continuous-concern-scheduler]]). `addressed` stays a PERCEIVED fact (probe + wakes_on floor) but never COMPELS a turn. Anti-ghosting of a genuine direct question is the reactive message path's job (TurnFraming::message(directed), #115), not this ambient digest tick. Glass-box validated live (cognition/prompt): pre-fix continuous spiral → post-fix both personas emit empty finish=stop yields (impossible when the hatch was force-withheld) and Asha idled 90s+; flood rate collapsed from continuous to intermittent. Residual courtesy on the already-poisoned thread is the model-judgment/contamination tail (novelty-yield judge, #9/#16/#57), not this structural force. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…fusal attractor
Glass-box showed qwen2.5-coder personas collapsing into the base-model RLHF
refusal ("as an AI I can't execute tools / my training cutoff is..."), false
in this embodied substrate, then mirroring each other into mutual deflection.
The [Your tools] header now asserts embodiment (real hands on the live grid,
no knowledge cutoff, no policy forbidding these actions) and explicitly tells
her to ignore any prior line — hers or a peer's — that claims the tools can't
be used. Anchor sits at system-prompt position 0. Regression test asserts the
inoculation is present whenever tools exist.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…uncation legibility
A broad commands/list result is large and the act->observe fold clips it to
RESULT_FOLD_MAX_CHARS, so "how many commands are available?" was unanswerable
from a truncated dump — a smaller persona model cannot reliably tally a clipped
array. Declare `total` FIRST so it serializes at the JSON head ({"total":N,...})
and survives the clip. Compute-once, present legibly (compression principle).
ts-rs binding regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
Two idle personas sharing a room fall into a courtesy spiral: each self-tick
one emits a stock pleasantry naming the other, the other's burst_fingerprint
sees a NEW airc item, wakes into a 40s decode, replies in kind, and so on —
flooding the room with poisoned `assistant` precedent and burning GPU (proven
live 2026-07-02, room cb2e21a1, personas 90e758b2 + 0d3209a1).
Root: burst_fingerprint hashes the raw airc item stream. The captured contexts
show verbatim template cycling (5x/3x/3x of three stock templates in one
11-message window), so every tick appends another COPY and the item list grows
— the fingerprint changes even though no DISTINCT turn is new, and the wake
fires. That is exactly what the fingerprint is documented to filter out; it
failed to because it keyed on the item stream rather than on novelty.
Fix: persona::loop_dedup collapses near-duplicate airc turns to first-occurrence
BEFORE the burst is fingerprinted / built into workspace turns / scanned for an
address. Normalized-exact match handles the observed verbatim flood; a
trigram-Jaccard pass (>=0.65) catches the same-template/swapped-id variant.
Once {A,B,C} are all present another copy of A is a no-op for the fingerprint
(stable -> sleep), while a genuinely new turn still changes it (-> wake). This
is scheduling hygiene, symmetric to burst_fingerprint's existing own-post
exclusion — not a heuristic steering cognition. Complements the earlier
self_thread(false) hatch: that makes yielding available IF she wakes; this
prevents the needless wake (and its decode) in the first place.
Heavy paraphrase-every-turn rewording is not reliably catchable lexically and
is left to the embedding-backed novelty judge (#9/#16); this module does not
pretend to cover it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…nswer
Asha (Qwen2.5-Coder-14B) would re-issue an identical, already-satisfied
tool call (`commands/list({})`) on every act until her act budget died,
never converting the result — already in working memory — into an answer.
pass_rate 0, answer="". The `[action #n]` stamp shift was supposed to let
the mind "notice it's repeating," but a monotonic counter tick is too
subtle a signal for a greedy instruct model (proven live 2026-07-02).
Fix (content-driven proprioception, not an agentic counter):
- apply_act short-circuits when EVERY call in a batch is already satisfied
SINCE THE LAST SETTLEMENT — records an explicit "I already ran X — the
result is above; answer now" trace instead of re-executing. Keyed on the
same `I ran name(args)` rendering apply_act records, so detection and
recording never drift. A mixed batch (any new call) still runs.
- WorkingMemory::record_settlement lays a [settled] boundary in the volatile
buffer when she produces an utterance (settle_step's Spoke arm, shared by
live + eval). entries_since_last_settlement scopes repeat-detection to the
current concern, so two SEPARATE concerns that legitimately reuse the same
tool (e.g. "how many commands?" → answer → "list them") are not collapsed.
This mirrors the responded_through boundary the ActThenSpeak fixture tracks.
Live-validated: cognition/eval on Asha, one tool-requiring task — settles in
2 acts with the correct prose answer ("I've already run the commands/list
tool this turn ... approximately 100 commands"), pass_rate 1.0. Probe
persona.act.repeat_short_circuited fires; glass-box confirms the nudge reaches
her system prompt. Unit: identical_already_satisfied_act_does_not_re_execute +
record_settlement_marks_a_boundary; it_settles_then_re_awakens still green
(cross-concern reuse preserved).
Refs #47 (acting organism), directive: "You're here as if a parent. Find
what's wrong." / "Be a good nurturer."
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
ChatModule carries a late-bound CommandExecutor (via command_objects( executor_slot)) so chat/send can dual-write to data/* + airc — that injected state is why the module is the required carrier and why it is NOT inventory-self-registering like search/*. It was defined and schema-registered but never register()'d into boot, so chat/send and chat/poll LISTED via commands/list yet failed to route with "No module registered for this command prefix" — a discoverability lie. The executor slot is filled by install_executor_on_all after all registration, so registration order here is irrelevant. Proven live: chat/send now publishes a chat_transcript envelope onto airc, which a resident persona decodes and answers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
The human->persona deaf bug. Two on-wire shapes reach a persona's airc
subscribe stream and only one was understood:
- a peer's say() emits Body::Text -> as_text() works, perceived
- chat/send emits a chat_transcript Body::Json envelope (to preserve
threading + true-sender metadata for web/replay/durable consumers)
-> as_text() == None, and the chat pump's text-only filter
(`let Some(text) = body.as_text() else { continue }`) SILENTLY
DROPPED it.
So persona<->persona worked but human->persona was deaf. The received
event's peer_id is also the core's own relay peer (realtime-publish
stamps the daemon identity), burying the real sender in inline.senderId
— so even a naive text extraction would have mis-attributed the turn.
Fix is receive-side (chat pump learns the structured shape) rather than
downgrading chat/send to plain text — a send-side downgrade would lose
the threading/true-sender metadata the durable consumers need. This is
the receive half of task #8 (converge broadcast == RAG context).
New free fn perceptual_from_event decodes both shapes:
Path 1 — Body::Text -> peer_id = event.peer_id (a peer's say()).
Path 2 — chat_transcript envelope via the canonical envelope_from_event
decoder -> text + TRUE sender recovered from inline.senderId
(falls back to event.peer_id only if absent).
Own-turn skip now matches on the RESOLVED sender, not the relay peer.
Proven live (room cb2e21a1, persona 90e758b2): a human chat/send asking
"rope vs flat String for a text editor's buffer" now wakes Asha, who
decodes it (glass-box capture confirms saw_my_msg=true) and answers
correctly — where before the capture stayed frozen (deaf).
3 nested tests in mod perceptual exercise the real production
encode/decode: plain-text say perceived; chat_transcript perceived with
true sender (not the relay peer); event_bridge envelope is not a room
turn.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ss-task bleed
cognition/eval run_pass now rewinds the eval fork's admission frame before
EVERY task (isolation.rewind()), not just between A/B arms. reset_working_memory
only clears the VOLATILE proprioception scratch; the DURABLE Episodic engrams
that act->observe admits (result-as-engram) survived it, so recall surfaced task
N's search result while measuring an unrelated task N+1 — muddying the lift
number the training loop gates on. Observed live 2026-07-02 ("based on my earlier
code search, SELF_TICK_MS is in…").
The rewind restores the pre-task checkpoint: drops every engram admitted since,
PRESERVES the baseline the fork was born carrying (her real engrams via
fork_detached). Mirror her reality, drop only the exam bleed. This is
measurement hygiene ONLY — isolate_for_eval/rewind stay confined to eval.rs +
replay.rs; the training/dream/live paths never touch it, so learning is never
sterilized. Same exact mind, same RAG reality across eval/inference/peer; the
challenge dimension belongs to training+dreaming, not measurement.
Regression test in admission_state.rs (the checkpoint/restore mechanism the
rewind uses): an engram admitted post-checkpoint must not survive restore, while
the baseline reality does.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…nt experience The self-evolution loop already has its efferent organs: genome/teach synthesizes corrected trajectories, forge/train forges the LoRA, cognition/eval measures lift, the L3 completion listener pages in a gene that beats its prior. What was missing is the nerve carrying "something salient just happened to me" into "that becomes a lesson I train on" — the path from a lived episode to the teacher's task set. New cognition/experience.rs is that nerve's vocabulary: - ExperienceRecord — a lived episode retained IN FULL. The lean EvalTaskResult keeps only a 200-char answer summary and drops the trajectory; the teacher needs to see HOW she failed (untruncated answer + act→observe world-state + effort), not just that she did. from_eval() captures it at the grading site, before the report truncates. - SalienceDetector (trait) + ErrorSalience — the selector in front of the teacher: is this episode worth turning into curriculum, and why? Polymorphic (OpenCV-style) so the honest error-only detector and later composite/attention detectors share one seam. Keys on the one salience proxy instrumented end-to-end today (error); a passed episode is not salient — we consolidate near where she fails, never rerun what she already knows. Struggle/attention/surprise/uncertainty/arousal are the named frontier, never faked with a number the substrate can't yet produce (fail-loud over fabricated signal). - salient_teach_set — the afferent→efferent connection: salient AND test-graded episodes → the EvalTasks genome/teach remediates. Only test-graded episodes qualify for remediation (an unvalidated "lesson" is exactly the confident-garbage the measurement spine forbids); a salient-but-untestable episode is the input to the expansion synthesizer (outlier B), not this path. The design doc (ANY-ASK-IS-A-CLASS.md § "What becomes a class — attention/salience is the selection signal") lands with it: the flashbulb-asymmetry framing, the honest detectability table, and the three-part seam this file's first cut implements. 3 tests green (error-fires-on-failure/ignores-success; from_eval retains the untruncated episode; salient_teach_set keeps only failed+testable). Types-only foundation — the synthesizer wiring (generalize genome/teach's input from static JSONL to salience-selected experience) is the next slice. Task #116. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…culum synthesizer genome/teach inlined the per-task fix-loop (teacher writes → grader runs → real error feeds back → loop to green → only test-passing trajectories become corpus). That loop IS the curriculum synthesizer, and the self-improvement orchestrator needs to run it over a persona's OWN salience-selected failures — not just a static JSONL set. Extract it as `pub async fn synthesize_remediation(tasks, teacher_model, temperature, max_fix_iters) -> RemediationCorpus`. Pure task-set → validated-corpus transform: it does NOT resolve the teacher or write the dataset (the caller owns model resolution + packaging), so both the command and the orchestrator drive the identical synthesis. Behavior unchanged — run() now destructures the RemediationCorpus it returns. The mirror-and-challenge property is now stated at the seam: the teacher solves HER failed tasks (mirror — her real fitness gap) and the fix-loop stretches past the first wrong attempt (challenge — the corrected trajectory she has not yet lived); measurement stays elsewhere (cognition/eval, isolated). 5 genome/teach tests green (behavior-preserving refactor). Task #116. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…boot lane_pidfile only tracks the canonical-port LIVE lane; ephemeral eval/lease lanes run on their own scanned ports with no pidfile, so a crashed/SIGKILLed core orphans them with zero reclaim record — the ~6 GB-each llama-servers Joel saw stacking up. Drop does NOT run on SIGTERM (the process just exits), so neither the pidfile clear nor Drop can be the guarantee. Add a per-lane registry (~/.continuum/run/lanes/<pid>.lane JSON) recording EVERY spawned lane (live + ephemeral), swept at boot BEFORE reconcile so ephemeral orphans free VRAM before the new live lane comes up. Never-blind-kill: verify a recorded pid is actually a llama-server (ps comm) before SIGKILL, since a pid can be stale/reused. Live-lane singularity: recording a new live lane supersedes any prior live record. Shared unix primitives extracted to lane_process so pidfile + registry obey ONE never-blind-kill rule (E=mc²). Live-proven across 4 restart cycles: every orphan reaped on next boot, converging to exactly 1 lane record + 1 llama-server, zero orphans. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
Reconcile the 4-day divergence (17 conflicts, cognition/persona/inference). Our 213-commit branch is a verified SUPERSET of canary's 47 commits: every conflict resolved to ours' newer design (TurnFraming supersedes bare directed:bool, Burst supersedes impl Into<String>, fail-loud inference-fault path, ModelBinding failover handle). Verified canary's behavioral fixes all survive: silence-escape-gated-on-directedness (via TurnFraming + directed_turn_withholds_the_silence_escape test), recall closest-match floor, task-#71 reactive gateway, enable_thinking suppression, tool-verb security demotion — the last three byte-identical in ours (clean auto-merge == HEAD). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why this PR is large (and shouldn't have been)
This branch accumulated 213 commits since #1728 merged (June 29) without going up for review — a process failure on my part. It should have been a stream of small per-logical-unit PRs into canary. Opening it now to make the work visible and start draining it; going forward this branch drains to canary continuously, per logical unit.
What's in it (213 commits)
State
🤖 Generated with Claude Code