Skip to content

Keep a pasted link over a waiting save, and a size the drawer could not take - #65

Merged
Oliver-Johnson merged 19 commits into
mainfrom
claude/robustness-security-review-5ir9ye
Oct 10, 2026
Merged

Oliver-Johnson merged 19 commits into
mainfrom
claude/robustness-security-review-5ir9ye

Conversation

@Oliver-Johnson

@Oliver-Johnson Oliver-Johnson commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Requested by Oliver · project thread

What this changes

The page-code half of the optional findings left after #31, with #63's review optionals on top.

A link pasted over a page as an edit's save comes due (R11O1).

Before: you change the drawer width, and within the 400 ms before that save you paste a link into the address bar. The paste puts the link in the address at once, and its hashchange, which reloads onto it, comes after. A page still drawing the edit came to the waiting save first. That save wrote the page's own design back over the link, the hashchange found that design and reloaded it, and the link was gone with nothing said. It failed on every build, main included (the reviewer's R11O1 probe on Baseplates).

After: the link loads, with "This link replaced the layout you had here." Put back brings your layout, the edit included. The save is still made, so the layout the link sets aside has the edit. It just leaves an address the page did not write alone.

Back from the link goes to the address before the paste, which never had the edit. The edit is aside and in no earlier address, so Back offers it there: "This page went back to an earlier layout of yours. The later one is set aside." (round 1, O-a). Back offers only that save. Any other layout aside can be older than the page Back brings (round 2, S2).

How: each page keeps the address as it last left it (ownHash: as it arrived, then as each save wrote it). A save that finds a different address with a layout in it does not call replaceState. It records, for the tab, the fingerprints of the save and of the address it left (PASTED_KEY), and Back offers the layout aside only when it is that save and the address is that one. Both pages have the same code, so both get the fix.

A size changed with the drawer's saves refused (O4).

Before: with the browser's storage full, Kitchen open, you change the drawer width on Baseplates from 400 to 410 and go to Bins. The drawer could not take 410, so the hand-over carried it in the address alone. Bins caught up with the drawer on the way in, as it does when another tab has moved the drawer on, and showed 400. It did the same on main.

After: Bins shows 410, and so does Baseplates after the trip back. So does Bins reloaded, shown again from the back-forward cache, or gone Back to (round 1, S1). So does the page that made the change, reloaded, in a drawer only its own tool has saved into (round 2, S1b). Once the storage has room, the next change on either page saves 410 into the drawer.

The 410 also outlives a catch-up for something another tab changed, such as the depth, reloaded or from the cache (round 3, the reviewer's N1 and N1f). It outlives a hand-over onto a page that takes its own half from the drawer (round 3, R1). And a size another tab moved on is still caught up, even when the page's last save that went through kept that tab's size (round 3, M2).

Once the 410 is saved, it is no longer the page's change to keep (round 4, S3). Say Bins saves it into the drawer once there is room, and you then set the width back to 400 there. Back to Baseplates shows 400, and the drawer stays at 400. It does not matter whether Bins saved the 410 as it arrived, with a change of its own first, or as you set it back, or whether Baseplates saved it on the way to Bins.

A change refused on one page also outlives a save of another page's change (round 5). Say Bins, handed the 410, has its depth changed to 510, which the drawer refuses too. Back on Baseplates, with room, a change saves the 410. Forward to Bins still shows 510, and its next change saves it (F1). And a 410 handed over and then saved as another drawer, Pantry, is still Kitchen's change on Back to Baseplates, whatever Pantry's saves do (SA).

And once the 410 is saved and set back, it is no change of a later page's (round 6, F2). Say Bins, handed the 410, has its own save as it arrives refused too. Back on Baseplates, with room, a change saves the 410, and you set the width back to 400. Forward to Bins shows 400, and its next change keeps it.

How: a page that holds a change the drawer refused passes on its own base with it. That is the page's shared keys (size, bed, printer) as it last saved them or arrived with them. The flag and that base go in the address's own mark, the hand-over note, the tab's record of its last save, and the notes for a catch-up and for restore. A page that arrives with them counts its changes from that base, not from its design. The change is still to be saved, and whatever another tab moved on since is caught up, as on main. The pageshow return from the back-forward cache is main's again, since the page's base is already its own.

Each page's refusals also have ids: the ones it arrived with, and one of its own for the saves the drawer refuses before it next hands them on, by a hand-over or into the back-forward cache. Its records carry the list. When a save goes through while the page holds refusals, the page notes their ids for the tab, keeping the last 128. A record whose ids have all landed then counts for nothing, so Back to it is a page of yours come back, caught up with the drawer. The ids go when a refusal is dropped rather than saved: by Save as, or by the drawer being deleted.

Each id also has the base its own changes are counted from (round 6): the page's shared keys as it arrived, last saved, or came back from the cache. A record counts from its first id still to land. An id counts as landed once it or any id after it has, since the page that held the later one saved the earlier ones' changes with its own. A page keeps the last 32 ids, the first of them counted from the page's base, so the first page's change in a long chain is still saved.

A page shown again from the back-forward cache does the same once its refusals have all landed, and counts from its first id still to land once some have (round 6). A change to its own half refused with the size is the exception, since the later page saved only the size: the bar still says "not saving" until a save goes through (round 5, O1). And a catch-up that leaves the page's design as it is takes the drawer's settings as its base without reloading (round 4, optional 1), and rewrites the address's mark to say so (round 5, CU1).

  • Round 5 counted every id from the first page's base (F2). Bins' own refusal, made only by its save as it arrived, kept Baseplates' 410 as Bins' change after it was saved and set back, and Forward to Bins wrote 410 over the 400. A size typed after the page came back from the cache was caught up with the other page's the same way (SEALW).
  • Round 4 gave a whole chain of pages one id (F1). The save on Baseplates counted Bins' refused depth as saved too, and Forward to Bins showed the drawer's 380. It also kept the id through Save as (SA). A refusal in Pantry took Kitchen's id, its landing counted Kitchen's 410 as saved, and Back to Baseplates showed Kitchen's 400.
  • Round 4's return from the cache also cleared "not saving" whatever the page's own half held (O1). Its catch-up with nothing to reload left the mark with the base from before (CU1), so a reload, once another tab had set the size back to 400, wrote 410 over it.
  • Round 3 kept the refusal in the page's records after the 410 was saved (S3). Back to one of them found the drawer at 400, its base, so it kept 410 as a change still to save. It showed 410, and its next save put 410 over the 400 set back.
  • Round 2 counted the change from the drawer's settings instead (M2). A page's last save that went through could have kept another tab's 420, because mergeDesign keeps a size the page did not change. That page then took 420 as its base and its own stale 400 as the change, and once there was room it wrote 400 over the other tab's 420. Now it catches up to 420 and keeps it.
  • Round 1 counted it from the design, so the next save that went through left the drawer's 400.
  • Round 2 also kept a refused change only while the drawer had both marks as the page last knew them (M1). With the base carried, the marks play no part in this. A tab whose save kept another tab's size catches up from its base, as on main.

The set-aside record with the storage full (O1).

Before: a layout set aside where the layout fit and its record of whether it is a link's did not kept the record of the layout set aside before, which then answered for it.

After: that record goes, so put back, the layout counts as yours. The layout itself is kept and still offered back. Removing it as well, as suggested, would have left it only in the kept-back save until the first change on the link.

Start fresh undone by a change as the page goes.

Before: Start fresh cleared this browser's save and set the address, and the page ran on until the bare one arrived. A change that landed in between, such as a field's change or a Bins edit still being read in, was saved after the clearing. The bare page came back with it and said it had been restored. Main does the same. The reviewer's random sequence on #69 hit it once (seed 11, sequence 4).

After: Start fresh leaves by leaveFor, as Put back does, which stops the page's saves. It is still wired before the boot reads a link. hashReady and hashSaveT are declared above the wiring, so leaving works from there too.

Start fresh and Put back also forget the pasted-link record (round 3, O14b). Kept past Start fresh, a page at the address it names offered the edit as "the later one" after another link and Back, though Start fresh came after the edit. The record's key is now declared with the save's, so Start fresh clears it even when the boot stopped before reading a link (round 4, optional 2).

A page left by Start fresh and shown again from the back-forward cache saves again, as it does loaded (round 4, optional 3). This applies after Start fresh only. After Put back or Try anyway, the same page loaded is a link again and sets your layout aside first. From the cache, its first save would go over the layout just put back. So Put back or Try anyway pressed after Start fresh, before the fresh page arrives, turns it off again (round 5, optional 1). Start fresh pressed twice keeps it on (round 6, optional 5).

A page shown again from the cache after its drawer was deleted.

Before: delete Kitchen on Bins, then go Back to Baseplates, which the browser shows again from the back-forward cache. Its bar still says "Kitchen". Main does the same.

After: the bar says "not saved", as it does when the page is loaded (round 6, optional 4).

From #63's review:

  • The links tests wait for the save itself before going to another address, which drops a save still waiting. That is 16 fixed 900 ms waits before an arrive(), as Run the links tests on the served pages, and poll the saves they wait for #63 did in persist.spec.js. The new saved() also waits for a Bins field edit still to be read in.
  • The guide anchor test follows the guide's own link to "Drawer size to grid", so the hub-to-page carry is covered too.
  • The polls name what they wait for. The declined-link test checks the link is still recorded before the edit.
  • The markHere comment no longer says replaceState is refused on file://.

Also in round 1:

Round 2 (the re-check of round 1) is M1, S1b and S2, described above, with the reviewer's B3, B4, B5 and O8 as tests.

Round 3 (the re-check of round 2) is M2 and optionals 1 to 3, described above, with the reviewer's P2s, N1, R1 and O14b as tests. Optional 4 has no test: the page's own first save, 400 ms after it arrives, sets the same base, so only a race inside that window could tell the line is there.

Round 4 (the re-check of round 3) is S3 and optionals 1 to 3, described above, with the reviewer's A1 and A1b among the tests.

Round 5 (the re-check of round 4) is F1, SA, O1 and CU1, described above, with the reviewer's F1, SA and CU1 as tests. Of its optionals:

  • 1 is done: leaveFor clears freshLeft, and Start fresh sets it after.
  • 2: the tab keeps the last 128 ids landed.
  • 3: S3 and O1 are now also tested with a real back-forward-cache Back (see Checks).
  • 4: the hand-over's own save still lands the refusals, as every save that goes through does. It is redundant while the page saves before it hands over, and it keeps the rule the same in all three places.
  • 5: ids are read only as /^[a-z0-9]{1,16}$/, from a record or the tab's note.
  • 6: the pair saved before the hand-over is now titled "saved here as it leaves for that page".

Round 6 (the re-check of round 5) is F2, described above, with the reviewer's F2 and SEALW as tests. Of its optionals:

  • 1 (DEEP33) is done another way. A page still keeps the last 32 ids, but the first of them counts from the page's base, and an id counts as landed once a later one has. Checking only the last 32 ids of a record, as suggested, would also have counted the chain's first change as saved, so the last page took the 410 for no change of its own. A test now runs 33 hand-overs.
  • 2: the seal test is the reviewer's SEALW, from the cache, which also covers counting from the first id still to land. There is a test for ids and bases that are not what the page writes, too.
  • 3: drop() on deleting the drawer stays, as a defence.
  • 4 and 5 are done, described above.
  • 6: the known limits stand. The hand-over still makes an id after a save that went through, which the next page's first save lands.

O2 (no visible effect) and O3 (new on-page wording) stay open, as do round 1's O-b and O-c, which main has too.

Checks

  • I edited the sources in src/, not the generated index.html / bins/index.html
  • I ran node build.js and committed the regenerated pages alongside the sources
  • npm run test:all passes:
    • on d231b63 (round 6): the whole UI suite 814 of 814, npm test and node test/ci-sim.js clean, and GitHub's check and ui jobs green;
    • on ed19d84 (main 326cc48 merged in): the whole UI suite 804 of 804, npm test and node test/ci-sim.js clean, and GitHub's check and ui jobs green;
    • on 47987bd (round 5): the whole UI suite 802 of 802, npm test and node test/ci-sim.js clean, and GitHub's check and ui jobs green;
    • on 27af93c (main 88fef25 merged in): the whole UI suite 790 of 790, npm test and node test/ci-sim.js clean;
    • on 95ba00b (round 4): the whole UI suite 788 of 788, npm test and node test/ci-sim.js clean, and GitHub's check and ui jobs green;
    • on 00ccddf (main f9873f5 merged in): the whole UI suite 774 of 774, npm test and node test/ci-sim.js clean, and GitHub's check and ui jobs green;
    • on 6f2045f (round 3, main 5da18ce merged in): the whole UI suite 770 of 770, npm test and node test/ci-sim.js clean, and GitHub's check and ui jobs green;
    • on 23b5376 (round 3, main f319c83 merged in): the whole UI suite 769 of 769, npm test and node test/ci-sim.js clean;
    • on 984bd8a (round 2, main 3145cc7 merged in, and Start fresh): the whole UI suite 746 of 746, npm test and node test/ci-sim.js clean;
    • on cbf4970 (round 2 alone): the whole UI suite 741 of 741, npm test and node test/ci-sim.js clean. The branch's own changed lines come through each merge of main unchanged.
  • The reviewer's probes on round 6, loaded and with a real back-forward-cache Back:
    • F2 ends at 400 and 380 on either page, F2d at 400 and 510, SEAL at 410 and 520, and SEALW at 420;
    • DEEP32 and DEEP33 end at 400. O1E, O1R, O1H and O2 save the own change, and SAB, DEL and DELX read OK;
    • r65j: F1 ends at 410 and 510, F1s at 400 and 510, SA and SA0 keep 410, and CU1, CU2, RL1 to RL3, H2 and TWO read OK. The hostile ids read OK, 46 of 46;
    • CAP127 and CAP128 still show 410 on Back, as on round 5 (CAP127 on ed19d84 reads the same): the page there made an id of its own as it arrived, so with 127 more, 129 ids land, and the tab keeps the last 128;
    • r65d (20 of 20) from the cache reads as on round 5, line for line where it prints.
  • The reviewer's probes on round 5, loaded and with a real back-forward-cache Back:
    • F1 ends with Bins' depth at 510 on Forward, and the drawer at 410 and 510 once that page saves. F1s reads 410 and 510, as on round 3;
    • SA and SA0 keep Kitchen's 410 on Back to Baseplates after Save as. CU1 and CU2 end at 400, reloaded once the other tab set the size back;
    • A1 still ends at 400, and H2, RL1 to RL3, TWO and CAP31 and CAP32 read OK. X reads OK but for "landed withRid", which checks the round-4 field rid, now rids;
    • the hostile probe adapted to rids reads 30 of 30 OK, and a landed list of 100,000 ids is cut to the last 128 (a 1,400-character note);
    • DUP2 is as before, 410 on both pages (informational). O1 from the cache leaves the size out of the drawer, with the bar "not saving · Kitchen";
    • Start fresh then Put back, from the cache, leaves hashReady false; Start fresh alone sets it true;
    • r65c (46 of 46) and r65d (20 of 20) read as on round 4, line for line where they print.
  • The reviewer's probes on round 4, loaded and with a real back-forward-cache Back:
    • A1 and A1b end at 400 on either page, on the page and in the drawer;
    • A2, C1, H1 and S1 give what the reviewer found right on round 3. A3 shows one load, or one return from the cache, with no reload to catch up;
    • r65c and r65b's s1, s1b and oa probes read as on round 3, line for line. O6 still fails under the cache, as on every build;
    • T1 now ends the pasted-link record through Start fresh on either page. On Baseplates it still reads BROKEN, as on round 3, because the bare page writes its own address once it has loaded;
    • the hostile records (r65e) give no page errors. NAV other, a page left by leaveFor for the other tool and shown again from the cache, still saves nothing, as on main. No button leaves that way.
  • The reviewer's probes on round 3, with a real back-forward-cache Back:
    • P2s keeps the other tab's 420 in the drawer 4 of 4, and the page shows 420 after the reload or hand-over;
    • P2a keeps the other tab's depth 4 of 4; P2c, P2d and P2b save the 410 once there is room;
    • R1 keeps the 410 2 of 2, and N1 and N1f end with the page and the drawer both at 410, 4 of 4;
    • O14b offers nothing after Start fresh, and the lifecycle probes (LA to LH) read as before;
    • P2w is as before: the first tab's 410, refused before the other tab's 420, is saved once there is room;
    • B1 to B5, O4r and O8 to O10 as on round 2, below. O6 fails under the cache on every build, as the reviewer found.
  • The reviewer's probes on round 2: B2 catches up 16 of 16, B3 matches the drawer 8 of 8, B4 catches up 8 of 8 (420 handed over and reloaded), and B5 keeps 410 4 of 4, with a real back-forward-cache Back. B1 and B1b keep 410 throughout. O4r still offers the edit on Back; O8, O9 and O10 offer nothing.
  • With saved() waiting for nothing, 11 links tests fail, so the wait is doing the work.
  • Each new test fails without its fix:
    • the pasted link on either page shows 412 where 333 was pasted;
    • with Back's offer undone, Back offers nothing;
    • the size on the other page is 400 where 410 was typed, on either page;
    • with the reload kept only for hand-overs, it fails at "reloaded" (400);
    • the set-aside record is the planted one from before;
    • round 2, with equal marks enough again (M1): B3 on either page, reloaded or from the cache, and B4 from either page, all at 400 where the other tab set 420;
    • with a mark from each tool required again (S1b): B5 on either page, 400 reloaded;
    • with the kept change counted from the design: the drawer still 400 once the storage has room;
    • with Back offering any layout aside (S2): O8 on either page, offered;
    • with Start fresh setting the address as before: the change made 50 ms after it is what the bare page restores, 345, on either page;
    • round 3, with drawers.js as round 2 left it: the four P2s tests (400 where the other tab set 420), the four catch-up tests and the two restore tests (400 where 410 was typed);
    • with the catch-up note dropping the refusal: the four catch-up tests; with the restore note dropping it: the two restore tests;
    • with the carried base ignored: 12 tests, the O4 and S1b tests and the two from the cache among them (the reviewer's count; round 3 here gave 10, from a filter that left the cache tests out);
    • with the hand-over note or the address's mark dropping it: the O4 and restore tests, and the mark also S1b and the reloaded catch-up;
    • with the raced reload's record dropping it: the raced-reload test, which now refuses a size too (400 once there is room);
    • with Start fresh keeping the pasted-link record: O14b on either page, offered;
    • round 4, with a landed refusal's records still counted: six of the eight S3 tests, saved as set back, by a change first and as the page arrives, on either page;
    • with the page's own save not landing it: those saved as set back and by a change first; with the save on arrival not landing it: those saved as the page arrives;
    • with every catch-up reloading: the page holding a refused size is reloaded, on either page;
    • with Start fresh not turning saves back on from the cache: that page saves nothing, on either page;
    • with the pasted-link record's key declared where it was: a boot made to stop just after Start fresh is wired keeps the record through Start fresh, on either page;
    • round 5, with drawers.js as round 4 left it: the F1, SA and CU1 tests and the cache test with an own change refused, on either page (the size or depth of the drawer before, and "Kitchen" for "not saving · Kitchen");
    • with no id of a page's own once it arrives with one, or a record counted as landed once any of its ids has: the F1 tests;
    • with Save as keeping the ids: the SA tests; with the catch-up leaving the mark: the CU1 tests;
    • with the cache return not resetting a landed refusal: the cache test with the size set back, 410 where 400 was set back; with it clearing "not saving" whatever its own half holds: the cache test with an own change refused;
    • with leaveFor leaving freshLeft set: the Start fresh then Put back test, on either page;
    • round 6, with drawers.js as round 5 left it: the F2 and SEALW tests on either page, the 33 hand-overs, and the bar after a delete on either page;
    • with a record counted from its own base, not its first id still to land: the F2 tests; with a page's own id counted from its base: F2 and SEALW;
    • with the first id counted from where the page sealed, with ids not filtered in a record, or with the tab keeping 2 landed: the hostile ids test;
    • with no seals, or the cache return keeping where the page sealed before, or keeping the ids landed since: SEALW;
    • with an id landed only by itself, or the first id kept keeping its own base: the 33 hand-overs;
    • with a catch-up leaving each id's base: the CU1 tests; with the cache return after a delete leaving the bar: the delete tests;
    • with Start fresh as round 5 left it: Start fresh pressed twice, on either page.
  • The two S3 tests saved here as the page leaves pass with or without the ids. The page saves before it hands over, and once that save goes through, the address's mark carries no refusal. They are there to keep that so. The hand-over's own save landing the refusals has no test, since that earlier save lands them first.
  • The cache tests launch the full Chromium build with its back-forward cache on (channel: 'chromium', which npx playwright install chromium installs beside the headless shell). The headless shell the other tests run in keeps nothing in the cache.
  • Untested in round 6: deleting the drawer, here or in another tab, dropping the ids. Either seal alone: the return from the cache seals the page again after the hand-over did, so the test fails only with both taken out.
  • Put back forgetting the pasted-link record has no test. After Put back, the layout aside is the page it left, which the record does not name, unless that page was its untouched defaults.
  • Start fresh still clears the save and loads the bare page when a link throws partway through the boot, on either page. That test passes with or without the change: it is there to keep the wiring early.

If this touches geometry

It does not.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk

claude added 6 commits October 9, 2026 01:39
…ake, and the set-aside record in step

A link pasted over a page as an edit's save came due was written over: a page
still drawing came to the waiting save before the paste's hashchange, the
save wrote the page's own design into the address, and the hashchange then
reloaded that. The save now leaves an address the page did not write alone
(ownHash), so the link loads, and the layout it replaces, with the edit, is
set aside.

With the drawer's saves refused, a hand-over carried a size the drawer did
not have, and the page at the other end caught up with the drawer's older
one. A drawer with both marks as the note has them has not moved on since
the page that handed over, so it is not caught up with.

And with the storage full, a layout set aside whose link record would not
fit kept the record of the layout set aside before. That record now goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
… the guide's own pages

From #63's review: the links tests waited a fixed 900 ms before going to another
address, which drops a save still waiting; they now wait for the save itself
(saved), and for a Bins field edit still to be read in. The guide anchor test
goes on by the guide's own link to "Drawer size to grid", the polls name what
they wait for, the declined-link test checks the link is still recorded
before the edit, and markHere's comment no longer blames file://.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
Only sw.js conflicted (its VERSION line): taken from main and rebuilt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
…ffer a pasted-over edit on Back

From #65's review. The page that took a hand-over with the storage full was
caught up with the drawer's older size again when reloaded, gone Back to or
shown from the back-forward cache: the guard covered hand-overs only. Now any
arrival whose marks show the drawer as the page last knew it (unmoved), and
the pageshow return, are not caught up.

Back from a link pasted as an edit's save came due goes to an address that
never had the edit, which is aside: it is now offered there.

The O1 test checks the old record is gone, and the pasted-link test makes
the edit and the paste in one task.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
…ixes

Only sw.js conflicted, on its VERSION line: took main's and rebuilt the pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
Only the CI workflow changed on main; no conflict, and the pages and sw.js are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
claude added 6 commits October 9, 2026 08:51
…r only the pasted-over save on Back

Round 2 of the review of the pasted-link and storage-full fixes:

- A page keeps a size the drawer does not have only when its design holds
  a change the drawer refused. Equal marks alone did not prove that: a
  page that saves keeps another tab's size where it has not changed its
  own, and sets its mark all the same, so a tab that saved after another
  tab resized the drawer kept the old size through a reload or Back. The
  refusal now travels in the address's mark, the hand-over note and the
  tab's record of its last save.
- A drawer only one tool has saved into counts too.
- The kept change is counted from the drawer's settings, so the next save
  that goes through writes it into the drawer.
- Back to the address a pasted link came over offers only the save the
  paste came over, recorded for the tab; any other layout aside could be
  older than the page Back brings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
Only sw.js conflicted, on its VERSION line: took main's and rebuilt the pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
Only sw.js conflicted, on its VERSION line: took main's and rebuilt the pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
…learing

Start fresh cleared this browser's save and set the address, and the page ran on
until the bare one arrived. A change that landed in between was saved after the
clearing, and the bare page came back with it, saying it had been restored. It
now leaves by leaveFor, which stops the page's saves, on both pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
A page whose saves into the drawer are refused passes its base (its shared
keys as it last saved them or arrived with them) along with the flag: in the
address's mark, the hand-over note, the raced reload's record, and the notes
for a catch-up and for restore. The page that takes the change on counts it
from that base, so the change is still to be saved and whatever another tab
moved on since is caught up, as on main.

Round 2 counted it from the drawer's settings instead, so a page whose last
save had kept another tab's 420 took its own stale 400 for the change and
wrote it over that tab's once there was room (M2). The catch-up and restore
notes dropped the flag, so a refused size went back after either (N1, R1).
The pageshow return is main's again, and the marks no longer decide it.

Start fresh and Put back also forget the pasted-link record, which offered
an edit older than the fresh page on a later Back (O14b).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
Only sw.js conflicted, on its VERSION line: taken from main and rebuilt
with node build.js, which regenerated both pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
claude added 7 commits October 9, 2026 18:10
Only sw.js conflicted, on its VERSION line: taken from main and rebuilt
with node build.js, which regenerated both pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
Only sw.js conflicted, on its VERSION line: taken from main and rebuilt
with node build.js, which regenerated both pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
A page holding a size the drawer refused hands its base on with an id for
the refusal. A save that goes through while the page holds it records the
id for the tab, and a mark, note or record whose refusal has landed counts
for nothing. Back to the page after its size was saved on the other page and
set back there now shows the size set back and leaves the drawer at it,
rather than saving the refused size again over it.

Also: a catch-up that changes nothing in the page takes the drawer's
settings as its base without reloading; PASTED_KEY is declared with
SAVE_KEY, so Start fresh clears it even when the boot stopped short; and a
page left by Start fresh and shown again from the back-forward cache saves
again, as it does loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
Only sw.js conflicted, on its VERSION line; taken from main and rebuilt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
A page now holds the ids of the refusals it arrived with, and adds one of its
own for the saves the drawer refuses before it next hands them on. Records
carry the list, and count as landed only once every id in it has. With one id
for a whole chain, a save on an earlier page counted a later page's refused
depth as saved, and Forward lost it. Save as and deleting the drawer drop the
ids, so a refusal in the new drawer no longer counts Kitchen's as saved.

From the back-forward cache, a page whose refusals have landed keeps "not
saving" while its own half differs from the drawer's, and a catch-up with
nothing to reload rewrites the address's mark with the new base. leaveFor
clears freshLeft, ids are read only as short lowercase words, and the tab
keeps the last 128 landed. S3 and O1 are also tested with a real back-forward
cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
Only sw.js conflicted, on its VERSION line; node build.js regenerated it
and the pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
Each id a page holds now carries the base its own changes count from:
the page's shared keys as it last sealed, arriving, saving or coming
back from the cache. A record counts from its first id still to land,
and an id counts as landed once it or any id after it has, since the
page that held the later one saved the earlier ones' changes with its
own. Counted from the first page's base, a later page whose save as it
arrived was refused took the size handed to it for its own change after
that size was saved and set back, and Forward to it wrote the size over
the one set back.

A page keeps the last 32 ids, the first of them counted from the page's
base, so a long chain of hand-overs still saves the first page's size,
and Back to the page holding the first id finds it landed.

From the cache, a page whose drawer was deleted on the other page says
it is not saved, and Start fresh pressed twice before the page goes
still saves again from the cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjNf7SQF9o4ZVeWiSbWQmk
@Oliver-Johnson
Oliver-Johnson marked this pull request as ready for review October 10, 2026 12:40
@Oliver-Johnson
Oliver-Johnson merged commit 64f91bd into main Oct 10, 2026
3 checks passed
@Oliver-Johnson
Oliver-Johnson deleted the claude/robustness-security-review-5ir9ye branch October 10, 2026 12:41
Oliver-Johnson pushed a commit that referenced this pull request Oct 10, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY
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