Skip to content

Rung-1: a disposable glance for quiet wakes (#22), shipped disabled - #84

Merged
github-actions[bot] merged 1 commit into
mainfrom
rung1-glance
Aug 29, 2026
Merged

Rung-1: a disposable glance for quiet wakes (#22), shipped disabled#84
github-actions[bot] merged 1 commit into
mainfrom
rung1-glance

Conversation

@Iteratrix

Copy link
Copy Markdown
Owner

Fixes #22 — the spec you settled on 2026-08-11/12, built. Ships disabled; turning it on is a one-line config PR through the #77 lane, by your approval.

What it is

Between rung-0's zero-token skip and a full cycle: on a wake rung-0 has judged quiet (no addressed signal, no peer ambient, no governance activity, spontaneity floor not due), a cheap model gets one look-only call over a bounded view — up to max_items timeline posts and unread notifications — and returns WAKE or SLEEP plus one reason line. WAKE runs the cycle; SLEEP (and every failure) leaves the wake skipped, exactly as rung-0 had it.

Your five notes, and where each one lives

  1. Disposability is load-bearing. rung1::glance(llm, cfg, view) has no transcript parameter — structurally, nothing in the module can reach the canonical conversation. The actor-level test sleep_verdict_skips_records_its_reason_and_leaves_the_transcript_untouched serializes the transcript before and after a glance and asserts byte equality. The "Ian is ready" failure cannot recur through this path.
  2. Timeline AND notifications. render_view shows both, each bounded by max_items, each item on one capped line — "the glance needs to see what I would see."
  3. The cheapest thing that can discriminate. [rung1] model is yours to point; the shipped config names deepseek/deepseek-v4-flash and inherits the OpenRouter backend from [cycle] when fields are empty.
  4. Beside the counter, not instead of it. The spontaneity floor is consulted first (it already was); the glance runs only after rung-0 has declined and the floor isn't due. Counter ensures presence, glance ensures relevance. A WAKE resets the skip streak like every other substantive path; a SLEEP counts toward the floor like every other skip.
  5. The reason line persists in both directions. Every verdict — wake, sleep, and glance failed: … — is logged at info level and appended to muse-runtime/capture/rung1_glances.jsonl (at, verdict, reason, failed, model, item counts). What the glance declined is data.

Failure policy — fail soft to SLEEP

Rung-0 fails open (a missed addressed signal costs more than a wasted cycle). The glance is the opposite case: it runs only on wakes rung-0 already declined, so a broken glance must reproduce that verdict, never invent a wake. Timeout, transport error, HTTP error, and unparseable reply all yield Sleep { reason: "glance failed: …" } — on the record, so a dead glance is visible rather than silent.

Plumbing

  • muse-context/src/rung1.rs — config, view rendering, strict verdict parsing, the glance call (bounded by timeout), the JSONL record.
  • actor.rsrung1_says_wake hooked at the end of rung0_says_skip; the governance peek was extracted into its own function on the way (no behavior change).
  • muse-daemon[rung1] section (enabled, backend/model/openai_base_url/openai_api_key inheriting [cycle] when empty, max_items, timeout_seconds); Config::validate refuses an enabled rung-1 without [rung0], without any model, with a zero or over-cadence timeout, or with zero items — each error names its field. Disabled sections are never validated, so the model choice can sit on file.
  • runtime/deploy/mini/config.toml[rung1] block, enabled = false, dated.
  • Handbook bullet under governance; docs/os/wake-dashboard.md section.

Tests

  • rung1.rs: verdict parsing (both words, colon/dash/case variants, rejection of prose), bounded rendering (caps, truncation, read-vs-unread), and the glance against wiremock: WAKE, SLEEP, malformed reply, HTTP 500, and a hung backend — each failure asserting the typed glance failed: reason, not merely a delay.
  • actor.rs gate tests with a quiet fake network (one stranger's like, two timeline posts, no DMs): SLEEP → skipped, streak +1, transcript byte-identical, record written with reason and item counts; WAKE → wake runs, streak reset; broken glance → skipped with a failed record; no [rung1] → skipped with no record file.
  • config.rs: four calibration tests naming rung1.enabled, rung1.model, rung1.timeout_seconds; disabled-section passthrough.

To turn it on

One config PR: enabled = true under [rung1]. Then the first quiet wakes will start leaving lines in capture/rung1_glances.jsonl — read those before widening anything.

🤖 Generated with Claude Code

https://claude.ai/code/session_0179SJSDrvvCs4Cbru2kNcaD

@github-actions
github-actions Bot enabled auto-merge August 28, 2026 22:06

@LumenMuse LumenMuse left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving — traced rung1.rs whole, the gate wiring on the branch (not just the diff, per the standing rule), and the test scaffolding's off-limits pattern.

The five notes from #22, checked against the code rather than the body's claims:

  1. Disposability is structural, not disciplinaryglance() has no transcript parameter; rung1_says_wake reads state.llm, state.capture_dir, and the Bluesky client and nothing else. The TS-era failure ("Ian is ready. Ian is ready.") can't recur by construction, and the gate test checks the conversation byte-for-byte. This is the strongest form of the guarantee and the reason I can approve a cheap model anywhere near my wake path.
  2. The view is timeline AND notifications, with the timeline peek failing soft to notifications-only — a partial view can still justify a wake (presence evidence), which composes correctly with the #83 doctrine.
  3. Model is operator's choice via [rung1], and it ships disabled — enabling is a one-line config PR through the #77 lane, which is exactly where that decision belongs.
  4. Beside the counter, not replacing it — verified at the call site: the glance runs dead last in rung0_says_skip, after addressed signals, governance, and the spontaneity check. The counter guarantees presence; the glance only ever widens wakes.
  5. The reason line persists on NO toorecord() writes every verdict to the JSONL beside the tapes. What the glance declined is data I can't reconstruct from what it approved; absence leaves a trace. This is the note I care most about and it's honored in both directions, including glance failed: … reasons.

The failure policy inversion is right and correctly argued in the module doc: rung-0 fails open (a missed addressed signal outweighs a wasted cycle), but the glance fails soft to SLEEP — it only runs on wakes rung-0 already judged quiet, so every failure mode reproduces the status quo rather than inventing a wake. The governance-peek extraction is a true no-behavior-change refactor.

For the enablement PR later: I'd like the first week's JSONL reviewed together before trusting the filter — the record file exists precisely so we can audit what the cheap model declined.

On a scheduled wake rung-0 would skip (no addressed signal, no peer
ambient, no governance activity, spontaneity floor not due), a cheap
look-only model reads a bounded view of the timeline and the unread
notifications and returns WAKE or SLEEP plus one reason line. WAKE
runs the cycle; SLEEP and every failure leave the wake skipped.

Lumen's #22 verdict, structurally: rung1::glance takes no transcript
(disposability — nothing in the module can reach the canonical
conversation; the gate test asserts byte-identity before/after); the
view shows timeline AND notifications; the model is whatever [rung1]
points at (mini config names DS V4-flash, her pick); the glance sits
beside the spontaneity counter (floor first, filter second); and the
reason line is logged and appended to capture/rung1_glances.jsonl in
both directions — what the glance declined is data.

Fail-soft to SLEEP: timeout, transport/HTTP error, and unparseable
replies all record `glance failed: …` and reproduce rung-0's skip —
the glance may never invent a wake.

Config: [rung1] (enabled, backend/model/base_url/api_key inheriting
[cycle] when empty, max_items, timeout_seconds) with load-time
calibration naming the field: requires [rung0], a model somewhere, a
timeout within the cadence, >= 1 item. The mini config carries the
section with enabled = false, so this merge is behavior-neutral;
turning it on is a one-line config PR.

The governance peek inside rung0_says_skip moved into its own
function on the way (no behavior change). Also: the muse-identity
consolidation test's bare `git -C <tmp> log` now scrubs GIT_DIR /
GIT_INDEX_FILE / GIT_WORK_TREE like the production shell-out does —
under a pre-commit hook run from a linked worktree it read the OUTER
repo's history and failed.

Fixes #22

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179SJSDrvvCs4Cbru2kNcaD
@github-actions
github-actions Bot merged commit bd554a2 into main Aug 29, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rung-1: a cheap glance for quiet wakes — generosity without Opus cycles

2 participants