Skip to content

program: declare per-block shimmer by stable content-id (spec 0053 + implementation) - #567

Merged
edwin-zvs merged 3 commits into
mainfrom
spec-program-shimmer-block-addressing
Jun 29, 2026
Merged

edwin-zvs merged 3 commits into
mainfrom
spec-program-shimmer-block-addressing

Conversation

@edwin-zvs

Copy link
Copy Markdown
Contributor

Why

A live program showed the shimmer animation inverted: the inert sections (# Rule, ## TODO, ## Done) shimmered for the whole turn while the two actively-worked in-progress items went calm — the opposite of "pending work shimmers."

Root cause: shimmer is inferred from block-content changes. A block shimmers from Run start until its text changes (narrow_program_run retains a block's signature while its text is unchanged; the agent contract's "any edit without shimmer:true clears the block" is not what the daemon actually does). That model cannot express "this block is settled but its text is unchanged" — so no-work blocks, which by definition never change, can never be cleared by the planning pass, while worked blocks settle the moment the agent rewrites them. Net: the inverse.

What

Spec-only change establishing the fix design (implementation to follow):

  • New 0051-program-shimmer-block-addressing — shimmer becomes a declared per-block state addressed by a stable, content-derived block id:
    • Read returns an ordered per-block projection (id + text + shimmer), derived (never persisted).
    • Edit takes an optional partial {id, shimmer} list that can target any block, not only edited ones — so the planning pass clears # Rule/## TODO/## Done without touching their text.
    • Update requires a complete declaration over the new doc's blocks.
    • Execute stays optimistic-all (optional initial set).
    • Content-id addressing is stable vs position, fails closed under a concurrent human edit (drops the stale declaration; never retargets a neighbor the way an index would), and is concurrency-consistent with the existing anchored content edits. Writes echo the fresh projection.
  • Update 0042 — narrow rule and published run facts are now the per-block projection rather than opaque pending signatures.
  • Update 0048 — planning pass declares the pending set (no need to textually touch settled blocks); removed the false "daemon clears any edit without shimmer:true" claim and pointed the mechanism at 0051.

Design decisions (locked in discussion)

  • Content hash, not index — index fails open (silently flips the wrong block on insert/reorder/concurrent edit); content-id fails closed.
  • No persisted/sticky block IDs — would need hidden markdown markers or a reconciliation side-table, rejected by 0042's plain-markdown stance and overkill for a transient, turn-bounded animation.
  • Partial on edit, complete on update — edit is frequent/targeted (partial avoids accidental-settle); update carries the whole doc (complete is unambiguous and prevents inheriting a stale set).

Scope

Docs only (specs/). No code; nothing to build or run. Implementation across crates/{protocol,daemon,mcp,cli} is a follow-up PR.

Shimmer was inferred from block-content changes: a block shimmered from
Run start until its text changed. That can't express "settled but text
unchanged", so no-work blocks (which never change) stayed shimmering for
the whole turn while actively-worked blocks settled — the exact inverse
of intent reported on a live program.

Add spec 0051: shimmer is a declared per-block state addressed by a
stable, content-derived block id (not a positional index, not an inferred
diff). get returns an ordered per-block projection (id + text + shimmer);
edit takes an optional partial {id, shimmer} list that can target any
block; update requires a complete declaration; execute stays optimistic.
Content-id addressing fails closed under concurrent human edits (drops the
stale declaration, never retargets a neighbor) — same semantics anchored
content edits already have. Writes echo the fresh projection.

Reconcile 0042 (narrow rule + published run facts now per-block projection)
and 0048 (planning pass declares the pending set without touching settled
blocks; fix the false "daemon clears any edit without shimmer:true" claim).

Spec-only; implementation to follow.
…-id (0051)

Replace the inferred shimmer mechanism (a block shimmered until its text
changed, which could never clear an unchanged no-work block — the reported
inversion) with the declared model from spec 0051.

protocol: add program_block_id (FNV-1a, dependency-free, shared by daemon
and clients) and program_block_spans (one parser for block identity); add
ProgramBlockSpan / ProgramBlockView / ProgramShimmerDecl. Rename
ProgramRunProgress.pending_block_signatures -> pending_block_ids (serde
alias keeps the old wire name). Drop ProgramEdit.shimmer; add shimmer to
edit (partial {id,shimmer} list), update (required complete Vec<bool>) and
execute (optional initial Vec<bool>); echo `blocks` from get/edit/update/
execute results.

daemon: narrow_program_run now applies the per-block declarations against
the post-edit document (changed ids drop, declared ids set, unknown ids
ignored — fail closed); set_program_run_pending for update's authoritative
declaration; program_blocks_projection builds the get/echo projection;
start_program_run accepts an optional initial set. Run prompts rewritten to
the declared-id planning pass.

mcp: tool schemas + dispatch for the new shimmer params across edit/update/
execute, and get now documents the per-block projection.

cli: render and re-run logic key on block id via the shared parser; param
sites pass shimmer; remove the now-unused signature field.

Tests: protocol id invariants; daemon planning-pass clears inert blocks
without text change (the bug), update complete-declaration + length check,
edit unknown-id fail-closed.
@edwin-zvs edwin-zvs changed the title spec: declare program shimmer per-block by stable content-id (0051) program: declare per-block shimmer by stable content-id (spec 0051 + implementation) Jun 28, 2026
@edwin-zvs

Copy link
Copy Markdown
Contributor Author

Implementation added (commit 2a46c7c)

This PR now carries the full implementation, not just the spec.

protocol — program_block_id (FNV-1a, dependency-free, shared by daemon + clients so ids always agree) and program_block_spans (single source of truth for block boundaries/identity). New ProgramBlockSpan / ProgramBlockView / ProgramShimmerDecl. ProgramRunProgress.pending_block_signatures → pending_block_ids (serde alias keeps the old wire name). ProgramEdit.shimmer removed; shimmer added to edit (partial {id,shimmer} list), update (required complete Vec<bool>), execute (optional initial Vec<bool>). Results echo blocks.

daemon — narrow_program_run applies the per-block declarations against the post-edit doc: changed ids drop, declared ids set pending/settled, unknown ids are ignored (fail closed). set_program_run_pending handles update's authoritative declaration; program_blocks_projection builds the get/echo projection; start_program_run takes an optional initial set. Run prompts rewritten to the declared-id planning pass.

mcp — schemas + dispatch for the new shimmer params across the three writers; get documents the projection.

cli — render + re-run logic key on block id via the shared parser; param sites updated.

Tests

  • protocol: id is position-stable, changes with content, identical blocks share an id; legacy field still deserializes.
  • daemon: planning pass clears inert blocks without any text change (a direct reproduction of the reported inversion — was impossible before), update complete-declaration + length validation, edit unknown-id fail-closed.
  • Full cargo test --workspace green; cargo build clean.

Relevant binary (debug): construct (daemon + cli + mcp ship in one binary) at .claude/worktrees/spec-program-shimmer-block-addressing/target/debug/construct.

…053, break pending_block_signatures compat

main advanced with #569 (keep run shimmer alive past owning-session idle),
which reworked the same shimmer lifecycle and added agent_managed to
ProgramRunProgress, plus #563/#568 (specs 0051 custom-templates, 0052
session-removal-cascades).

