Skip to content

refactor(daemon): remove the superseded coarse snapshotRefsStale marker (ADR 0014 step 8)#1268

Merged
thymikee merged 5 commits into
mainfrom
claude/dazzling-hawking-73p6kw
Jul 15, 2026
Merged

refactor(daemon): remove the superseded coarse snapshotRefsStale marker (ADR 0014 step 8)#1268
thymikee merged 5 commits into
mainfrom
claude/dazzling-hawking-73p6kw

Conversation

@thymikee

Copy link
Copy Markdown
Member

Summary

Follow-up to the merged #1257 (ADR 0014 steps 1–7 + docs). This lands the final migration step 8: removing the superseded coarse snapshotRefsStale client-stale marker, now fully redundant with the ref-frame model. It carries no behavior change for mutations (those are already frame-driven) and refines only the read-only staleness warning to be frame-derived.

What changes

  • setSessionSnapshot / buildNextSnapshotSession no longer set or clear the marker — replacing the latest observation is a read that never touches the authorized frame.
  • Read-only ref staleness derives from frame state (resolveRefStalenessWarning): a plain @ref warns once the frame has expired (a device side effect changed the screen since issuance); a read-only capture no longer marks refs stale, because it does not expire the frame (ADR 0014 evidence bug: heading element/group missing from the tree #6). Pinned-ref warnings keep comparing against the frozen frame epoch.
  • Deletes markSessionSnapshotRefsIssued (its only remaining job was clearing the marker) and its find call site, and removes the SessionState.snapshotRefsStale field.
  • Refreshes the module/handler docs and the ADR status note (step 8 recorded as landed; read-staleness described as frame-derived).

Tests

Migrates every test off the marker to the frame model — frame-expiry drives the read warning; complete/partial activation drives admission. Notable conversions:

  • resolveRefStalenessWarning unit test → plain ref warns only while the frame is expired.
  • snapshot/diff → asserts the frame is re-activated by snapshot and preserved by diff (a read).
  • get text @ref → warns while the frame is expired but still resolves against the retained frame tree.
  • evidence bug: heading element/group missing from the tree #6 → a real read-only capture (setSessionSnapshot) bumps the generation but leaves the frame active, so the plain-ref mutation is admitted.

Why now / why a separate PR

The plan was to land this inside #1257, but that PR merged first. Per the repo's git guidance, this restarts the branch from the freshly-merged main and ships step 8 as its own reviewable PR rather than stacking on merged history.

Validation

  • tsc, oxlint, oxfmt, fallow audit, check:production-exports all clean.
  • Full unit-core + provider-integration green (4082 tests).

🤖 Generated with Claude Code


Generated by Claude Code

@thymikee thymikee force-pushed the claude/dazzling-hawking-73p6kw branch from 06ba45d to bfbafa3 Compare July 15, 2026 05:40
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.7 MB 1.7 MB -61 B
JS gzip 550.9 kB 550.9 kB -26 B
npm tarball 665.2 kB 665.2 kB -36 B
npm unpacked 2.3 MB 2.3 MB -61 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 27.8 ms 29.4 ms +1.6 ms
CLI --help 59.5 ms 60.0 ms +0.5 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/interaction.js +84 B +6 B
dist/src/gestures.js +19 B +5 B
dist/src/internal/daemon.js -69 B -2 B
dist/src/selector-runtime.js 0 B +2 B
dist/src/session.js 0 B -1 B

@thymikee

Copy link
Copy Markdown
Member Author

Review of exact head bfbafa3fb found three blockers in the read/warning migration.

  1. get text @ref still resolves through requireSnapshotSession, which reads session.snapshot, not the retained ref-frame tree. After an internal read-only capture replaces the observation, an active plain ref can therefore resolve the new tree positional @eN silently. ADR 0014 requires reads to use retained frame evidence and never fall through by positional coincidence. Add a regression with divergent observation and frame trees; resolve from the frame tree or fail when that evidence is missing.

  2. Removing markSessionSnapshotRefsIssued from mutating find exposes a spurious warning path. find ... fill ... internally invokes fill @ref with internal.findResolvedTarget; interaction-touch still derives and returns a stale-ref warning from the prior frame. The user supplied a locator and the ref was minted only for the internal action, so the public find response must not claim the user consumed stale refs. Add a locator-based mutating-find contrast starting from an expired frame.

  3. resolveRefStalenessWarning returns clean for a pin matching the frame epoch before checking whether that frame is expired. A matching pin proves identity within the retained frame, not that the UI is current after a device side effect. ADR 0014 required evidence 17 says retained stale reads return the structured warning. Check frame expiry first for read warnings, then use epoch mismatch for the precise generation warning; update the unit test that currently blesses pinned-current plus expired as clean.

CI is green and the coarse-marker removal is otherwise well scoped, but this head is not merge-ready.

Copy link
Copy Markdown
Member Author

All three blockers addressed in c8c093b:

  1. @ref reads bind against the frame treerequireSnapshotSession now resolves against refFrameSnapshot ?? snapshot, so an internal read-only capture that replaced the observation can no longer let a plain @eN resolve a different element by positional coincidence. Missing frame evidence fails instead of falling through to the newer observation.
  2. Mutating find's internal dispatch carries no stale-ref warning — both the press and fill paths now gate staleRefsWarning on !internal.findResolvedTarget, so the public find response never claims the caller consumed a @ref it didn't.
  3. Frame expiry is checked before the epochresolveRefStalenessWarning returns the coarse warning for an expired frame for any ref, including a pin matching the epoch; the unit test that previously blessed pinned-current-plus-expired as clean was rewritten.

Regressions added:

  • evidence #17: get text @ref reads the retained frame tree, not a newer observation — divergent observation vs. frame trees; resolves @e2 from the frame tree (Continue), fails on missing evidence (@e9), never recaptures.
  • blocker-2: a mutating find's internal fill from an expired frame carries no stale-ref warning — a user @ref rejects (ref_frame_expired) while the exact internal request find hands the leaf succeeds clean.

tsc, oxlint, oxfmt, fallow audit, check:production-exports clean; full unit-core green (3970 tests).


Generated by Claude Code

@thymikee thymikee force-pushed the claude/dazzling-hawking-73p6kw branch from c8c093b to 51b25d9 Compare July 15, 2026 08:18
@thymikee

Copy link
Copy Markdown
Member Author

Addressed the reviewer's two documentation/comment defects on top of 51b25d930 (new head 79fada087). This is a docs/comments/warning-text-only pass — no runtime behavior changed.

Defect 1 — stale-ref warning comments describing the removed coarse-marker mechanism

Two comments in src/daemon/selector-runtime.ts still described the pre-removal mechanism ("warn when that tree was replaced since the client last received refs" / "warn when the stored tree already drifted from the refs the client holds") as if it still happens:

  • dispatchGetViaRuntime (was line 156-158): now describes the actual frame-derived mechanism from resolveRefStalenessWarning — an expired ref frame warns (any ref), otherwise a pinned @e12~s3 ref whose epoch no longer matches gets the precise generation-mismatch warning.
  • dispatchWaitViaRuntime (was line 259-263): same correction.

The warning STRING itself (STALE_SNAPSHOT_REFS_WARNING in src/daemon/session-snapshot.ts) and its own doc comment were already accurate post-removal — no change needed there.

Defect 2 — present-tense references to the removed marker

  • src/daemon/session-snapshot.ts (was line 75): markSessionPartialRefsIssued's early-return comment said an empty partial result "leaves ALL session state untouched, including the coarse marker" — that field no longer exists. Reworded to name what's actually preserved: "the ref frame fields set below."
  • docs/adr/0014-session-ref-frame-lifetime.md (line 66, in "Ref frames are separate from operational observations"): still described snapshotRefsStale as part of "the existing... implementation" in present tense, contradicting the Decision section's own line 39 (which already correctly states migration step 8 removed it). Reworded to keep the historical mention ("the marker it originally paired with has since been removed") while stating the removal, consistent with line 39's phrasing.

Classified as historical/keep-as-is (no change), since they describe the problem/plan rather than claim the marker still exists:

  • ADR line 24 (Context section — motivating problem before the decision)
  • ADR line 39 (Decision section — already states removal correctly)
  • ADR lines 438, 446 (Migration section — step list and completed-status description)
  • src/daemon/ref-frame.ts:17 — already accurately describes the frame-derived mechanism plus the historical removal note; no change needed
  • src/mcp/command-tools.ts:151, 183 — "coarse" here is a granularity adjective for the still-existing STALE_SNAPSHOT_REFS_WARNING (blunt/any-ref) contrasted with the "precise" pinned-generation warning; it's not describing the removed marker's implementation, and remains accurate
  • src/daemon/session-snapshot.ts:114 (in resolveRefStalenessWarning's docstring) — same "coarse" (vs. "precise") granularity usage, already accurate

Verification on this branch: tsc --noEmit clean, oxlint --deny-warnings clean, oxfmt --check clean on the touched files, and the specified suites (session-snapshot, find, interaction-settle, interaction, snapshot-handler — 152 tests) all pass.

@thymikee

Copy link
Copy Markdown
Member Author

Follow-up review of exact head 79fada087 found one remaining blocker despite green CI.

STALE_SNAPSHOT_REFS_WARNING in src/daemon/session-snapshot.ts still says:

The session snapshot changed after these refs were issued, so @refs may now point at different elements.

That is no longer the condition represented after ADR 0014 step 8. The warning is emitted when the retained ref frame is expired, including device-side effects where no session snapshot changed. Read-only refs also resolve against the retained frame tree, so they do not silently retarget to a newer positional tree.

Please replace this with frame-lifetime wording that is valid for both reads and mutation rejection, for example:

The UI may have changed since these refs were issued, so they no longer represent current device state. Take a new snapshot before relying on or interacting with them.

Also clean up the remaining stale terminology while touching this area:

  • src/daemon/selector-runtime.ts: comments should say reads resolve against the retained ref-frame evidence, not the "stored snapshot" or fresh polling captures.
  • src/commands/interaction/runtime/settle.ts: the comment should describe frame expiry rather than marking refs stale through the old writer mechanism.
  • src/daemon/handlers/__tests__/interaction-settle.test.ts: rename the test that still says it clears a stale marker.

The runtime migration itself looks coherent and the latest ADR correction is accurate. The user-facing warning text is the merge blocker; the comment/test-name cleanup is non-behavioral but should land in the same pass.

claude and others added 4 commits July 15, 2026 17:19
…er (ADR 0014 step 8)

The coarse `snapshotRefsStale` client-stale marker is fully superseded by the
ref-frame model and is removed:

- `setSessionSnapshot` and `buildNextSnapshotSession` no longer set/clear it —
  replacing the latest observation is a read that never touches the frame.
- Read-only ref staleness now derives from frame state: a plain ref warns once
  the frame has EXPIRED (a device side effect changed the screen), and a
  read-only capture no longer marks refs stale because it does not expire the
  frame. Pinned-ref warnings keep comparing against the frozen frame epoch.
- Deletes `markSessionSnapshotRefsIssued` (its only job was clearing the marker)
  and the `session.snapshotRefsStale` field.

Migrates every test off the marker to the frame model (frame-expiry drives the
read warning; complete/partial activation drives admission), and updates the
ADR status + module docs to record step 8 as landed. Ships as follow-up to the
merged #1257 since that PR closed before this step.

Full unit-core + provider-integration green; tsc/lint/fallow/production-exports clean.

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

Address three review blockers on the coarse-marker removal (ADR 0014 step 8):

1. @ref reads now bind against the authorized frame tree
   (`refFrameSnapshot ?? snapshot`) in `requireSnapshotSession`, so an
   internal read-only capture that replaced the observation cannot let a
   plain `@eN` resolve a different element by positional coincidence.
   Missing frame evidence fails instead of falling through to a newer
   observation.

2. A mutating find's internal leaf dispatch (`internal.findResolvedTarget`)
   no longer attaches a stale-ref warning in either the press or fill path —
   the caller never consumed a `@ref`, so the public find response must not
   claim it did.

3. `resolveRefStalenessWarning` checks frame expiry FIRST, matching the
   admission order: an expired frame is stale for any ref, even a pin that
   matches the epoch (a matching pin proves identity within the retained
   frame, not that the UI is current).

Regressions: divergent observation-vs-frame trees resolve from the frame
tree or fail when evidence is missing; a locator-based mutating find from an
expired frame carries no stale-ref warning; the reordered resolver unit test.

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

The get/wait dispatch comments in selector-runtime.ts still described the
superseded coarse snapshotRefsStale marker ("warn when that tree was
replaced since the client last received refs") even though staleness is
now derived from ref-frame expiry (ADR 0014 migration step 8). Reworded
both to describe the frame-derived mechanism actually implemented by
resolveRefStalenessWarning.

session-snapshot.ts's early-return comment in markSessionPartialRefsIssued
referenced "the coarse marker" as something still left untouched, but that
field no longer exists — reworded to name the ref frame fields it actually
preserves.

ADR-0014's "Ref frames are separate from operational observations" section
still described snapshotRefsStale as part of "the existing... implementation"
in present tense, contradicting the Decision section's own note (line 39)
that migration step 8 already removed it. Reworded to keep the historical
mention while stating the removal.
Address the follow-up review blocker plus the co-located terminology cleanup
(ADR 0014 step 8):

- STALE_SNAPSHOT_REFS_WARNING no longer claims "the session snapshot changed";
  it now describes frame lifetime in terms valid for both read warnings and
  mutation rejection — the UI may have changed since the refs were issued, so
  take a new snapshot before relying on or interacting with them. The warning
  fires on frame expiry, including device side effects where no stored snapshot
  changed.
- selector-runtime.ts: the get/wait @ref comments now say the read binds to the
  retained ref-frame evidence and its staleness is frame-derived, not a property
  of the stored snapshot or the live polling capture.
- settle.ts: an unsettled stored capture replaces the observation without
  touching the ref frame; read staleness is driven by side-effect-seam expiry,
  not by storing a fresh observation.
- interaction-settle.test.ts: renamed the settle test off the removed
  stale-marker language to "activates a partial ref frame" (what it asserts).

Comments/test-name/warning-text only — no runtime behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmnraQhvxzEFEUoUiqRpJa
@thymikee thymikee force-pushed the claude/dazzling-hawking-73p6kw branch from 79fada0 to 87bdaf6 Compare July 15, 2026 17:19

Copy link
Copy Markdown
Member Author

Addressed on 87bdaf6 (rebased onto latest main cc1a0ec). Warning-text/comments/test-name only — no runtime behavior change.

Merge blocker — STALE_SNAPSHOT_REFS_WARNING now reads:

The UI may have changed since these refs were issued, so they no longer represent current device state. Take a new snapshot before relying on or interacting with them.

Valid for both the read warning and mutation rejection, and no longer claims a session snapshot changed (the condition is frame expiry, including device side effects where no snapshot changed). Every test references the constant, so no fixture churn.

Terminology cleanup in the same pass:

  • selector-runtime.ts: the get/wait @ref comments now say the read binds to the retained ref-frame evidence and its staleness is frame-derived — not a property of the "stored snapshot" (get) or the live polling capture the condition is checked against (wait).
  • settle.ts: an unsettled stored capture replaces the observation without touching the ref frame; read staleness is driven by side-effect-seam expiry, not by storing a fresh observation.
  • interaction-settle.test.ts: renamed the settle test off the stale-marker language to …activates a partial ref frame (what the body already asserts).

tsc, oxlint, oxfmt, fallow audit, check:production-exports clean; full unit-core green (3982 tests).


Generated by Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

One remaining wording/test issue at head 87bdaf6a59c132709a64729987daebe90cfc0736:

buildPinnedStaleRefWarning still says the pin no longer matches “the stored tree” and that “the session tree is now sN” (src/daemon/session-snapshot.ts:93-105), but the resolver now compares against refFrameEpoch(session), not the latest observation generation. After a read-only capture those values can diverge, so this wording is ambiguous/misleading under ADR 0014. Please name the current ref-frame epoch (or equivalent) and add a regression where observation generation advances while the frame epoch remains frozen.

The frame-based admission/read behavior otherwise looks consistent, and all 22 checks are green.

The pinned-ref warning is compared against refFrameEpoch(session) — the frozen
frame epoch — not the latest observation generation, and after a read-only
capture those two diverge. The message still said "the session tree is now sN",
which is ambiguous once the observation counter has advanced past the frame
epoch. Name the ref-frame epoch instead:

  Ref @E12 was minted from snapshot s3 but the session's ref frame is now s15 —
  re-run snapshot -i.

Renames the builder param to `currentFrameEpoch` and corrects its doc comment to
say the pin is compared against the frame epoch, not the stored tree generation.

Regression: `resolveRefStalenessWarning` names the frozen frame epoch, not the
bumped observation generation — a read-only `setSessionSnapshot` advances the
observation counter (15 -> 16) while the frame epoch stays frozen at 15; a pin
at s15 is clean and a pin at s12 names s15, never s16.

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

Copy link
Copy Markdown
Member Author

Fixed on 1fa7dc2.

buildPinnedStaleRefWarning now names the ref-frame epoch it actually compares against, instead of "the session tree":

Ref @E12 was minted from snapshot s3 but the session's ref frame is now s15 — re-run snapshot -i.

The builder param is renamed currentFrameEpoch and its doc comment corrected to state the pin is compared against refFrameEpoch(session) (frozen at issuance), not the latest observation generation — the two diverge after a read-only capture.

Regression added (resolveRefStalenessWarning names the frozen frame epoch, not the bumped observation generation): a real read-only setSessionSnapshot advances the observation counter 15 → 16 while refFrameGeneration stays frozen at 15; a pin at s15 resolves clean, and a pin at s12 names s15 (the frame epoch) — never s16.

tsc, oxlint, oxfmt, fallow audit clean; unit-core (3983) and the provider-integration versioned-refs scenario green.


Generated by Claude Code

@thymikee thymikee merged commit 22a3c47 into main Jul 15, 2026
22 checks passed
@thymikee thymikee deleted the claude/dazzling-hawking-73p6kw branch July 15, 2026 18:55
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-15 18:55 UTC

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.

2 participants