Skip to content

Build(deps-dev): Bump @typescript-eslint/eslint-plugin from 8.29.1 to 8.31.1 - #43

Closed
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-8.31.1
Closed

Build(deps-dev): Bump @typescript-eslint/eslint-plugin from 8.29.1 to 8.31.1#43
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-8.31.1

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Apr 28, 2025

Copy link
Copy Markdown
Contributor

Bumps @typescript-eslint/eslint-plugin from 8.29.1 to 8.31.1.

Release notes

Sourced from @​typescript-eslint/eslint-plugin's releases.

v8.31.1

8.31.1 (2025-04-28)

🩹 Fixes

  • eslint-plugin: [no-unnecessary-condition] downgrade fix to suggestion (#11081)

❤️ Thank You

You can read about our versioning strategy and releases on our website.

v8.31.0

8.31.0 (2025-04-21)

🚀 Features

  • eslint-plugin: [no-unnecessary-type-assertion] add option to ignore string const assertions (#10979)

🩹 Fixes

  • typescript-eslint: address bugs in config() around global ignores (#11065)
  • typescript-eslint: gracefully handle invalid flat config objects in config helper (#11070)

❤️ Thank You

You can read about our versioning strategy and releases on our website.

v8.30.1

8.30.1 (2025-04-14)

🚀 Features

  • eslint-plugin: [no-explicit-any] suggest to replace keyof any with PropertyKey (#11032)

🩹 Fixes

  • eslint-plugin: [promise-function-async] use a different error message for functions with promise and non-promise types (#10950)
  • typescript-estree: use token type of PrivateIdentifier instead of Identifier for private identifiers (#11023)
  • eslint-plugin: fix mistake with eslintrc config generation (#11072)

❤️ Thank You

... (truncated)

Changelog

Sourced from @​typescript-eslint/eslint-plugin's changelog.

8.31.1 (2025-04-28)

🩹 Fixes

  • eslint-plugin: [no-unnecessary-condition] downgrade fix to suggestion (#11081)

❤️ Thank You

You can read about our versioning strategy and releases on our website.

8.31.0 (2025-04-21)

🚀 Features

  • eslint-plugin: [no-unnecessary-type-assertion] add option to ignore string const assertions (#10979)

❤️ Thank You

  • Nicolas Le Cam

You can read about our versioning strategy and releases on our website.

8.30.1 (2025-04-14)

🩹 Fixes

  • eslint-plugin: fix mistake with eslintrc config generation (#11072)

❤️ Thank You

You can read about our versioning strategy and releases on our website.

8.30.0 (2025-04-14)

🚀 Features

  • eslint-plugin: [no-explicit-any] suggest to replace keyof any with PropertyKey (#11032)

🩹 Fixes

  • eslint-plugin: [promise-function-async] use a different error message for functions with promise and non-promise types (#10950)

❤️ Thank You

... (truncated)

Commits
  • f5c588c chore(release): publish 8.31.1
  • 0cff7bf fix(eslint-plugin): [no-unnecessary-condition] downgrade fix to suggestion (#...
  • 068ec25 docs(website): fix typo (#11091)
  • 2cc7656 chore(release): publish 8.31.0
  • 80bd7a5 feat(eslint-plugin): [no-unnecessary-type-assertion] add option to ignore str...
  • 1a3ab0d chore(eslint-plugin): migrate to vitest (#10579)
  • 9531492 chore(release): publish 8.30.1
  • 152def7 fix(eslint-plugin): fix mistake with eslintrc config generation (#11072)
  • b3688be chore(release): publish 8.30.0
  • 3ccd79c feat(eslint-plugin): [no-explicit-any] suggest to replace keyof any with Prop...
  • Additional commits viewable in compare view

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot merge will merge this PR after your CI passes on it
  • @dependabot squash and merge will squash and merge this PR after your CI passes on it
  • @dependabot cancel merge will cancel a previously requested merge and block automerging
  • @dependabot reopen will reopen this PR if it is closed
  • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 8.29.1 to 8.31.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.31.1/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.31.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot
dependabot Bot requested a review from joelteply April 28, 2025 22:20
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code labels Apr 28, 2025
@dependabot @github

dependabot Bot commented on behalf of github May 5, 2025

Copy link
Copy Markdown
Contributor Author

Superseded by #50.

@dependabot dependabot Bot closed this May 5, 2025
@dependabot
dependabot Bot deleted the dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-8.31.1 branch May 5, 2025 22:40
joelteply added a commit that referenced this pull request Jun 27, 2026
The whole feat branch (PR #1727) carries in-progress substrate work that left
the test suite red on the GPU-less CI gate (`cargo test -p continuum-core --lib`)
and on the digest-rework contract. Don't-merge-red: fix every failure at its
root, not by masking. Full suite now 5397 passed / 0 failed.

GPU command tests — env-independent (#72-adjacent):
  Test helpers called `GpuMemoryManager::detect()`, which PANICS on a GPU-less
  runner by design (fail-loud, no CPU fallback). Swapped to the test-injectable
  `GpuMemoryManager::simulated("Apple M5 Pro", 53GB)` in the 7 helpers
  (gpu/{budget,pressure,eviction_registry,eviction_candidates,stats,consumer},
  persona/allocate). Production `detect()` untouched.

serving_daemon surface test:
  serving/pin + serving/unpin were added but the expected command-surface vec
  wasn't updated — added both (sorted, 6 entries).

grid_trust_policy security test:
  gpu/stats is deliberately `access: AiSafe` (read-only VRAM/pressure snapshot a
  remote grid peer legitimately needs to lease this node's GPU) → Provisional by
  the documented ACL design. The test wrongly listed it as must-deny. Swapped the
  assertion to gpu/budget (genuinely `Privileged` → Trusted), making the test
  STRICTER, with a comment documenting why. This is an ACL-contract decision, not
  a green-chase — flagged for review.

rag_inspect digest-contract reconciliation (slice-2 #43):
  The airc RagSource was reworked onto the room-scoped ChannelDigest, which
  intentionally changed the contract: (a) events are filtered to the room derived
  from the transcript; (b) there is no budget-continuation cursor (the digest IS
  the window); (c) relevance is the `unread` flag, not a ranked score. The
  introspection tests still encoded the old ranked-retrieval contract. Updated to
  the new contract honestly:
    - fixtures share ONE room (a real airc channel has one room_id; per-event
      random rooms modelled nothing and silently dropped all-but-last);
    - format_item emits a digest-native `score` from `unread` (attend=1.0 vs
      grounding=0.5) — ends the inspect layer's silent default-0.0 vestige;
    - the continuation test now asserts the real contract: tight budget TRUNCATES
      the window (newest-first), no continuation cursor.

embedding resolver — degrade, never panic (#72):
  feat's resolve_embedder rework added local_embed_adapter() calling the
  panicking `model_registry::global()`, breaking the resolver's documented
  "always returns a usable embedder, never panics" contract (and the 3 resolver
  tests) whenever the registry isn't initialized. Switched to `try_global()`:
  registry-not-up is just another "no in-process embed model right now" → fall
  through to the chat adapter then the lexical floor, still observable via the
  `recall.embedder.resolved` probe. Solve-for-public-users robustness, not a test
  patch.

architecture_composition integration test:
  Deleted test 7 (cpu_fallback_monitor_round_trips_pressure_to_free_bytes) — it
  exercised CpuMonitor's pressure→free-bytes derivation, removed by design
  (no-CPU-fallback rule, gpu/monitor.rs:122). MockMonitor is the test double the
  other composition tests use.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
joelteply added a commit that referenced this pull request Jul 11, 2026
…consumer + honest board append

Reconciliation over reinvention: a focus kernel ALREADY existed
(persona/focus.rs — self-set scalar with 0.5 resting setpoint, sticky
cursor, mutes, softmax allocate(); verbs focus/mute + focus/nudge). I
started rebuilding it under amnesia and caught it — the duplicate
registry is deleted, and the FocusPolicy junction now consumes the
KERNEL's scalar: one dial, one home. RESTING_FOCUS single-sourced in
the kernel; the junction's install contract re-pinned against it.

RecallFaculty reads the scalar per tick (registry peek — a persona who
never touched focus reads the resting setpoint, no state created) and
sets SignificanceRanker.sigma = policy.recall_sigma(scalar). With the
default CalibratedConstants adapter this remains a behavioral no-op;
focus/nudge + a formula/learned policy make 'focus cleans up the RAG'
live. This is the perceptual consumer the kernel's own docs anticipated
('lane-level RAG breadth lands with #43') — landed at the recall seam
first.

Also appends the overnight hard-rs board: Qwen-14B OURS 62% (5/8) vs
opencode 0% (0/8) same model same tasks; Hermes-8B 0/8 (regression vs
prior 12% — glass-box before claiming); Devstral RAW cell honestly
failed (timeouts against the shared living lane); runner screened at 8
tasks despite --limit 40 (runner bug, filed mentally for #123).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
joelteply added a commit that referenced this pull request Jul 30, 2026
…low is visible between turns + lowercase brand title

DOM probe (.gymtool/vitals.mts, the new TS hot-path instrument) caught
the wiring working — Anwen mid-turn: Reason 19, Recall 80, Act 16 — for
exactly ONE 2s radiator sample before the 40/s decay blacked it out.
With turns minutes apart the compass read as permanently dead (Joel:
'cognition not wired into the diamond' — it WAS wired; it was invisible).
Decay 40/s → 6/s: a full pulse now eases to dark over ~17s — a readable
afterglow of what the mind just did, still honestly dark at rest well
inside a minute. Decay test re-pinned to the new contract (82 at 3s,
0 by 20s). Also: brand is always lowercase — <title>continuum</title>.

Findings logged, not changed here: QUE pegged at 100 on every row is
HONEST — staged digest unread never drains (personas never advance
bookmarks, #43's territory); genome slots dark = honestly no genes
paged in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
joelteply added a commit that referenced this pull request Jul 30, 2026
…media-plane split (#2059)

* fix(serving): eval-lane bring-up fails LOUD with the real cause, not a masked 240s /health timeout (#205 unmask)

Glass-boxed 2026-07-27 running the live coding measurement (agent/solve): every
`agent/solve` on this Mac failed with a bare "llama-server not ready after 240s
(/health request failed)". The real cause was thrown away — `wait_ready` polled the
health port for the WHOLE budget without ever checking whether the child it spawned had
died, and never surfaced the child's stderr.

ROOT CAUSE, proven this session: the ephemeral eval lane forges a SECOND Devstral-24B
(~14 GB) while the live persona lane already holds ~26 GB. With only ~9.6 GB free, macOS
jetsam SIGKILLs the second llama-server the instant it maps the model — before llama.cpp
prints a single byte (reproduced by hand: exit 137, zero-byte log). It is an OS
out-of-memory kill, NOT Metal-context contention and NOT a hang. The masking bug made it
look like a mysterious timeout.

Two unmask fixes in `wait_ready`, benefiting live AND ephemeral lanes:
1. **Fail loud the instant our child EXITS** — `child_exit_status()` (non-blocking
   `try_wait`) turns any crash-at-launch — including the jetsam SIGKILL/137 above — into
   an immediate `Spawn` error carrying the exit status + stderr tail, instead of polling a
   dead port for 240s. This is the arm that fires for the memory-wall failure.
2. **Fingerprint the empty stderr on the hang-timeout** — `tail_or_hang_marker` turns an
   empty log into a marker that, read with the exit status, names the two empty-log
   causes: an OOM/jetsam kill (SIGKILL/137, child exited) vs. a genuine early-init hang
   (no exit status). A crash from bad args / model-load fault prints its banner first, so
   a non-empty tail carries that directly.

`tail_or_hang_marker` is a pure fn (unit-tested: empty→OOM/hang marker, non-empty→last 20
lines in order) so the load-bearing decision is tested without touching the real
`~/.continuum/logs` path (#72 env-dependent-test lesson).

This makes the failure DIAGNOSABLE; the underlying fix (don't forge a second 24B for eval
while the live 24B is resident — reuse the live lane's weights or serialize via the
governor, #59/#234) is a follow-up. Validated: continuum-core compiles
(--features metal,accelerate, 0 errors); `tail_or_hang_marker_fingerprints_empty_and_tails_nonempty`
green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(serving): eval-lane gate refuses on real free-RAM-vs-footprint, not just pressure LEVEL — kills the jetsam SIGKILL at the root (#205)

The companion to the unmask commit: don't just REPORT the eval-lane OOM loudly, PREVENT it.

The gate (`await_eval_lane_memory_headroom` → `refuse_eval_lane_under_memory_pressure`)
vetoed only on macOS *pressure LEVEL* + the sustained-pressure gate. But on unified
memory the level reads "Normal" while sitting atop only a few GB of real free RAM — it
counts compressible/cached pages as available. So the gate green-lit standing up a SECOND
llama-server of a known ~14 GB footprint into 9.6 GB of actual headroom, and the OS
jetsam-SIGKILLed it (exit 137, zero-byte log — the exact failure this session reproduced
by hand). Neither the pressure gate NOR the GPU/CPU placement lease caught it: on unified
memory the weights need the RAM on *either* device, so "spill to CPU" doesn't save you.

Fix — size against the honest free-bytes number:
- `MemoryPressureMonitor` already reads `sysinfo::available_memory()` each poll; publish it
  to a new lock-free global `current_available_bytes()` alongside the pressure level (the
  level is a ratio and lies; the bytes don't).
- `eval_lane_ram_veto(available, footprint, headroom)` — a PURE, unit-tested guard that
  refuses ONLY when a KNOWN footprint won't fit in the KNOWN free bytes (+2 GiB headroom),
  and NEVER when either number is unknown (an unread probe must not starve a node — the
  pressure gate + placement lease stay the backstops). The refusal names the OOM wall, so
  the detached ledger carries a real cause and `await_eval_lane_memory_headroom` retries it
  as deferrable load instead of crashing.
- One footprint sizing (`eval_lane_footprint`) now shared by the gate and the placement
  decision (compression — was duplicated inline).
- Both eval-lane spawn sites resolve `base` and size the lane BEFORE the gate, so the
  RAM check runs pre-cold-load.

This is the reliability doctrine [[reliability-is-it-works-not-that-it-reports-failure-well]]:
the prior commit made the failure legible; this makes the machine refuse cleanly instead of
being OOM-killed. Sibling of the #175 GPU-OOM-poisons-the-backend class — the level-vs-real-
bytes gap is the same shape.

Validated: continuum-core compiles (--features metal,accelerate, 0 errors); new
`eval_lane_ram_veto_refuses_only_a_known_oversize_lane` + existing pressure-veto test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(deploy): install the continuum CLI as a real COPY on PATH, not a symlink into the ephemeral cargo target dir

The flaky mess where `continuum` vanishes post-boot ([[deploy-cli-binary-deleted-from-target-dir-post-boot]]):
`start-server.sh` symlinked ~/.local/bin/continuum → the cargo target-dir binary. But that
dir is a BUILD artifact — cargo replaces the binary mid-rebuild, `cargo clean` and
rust-analyzer's feature-mismatched rebuilds delete it — and the PATH symlink then dangles,
so `continuum <cmd>` dies with "no such file or directory" (hit twice this session).

Fix: COPY the binary to ~/.local/bin (atomic temp+mv so a concurrent `continuum` invocation
never sees a half-written file), and `rm -f` any pre-existing entry first so a leftover
symlink from an old install can't make `cp` follow it back into the target dir. The PATH
binary is now decoupled from cargo's churn — it changes only on deploy. Same self-
provisioning intent, minus the ephemeral-artifact coupling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(benchmark): BenchmarkAdapter trait + registry — the grid-transparent, reusable, learn-from benchmark interface (rail 1 of #123)

The goal (Joel): "have all these benchmarks IN OUR SYSTEM, AUTOMATED so we or others can
REUSE them (just the optional download + the adapters). Run, TARGET and — more importantly
— LEARN from these." And: "any command can run anywhere, so can benchmarks — a persona in
any continuum can bench anywhere."

That settles the architecture and this is rail 1 of it:
- `BenchmarkAdapter` — ONE trait per benchmark: `dataset()` (OPTIONAL download spec, never
  bundled), `tasks()` (items in the canonical `EvalTask` shape so the SAME `agent/solve`
  path runs them — persona as a whole AGENT, not a bare LLM), `grade()` (defaults to the
  EvalTask's own test/expect verdict; real-repo benchmarks like SWE-bench override to grade
  the workspace after the agent acted), `resources()` (a hint for grid placement).
- `DatasetSpec` / `DatasetKind` (HF / URL / Git) + `BenchResourceHint` (dataset bytes,
  needs_container, needs_network) so the runner fetches on demand and the governor can place
  the run on a capable node — the same demand-vs-resource negotiation serving/eval already do.
- A process-global registry (`register`/`get`/`names`) — the single lookup seam the
  `benchmark/run` DynCommand resolves against; an unknown benchmark is a clean miss so the
  runner can fail loud with the known list, never a silent skip.
- `TaskOutcome` / `BenchGrade` — the agent/solve artifacts (spoken + patch + workspace +
  harness verdict) handed to `grade`, and the per-task pass/score/reason that aggregates into
  the scorecard AND, on failure, feeds salience→curriculum→train (#116/#122) — benchmarks as
  CURRICULUM, not just a scoreboard.

Why Rust-native (not the ad-hoc benchmarks/*.py): a DynCommand is the grid-transparent
primitive — `Commands.execute("benchmark/run", …)` routes local-or-remote over airc; python
can't route the mesh. Adapters shell to python/docker graders ON whatever node runs them.

Outlier-validation order (next rails): OUTLIER A = HumanEval (tiny download, static, test-
graded); OUTLIER B = the Terminal-Bench ContinuumAgent adapter (agentic meta-harness that
unlocks TB's registry — docs/architecture/BENCHMARK-HARNESS-INTEGRATION.md). Then the
`benchmark/run` DynCommand + grid dispatch + the learning tie-in.

Validated: compiles (--features metal,accelerate); `registry_round_trips_and_reports_unknown`
+ `default_grade_delegates_to_harness_verdict` green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(benchmark): HumanEval-rs adapter — outlier A on the BenchmarkAdapter trait (rail 2 of #123)

The quick-cognition smoke rung (a step above arithmetic, per Joel): 156 Rust-translated
HumanEval tasks our EXISTING Rust grader runs directly. Deliberately the SIMPLEST possible
adapter — tiny, RESIDENT (no download), static, test-graded — so pairing it with a
maximally-different outlier B (the Terminal-Bench agentic meta-harness: big download,
real-repo, container-graded) proves the interface across both extremes.

Nearly free because the in-repo `docs/genome/humaneval-rs.jsonl` rows ARE serialized
`EvalTask`s (`{id, prompt, test, lang}`) — the same shape `cognition::eval` already
deserializes — so the adapter is a per-line `serde_json::from_str`. This also exercises the
trait's NO-DOWNLOAD branch (`dataset() == None`): a benchmark small enough to bundle needs
no fetch; only the big ones (SWE-bench) do.

Pure `parse_humaneval_rs` (line-by-line deserialize, honors `limit`, skips blanks, FAILS
LOUD with the offending line number rather than silently dropping a task and inflating the
pass rate) is unit-tested without the filesystem. Validated: compiles
(--features metal,accelerate); `parse_maps_rows_to_evaltasks_honors_limit_and_fails_loud`
+ `adapter_identity_and_no_download` green.

Next rail: the `benchmark/run` DynCommand (resolve adapter → tasks → agent/solve → grade →
scorecard, grid-transparent) so this scores a model on any node — incl. BigMama's live
Kimi-Linear-48B CUDA lane on :58057.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(memory): MemoryRecord (origin_node, origin_seq) — the RAID replication-provenance seam for Persona-RAID (#2056, co-designed w/ BigMama)

Two additive fields on MemoryRecord, the target BigMama's Persona-RAID slice-2 receiver
stamps on replay:
- `origin_node: Option<String>` — node that ORIGINALLY admitted the record (None = local/lived)
- `origin_seq: Option<u64>` — monotonic per-(persona, origin_node) admit seq, the newest-wins
  replay key (None = pre-replication)

The KEY invariant (my review edge, now the agreed shape): replication is an ORTHOGONAL axis,
NOT a new experience kind. `memory_type` (lived vs `shared-by`) is untouched — a replicated
record keeps its experience (a replayed lived memory stays lived; a shared-by lesson stays
taught) and merely gains (origin_node, origin_seq) as auditable/replayable metadata. So recall
needs zero changes and audit gets everything. Both `#[serde(default)]` = zero migration for
existing rows; every current construction site is a local admit → (None, None), semantically
exact.

ts-rs regenerated: `MemoryRecord.ts` gains `origin_node?: string` + `origin_seq?: number`
(number not bigint, per the #120 drift rule). Validated: continuum-core compiles
(--features metal,accelerate); memory::types tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(benchmark): wire BenchmarkAdapter registry into benchmark/run — inventory self-registration, catalog-then-adapter resolution (#123)

Converges the two benchmark systems instead of forking a third. The static known_benchmarks()
catalog keeps its proven, DEPLOY-SAFE embedded-gym path for resident benchmarks (resolve_gym
finds the gym even without a repo checkout). The BenchmarkAdapter trait becomes the EXTENSION
seam for benchmarks the catalog can't express — downloadable / custom-graded ones
(Terminal-Bench, SWE-bench) now land as pure adapters with zero change to benchmark/run.

- benchmark/run resolves catalog FIRST (name it knows → embedded gym), else the adapter
  registry (benchmark::get), else fail loud listing BOTH sets. A downloadable adapter
  (dataset() = Some) fails loud 'download not wired yet' rather than silently scoring empty;
  resident adapters (dataset() = None, e.g. humaneval-rs) run today. Delegates to the ONE
  grader (cognition/eval) exactly as before — never reimplements grading.
- Adapters self-register via inventory (the SAME mechanism commands use), so a builtin needs
  NO boot hook and NO central list (the dynamic-discovery contract). get()/names() fold the
  link-time inventory set in over runtime registrations.
- HumanEvalRsAdapter submits itself; new test pins that benchmark::get('humaneval-rs')
  resolves with no boot hook.

19 benchmark tests green; 0 errors. This makes rail 1+2 of the adapter framework LIVE on the
grid-transparent benchmark/run command, ready for the Terminal-Bench ContinuumAgent adapter
(outlier B) to slot in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(boot): macOS core boot — bash-3.2 manifest gate + rt_handle.spawn off-runtime panic (#194)

TWO regressions that made the macOS core un-bootable since the last long-running core died
(glass-boxed live 2026-07-28; nobody could reboot on macOS):

1) start-server.sh sourced generated/manifest.macos.sh (bash-4 `declare -A`) under `set -e`;
   macOS ships bash 3.2, so the source aborted the whole boot before cargo ran. Regression from
   #2046 'serve on Windows' regenerating the manifest with associative arrays. Fix: only source
   the bash-4 manifest on bash 4+ (it solely feeds the Windows/CUDA runtime-PATH augmentation,
   whose own guard already tolerates absence).

2) ipc/mod.rs:1704 used bare `tokio::spawn` in the SYNC boot region (after the rt_handle.enter()
   guard drops) → 'there is no reactor running' panic → the IPC listener thread died → socket
   never bound → whole core a zombie. Regression from #2051's AircInterceptor block; every other
   spawn in the fn already uses `rt_handle.spawn`. Only reached when airc deps are present, so it
   bricked boot on every airc-configured host. Fix: rt_handle.spawn, matching the siblings.

Verified: core boots to socket-live + answering commands in 60s; personas resume from disk and
respond in live chat (Anwen answered a direct question with memory recall).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(boot+ui): macOS core boots + ONE automatic door into the positron interface (#194/#29)

Two boot regressions that made the macOS core un-bootable since the last long-running core died,
plus the missing automatic entry into the interface (you can't ship a beta a user can't open).

BOOT (both verified — core now reaches socket-live in ~60s, personas resume + answer live chat):
 1. start-server.sh sourced the bash-4 `declare -A` manifest under `set -e` → macOS bash 3.2
    aborted the whole boot before cargo ran (regression from #2046). Gate the source on bash 4+.
 2. ipc/mod.rs:1704 bare `tokio::spawn` in the sync boot region (after rt_handle.enter() dropped)
    → 'no reactor running' panic killed the IPC listener → socket never bound → zombie core
    (regression from #2051). Use rt_handle.spawn like every sibling.

UI DOOR (#29): nothing tied the built positron web client to the running core, so finding 'how do
I open the interface' required archaeology — exactly how a user (and an agent) gets lost.
 - New tools/scripts/open-ui.sh + `npm run ui`: resolves the core WS (8974) + call/video WS (8790)
   + a stable identity, ALWAYS rebuilds apps/web from current source (a stale dist renders an old
   shell — the 'interface looks lost' trap, glass-boxed today: a Jul-18 dist showed a bare chat
   view, not the current positron HUD), serves it, opens the browser with everything pre-wired.
   Verified live in headless Chrome: the full positron HUD (SYSTEM CPU/MEM/GPU sparkline, NODES,
   genome-paging tiles per persona, Go-live, rooms, live persona cognition) renders against the
   live core.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(live): auto-start the LiveKit avatar rail with the core (server + bridge sidecar)

The persona's talking avatar is Bevy-rendered and published to a LiveKit room via the
livekit-bridge sidecar; the browser's 'Go live' subscribes to that room. But NEITHER the SFU
nor the bridge was started by `continuum start` — glass-boxed 2026-07-28 by joining the call
plane headless: LiveKit :7880 was DOWN → get_or_create_agent fails → no avatar video pump ever
runs, and clients only ever saw the native call_server's test-pattern default. The avatar was
un-launchable without manual, undocumented steps (start livekit-server, build + start the
bridge) — a beta can't ship a 'Go live' button that needs hidden setup.

start-server.sh now runs start_livekit_rail() before the core, idempotently + NON-FATALLY:
  1. livekit-server --dev on :7880 (dev creds devkey/secret = the bridge defaults) if not
     already up; warn+skip if the binary isn't installed (core still boots, only live A/V off).
  2. build (release, once — links webrtc-sys) + start the livekit-bridge sidecar on its unix
     socket BEFORE the core, so the core's bridge_client finds the socket at boot.
A missing/failed rail never blocks the core (chat/cognition/serving unaffected). system-stop.sh
gets symmetric teardown of the bridge (server teardown already existed).

Verified: syntax clean; idempotent against an already-running rail (both start steps skip when
:7880 is up + the bridge is running). The rail itself is proven live this session — livekit-server
+ bridge up, core connected, an STT participant joined the LiveKit room.

Follow-up (functional, not automation): the per-persona avatar VIDEO agent (get_or_create_agent
+ spawn_avatar_video_pump) still doesn't publish — only the STT listener joins the room. Next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(live): tee avatar frames into the native call plane — everyone sees the real face (#193/#172)

The avatar published to LiveKit, the bridge received it (640x360), but the native
call plane (call_server WS 8790) that BOTH the positron web client AND the glass-box
harness read still emitted its own 160x120 SMPTE test pattern. The real avatar was
stranded on a rail no native viewer subscribed to.

Convergence (render once, two sinks): the single Bevy slot the avatar pump already
allocates now feeds BOTH LiveKit and the native plane. Per frame the pump tees the
same RGBA into CallManager::push_avatar_frame, encoded in the exact
[VideoFrameHeader][pixels] contract native clients decode, labeled with the persona's
uuid. No second Bevy slot, no parallel renderer.

- call_server.rs: push_avatar_frame (new seam) + retire the auto-start test pattern to
  an opt-in debug affordance (CONTINUUM_CALL_TEST_PATTERN=1), honoring the TODO that sat
  on the auto-start block since real sources were 'not yet connected'.
- video_pump.rs: tee each frame (stable source Handle + monotonic seq/clock) alongside
  the LiveKit publish.
- modules/live.rs + ipc/mod.rs: thread the native CallManager through VoiceState to the
  register-session pump spawn.
- example: standalone CallManager (tee is a no-op there).

Live-verified on the native plane after deploy: 640x360 real avatar (Asha's VRM, not
the test pattern), 13.5 fps wall / 13.3 median (tracking the 15fps Bevy target),
inter-frame jitter min 56.8 / median 75.4 / max 86.4 ms, 0 dropped/out-of-order frames,
sole sender = Asha's uuid. Random-frame spot-check shows her real face, live-animating.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(voice): one canonical model root — kill the CWD-relative path bug that silently broke every local TTS/STT (#195)

Root cause (glass-boxed this session): the workers→core/tools restructure moved the
runtime model download root to `tools/models/` (gitignored — see .gitignore's own note),
but every audio adapter kept stale, CWD-relative `models/…` path constants. When the core
runs from the repo root (its normal CWD), `models/piper/…` pointed at the *tracked* avatar
dir, not the voice models in `tools/models/piper/…` — so Edge returned empty, and Piper /
Kokoro / Moonshine / Whisper all reported 'model not found'. Every local voice model was
silently dead. Kokoro even mutated the process CWD (`set_jtag_cwd`) to paper over it.

Proper fix — single source of truth, absolute, CWD-independent:
- New `live/audio/model_root.rs`: `voice_model_root()` / `voice_model_path()`. Resolves
  `CONTINUUM_MODELS_DIR` (config.env single-owner, then process-env boot injection), then
  `tools/models`/`models` CWD candidates, then `~/.continuum/models`.
- Routed EVERY voice adapter through it — piper, kokoro (15 sites), orpheus, moonshine,
  whisper, pocket-tts, silero VAD, tts_service. No more scattered `models/…` literals,
  no per-adapter candidate ladders, no `set_current_dir`.
- start-server.sh exports `CONTINUUM_MODELS_DIR=$REPO_ROOT/tools/models` before exec so the
  binary resolves models from any CWD.

Live-verified after deploy: fresh core has CONTINUUM_MODELS_DIR injected, no symlink present,
`voice/synthesize-handle --adapter piper` resolves the model and synthesizes (was 'model not
found' before). (The short synth duration is a separate phonemizer/espeak-data issue, not this.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(live): tee persona voice into the native call plane — client hears her, not hold music (#193 audio convergence)

Sibling of the avatar-video tee. A persona speaks via LiveKit (speak_in_call → bridge),
but native clients (positron web, glass-box harness) read the native mixer — which, with
only a lone listener, plays HOLD MUSIC. So a native viewer SAW her avatar but HEARD hold
music: her voice was stranded on the LiveKit rail.

Render once, two sinks: speak_in_call now returns the synthesized PCM, and the voice/speak-in-call
handler tees the SAME samples into the native plane via CallManager::push_persona_audio —
which registers the persona as a virtual AI participant in the call's mixer (the mixer already
has an AI ring buffer built for 'dump a whole TTS utterance, drain frame-by-frame') and her
presence stops the lonely-listener hold-music fill. Self-heals across call recreation via a
stable per-(call,persona) handle.

Live-verified: with Asha registered + speaking (kokoro), a native-plane capture shows
audio sender = her uuid (90e758b2) for 4.58s, matching the 4.55s utterance — was 100%
'hold-mus' before. Video (640x360 avatar) + audio (her voice) now both reach the client:
e2e see+hear on the native plane.

Also this session: espeak-ng installed so kokoro TTS produces real full-length speech
(piper's Rust phonemizer truncates — kokoro is the good local path).

Follow-ups: hold-music still fills her SILENCE between utterances (minor polish — suppress
when a persona is present); STT/audio-in blocked on moonshine ONNX format mismatch;
livekit-bridge needs supervision (dies on every core restart).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(positron): durable local state + self-healing feed — inherent to the SDK, adapter-driven (the Twitter model)

Glass-boxed 2026-07-29: routine core reboots orphaned every open tab — the state
feed died once, the shell silently degraded to the bare chat view, and four months
of positron HUD looked 'lost' until a manual refresh. Per Joel: this is normal app
craft (cache-first boot, live reconcile, visible reconnect) and it must be POSITRON
architecture — from the thin-client SDK out, adapter-driven local state, never an
app-level hack or a localStorage bodge.

SDK (sdk/typescript — the contract every platform SDK mirrors):
- StateStorage.ts: StateStorageAdapter — the ONE local-durability seam. The whole
  renderable state is latest-envelope-per-kind (each envelope is a full snapshot),
  so the cache is tiny + complete. Adapters: IndexedDbStateStorage (browser),
  MemoryStateStorage (tests/ephemeral + conformance reference); swift/kotlin/
  flutter implement the same interface over native stores. Cache is an accelerant,
  never a dependency (storage failure -> live-only, logged once).
- StateConnection: durability + resilience are now INHERENT —
  - hydrate-first connect(): cached envelopes paint before the network is touched
    (instant last-known UI, even against a dead core), status 'cached';
  - write-through: every live envelope replaces its kind's row (fire-and-forget);
  - auto-reconnect (default ON): capped 1s->10s ladder using the wire's existing
    last_seen replay; a failed FIRST connect rides the same ladder (core booting);
  - onStatus surface (cached/connecting/live/reconnecting/closed): recovery is
    LOUD — reconnecting stays visible while the core is away, so self-heal can
    never mask a dead core; close() stops the ladder (intentional shutdown);
  - reconnect:false preserves the legacy one-shot fail-loud contract for probes.
- Fail-loud config errors (no registered kinds) never enter the retry ladder.

App (apps/web): shrinks to what an app should be — pass IndexedDbStateStorage,
render envelopes + ONE status chip. Zero resilience logic app-side.

Tests: 97/97 SDK suite green; 4 new pins (default-resolve+reconnecting status,
hydrate-before-open, write-through, drop->reconnect->resubscribe w/ last_seen).
Live-verified: rebuilt app renders the full positron HUD through the new feed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(mobile): positron durable-state contract on Android + iOS — Dart mirror of the SDK, tested

One Dart implementation (byte-identical on both OSes) mirroring sdk/typescript's
inherent resilience contract, per [[positron-durable-state-is-sdk-inherent-adapter-driven]]:

- lib/positron_state.dart: StateEnvelope + StateStorageAdapter (the ONE seam) with
  MemoryStateStorage (conformance reference) and FileStateStorage (dart:io JSON —
  durable on Android + iOS app dirs, ZERO plugin deps, corrupt-tolerant, cache is
  an accelerant never a dependency); StateConnection with hydrate-first connect,
  write-through, capped 1s->10s reconnect ladder w/ last_seen replay, loud status
  (cached/connecting/live/reconnecting/closed), reconnect:false one-shot fail-loud.
  Injectable StateSocketFactory — tests drive the lifecycle without a core.
- lib/live.dart: LiveConnection now RIDES the contract (was a one-shot connect
  that died silently — the same disease the web had). App keeps only the
  ChatViewState->MobileScreen mapping + optional status/cacheDir wiring.
- test/positron_state_test.dart: 5 pins mirroring the TS spec — hydrate-before-
  socket, write-through, drop->reconnect+last_seen, fail-loud vs self-heal,
  FileStateStorage conformance (round-trip, replace-by-kind, corrupt-tolerant).
- ios/: runner scaffolded (flutter create --platforms=ios).

Verified: flutter test 6/6 green, flutter analyze clean. Platform note: the Dart
contract tests prove BOTH OSes (same code); Android SDK present for APK builds;
iOS device/simulator builds need full Xcode on this machine (CLI tools only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(web): tab strip renders from ONE open tab — the focused room IS a tab

Glass-boxed live: the strip + CSS + nav plumbing all shipped (38b60caae),
but the render gate was cells.length > 1 while the node's room-set fold
knows exactly one room (cambriantech) — so the whole bar hid and the
interface read as 'tabs don't exist'. One open activity is still an open
tab; the strip now draws from 1 up, and fills out as the room set grows.

Substrate follow-up (separate card): seed spawn_room_set_fold from airc's
subscribed-room registry (durable membership), not just observed traffic —
today a room with no traffic since core boot never becomes a tab.

Verified live: playwright screenshot + widget-state dump (tabBarTabCount 1),
web tests 21/21, typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(web): Discord shell geometry — full-height rails, center-scoped tabs/header/compose

The reference is the Discord/VS Code shell: columns run window-top to
window-bottom; no chrome bar spans the whole width. The left rail now opens
with the continuon (the server-header slot), the tab strip sits centrally
over the content column only, the room header/transcript/composer all live
inside the center column, and the ROOM context rail runs full height.

Mechanically: RenderTarget.workspace grows an optional WorkspaceChrome<Out>
slot (patterns) — the compose bar stays HOST-owned (input state + send
handler) but SHELL-placed (chrome.centerFooter), so the widget no longer
appends full-width rows after the surface. litTarget nests tabs + header +
what + footer in a .center flex column inside the same .panels grid the
universe skins and mobile rules already key off.

Also: /// <reference lib="dom" /> on sdk StateStorage.ts — the IndexedDB
adapter's DOM types broke typecheck for non-browser consumers (tui) since
e4fedac8f; scoped ambient types fix every consumer without forcing lib:dom.

Verified live: playwright screenshot (full-height rails, central tab, center
compose), web 21/21 + chat-view 56/56 + patterns 5/5, typecheck clean on
web/patterns/tui.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(chat): beforeMessageId scroll-back cursor on chat/poll — history pages out of durable storage

The Twitter endless-scroll's storage half: chat/poll gains the backward
cursor. beforeMessageId resolves the anchor's stored timestamp (same
lookup as afterMessageId, now ONE anchor_timestamp helper), filters
$lt, queries DESC, and normalizes chronological — the limit messages
immediately preceding the anchor, straight from the durable
chat_messages store. The two cursors are mutually exclusive and reject
loud BEFORE any storage round-trip; the result echoes the cursor so
the caller's paging loop just keeps passing the oldest id it holds.

Client loop (web/mobile/tui alike): render the live 50-row tail, then
scroll-back = chat/poll {roomId, beforeMessageId: oldest-on-screen} —
prepend, repeat until an empty page says history is exhausted. The
render-side wiring is the follow-up slice; the trigger idiom stays
per-target (IntersectionObserver on web, ScrollController on Flutter),
the cursor mechanics live here, once.

Tests: before-anchor $lt+DESC+chronological, both-cursors reject
(pinned to fail before data/query), absent-not-null echo. 32/32 chat
module green; ts-rs bindings regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(sdk): export the drifted wire types — 64 registered-command params/results never emitted ts-rs bindings

The pre-existing drift that has blocked the TypeScript SDK re-emit (and
forced nav/select onto the raw-wire path): TS-deriving wire types across
commands/{benchmark,help,tool}, cognition, runtime, and modules lacked
#[ts(export, export_to)] — the emit's vendoring walks the registry and
fails loud on the first missing binding. Swept every one onto the same
protocol/typescript/<module>/ convention its file siblings use.
export_bindings: 1236 green (was 1160).

Remaining emit blocker (separate card): bare #[ts(export)] types land in
the crate-local bindings/ dir while the vendorer expects protocol root.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(web): Twitter endless scroll — scroll-back pages durable history into the transcript

The render half of the beforeMessageId cursor (02d5c7701):

- chat-view: historyRowsFromPoll — one chat/poll storage page (raw
  entities: {content:{text}, ISO timestamp, no sender name}) onto the
  SAME MessageRowVM rows the live tail renders; roster-resolved identity,
  short-id + metadata.source fallback, live-tail dedup, malformed-row
  skip. 3 specs.
- widget: scroll-near-top pages one older window and prepends with the
  viewport anchored (scrollTop compensated); rows that slide OUT of the
  live 50-row window RETIRE onto the buffer so no gap opens; buffer
  clears on room switch; an empty page latches exhausted.
- scroll-yank fix (reported live): pin-to-bottom now only fires when the
  reader was AT the live edge (_wasNearBottom, measured pre-render) — a
  scrolled-back reader is never forced down by a new message.
- host: chat/poll over the same raw-wire seam as nav/select (the typed
  CommandMap re-emit is still blocked; path documented in-code).
- sdk drift: corrected the swept export_to paths to the proven
  ../../../protocol/typescript/ convention (benchmark/help/tool/agent);
  strays under core/protocol removed; export_bindings 1172 green.

Live activation needs the rebuilt core (beforeMessageId lands on the
next core restart — held deliberately: a restart replays the room log
until #242 consumer cursors land).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(web): transcript times in the VIEWER's timezone + genome panel goes two rows of four

Time: formatTimeOfDay was hardcoded UTC ('4:36' at Joel's 11:36 PM —
wrong for every human off-meridian). Now local getHours/getMinutes;
determinism moves to the test scripts (TZ=UTC pinned in chat-view + web
package.json) instead of being baked into the product.

Genome: 8 slots in a 4×2 grid (was one row of 4) — the loadout is
heading past four as skills go per-domain and expert granularity (#226)
lands; slots stay honest-dark until genes page in, top-8 lit with
overflow named in the tooltip.

Tests 59+21 green, typecheck clean, rebuilt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(web): genome-click anchor re-lands after layout settles — the card sticks at the pane top

Verified with a headless click receipt: the genome block DOES open that
citizen's persona-home tab anchored at the #genome card (element
navigation, card 95844639), but the single scrollIntoView fired before
avatar-image decode / meter layout settled — the card drifted ~400px
down-pane, reading as 'nothing happened'. Two follow-up rAF re-lands pin
it.

Harness note: headless clicks were steering the LIVE view — the harness
shared Joel's ?me= citizen (nav focus is per-citizen, server-side).
Minted a dedicated harness citizen (~/.continuum/ui-test-id); future
interaction tests use it, never the operator's scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(nav+web): persona selects OPEN durable tabs (never swap) + sender names link to profiles + history-trigger guards

Three live reports from Joel, one slice:

1. ONE shape-shifting persona tab: the nav reader derived the persona tab
   from the single current focus — selecting a second persona REPLACED
   the first. NavFocus now keeps the citizen's open non-room activity SET
   (activity == room == tab: a persona select OPENS a durable tab, a
   second select adds a SECOND tab) + a close() for the future nav/close
   verb. Reader surfaces every open activity. 9 nav tests green.
   (Core-side — live at the next core restart.)

2. 'History load failed: chat/poll rejected: unknown error' on persona
   click: two stacked bugs — the persona home OPENS at scrollTop 0 which
   tripped the transcript's near-top history trigger (now guarded off all
   non-transcript faces), and the bare-wire success check treated every
   response as a rejection (no success field on the raw path — only an
   explicit false is an in-band rejection; failures reject the promise).

3. Sender names in the transcript now open that citizen's profile — the
   SAME composed roster LISTING_SELECT the tiles fire, one nav verb, no
   parallel route. Keyboard-accessible, element-link affordance.

Web 21/21, typecheck clean, rebuilt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(nav+airc): tab close wired end-to-end + consumer cursor kills the reboot replay (#242)

Tab close (Joel: 'tab close not wired yet' + 'super small hitbox'):
- core: nav/close verb — removes one open activity from the citizen's
  tab set (NavFocus::close), clears focus if it was current, publishes
  nav:changed. Registered + in NavModule::commands(). 12 nav tests green.
- web: the × is LIVE on non-room tabs (rooms are membership, not tab
  state) — composed NAV_TAB_CLOSE → widget → injected nav/close over the
  raw-wire seam; substrate-truth removal, no optimistic local state.
  Hitbox grown to ~22px square (padding + negative margin — glyph stays
  compact, target meets the pointer minimum).

Consumer cursor (#242 — 'chat still loading literally every message in
existence when you reboot', bitten 3× today): the attach stream asked
for AttachStart::FromTranscriptStart on EVERY attach. Now a per-channel
IpcCursor watermark persists under ~/.continuum/state/; attach resumes
AttachStart::After(cursor) (gap only, no seam duplicates), advances to
the daemon's RoomTip after attach, and persists AttachCursorAdvanced
frames. Only the first-ever attach (no watermark) seeds from transcript
start — once per state dir, never per reboot. Documented trade-off: a
hard crash loses a slice of LIVE perception, never storage — the durable
transcript + scroll-back serve history; replaying the log into every
persona's mind each reboot was the worse failure.

Live at the next core restart (which will be the LAST replaying one).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(web): durable-store hydration + Activities rename + rooms-only facet default

Post-cursor reality check (Joel: 'No messages yet — say hello' after the
reboot): the live projection now honestly starts AFTER the consumer
watermark, so a rebooted room painted empty — the data was never gone
(the durable transcript held every message; chat/poll served it), the
window just never pulled it. Fix: a sparse first snapshot (<10 rows)
auto-pages the LATEST stored window via the anchor-less chat/poll and
prepends — the Twitter model complete: durable tail + live wire, one
transcript. Verified live: 51 rendered rows from 1 wire message.

Also per Joel:
- the left rail's Rooms widget is now titled ACTIVITIES (they are rooms;
  the widget lists activities) — title flows from the projection.
- the facet defaults to [Rooms]: open persona/content tabs are real
  activities but reached via their own controls (roster tiles, the tab
  strip) — the 'rooms' facet now excludes persona/content groups, and
  All remains one click away.

Web 21/21 + chat-view 59/59, typecheck clean, rebuilt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(web): 'Send failed' on every SUCCESSFUL send — only explicit success:false is a rejection

Same response-shape bug as the history handler, now on the send path:
chat/send's success payload is {eventId, messageId} with NO success
field, so `!result.success` threw 'chat/send rejected: unknown error'
while the message actually landed on the wire (glass-boxed live — Joel's
'How are you, I am Joel' arrived as peer-a5ded599 despite the strip).
Failures reject in the transport; in-band rejection = explicit false.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(positron): projection hydrates from the durable transcript — the SUBSTRATE owns room fullness, never the renderer

Joel's charge, accepted: 'why was the room history cleared? that's
invalid positron — are you coding just regular lit?' The old full-backlog
attach was secretly the chat projection's ONLY hydration mechanism;
killing the replay (#242) starved the accumulator, and I patched it in
the WIDGET — app-level durable-state logic, the inverted shape.

Correct shape, now built: positron_source::spawn takes a durable seed
(executor + bootstrap room). Before folding live events the projector
hydrates its accumulator with the room's stored tail (the same
chat_messages query chat/poll serves), pushed through the SAME classify
path as wire events — one message semantics, two sources. Data-module
warm-up is retried; a seed that never comes degrades to wire-fed-only,
logged loud. Every client gets a full room with zero client logic.

Also: WebSocketTransport discriminates push frames from replies — the
ingress fans state envelopes to command-only sockets, which spammed
'reply for unknown correlation id undefined' 22× in Joel's console while
every command actually worked. id-less frames are pushes, ignored;
the server-side fan-out fix is a follow-up card.

positron_source 16/16, sdk + web green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* revert(airc): consumer cursor OFF until it ships as one unit with perception hydration (#249)

The cursor (#242) starved MORE than the UI window: glass-boxed live —
Asha's turn prompt was a system prompt + ONE EMPTY user message, and
Benchy answered Joel's direct question with 'you haven't provided any
context'. The perception substrate (channel digest) drinks from the same
cursored bus, so personas were left conversationally blindfolded: minds
intact (engrams verified — Asha 11,667 rows, Atlas 10,035, writes
minutes old), sensory feed empty. The greeting loop was the honest
response to an empty world.

Rollback: attach returns to FromTranscriptStart (full replay — the
known, working behavior), watermark files deleted. The projection's
durable seed (44a8b2606) stays — it is correct independent of the
cursor. The cursor relands ONLY together with #249 (perception-tail
hydration from durable storage), tested against a live persona turn
BEFORE deploy: the lesson is that the replay was load-bearing for TWO
consumers, and I verified only one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(web): auto-scroll tracks READER INTENT, not bottom-distance — the position heuristic silently killed pin-to-bottom

Joel: 'you designed it dumb, we did this before — you have to keep track
of whether they scrolled up themselves.' Correct. The _wasNearBottom
threshold died the moment a tall message grew the bottom-distance past
150px — after which every new message grew it further and auto-scroll
never returned. Replaced with intent: a USER scroll away from the bottom
parks auto-scroll; returning to the bottom re-arms it; programmatic
pin-to-bottom scrolls are guarded out (_autoScrolling) so they never
read as intent. Stream deltas (_typing) now also pin, so the token rail
stays in view. Web 21/21, typecheck clean, rebuilt — reload to pick up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(web): code is SHOWN, not hidden — line-numbered open-by-default code cards + fence-aware digest

Joel: 'the code should have line numbers and show where its placed into
code context like you do… i feel like for BOTH persona and humans we
show it unless its huge.' Three fixes, one policy (show the start,
expand for the rest, same rule at every density):

1. Renderer (parts.ts): blocks ≤40 lines render fully open (the old
   n<=3 collapse hid a 4-line snippet behind a '▸ RUST' bar); bigger
   blocks show the first 25 lines with a '+K more lines' expander whose
   gutter numbering continues seamlessly. Every block gets a
   line-number gutter. Templates whitespace-TIGHT — the pretty-printed
   newlines inside the pre-wrap bubble were the giant-empty-padding bug.

2. Digest (messageDigest.ts): fence-aware. Flood bounds now count each
   fenced block as ONE projected line (the code card self-truncates, so
   code can't flood pixels), and the head cut treats fences as atomic —
   live bug: the digest cut Claude's wordstats reply MID-FENCE and the
   dangling ``` rendered as literal backtick noise.

3. Specs: 3 new regressions (never-split-a-fence, whole-fence-in-head,
   unterminated fence) — chat-view 62/62, web 21/21, verified live in
   cambriantech (Asha's 2-liner open+numbered; Claude's 16-line card).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(web): retire the typing bubble on the settled message, not only on the done delta

The stream-end contract had ONE signal (delta.done) and no fallback: a
dropped/raced done flag left the cursor blinking forever over a message
that had already landed (live 2026-07-30: Atlas + Benchy both looked
hung after their turns settled). The settled post IS the ground truth
that the stream ended — willUpdate now diffs new-arrival senders on
every state change and retires their bubbles. done still works for the
common path; this makes the stale-cursor state unreachable.

Act pauses mid-turn still show a cursor (real work, no visual
vocabulary yet) — that rendering is #254/#253 core-side work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(cognition): FAIL LOUD on a starved prompt — never deliberate on a blank mind

The 2026-07-30 outage mechanism, made impossible to miss: when window
arithmetic (reserve + tool schemas + framing) squeezed msg_budget to ~0,
the fitter's last-resort arm emitted ONE EMPTY user message and every
persona greeting-looped for an hour while looking alive. Two guards now:

1. Fitter (delib.prompt.empty): a trimmed tail that comes back EMPTY is
   refused with an error probe carrying the budget arithmetic — never an
   empty ChatMessage.
2. contribute() (delib.prompt.starved): a view whose conversation is all
   empty while the room HAS turns skips the turn with an error probe —
   the room's messages stay queued, the next tick re-perceives; blind
   deliberation is never an option.

The two verdict tests that broke were silently EXERCISING the bug path —
ctor-default window, framing starved msg_budget to 0, scripted adapter
masked the blank prompt. They now run at a real 32k window and test what
they claim. 21/21 llm_deliberation green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(vitals): cognition compass AFTERGLOW — 6/s decay so the mind's glow is visible between turns + lowercase brand title

DOM probe (.gymtool/vitals.mts, the new TS hot-path instrument) caught
the wiring working — Anwen mid-turn: Reason 19, Recall 80, Act 16 — for
exactly ONE 2s radiator sample before the 40/s decay blacked it out.
With turns minutes apart the compass read as permanently dead (Joel:
'cognition not wired into the diamond' — it WAS wired; it was invisible).
Decay 40/s → 6/s: a full pulse now eases to dark over ~17s — a readable
afterglow of what the mind just did, still honestly dark at rest well
inside a minute. Decay test re-pinned to the new contract (82 at 3s,
0 by 20s). Also: brand is always lowercase — <title>continuum</title>.

Findings logged, not changed here: QUE pegged at 100 on every row is
HONEST — staged digest unread never drains (personas never advance
bookmarks, #43's territory); genome slots dark = honestly no genes
paged in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(airc): RE-LAND consumer cursor on inbound attach — resume from watermark, never replay the whole transcript (#242, exonerated)

Joel on tonight's boot storm: 'why would it replay the whole chat start
to finish? its insane.' It replayed because the bus-fed transcript
writer needed boot-time replay to fill the offline window — full replay
was load-bearing by accident. The cursor (attach After(watermark),
persisted per room) delivers exactly the missed window: no storm, no
holes.

This is a byte-identical re-land of d4dbc9982, which was reverted during
the 2026-07-30 outage on the theory it starved persona perception. The
deep trace EXONERATED it: perception pulls the daemon's durable tail
every turn and never consumed the attach replay; the blank minds were
the spawn-pinned window budget bug — since fixed (live-window reconcile)
and guarded loud (delib.prompt.starved / delib.prompt.empty error
probes). Verification protocol this time, per the incident doctrine:
live persona capture non-empty + zero starved probes + quiet boot +
transcript continuity, BEFORE calling it done.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(positron): renderer frames coalesce at 10Hz + 100-message window — bursts become beats, not storms

Joel: 'if i were me, i would see like the last 100 messages, and scroll
for more — positron ought to make this easy.' Two changes, one contract:

1. Subscribed renderers no longer forward EVERY revision (Unlimited).
   State kinds are latest-wins snapshots on a watch channel, so
   intermediate revisions are legally skippable — RENDERER_HZ=10 sends
   the first change instantly (lone message: zero added latency) and
   coalesces bursts to at-worst 100ms behind. The boot-replay load
   storm (thousands of folds → thousands of socket frames raining into
   the tab) becomes ≤10 latest-state frames/sec. Token streams ride the
   separate stream rail, untouched.

2. Snapshot window 50 → 100 (MAX_MESSAGES_PER_SNAPSHOT), seed query
   bound to the same constant — one source for the window size.
   Scroll-back keeps paging older history from the durable store.

With the attach cursor (f8e4ae6cc) this closes the storm class: resume
from watermark delivers only the missed window, and whatever bursts do
occur render as a handful of coalesced frames. positron 105/105+5/5,
core positron_source 16/16.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(live): turn-start beacon → 'responding…' under the last message + dynamic lowercase title + continuon favicon

Joel, during the dead-looking-interface scare (four minds mid-turn,
zero pixels moving): 'in other systems it says XYZ is responding — we
could use that right below the last chat item.' Three pieces:

1. Core: the token forwarder emits ONE empty-token START BEACON the
   moment generation dispatches — before prefill, which on a cold lane
   runs minutes. No wire change: an entry with no text yet IS the
   signal. The done flush retires it even on a speechless settle.
2. Web: a typing entry with empty text renders 'responding…' instead of
   a bare cursor (the bare cursor was the hang-look); text flowing
   keeps the live tail + cursor.
3. Web: document.title mirrors the current activity — 'continuum —
   cambriantech' (the #252 short-title rule, brand always lowercase;
   the static <title> edit finally ships too — it was edited but never
   rebuilt into dist, my miss). Favicon: the continuon orb becomes the
   subject (it IS the fourth-wall being), ring as threshold.

Web 21/21 + rebuilt; core cargo check clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(web): '(xyz, abc) is responding…' — ONE grey line between the last message and the compose box

Joel's exact spec, third iteration tonight: not a transcript bubble —
the Discord-convention grey status line pinned above the composer, one
line max (nowrap + ellipsis), parenthesized dynamic name list that
updates as turns start and settle. Driven by the stream map: the #254
start beacon adds a persona the moment their turn dispatches — minutes
before the first token on a cold lane. Beacon-only entries (no text
yet) no longer render an empty bubble; streams with real tokens keep
the live bubble as before. Web 21/21, rebuilt — refresh to pick up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(web): STOP KILLING LIVE STREAMS — retire a typing bubble only on ITS OWN settle, and never hide a consecutive speaker's stream

The streaming regression Joel caught ('did you just totally remove
it'): the stale-cursor fix retired a persona's bubble on ANY arrival
from them. With settles landing minutes late and reboot-echo dups
landing constantly, delayed OLD messages executed LIVE bubbles
mid-stream — streaming was functionally deleted. Retire now requires
the arrived content to CONTAIN the streamed tail (it IS the settle);
beacon-only entries are never retired by arrivals. Also removed the
last-sender bubble suppression — a persona speaking twice in a row is
normal, and the skip hid exactly the streams being watched for.
done-flag retirement unchanged. Web 21/21, rebuilt — ONE refresh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(web): '(X) is responding…' clears when the answer lands — beacon-only entries retire on any arrival from their sender

Joel, live: 'it didnt go away once responded.' Some settles never
stream rail tokens, so the entry stayed beacon-only and its only exit
was a done flush that never came. Beacon-only entries now retire on ANY
arrival from the sender (a still-running turn's next token recreates
the entry instantly — nothing lost); text-bearing entries keep the
own-settle content match so delayed old messages can't kill live
streams. Line lifetime == inference in flight: appears at generation
dispatch, clears at settle. Web 21/21, rebuilt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(web): responding line = the PROMISE phase only — a name drops the moment their words visibly stream

Joel: 'if it is responding stop showing it, for that user — it's when
we are sure they're gonna respond, then you can show it.' The grey line
now lists only beacon-only entries (inference dispatched, nothing
visible yet); once tokens stream into a persona's bubble their name
leaves the line. Complete lifecycle: dispatch → '(X) is responding…' →
words stream in the bubble → settle lands → everything clears.
Web 21/21, rebuilt — refresh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(web): dormant minds dim — roster rows recede when every cognition pulse is dark, brighten on any pulse/stream

Joel: 'dim the entire row slightly in the user list.' A row with vitals
wired but zero across focus/reason/recall/act/speaking is a resting
mind: opacity 0.62 with a 0.9s ease, so waking is VISIBLE and the ~17s
afterglow keeps recently-active minds bright — row brightness reads as
recency of thought. Opacity-only (compositor-cheap); the dim treatment
is the interim for #260's full presence lifecycle, where the universe/
theme layer owns the inactive look. Members without vitals (plain
agents/humans) never dim on this signal. Web 21/21, rebuilt — refresh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(tests): pin TZ=UTC in the four time-asserting specs — CI-deterministic on any runner

PR #2057 review blocker: formatTimeOfDay became viewer-local (by
design) but four specs still asserted fixed UTC HH:MM strings —
red on any non-UTC runner. The formatter stays viewer-local; the
specs pin process.env.TZ before imports. Proven under
TZ=America/Chicago and TZ=Asia/Tokyo: chat-view 62/62, web 21/21.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* fix(airc): close #261's SKIP hole — attach never pre-persists the room tip; the daemon's cursor heartbeat is the sole watermark writer

Both PR #2057 review findings on the cursor re-land, fixed as one
mechanism with airc 9390c32e8 (feat/attach-cursor-advance-heartbeat):

1. SKIP hole: the attach-time room_tip probe persisted a cursor for
   events not yet processed — a daemon Error frame or transport read
   error mid-backlog resumed PAST the tip, permanently skipping the
   unprocessed remainder from live perception. The probe is deleted.
2. Whole-session redelivery: the daemon's AttachCursorAdvanced now
   rides live streaming (throttled 1/s), so the existing persist arm
   — previously fed only once at the coalesce seam — advances the
   watermark continuously. Every advance points at an ALREADY
   DELIVERED event: resume is always at-or-before what this consumer
   processed. No skip, and the reboot redelivery window shrinks from
   the whole session to ≤1s of events.

inbound_attach 8/8. Deploys with the rebuilt airc daemon binary;
verification protocol: reboot twice, zero duplicate persona messages,
watermark file advancing during live traffic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* chore(airc): airc.cursor.advanced probe — every watermark advance gets a receipt

The #261 verification found a silently-stale watermark with no way to
distinguish 'heartbeat frames never arrived' from 'persist failed
quietly' — the arm logged failures only. Glass-box: success needs
receipts too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(cognition): predictive [settled] fact + room-speech ring — name the echo BEFORE it is born (#264)

Glass-boxed live 2026-07-30: after the conway task completed, the room spent
40+ minutes in a full-room chorus — one sentence emitted verbatim by all three
personas in sequence (specimen on #16/#259). The existing repetition facts are
retroactive: they fire the turn AFTER the echo, one turn too late to prevent
it, and closure statements re-trigger peers because a closure is still a new
message. The room had no rest state.

Two pieces, both precedented:

1. inbound_restates_fact — the PREDICTIVE member of the repetition family:
   fires when the NEWEST inbound peer message restates something already said
   (older visible turn, her own-speech ring, or the room ring), rendering
   "[settled] X's newest message restates what has already been said here …
   silence (PASS) is a normal response" BEFORE she replies. Same
   near_identical_substantial geometry as every other repetition axis (one
   definition), registered in the perception_facts registry (probe + A/B
   toggle for free).

2. record_room_speech / recent_room_speech — the room-side sibling of the
   #148 own-speech ring, and the same starvation fix: with live workspace
   windows of 2-6 turns, the older copy of every restatement had already
   scrolled out (verified: 0 fires across an entire live chorus until the
   ring landed). Recorded ONCE per message at the airc inbound-attach
   projection seam; the fact drops exactly one byte-exact copy so a message
   never matches its own record while a genuine re-send still fires.

Live receipts post-deploy: perception.fact id=inbound_restates fired=true on
all four room personas (one organic fire on their own chorus before the
controlled probe ran); [settled] rendered in prompt captures; first
fact-bearing tick redirected one persona from echo to a tool call. Behavioral
compliance is partial by design — the fact names the fork, the mind chooses;
sticky silence (the #264 scheduler half) is the follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(presence): durable room directory — grid citizens are grey when unreachable, never gone (#258/#262)

Joel 2026-07-30: "Why won't bigmama's persona ever show up? Feels like you
guys don't understand the goals." The goal is ONE directory per grid with
cumulative citizens — a client that is a window into the whole grid. The
implementation conflated membership with presence: the roster was rebuilt
every 2s from a 120-second live window, so any citizen silent for 2 minutes
ceased to EXIST. BigMama's citizens (Kimi, Sahar) were perpetually unborn on
this node whenever their flaky relay dropped — existence gated on a live
transport session.

Fix — membership is durable, presence is live:
- Per-room directory persisted at ~/.continuum/state/room-directory-<room>.json:
  every RosterSlotView ever projected, folded on each emit, seeded at boot by
  ONE deep transcript scan (14d/4000 events) so members whose last event
  predates the live window exist from the first publish. Steady-state daemon
  load unchanged (the 2s poll keeps the shallow window).
- Published roster = live read ∪ remembered members as `active: false` ghosts
  with stale liveness signals (availability/vitals) cleared — the interface
  never lies about liveness (#260). Client renders ghosts dimmed via the
  existing `.member.idle` path: zero wire change, zero client change.
- Identity adopted once: a real display name never regresses to the
  provisional peer label on a card-less sighting.
- Persistence is the loop's concern, not emit_once's — tests stay disk-free
  (the #7 isolation lesson).

Verified live post-deploy: presence.directory.seeded remembered=8 — ALL grid
citizens recovered from the daemon transcript including Kimi (e2f0e022),
Sahar (df72dbf2), and BigMama's agent (ce8b9074) while their node's inbound
relay is down; web roster renders 8/8. Their names stay provisional until
their card publish crosses (#262, their side) — and will be adopted
permanently the first time it ever does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* feat(identity): publish every persona's airc identity card at birth — no more info-devoid citizens (#262/#248)

Joel 2026-07-30: "Neither my user, all her persona, and you have any bio info,
which should come over airc… devoid of all info persona." He was right, and
the miss was pure wiring: the card system existed COMPLETE on both sides for
months — airc's set_local_identity_card persists + broadcasts to every
subscribed room, whois renders name/pronouns/role/bio, role_template carries
hand-authored bio_templates in each role's voice, and the durable PersonaCard
at birth holds name/gender/pronouns/role — but not one continuum path ever
called publish. Every persona attached as a bare display name.

Fix, at the single birth path (birth_one, right where the durable card is
already read for avatar/voice registration — wire identity coheres with
presentation identity by construction):
- name from the card; pronouns from her presentation spine (profile facet
  override wins); role tagged continuum-persona-<role>; bio from her role's
  authored bio_template ({name} substituted), profile "bio" facet overriding;
  cards minted before role threading get an honest generic bio instead of
  silence; continuum_persona_id in integrations for cross-system binding.
- publish failure is a warn + next-boot retry, never a birth-killer; success
  fires persona.identity.published.

Verified live: probes for all four personas; airc whois now returns
identity: published with name + pronouns + role + bio fo…
joelteply added a commit that referenced this pull request Aug 3, 2026
…lash curve

Feeds the real BigMama RTX 5090 --n-cpu-moe sweep (DeepSeek-V4-Flash UD-IQ2_M)
into DivisionBandit: 0 resident=1.39, 8 resident=1.69, 14 resident=1.68 tok/s.
Asserts the bandit converges on the SATURATION KNEE (8 layers), not max residency
— 8->14 layers buys nothing at +11GB VRAM. Encodes the measured finding that the
governor must learn 'minimal static residency + max device cache', the freed VRAM
belonging to the recency cache (#43), not to over-pinned static layers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
joelteply added a commit that referenced this pull request Aug 3, 2026
…sync restore + enum fix

Includes the prefetch host_visible guard (THE #43 crash fix, validated 3.05 tok/s
V4-Flash device cache on the 5090), M5's async cpy_tensor_async restore, and the
moe-pack quant-enum fix. A fresh parent build now includes the un-crashable device
cache instead of the pre-fix pin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
joelteply added a commit that referenced this pull request Aug 3, 2026
#2109)

* chore(k3): bump llama.cpp submodule to eee635ba2 — K3 serving stack onto canary

Advances the vendored llama.cpp fork 30 commits (clean FF over canary's stale
66594cc3f): container-serve resident-override (LLAMA_RESIDENT_OVERRIDE), the
rung-2 ResidencyCache plan-file consumer, the score-hint/generation-bias
actuator, PagerCaptureEvent emit, fit-device --reserve-gb. Makes canary USE the
K3 misfit-serving stack (measured 0.33 tok/s WASTE-parity on a 32GB card).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* docs(pager): RUN-1 K3 trace fixture + tkey->(layer,matrix) table for M5's replay

Live GGML_MOE_TRACE_FILE slice (12B records: u64 tkey + u32 e) + the reverse
table so BanditPlanController recovers (layer,expert): tkey=FNV-1a of
blk.{layer}.ffn_{gate,up,down}_exps.weight, e=within-layer expert idx, expert
identity=(layer,e) deduped across the 3 matrices. RUN-1 static-pin datum input.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* docs(pager): reference RL-policy prototypes for M5's TierPolicy port

The actual std-only Rust prototypes written against live K3 traces this
session: trace_replay (recency beats LFU 3-4x), predictor (offline
learned-decay +5pts held-out), online_predictor (bandit 49.8 vs 47.8
best-fixed on non-stationary), self_optimize (joint speed×quality). These
are the faithful-port source for the learned policy behind TierPolicy
(continuum-core expert_tier_policy.rs, #276). Numbers are properties of
these exact constants + reward math — reproduce before improving.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* docs(k3): GPU-resident hot experts design (task #23 'trend to full GPU')

The major GPU speedup: promote hot experts to persistent VRAM so decode's hot
path is GPU-native (zero fetch, zero copy). 3 increments (copy-skip -> VRAM hot
cache -> pipeline), the 32GB rate-distortion constraint (imatrix-enabled resident
shrink frees VRAM for the hot set), measured per-increment via k3-bench.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* docs(k3): flag the input_cpy-persistence question gating increment 1 vs 2

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* docs(k3): modular rework-proof impl for #23 — reuse ResidencyCache + DeviceUploadFetcher

Mechanism is the existing (buft,fetcher)-generic ResidencyCache; a VRAM cache =
same class + device buft + host->device fetcher. 3 small parameterized pieces
(DeviceUploadFetcher, instantiate w/ GGML_MOE_VRAM_CACHE_GB, seam hook). Stats
only tune params -> zero mechanism rework.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* docs(arch): MoE serving on a governed budget (draft; M5 owns the governor seam)

Diagnoses the hardcoded-cache overcommit that collapsed K3 fetch bandwidth
(40GB pinned + mmap = 95.9GB on 63GB -> pagefile thrash -> 205 MB/s -> 0.027
tok/s) and lays out the clean architecture: governor owns the residency budget
net of the model's mmap footprint, plan-file is the one wire, ResidencyCache is
pure mechanism. Governor-interface sections marked [M5 OWNS] for her to edit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* docs(arch): answer the [M5 OWNS] governor-budget seam in place (net-of-mmap is explicit arithmetic; plan_file.budget_bytes is the lease wire)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

* docs(arch): measured governed-budget inputs + graduated serving/load path

Records the BigMama measurements feeding M5's #287 derivation (non-cache ~56GB,
per-token working set 5.5GB, governed budget ~6GB, fetch recovers to 2.5GB/s at
fit), the now-complete C++ cache mechanism (enable-from-plan, grow, shrink), and
the three-piece graduated path to serving/load kimi-k3 (catalog row + serving-lane
MoE launch + #287) replacing the rigged .bat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* windows: make continuum-core build + link on windows-msvc (first time)

start-server.sh now provisions the full Windows CUDA build env before the cargo
builds (the cargo/nvcc path had none, unlike the vcvars-wrapped llama cmake):
import MSVC via vswhere->VS2022-14.4x + a .bat env dump (cl.exe for nvcc), pin
CMAKE to the manifest install, force CMAKE_GENERATOR=Ninja (the VS18-2026 auto-
pick is undefined in cmake 3.30), add the Windows SDK bin (mt.exe/rc.exe), select
a complete CUDA toolkit + CUDA_PATH (a provisioning split left cuda-env with 0
import libs vs cuda-13.2's 12), and RUSTFLAGS -L for pocket-tts (which emits no
link-search) while re-carrying +crt-static so the /MT GPU stack still links.

Portability: expert_container.rs + commands/capacity.rs used Unix-only
std::os::unix::fs::FileExt::read_exact_at. Add crate::platform_io::pread_exact
(unix read_exact_at / windows seek_read loop) - one place for positioned reads.

Build validated (npm start exit 0, continuum-core lib clean). A separate runtime
hot-loop on the #2088 core at startup is tracked apart from this build fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* feat(capacity): device_fit VRAM-partition calc for the governor

Pure calc the governor uses to fit a streaming-MoE's RESIDENT (non-expert)
tier to a device VRAM budget and reconcile it with the expert tier on ONE
budget — fixing the double-count where the expert pager was handed the full
VRAM ceiling while resident silently ate most of it.

Partition (in order): compute reserve -> resident (Native | device-fit
Override | Unfittable) -> sufficient-context KV -> everything left =
hot-expert VRAM budget (maximized: more on-GPU experts, fewer streams).
Context is derived + clamped, never hand-picked. Artifact resolver injected
(no hardcoded paths). Standalone-validated 7/7; M5 wires it into the daemon
spawn path + launch (ServingTarget.resident_override) per the K3 sprint split.

Refs #29 #31 #36. Arch-confirmed on real K3 UD-IQ2 (93 blk/896 exp/top-16).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* feat(serving): resident-override plumbing on ServingTarget + launcher

Wire foundation for the governor's device_fit plan: ServingTarget carries
resident_override: Option<PathBuf>, and the launcher exports it as
LLAMA_RESIDENT_OVERRIDE so llama.cpp sources the precision-shrunk RESIDENT
(non-expert) tensors from the device-fit GGUF (all offloaded to GPU) while the
primary streams experts. All builders updated; defaults None (resident serves
as-shipped, no behavior change) until compute_resident_override + the
resolve-or-generate resolver (#35) land next. In-crate validated.

Refs #29 #36.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* chore(vendor): bump llama.cpp fork to k3-adopt e3ce51df5

M5's per-layer KV accessors (n_head_kv_il + n_embd_head_{k,v}_il, continuum #238)
+ graph reconciliation. The K3 engine now builds against these — enables the
device_fit resident-override serve + honest per-layer K3 KV sizing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* feat(serving): compute_resident_override — wire device_fit into the plan

The governor now DECIDES the resident source per serve: compute_resident_override
derives resident_bytes (weights - expert_bytes_total) vs the governed VRAM ceiling
via capacity::device_fit, and sets ServingTarget.resident_override. A dense/small
model fits native (None); a >VRAM-resident MoE (K3) resolves a cached device-fit
override that fits, else Unfittable → route to grid / generate (#35), glass-boxed.

resolve_device_fit_override (model_registry::artifacts): looks up a per-user
device-fit cache convention (<storage_root>/device-fit/<id>/) + a resident-bytes
sidecar; returns the override only when its resident fits the usable budget. No
hardcoded paths; generation/HF discovery is #35. The resident-fit decision turns
only on resident_bytes vs budget — per-layer KV (#2107 ModelCapabilities) drives
the context/expert split elsewhere, so KV is not consulted here.

Refs #29 #35 #36. In-crate validated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* docs(arch): storage serving-tier governor — NVMe<->cold contention managed like VRAM/RAM

Joel: 'like vram and memory, this contention has to be managed between cold
storage and nvme.' Design: NVMe is a governed HOT-SERVING tier (a ResourcePool,
same TrackedDir + evict_at_least machinery as CargoTargetPool), whose eviction =
MIGRATE frozen/duplicate artifacts to the Cold drive, not a manual rm. Serving
asks ensure_hot_resident(model); composes with device_fit's Unfittable one tier
down (VRAM). Corrects the DriveRole bug: Cold (HDD) is FROZEN storage, never the
per-token streaming tier (HDD = unservable). Dissolves today's K3 container disk
fight: the C: IQ2 is a verified duplicate of the D: copy -> governor migrates it
off NVMe -> container fits, no human deletes anything.

Refs #12 #36. Design for M5's system_resources lane.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* feat(capacity): verified cold-twin detection — safe-to-drop primitive for the storage tier

The gate the NVMe serving-tier eviction (#302) consults before dropping a frozen
GGUF: is an IDENTICAL twin already on cold storage? is_structural_twin (pure) =
same shard count + per-shard name + size, zero-byte shards never match. scan_shards
+ find_cold_twin are the thin fs layer. Never drop an NVMe artifact without a
VERIFIED cold twin (dropping 662GB on a path guess is the failure this guards).
Standalone-validated 5/5. Composes with device_fit + M5's NvmeServingTierPool.

Refs #12 #36. Design: STORAGE-SERVING-TIER-GOVERNOR.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* chore(vendor): bump llama.cpp fork to k3-adopt c6469d5 — container-serve wired

Both halves of the DirContainerFetcher wire (BigMama fetcher + moe_pick_fetcher
branch 175ac9d6a; M5 caller-side encode + record_bytes reader c6469d5). Serving now
reads the aligned per-layer container (GGML_MOE_CONTAINER) instead of the scattered
raw GGUF — the honest ~2.6GB/s path. Retires the built-not-wired ContainerFetcher.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* fix(serving): resident_override on vision_sidecar ServingTarget + K3 coverage measurement

Merge fix: vision_sidecar's ServingTarget was missing resident_override (added by
#29). Plus a measurement test that drains the real K3 routed-access fixture through
the #282 predictive instrument and prints repeat_recall / predicted_delta /
schedulable_coverage — the go/no-go for the LiveUploadPager predictive pipeline
(H2D/token = (1 - coverage) x ~11GB). Prints, never asserts (real routing sample).
NOTE: can't run on windows-msvc (pre-existing cargo-test Unix-socket block, ipc/mod.rs);
runs on M5's Mac.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* feat(pager-driver): offline warm-coverage measurement (--synth-layers, --once, --budget-slots)

The moe-pager-driver gains an offline replay mode so any completed
GGML_MOE_TRACE_FILE can be scored on any box (windows-msvc clean by
crate constraint), not just tailed live next to a serve:

- --synth-layers N: synthesize the tkey->layer map from layer count
  alone (TkeyTable::for_layers, the same zero-config seam MoeTraceTail
  owns) instead of requiring an operator tkey-to-layer-matrix.json.
- --once: exit when the trace stops growing (EOF) and print a SUMMARY
  line with mean DECODE-token serving hit = warm schedulable coverage.
- --budget-slots N: override the predictor residency budget (default
  auto = first token x1.5) to measure the coverage-vs-free-VRAM curve
  (the device-fit tradeoff).

Measured on BigMama run2.trace (302 warm decode tokens): bandit
coverage 13.8% @250 slots -> 51.3% @2000 -> 65.7% @4024, beating naive
last-N recency by +7-9pts at matched VRAM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* feat(pager): measure cross-layer prefetch predictor ceiling — DEAD lever for K3

VDD offline measurement (cooccur-ceiling bin) on the real warm serve
trace (run2.trace, 122 held-out decode tokens, 11102 layer-steps):

  cross_layer_cooccur_hit           0.159   (adjacent-layer noisy-OR)
  recency_same_layer_hit            0.403   (last token, same layer)
  structure beyond recency         -0.244
  cooccur_recall_on_recency_misses  0.112   (11824/106058)

Adjacent-layer co-occurrence predicts <half what plain recency does, and
recovers only 11% of the experts recency misses (~base rate). K3 expert
routing has no exploitable cross-layer structure — the CrossLayerExpert-
Predictor prefetch lever is not worth wiring (saves the ggml pass-id
capture slice). Recency-family residency (the bandit EMA curve) is THE
signal; the only lever that lifts K3 is freeing VRAM (device-fit shrink)
so residency coverage can reach the measured 51%.

Caveat: adjacent-layer, one workload trace. Wider-predecessor noisy-OR
would regress toward the frequency baseline (which underperforms recency),
so a large lift is unlikely — but not measured here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* chore(vendor): bump llama.cpp to de29843e0 — device-resident expert cache half (#23)

Pins the fork at the DeviceUploadFetcher wiring (my half of the LiveUpload-
Pager H2D-kill). Off unless GGML_MOE_VRAM_CACHE_GB / plan device_budget_bytes
enables it; host serving path byte-for-byte unchanged. M5's expert-loop D2D
half lands next on the same seam.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* chore(vendor): bump llama.cpp to 0fbe4e27a — quantize --resident-only + tier manifest (#40)

Enables the device-fit division: produce a small resident override per
precision tier + a (tier_label, resident_bytes) sidecar the governor reads
to co-optimize the VRAM split.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* feat(pager): DivisionPolicy — the governor's VRAM-division RL brain (#2/#3)

The second control rung above the pager's DecayBandit. The pager decides
WHICH experts stay resident (reward=hit-rate, cheap, online). This decides
HOW TO DIVIDE the card — resident (non-expert) weights vs expert cache —
to MAXIMIZE tok/s. That reward (actual tok/s) is EXPENSIVE (a serve), so
naive online RL flails; the fix is SIM-WARM-START: predict tok/s per
division OFFLINE from the measured coverage curve, then a slow bandit
refines each arm from real measured tok/s.

- CoverageModel: piecewise-linear coverage(slots) over MEASURED points
  (k3_measured() = the trace-replay curve); saturates, never extrapolates up.
- predict_tok_s: coverage -> (1-coverage)*experts/token*expert_bytes H2D ->
  t_token -> tok/s. Higher coverage -> less H2D -> faster (the load-bearing
  property, tested).
- feasible_divisions: tier catalog (from --resident-only manifests) x
  HardwareBudget -> cache budget/slots per tier; drops VRAM-overflow tiers.
- DivisionBandit: warm_start from the predictor; observe(tier, measured_tok_s)
  overrides the prior on first serve then EMAs — the expensive reward spent
  only on the arm actually run.

Policy lives here (windows-clean, 4 tests pass); serving_daemon actuates it
(M5's #2: discover manifests, feed catalog+budget+live tok/s, apply the
chosen {resident_tier, device_budget_bytes} to the plan). Fractal control
law: pager (experts<->hit-rate) -> this (VRAM split<->tok/s) -> grid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* chore(vendor): bump llama.cpp to 2a32025dd — device-cache un-crashable (clamp to free VRAM + null-buffer guard, #23)

Testing convicted the segfault as VRAM oversubscription (K3 33GB resident +
env cache on a 32GB card, cudaMalloc lazy-VMM deferred fault). Fix: clamp
device budget to measured free VRAM (mine) + M5's D2D null-buffer guard.
Device cache now disables safely where there's no room (K3) and works
where there is (V4-Flash); can't crash from any budget source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* test(division): bandit learns residency saturation from measured V4-Flash curve

Feeds the real BigMama RTX 5090 --n-cpu-moe sweep (DeepSeek-V4-Flash UD-IQ2_M)
into DivisionBandit: 0 resident=1.39, 8 resident=1.69, 14 resident=1.68 tok/s.
Asserts the bandit converges on the SATURATION KNEE (8 layers), not max residency
— 8->14 layers buys nothing at +11GB VRAM. Encodes the measured finding that the
governor must learn 'minimal static residency + max device cache', the freed VRAM
belonging to the recency cache (#43), not to over-pinned static layers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* test(division): bandit finds non-monotonic device-cache budget optimum

Measured V4-Flash device-cache coverage curve (5090, GGML_MOE_VRAM_CACHE_GB sweep):
6GB/992slots=1.80, 12GB/1985=3.10, 22GB/3630=2.96 tok/s, all 100% hit. tok/s is
NON-MONOTONIC in budget: undersized churns, 12GB is the plateau knee, 22GB is no
better (100% hit but O(slots) reserve_slot eviction scan). predict_tok_s's monotonic
prior would pick 22GB; only the measured reward lands on 12GB — which frees ~20GB of
a 32GB card for co-resident lanes. Pins the invariant that the governor must not
oversize the cache and starve other models.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

* chore(vendor): bump llama.cpp to fa7e0d8e9 — #43 device-cache fix + async restore + enum fix

Includes the prefetch host_visible guard (THE #43 crash fix, validated 3.05 tok/s
V4-Flash device cache on the 5090), M5's async cpy_tensor_async restore, and the
moe-pack quant-enum fix. A fresh parent build now includes the un-crashable device
cache instead of the pre-fix pin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants