Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Async errors must retain account ownership instead of relabeling newer state

Date: 2026-10-28 UTC. Independent review of layout candidate
`c4df01e1567af10b6c4b0dc9d666ad946c1e0b75` found a Settings disclosure race.
The existing receipt-ordering lesson covered database writes; this incident
concerns the owner of already loaded UI state on an asynchronous error path.

## Evidence

The retained owned probe and unit regression ran with Node 22.18.0:

```sh
node /tmp/graphdone-resolution-20261027/layout-settings-review-negative/probe.cjs
npx vitest run src/hooks/__tests__/useLayoutRecovery.test.ts --root packages/web
```

A starts a discard while its account read is held. B signs in and B's pending
graph list loads. A's discard rejects after the held read returns. The old catch
spreads the newest state, including B's scopes, and overwrites its owner with A.
Returning to A before its next read resolves printed:

```text
visibleToA [{"graphId":"b-private-graph","count":1}]
error The signed-in account changed. No further layout changes were discarded.
```

`layout-settings-negative-unit.log` retains the failing hook test. After the
correction, `layout-settings-corrected-unit.log` reports 43 passed across seven
focused suites, including the new delayed account-switch regression.

## Cause and corrective rule

An owner label is provenance, not a presentation field to replace on failure.
Associate operations with the current owner scope identity, ignore stale
completion/error paths, and retain prior scopes only if their recorded owner
matches. Scope busy state too, so a previous owner's completion cannot clear a
new owner's pending operation. Number refresh requests to ignore older reads.
Test errors after a replacement account's data has loaded, and inspect the
first render after returning before the fresh read completes.

## Limits

The probe and unit test use a controlled account store. They prove the UI
interleaving, not hosted authorization or backend isolation. Existing real
browser account-switch and deleted-graph regressions remain required, as do
renewed exact-head review and strict CI before integration.
47 changes: 47 additions & 0 deletions .agents/inbox/2026-10-28-freeze-index-for-strict-runs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Freeze the index as well as runtime source during strict test runs

Date: 2026-10-28 UTC. Existing mobile touch evidence and the unified runner
already require matching source fingerprints. A layout recovery run exposed a
workflow mistake: staging the prepared implementation while the gate was active
changed the repository fingerprint even though the running renderer's bytes
were unchanged.

## Evidence

The owned run used:

```sh
TEST_URL=http://localhost:28818 QC_BROWSER_PORT=29818 xvfb-run --auto-servernum \
node tests/run-unified.mjs --sequence layout-recovery \
--out /tmp/graphdone-resolution-20261027/layout-recovery-strict-final
```

During execution, `git add` staged the implementation, tests and documentation.
The 2026-10-28 result was:

```text
layout-recovery: passed, 9 passed, 0 failed, 0 warned, 0 skipped (247.5s)
source-integrity: failed
The tested repository changed during this run; rerun against a stable source tree.
overall: FAILED, 9 passed, 1 failed, 0 warned, 0 skipped
```

The report remains retained. It is not counted as an accepted run. The existing
runner detected the mutation without a new implementation or weaker threshold.

## Cause and corrective rule

The source freeze includes repository/index state, not only files imported by
the running browser. Finish edits and staging, commit the candidate, then start
the exact-head gates. During a gate, keep that worktree's source, documentation,
index and runtime fixed. Read-only review and work in other owned worktrees can
continue. Wait for exit and inspect the entire report, including integrity and
execution records, before editing or crediting its case count.

## Limits

This incident establishes a provenance failure, not a failed browser behavior.
It also does not authorize ignoring source-integrity when only staging appears
to have changed. A subsequent complete, frozen-source run is required. Normal
ignored test outputs and owned external evidence remain separate from tracked
source; do not change the harness exclusions to hide an implementation mutation.
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# A persisted layout receipt does not by itself authorize replay

Date: 2026-10-28 UTC. Issue #181. Existing guidance distinguishes the old in-tab
queue from crash recovery; the first persistence prototype exposed additional
ordering boundaries during independent review. The corrections and regression controls now accompany this lesson. Final
exact-head review and CI remain the integration gate; preparatory local results
are not a release or hosted acceptance record.

## Evidence

The prototype is based on `525ca4e`, with its tracked patch and relevant source
hashes retained as `layout-reviewed-prototype.patch` and
`layout-reviewed-prototype-sha256.txt` in the owned dated evidence directory.
Twenty-three focused tests passed, but they did not cover the following cases.

On 2026-10-28, Node 22.18 ran the retained owned probes:

```sh
node /tmp/graphdone-resolution-20261027/layout-review-probes/probe.mjs
node /tmp/graphdone-resolution-20261027/layout-review-probes/discard-race.mjs
```

Sanitized output in `layout-review-negative.log`:

- Another tab saved `x=20` and cleared its receipt; the old tab then replayed
`x=10` and reported no pending work.
- A server accepted `x=30`, but local receipt cleanup failed. The prototype
reported `acknowledged=false`; retry sent the position again, replacing a
newer server value of `99` in the probe.
- A quota failure left one memory-only position. Discard tried to persist that
unwanted position first and failed, leaving it pending.
- Holding discard's commit and calling flush sent the position being discarded.

Source review also found that asynchronous recovery could arrive after D3
initialization without changing live coordinates, and that mapping pending
positions into a fresh work-item array on every camera render invalidated LOD
projection/label memoization. Both require actual browser regressions.

A second review reproduced a subtler variant after the first seven ordering
regressions passed. A quota-failed, memory-only edit could persist itself on
reconnect after another tab had already saved and cleared a newer receipt:

```text
oldAfterQuota {"pending":1,"local":0}
beforeOldReconnect {"server":20,"receipts":[]}
afterOldReconnect {"server":10,"sent":[["new",20],["old",10]],"pending":0}
```

The matching bundled probe is retained at
`layout-review-probes/corrected-prototype/volatile-stale.mjs`; the new failing
unit assertion is retained in `layout-volatile-negative-unit.log`. A real
IndexedDB/two-tab browser regression now covers the same ordering boundary.

The held-IndexedDB graph-switch regression also failed before the simulation
scope correction: after A→B, a synthetic pagehide raised **Layout needs
attention** because A's still-live simulation tried to queue through B. Graph
filters prevented a cross-graph server write, but did not establish correct
renderer ownership. `layout-switch-negative.log` retains that failed run.

The pending-layout LOD browser regression passed with stable memo inputs.
Deliberately recreating the source array during every camera render made it
fail after pan: labels moved between slots, including a 96px row change. The
corrected renderer was then restored byte-for-byte; the negative control remains
in `layout-lod-identity-negative.log`.

## Cause and corrective rule

Revision comparison during deletion protects newer receipts from old cleanup;
it does not establish eligibility to send an old receipt. Coordinate current
receipt eligibility, sending and acknowledgement across tabs. Preserve known
server acceptance separately from local cleanup, so cleanup retry cannot
silently become another network write. Serialize discard and flush, and do not
require a failed write to succeed before abandoning memory-only changes.

Persistence must also preserve eligibility. Compare the staged batch's original
observed graph generation inside the same account transaction that persists it.
Do not silently rebase a failed staged edit after reading a newer generation.
Retain generation fences after ACK/discard; a bounded graph-fence map falls back
to an eviction watermark advanced to the account counter only when an old entry
is evicted. Ordinary unrelated graph saves must not advance that fallback. Validate
safe integers and fail at corruption/overflow. The first fallback used the
current counter and unnecessarily rejected an unrelated graph’s first save;
`layout-fence-unrelated-negative.log` retains that failure. This deliberately rejects some
concurrent independent edits rather than guessing how to merge coordinates.

Hydrate layout recovery before initializing the graph, or explicitly apply an
eligible recovered revision to the live renderer. Keep semantic source identity
stable during camera changes. Durability status must reflect the actual boundary
completed, including the absence of a successful initial storage read. Stop the
previous owner/graph simulation during layout cleanup and validate its scope at
each persistence boundary; hiding its SVG does not stop its callbacks.

## Limits and remaining work

The probes use an owned account-store double; they are evidence of the ordering
failures. The subsequent strict browser run passes eight cases against the owned
API/Neo4j stack, with real IndexedDB and an isolated browser process group killed
by SIGKILL. It includes delayed hydration, reload before ACK, restart, quota and
two-tab ordering, account isolation, deleted/revoked access and Settings discard,
accepted-save cleanup, and unsupported capabilities. See the
[layout recovery record](../../docs/quality/LAYOUT_RECOVERY_2026-10-28.md) for
commands, the pending-layout LOD gate, retained fixture failures and limitations.

The first process-loss run failed strict profile removal with ENOTEMPTY. Killing
only the browser parent did not bound all profile writers. The fixture now owns
an isolated process group, kills that exact group and awaits the child exit
before reopening/removing its profile. No broad process matching or removal
retry was substituted for ownership. The next run caught a fixture viewport
mistake: the restarted browser used its default width and the desktop selector
was correctly hidden. The fixture now explicitly restores its intended viewport.
Both failures remain retained; neither was an application durability pass.

Issue #181 excludes server-side multi-actor conflict resolution. A crash after
server acceptance but before durable receipt deletion leaves an ambiguous
at-least-once boundary without a server idempotency/revision contract. Do not
claim exactly-once replay or protection from later writes on another device.
Known acceptance in the live process still requires truthful cleanup-only retry.
3 changes: 3 additions & 0 deletions .agents/issues/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

| Symptom | Canonical lesson and regression |
| --- | --- |
| All browser cases pass but unified source-integrity fails after staging | [Freeze the index as well as source](../inbox/2026-10-28-freeze-index-for-strict-runs.md); commit before the gate, leave repository content and index unchanged |
| Switching accounts briefly exposes another account’s pending graph IDs after a delayed error | [Async errors must retain account ownership](../inbox/2026-10-28-async-errors-must-retain-account-ownership.md); `useLayoutRecovery.test.ts` delayed A-discard → B-load → A-return regression |
| A persisted layout replays after another tab saved, cleanup failure resends accepted work, or discard races recovery | [Persisted receipts do not authorize replay](../inbox/2026-10-28-persisted-layout-receipts-do-not-authorize-replay.md); `recoverablePositionQueue.test.ts`, `layoutPersistence.test.ts`, `layout-recovery.spec.ts`; retained stale/volatile replay and hydration-switch negatives |
| A tool upgrade's negative test exits before the intended failing task executes, or a hoisted renderer path changes | [Require executed consumer evidence](../inbox/2026-10-27-tool-upgrades-need-executed-consumer-evidence.md); installed Turbo and Mermaid contracts |
| Pruned runtime starts failing imports, retains dev tools or loses auth across container replacement | [Installed consumer and persistence proof](../inbox/2026-10-27-runtime-images-need-installed-consumer-proof.md); `tests/runtime/run-image.mjs` |
| A toolchain upgrade changes test counts, loses matcher types or drops unimported coverage | [Preserve discovery and consumer behavior](../inbox/2026-10-27-toolchain-upgrades-must-preserve-test-contracts.md); [migration evidence](../../docs/quality/TOOLCHAIN_MIGRATION_2026-10-27.md) |
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@ jobs:
env:
TEST_URL: http://localhost:3127
CI: true
- name: Durable layout recovery acceptance
run: xvfb-run --auto-servernum node tests/run-unified.mjs --sequence layout-recovery --out test-artifacts/ci-layout-recovery
env:
TEST_URL: http://localhost:3127
CI: true
- name: Route compatibility acceptance
run: xvfb-run --auto-servernum node tests/run-unified.mjs --sequence router-navigation --out test-artifacts/ci-router-navigation
env:
Expand Down
9 changes: 9 additions & 0 deletions docs/quality/DURABILITY_REVIEW_2026-10-23.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,12 @@ The provider retains at most 16 graph/account scopes and 10,000 distinct pending
## Limits

Pending layouts are tab-memory state, not a persistent outbox: a forced close, crash, or browser ignoring the unload warning can still lose them. While in List or another route, the queue waits for the original graph to reopen; the user-facing retry message says so. Already-issued requests can complete under their original account, but later requests cannot use a replacement account's credentials. These tests do not establish browser rendering, live-service health, cross-device conflict resolution, or backend atomic deletion protection; those require separate integration/release checks.


## 2026-10-28 follow-up

The historical memory-only limit above is addressed by issue #181's
[network-backed layout recovery implementation and evidence](LAYOUT_RECOVERY_2026-10-28.md).
It reuses the existing account IndexedDB store, adds account/graph/node receipts
and bounded local generation fences, and keeps offline-sync durability separate.
The original navigation, owner/token and deletion regressions remain required.
Loading
Loading