Reconcile:
- protocol: keep pending_block_ids rename + the declared-shimmer types
  alongside #569's agent_managed field; DROP the pending_block_signatures
  serde alias (clean break, per request) and remove the legacy-compat test.
- daemon: fold #569's agent_managed + inactivity-backstop refresh into the
  declaration-based narrow_program_run and set_program_run_pending so a
  complete/partial declaration also marks the run agent-managed; #569's
  AwaitingInput/terminal stop-signal split is preserved. Adapt #569's two
  lifecycle tests to the id-based API.
- specs 0042/0048: combine #569's stop-signal model with the declared
  per-block-id model; my new spec collides with main's 0051/0052, renumbered
  to specs/0053-program-shimmer-block-addressing and all references updated.

cargo test --workspace green.
@edwin-zvs edwin-zvs changed the title program: declare per-block shimmer by stable content-id (spec 0051 + implementation) program: declare per-block shimmer by stable content-id (spec 0053 + implementation) Jun 29, 2026
@edwin-zvs

Copy link
Copy Markdown
Contributor Author

Rebased on latest main (merge fe2d0c9)

main advanced underneath this PR with #569 (keep run shimmer alive past owning-session idle — it reworked the same lifecycle and added an agent_managed field), plus #563/#568. Reconciled:

cargo test --workspace green post-merge (including #569's lifecycle tests + the four new ones).

@edwin-zvs
edwin-zvs merged commit 9cb66f5 into main Jun 29, 2026
1 check passed
@edwin-zvs
edwin-zvs deleted the spec-program-shimmer-block-addressing branch June 29, 2026 00:30
edwin-zvs added a commit that referenced this pull request Jun 29, 2026
…g (0053) (#576)

Diagnosed from the 'construct improvements' session: after the planning pass
declared the task pending, dispatching a subagent moved the task into the
"In progress" section and appended a @{session} clip — which changes the
block's text and therefore its id. The old id dropped from the pending set
before the new id was declared, so the set transiently emptied; the daemon
reaped the whole run on that empty, and the agent's follow-up re-declaration
of the new id hit a removed run and was a silent no-op. All shimmer vanished.
The agent's own narration: "the system won't re-light a settled/new block".

Fix #1 — survive transient empty. narrow_program_run / set_program_run_pending
no longer remove a run when its pending set empties; program_run_snapshot
reports no active run on empty but KEEPS the record so a later declaration
revives it. An empty run is reaped only on a terminal owning-session state,
the owning session going idle with nothing pending, or the inactivity
backstop (note_session_state handles the idle case).

Fix #2 — atomic keep_pending. Re-introduce a per-edit ProgramEdit.keep_pending
flag: an edit that changes a still-pending block re-adds the resulting block's
new id in the SAME narrowing call (anchored on the edit's new_string), so a
move/annotate never transiently empties the set and the agent needn't know the
post-edit id. The call-level shimmer id-list still handles declaring arbitrary
existing blocks (planning pass).

Specs 0042 (stop lifecycle: transient empty is not a stop), 0048 (contract:
use keep_pending when editing an in-flight block), 0053 (both mechanisms +
transient-empty survival). Three new daemon tests; #567/#569 tests unaffected.
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.

1 participant