Repository source: docs/plans/ui_first_post_identity/ui_first_post_identity_step4_implementation_summary.md
Debug mode: enabled
Effort study: railspike-effort / v3/ui_first_post_identity (ledger); standing enrollment P2
Extended estimates: enabled
Cycle estimate: 0.85
Step 4 entry
Step 3 approved 2026-10-28 ("approved step 3"). Branch
feat/ui-first-post-identityfrommainat74d044c1f; planning commit33a76fb31.Whole-cycle projection 0.85 reaffirmed at entry; scope v1.2 (G1) / R1 / P2 unchanged. Claude, single exposed rater. Stage originals are in the Step 3 plan; copied below without re-rating.
Uncommitted on entry and carried on the branch: the ledger's pending rows for this cycle (closeout in Stage 8) and two checklist notes (cycle 3 pause, recorded as separate work in Step 1; cycle 5 start, updated in Stage 7). Their links were repaired for the new folder.
Stage plan
| Stage | Original stage work | Status |
|---|---|---|
| 1 - Keys stay local; no dialogs | 0.15 | done |
| 2 - One registered check; guests shown as guests | 0.15 | done |
| 3 - Composer name row on first paint | 0.15 | done |
| 4 - Change the name from the composer | 0.12 | done |
| 5 - Creating your account, failures and Retry | 0.12 | done |
| 6 - Other signed paths and members-only sites | 0.10 | done |
| 7 - Sites, themes, copy and docs | 0.10 | done |
| 8 - Acceptance and closeout | 0.06 | done |
Stage estimates are not summed.
— Claude · 2026-10-28 UTC · Model: claude-opus-5-5
Stage 1 - Keys stay local; no dialogs
Original estimate: 0.15
Retrospective: 0.17
Changes:
public/assets/browser_signing.js: the name prompt and its "Continue with 'guest'?" confirm are gone. A new key takes the stored name, an invitation's temporary name, orguest. The Account page's prompt fallbacks (no name field; no inline name editor) are gone too, since its template always has both.Automatic guest keys are created with
publishPublicKey: falsein every case. The "Publish guest key immediately" preference is no longer read or written; a leftover stored value is inert.The background check on Like pages (
ensureStoredIdentityReady) now skips any key without a matching published fingerprint, so it only verifies registered keys; their readiness isnot_applicableand the first Like registers.public/assets/private_site_auth.js: on public sites, the automatic sign-in on page load skips a key that was never registered. This was the second reader-side registration path (found by the new browser test). Members-only sites are unchanged; Stage 6 covers them.templates/pages/account_key.php: the setting and its caption are removed.The debug-only
username_prompt_mstiming field is removed from both timing logs.
Verification:
New
tests/browser/first_post_identity.mjs(ZenMemes, automatic guest keys on, 390px): reading the board and a thread creates a localguestkey and sends noprepare_identity,create_identityorlink_identity; the repository head is unchanged. The first Like sends exactly oneprepare_identityand onecreate_identity, then votes. After a reload, the prewarm reportsreadyand a second Like registers nothing. Any dialog fails the test. Passed.php tests/run.php BrowserSigningNormalizationTest: 81/81 passed. Updated: no prompt in concurrent preparation; Account setup reads the name field; transient-retry uses a stored name; prewarm of a registered key unknown to the server republishes it. Added: prewarm leaves an unregistered key local. Removed: two tests of the publication preference.php tests/run.php PrivateSiteAuthTest: 11/11 passed. Fixtures for registered keys now carry the published fingerprint. Added: an unregistered key on a public resume page sends nothing (fails without the fix).AccountKeyPageHooksTest6/6 passed.AuthNavigationTest3/3 passed.InvitationIssuanceTest10/10 passed.LocalAppSmokeTest153/154 passed; the failure (testRepositoryArchiveDownloadFilenamesIncludeReadableTimestamp: a method that does not exist) is unrelated to this change.Browser tests passed:
signed_reactions,account_states,board_compose_focus,reply_compose_focus,invite_redemption.cjs,invite_signing_readiness,invitation_issuance.cjs,invite_signer_identity.cjs,post_retraction,fastmod,high_traffic_control,same_author_collapse,key_backup_uiandprivate_message_compose.cjs.invite_zero_clickfailed once, then passed on rerun (flaky).Fail on
maintoo, so not caused by this change:account_key_approval(timeout) andreply_continuation(timeout at line 147).Registration timing: a ZenMemes-sized copy (
repository.tar.gz, 1,619 posts, 1,595 commits) was rebuilt in the scratchpad and run on a local PHP server. Three first registrations took 314, 174 and 173 ms (prepare 20–77, sign 3–9, create 150–228). This was local, with no network or lock contention, so it is not phone evidence.
Notes:
The Account page's Continue still registers a new named key. Stage 2 makes it local.
The
forum_pki_compose_prompt_cancelledkey no longer has a writer. It stays in backup and clear handling so older backups restore unchanged.The production canary's simulated name prompt (
tests/browser/OpenPgpProductionCanaryTest.mjs) is left as defensive coverage.Retrospective 0.17 against the original 0.15, because of the extra public sign-in path and its fixture updates.
— Claude · 2026-10-28 UTC · Model: claude-opus-5-5
Stage 2 - One registered check; guests shown as guests
Original estimate: 0.15
Retrospective: 0.14
Changes:
browser_signing.js: newisRegisteredIdentity()(a key whose stored published fingerprint matches its fingerprint), exported on__forumBrowserIdentity. Identity-hint syncing skips unregistered keys, so the hint cookie never names a key the server doesn't have. The profile link stays hidden for an unregistered key.Account Continue: a registered key is renamed as in cycle 4. Otherwise (no key, or a local key) the page makes a new local key under the typed name and sends nothing. On members-only sites it still registers, because sign-in needs it.
account_key.php: the page script'sisRegistered()is true when the stored published fingerprint matches, or when the server's viewer identity is this key. It runs before first paint like cycle 4's state. An unregistered key shows the guest card, now "You’re posting as {name}", and the profile link stays hidden.toolbar_identity_status.js(Forte): for a local key, the toolbar shows the name without the filled icon and links to Account instead of a profile.
Verification:
tests/browser/account_states.mjs(updated) passed. A fresh guest naming "Alice" stays a guest, shows "You’re posting as alice", sends no identity or hint request and opens no dialog. A local key named mouse shows the guest card at parse time. Continue with kim replaces the key (new fingerprint) and sends nothing. The join hint now appears for a registered waiting key, not for a local one. The original cycle 4 checks still pass.tests/browser/first_post_identity.mjs(extended) passed. The Forte toolbar showsmouse,/account/key/and logged-out styling for a local key, and sends nothing. After the first Like registers the key, it shows the profile link.BrowserSigningNormalizationTest82/82 passed. Account setup is split into a public-site test (local key, no requests, no hint) and a members-only test (registers, then sets the hint).AccountKeyPageHooksTest6/6 passed.PrivateSiteAuthTest11/11 passed.LocalAppSmokeTest153/154 passed: the guest heading assertion was updated, and the unrelated archive-filename failure remains.Browser tests passed:
signed_reactions,key_backup_ui,invite_redemption.cjs,invite_zero_click,forte_activity_parityandprofile_public_details.
Notes:
Behavior change from cycle 4: naming a new key on Account no longer registers it, so the "Approved members already use …" hint waits until the first post or Like registers the key.
A key the server knows but whose published fingerprint isn't stored shows as a guest until its first action, which re-confirms it. No extra profile lookup was added.
The Forte toolbar label is still set by a deferred script, as before this cycle. The server renders "Guest" for a local key, because no hint is sent, so the name appears just after first paint. Stage 7's theme pass checks whether this flash is visible; the composer row (Stage 3) is set before paint.
Replacing a local key that already has queued Outbox items is handled with the composer rename in Stage 4.
— Claude · 2026-10-28 UTC · Model: claude-opus-5-5
Stage 3 - Composer name row on first paint
Original estimate: 0.15
Retrospective: 0.15
Changes:
New
templates/partials/compose_identity_row.php: "Post as {name} · Change". The server always rendershiddenwith "guest". A small inline script right after the row fills in the saved name and shows it before first paint, so server HTML is the same for every viewer. It followsforum:identity-changedandstorage. Without JavaScript the row stays hidden. Change is wired in Stage 4.The row is included once in
reply_form.phpandthread_compose_form.php, just above the actions. That covers board quick compose, thread reply, the New post and reply pages, QDB add, and Forte's reply panel and new post dialog. Collapsed inline composers hide it with their other non-editor parts, as before.compose.css: row styles (44px tap height, wraps, long names break). In the phone docked reply panel (keyboard open) the row is one text line. When the visible area is under 320px (landscape keyboard), it is hidden:reply_viewport.jsaddsreply-viewport-short.browser_signing.js: the old composer status messages "You may be asked to choose a username." (no key) and "Publish your browser identity before signed actions." (local key) are removed. Both were wrong now, and the row already says who you post as.
Verification:
New
tests/browser/compose_identity_row.mjspassed. It covers six composers × three key states (none, local named, registered) × 390/1280px. At parse time, before deferred scripts, every row is shown with the right name (guest / mouse / reaction-browser). Server HTML is the static hiddenguestrow on every composer. There is no page overflow, no dialog and no error. A rename elsewhere updates the row in place, and without JavaScript the row is hidden and the editor unchanged. 390px screenshots of the board quick compose and thread reply were reviewed: the row sits between the editor and Post, with no overlap.tests/browser/reply_viewport.mjs(extended) passed in light, Word97, Chicago and Midnight. The row is docked below the editor in the portrait keyboard panel and hidden in the 240px landscape case, and the editor and actions still fit.Browser tests passed:
board_compose_focus,reply_compose_focus,reply_viewport_flow,compose_disclosure,reply_actions_layout,inline_reply_linkandfirst_post_identity.BrowserSigningNormalizationTest82/82 passed.LocalAppSmokeTest153/154 passed (the same unrelated failure).Fail the same way on
main(74d044c1f, checked in a scratch worktree), so not caused by this change:compose_layout_stability("Draft remains intact" shown twice) andreply_full_card(Word97 card position).
Notes:
The Forte row and every theme beyond the four above are checked in Stage 7.
The first-paint check uses the document's parse point (DOMContentLoaded capture). That is the moment deferred scripts would otherwise change the page.
— Claude · 2026-10-28 UTC · Model: claude-opus-5-5
Stage 4 - Change the name from the composer
Original estimate: 0.12
Retrospective: 0.14
Changes:
compose_identity_row.php: Change swaps the row for a name field and Done, in place. Enter applies and Escape cancels. This is not a nested form: Enter is intercepted so it never submits the post. Errors show in the row, and the editor stays open to retry. The draft is never touched. If the signing tools aren't loaded yet, Done loads them throughForumLazyComposeSigningfirst. Change is hidden for a registered key where renaming is off;forumBrowserIdentityOptions.usernameChangesEnabledis new inlayout.phpandTemplateRenderer.browser_signing.js: Account's rename is moved into a sharedsaveRegisteredName(), and Account keeps its own status and editor handling. NewchangeIdentityName()(onForumBrowserSigning):a local or missing key is replaced by a new local key under the name, sending nothing;
a registered key is renamed on the server;
a local key with anything in the Outbox is registered and then renamed, so queued signed items keep their signer.
outboxHasItems()reads the Outbox database and aborts any upgrade, so it never creates the database.A dropped connection shows "Couldn't save your name. Check your connection and try again." instead of "Failed to fetch".
compose.css: editor and error styles (Done doesn't wrap; the error uses--status-error).
Verification:
tests/browser/compose_identity_row.mjs(extended, username changes on) passed:A new visitor writes a draft, then Change → Mouse → Done: the row shows mouse, nothing is sent, the key is unregistered and the draft is intact. The first reply then sends
prepare_identity+create_identityonce and posts, and the server profile has mouse.A registered key: rename with
prepare_profile_nameblocked shows the error in the row, the editor stays open and the draft is kept. Done again saves kim, with the same fingerprint and thefinalize_profile_namesent.With renaming off, the registered key has no Change.
A local key with an Outbox item: Change registers and then renames (
prepare_identity,create_identity,prepare_profile_name,finalize_profile_name) and keeps the fingerprint.No dialogs or page errors anywhere. Screenshots of the renamed row and the error state (390px) were reviewed.
Browser tests passed:
account_states(shared rename),invite_zero_click,reply_viewport,first_post_identity,board_compose_focus,reply_compose_focus,reply_actions_layoutandcompose_disclosure.BrowserSigningNormalizationTest82/82 passed,AccountKeyPageHooksTest6/6 passed, andLocalAppSmokeTest153/154 passed (the same unrelated failure).
Notes:
Retrospective 0.14 against the original 0.12, because of the Outbox read that must not create the database, and the lazy-loading path for Done.
The error in the row is plain text in the theme's error color. Stage 5 adds Retry for creation failures at Post and Like; the row's own retry is Done again.
— Claude · 2026-10-28 UTC · Model: claude-opus-5-5
Stage 5 - Creating your account, failures and Retry
Original estimate: 0.12
Retrospective: 0.12
Changes:
browser_signing.js: during a signed action, key creation and registration both show "Creating your account…". This replaces "Generating browser keypair..." and "Publishing your public key in the background...". NewaccountCreationFailure()turns any failure in key creation or registration into "Couldn't create your account."; the original message stays under details.Board, New post, QDB, Forte and the compose pages: the status line shows the failure with a Retry button that resubmits the same form with the same submitter. The draft stays, and Post anonymously stays in the menu. Inline replies show "Creating your account…" in the composer status while registering; a failure appears on the existing pending reply card with its own Retry.
thread_reactions.js: the first Like or tag on an unregistered key shows "Creating your account…" ("Preparing identity..." stays for registered keys). Failures, including the library-load ones, add Retry beside the message, which presses the same reaction again. The library-load messages keep their wording.site.css: inline Retry buttons (.compose-identity-retry,.reaction-retry), and the composer status in the error color.
Verification:
tests/browser/first_post_identity.mjs(extended) passed. Each case holdscreate_identity, sees "Creating your account…", then aborts it:Like: "Couldn't create your account." with Retry; Retry applies the Like.
Board quick compose: the same message and Retry; the draft "My first thread" is kept and Post anonymously is still there; Retry posts and opens the new thread.
Inline reply: the pending card shows the message; its Retry posts the reply.
No dialogs or page errors.
390px screenshots of both Retry states were reviewed. Retry sits inline with the message.
BrowserSigningNormalizationTest82/82 passed (one status assertion updated to "Creating your account…").PrivateSiteAuthTest11/11 passed.LocalAppSmokeTest153/154 passed (the same unrelated failure).Browser tests passed:
signed_reactions,board_compose_focusandreply_compose_focus(both include real failure/retry),compose_identity_row,account_states,invite_zero_click,invite_redemption.cjsandprivate_message_compose.cjs.
Notes:
Duplicate-post risk on Retry: a composer Retry re-runs the identity check (an already-registered key is not registered again) and starts the post from scratch, because the failure happened before any post was prepared. Inline replies keep the existing continuation, which confirms a prepared reply before resending.
In the thread card, the Like Retry ends next to the card's
#link at 390px. Stage 7 checks this across themes.
— Claude · 2026-10-28 UTC · Model: claude-opus-5-5
Stage 6 - Other signed paths and members-only sites
Original estimate: 0.10
Retrospective: 0.08
Changes:
outbox_sender.js: a reaction signed while offline now registers its key (ensureActionIdentity) beforeapply_signed_reaction. Before this, a key kept in the browser would have sent a signature the server could not check. Outbox posts and unsigned reactions already registered first.The other paths needed no code change: messages, invitation issuance and redemption, operator tools (Fastmod, high-traffic control, feature flags), approvals, and members-only sign-in through
private_site_auth.js. They all go throughensureActionIdentity/ensureReadyIdentity, which register an unregistered key first and, since Stage 1, never prompt. Stage 1's public-site guard leaves members-only sign-in unchanged.
Verification:
Lobby first: new
tests/browser/lobby_local_key.mjs(members-only, 390px) passed. A local, unregistered key opening/lobby/sendsprepare_identity,create_identity, thenauthenticate_identity, stores the published fingerprint and shows "Approval pending." A new visitor goes Lobby → Account → Continue as newbie, is registered there and the Lobby shows "Approval pending." No dialogs or page errors.OfflineOutboxSendTest3/3 passed. New: a signed reaction registers before/api/apply_signed_reaction; this test fails without the fix.Browser tests passed:
testing_self_approval(public andTEST_MEMBERS_ONLY=true),invitation_issuance.cjs,railspike_auth_navigation,private_message_feature_flag,private_message_operator_flag,private_message_history,fastmod,high_traffic_control,backup_feature_flags,feature_flags_toolbar,approval_review,root_approval_checksandaccess_history.invite_zero_click: 2 of 3 runs passed on this branch (one timeout). It is also flaky onmain(2 of 3 there, one assertion failure), so it is recorded as a flaky baseline.private_message_list_browsertimes out onmaintoo, so it is not caused by this change.
Notes:
Messaging with a never-registered key isn't a normal path (messages need an approved key). The code path registers first, as above.
— Claude · 2026-10-28 UTC · Model: claude-opus-5-5
Stage 7 - Sites, themes, copy and docs
Original estimate: 0.10
Retrospective: 0.11
Changes:
paned_toolbar.php: a small inline script after Forte's identity link shows a local key's name, logged-out styling and the Account link before first paint. This removes the "Guest" → name swap noted in Stage 2.toolbar_identity_status.jsstill keeps it current afterwards, and server HTML is unchanged.docs/reference/authentication_modes.md: keys stay in the browser until the first signed action, with no background publishing and no dialogs. Covers naming through the composer row or Account, how unregistered and registered keys are renamed, and that members-only sign-in still registers first. The "not yet published" viewer-state row is updated.ui_copy_audit_checklist.md: new entry "First post without dialogs (all sites)" listing the removed and added strings.ui_redesign/implementation_checklist.md, cycle 5: the inline-name and creation-state items are ticked. The "Post only signs and sends" item is marked revised by the Step 1 decision, with G2 as the follow-up. Only the cycle 5 lines are in this commit; the cycle 3 pause note recorded as separate work in Step 1 stays uncommitted.No theme fixes were needed. No navigation destination changed, so
theme-nav-icons.cssis untouched.
Verification:
Theme matrix (scratchpad script
theme_matrix.mjs, not committed), 236 captures. It covered every permitted theme plus Auto on ZenMemes (15), Chouse, MIT Rap Club and QDB (16 each), 59 combinations, using board quick compose, or QDB Add, with a local key named mouse. At 1280px it captured the row; at 390px the row, the open editor, and a forcedcreate_identityfailure with Retry. Automatic checks (row visible, exact text "Post as mouse · Change", no page overflow, no page errors, the resolved theme matching the requested one) found no problems. Contact sheets for ZenMemes and QDB (failure + Retry) and Chouse (editor) were reviewed: messages are readable, the error color is clear and nothing overlaps. In themes that style every button (Chicago, Word 97, Sticker, Whitehot, Midnight), Change and Retry take that theme's button look, as Account's Change does.first_post_identity.mjs(extended) passed. Withtoolbar_identity_status.jsblocked, Forte's toolbar still shows mouse and/account/key/, set by the inline script alone; the check fails without it. The server HTML still says Guest.Full suite, run once:
php tests/run.phpran 1616 tests: 1613 passed, 3 failed. All three were already failing before this branch, and none touches identity code:LocalAppSmokeTest::testRepositoryArchiveDownloadFilenamesIncludeReadableTimestamp(failing since 2026-10-27)PlatformDocsStaticTest::testStaticReleaseIncludesCompleteInventoryAndSafeAssets(since 2026-10-27)ReadModelBuilderTimingTest::testProgressSeparatesParsingDerivationAndWritingWithBoundedCounts(since 2026-10-18)
The browser suites are not part of
tests/run.php; they ran per stage above.
Notes:
Correction to Stage 3's note: a capture-phase DOMContentLoaded listener runs after deferred scripts' top-level code but before their DOMContentLoaded handlers. The composer row check still holds, because nothing deferred touches the row at top level. The toolbar check above blocks the deferred script instead.
Real phones and production were not verified. The real-iPhone check stays pending (Stage 8).
— Claude · 2026-10-28 UTC · Model: claude-opus-5-5
Stage 8 - Acceptance and closeout
Original estimate: 0.06
Retrospective: 0.06
Changes: this closeout and the ledger closeout row.
Verification: the reported failure was replayed with the scratchpad script
replay.mjs. Setup: a ZenMemes-sized copy (repository.tar.gz, 1,619 posts) on a local PHP server, automatic guest keys and username changes on, Playwright's iPhone 13 profile (390px, touch, mobile Safari user agent, Chromium engine), a new visitor, run twice. Results for each success criterion:No
window.promptorwindow.confirm: passed (no dialogs in either run, or in any browser test this cycle).Reading makes no registration and no commit: passed. After the board loaded and 2 s of idle work, there were no identity requests. The repository gained exactly 4 commits over both runs (2 identities, 2 threads), all from posting.
A first action registers exactly once, and later actions don't: passed. Each run sent one
prepare_identityand onecreate_identity;first_post_identitycovers later Likes.The name row is right on first paint for every key state, theme and size: passed (
compose_identity_row, Stage 7 matrix). In the replay it read "Post as mouse · Change" after Change → mouse → Done.Failure states show Retry, keep the draft and recover: passed (Stage 5 tests).
The replay showed "Creating your account…", then the new thread opened with the post "by mouse (unapproved)". Post to thread page took 787 ms (first run) and 97 ms (warm). Board load took 337 and 214 ms. Screenshot reviewed.
Notes: this is local emulation in Chromium, not WebKit on a phone over a mobile network. The real-iPhone check on ZenMemes is pending with the user and does not count as done until reported.
Closeout
Contract: the normal entry (every signed composer and Like, new visitor, phone and desktop, all four sites), the end-to-end outcome (first post or vote under the chosen name or
guest, no dialogs, readers send nothing) and the required recovery (Retry with the draft kept; Post anonymously stays) are covered by the browser tests and the Stage 8 replay.Commits: planning
33a76fb31; stages73c49904e,c8fbc7c7e,d81413f49,c17488068,089bf0640,e9d1ed102,e73d483cc, and this Stage 8 commit. That is 1 + 8 commits, meeting the minimum.Pending: the real-iPhone check (user), merge into
main(only when asked), deployment, and the maintainer's comfortable-fit judgment for the study.Follow-up proposed in Step 1, not started: G2, the server accepting the key with the first action (one request for a first vote), as its own cycle.
Uncommitted on the branch and outside this cycle: the cycle 3 pause note in
ui_redesign/implementation_checklist.md.
Retrospective scope assessment (cycle): 0.90 against 0.85 projected and reaffirmed at entry. This is a retrospective assessment, not a measured actual or a new projection. Scope was unchanged. The extra work found during implementation (the public sign-in registration path, Outbox signed reactions, and the Forte toolbar's first paint) was corrective work inside scope. Maintainer comfortable-fit judgment: pending.
— Claude · 2026-10-28 UTC · Model: claude-opus-5-5
Requests after closeout
| Request | Disposition and reason | Approval status | Outcome |
|---|---|---|---|
| 2026-10-28: "I clicked the Change button, but then got this message: Names can't be changed on this site." | Corrective, in scope (Stage 4 contract: Change works for a key not yet registered; it is hidden where it can't apply). Two causes found: (1) registering a key during a post or Like didn't notify the name row, so Change stayed visible for a now-registered key where renaming is off; (2) the Outbox guard counted every Outbox item (sent, cancelled, unsigned drafts), sending a local key down the server-rename path. Reproducing them exposed a third: after Change replaced a local key, the composer still named the old key and the post failed with "The signing identity changed." | Fix on this branch as corrective work; no scope change | See Fix - Change on sites with renaming off |
Fix - Change on sites with renaming off
Changes (
browser_signing.js):The Outbox guard (
outboxHasSignedWork, wasoutboxHasItems) now counts only unsent items already signed by this key: a reaction signed offline, or a post with a prepared delivery. Drafts and unsigned items are signed when sent; accepted, rejected and cancelled items are history. If a local key does have such work where renaming is off, the message now says what to do: "Your Outbox has items signed under this name. Send or discard them first."renderSavedStatewithout an Account root (inline replies, Outbox, composer Change) now firesforum:identity-changed. The name row now hides Change as soon as a reply registers the key on a site with renaming off.Each composer refreshes its hidden author field on
forum:identity-changed. A draft written before Change is then signed by the new key. A key switched in another tab (astorageevent) still trips the signing-identity check.
Verification:
tests/browser/first_post_identity.mjs(server with renaming off, the default) passed. With a sent reaction and a draft in the Outbox, Change → pip on a local key replaces it without error; this reproduced the reported message before the fix. An inline reply then posts, registers the key, and the row hides Change on the same page; this fails without the notification fix. That reply also reproduced the stale author field ("The signing identity changed.") before the fix.compose_identity_row.mjspassed. Its Outbox case now uses a realistic queued reaction signed offline, and still registers and renames instead of replacing.Browser tests passed:
account_states,signed_reactions,reply_compose_focus,board_compose_focusandreply_viewport.BrowserSigningNormalizationTest82/82,PrivateSiteAuthTest11/11,OfflineOutboxSendTest3/3 andAccountKeyPageHooksTest6/6 passed.
Notes: I don't know which cause the user hit, so both are fixed. The full suite was not re-run for this fix: it touches only browser scripts, which
tests/run.phpcovers through the unit tests above.
— Claude · 2026-10-28 UTC · Model: claude-opus-5-5
Fix - divider without Change
Request (2026-10-28): "divider is still visible even when no change button: Post as ok ·". Corrective, in scope (Stage 3/4 row).
Change: in
compose_identity_row.phpthe " · " now shares an element with Change (data-role="compose-identity-change"), so both hide together where renaming is off for a registered key.Verification:
first_post_identity.mjs(the row reads exactly "Post as pip" with Change hidden) andcompose_identity_row.mjspassed.
— Claude · 2026-10-28 UTC · Model: claude-opus-5-5
Request - offline Likes carry their key on a first action
Request (2026-10-28): store the public key with an offline Like so the composer's Change no longer depends on the Outbox; "store the key only if we are sending the first action, … not every single time."
Disposition: in scope as corrective work on Stage 4's Outbox guard; user-directed. The public key is added only when the signing key is not yet registered. The sender registers that item's own key from it, and Change drops its Outbox check. Outcome below.
Changes:
outbox_intent.js:createSignedIntentaddsintent.publicKeyonly when the browser's key is not yet registered (its stored published fingerprint doesn't match).outbox_sender.js: a signed reaction withintent.publicKeywhose signer isn't the current key registers that key with/api/link_identity("Identity already exists" counts as success); otherwise the current key is registered as before.browser_signing.js:changeIdentityNamealways replaces a local key.outboxHasSignedWork, its database read and the "Your Outbox has items signed under this name" message are removed.Docs:
authentication_modes.md, and the backlog note marks what this resolves.
Verification:
OfflineOutboxIntentTest2/2 passed. New: the key is stored for a first action and not for a registered key.OfflineOutboxSendTest4/4 passed. New: two Likes signed by an old key are sent after the key changed, each first registeringold-public-key; "already exists" is accepted.compose_identity_row.mjspassed three runs in a row. A real offline Like is signed by a local key and carries its public key. Change replaces the key and sends nothing. The Outbox then sends the Like (link_identity,apply_signed_reaction), and the server profile of the old key exists.The test now waits for a registered key's sign-in reload before typing. One earlier run had failed when the page reloaded mid-Change; that reload is existing behavior, the cycle 4 follow-up "sign-in reload elsewhere".
Browser tests passed:
first_post_identity,account_statesandsigned_reactions.BrowserSigningNormalizationTest82/82 passed, andLocalAppSmokeTest153/154 passed (the same unrelated failure).
Notes: Likes queued before this change have no public key and still depend on the current key. A Like sent this way is credited to the key that signed it, under that key's name (e.g.
guest), not the renamed one.
— Claude · 2026-10-28 UTC · Model: claude-opus-5-5
Cycle closed
Closed 2026-10-28 at the user's request ("let's close this cycle"), after three corrective fixes after closeout: the Change message (
a332f69f5), the divider (bef7f07da) and offline first-action keys (e08e5c737). Every check recorded above still stands; the full suite was not re-run after those fixes.Handed off, not part of this cycle: the unexpected page reload (handoff) and the Outbox and identity findings (backlog note).
Still pending: the real-iPhone check, merge (only on request), deployment, and the maintainer's comfortable-fit judgment.
— Claude · 2026-10-28 UTC · Model: claude-opus-5-5
Request - guest is a regular user
Request (2026-10-28): "If I choose to make one post as guest, I have no way to sign out or change it." On review, cycle 4 (
ui_account_states) showed an unapproved key namedgueston Account's guest card, which hides Name, Remove this key and Save a copy. Together with this cycle's rule that Change is hidden for registered keys where renaming is off, that left no way out. The user confirmed the direction: "guest was just a regular user with username guest, no special cases", then "Yes, OK."Disposition: corrective follow-up on this branch after closeout. Account shows the member card for any registered key, whatever its name; the guest card stays for "no key yet" and "not registered yet". Remove this key is how anyone signs out or starts over. Where renaming is off, Change in the composer stays hidden for every registered key (unchanged). This reverses part of cycle 4's design, with the user's approval.
Changes:
account_key.phpaccountStatedrops the cycle 4 rule that put an unapproved key namedgueston the guest card. Now the guest card means only "no key" or "not registered yet"; any registered key gets the member card (Name, Change where renaming is on, Fingerprint, Save a copy, Remove this key).Verification:
account_states.mjs(updated) passed. An unregistered automatic key shows the guest card. A registeredguestshows the member card from first paint, then "Waiting for approval", with Name "guest", Remove this key and Save a copy visible. Renaming a registeredguestuses Name → Change → Save and keeps the fingerprint. Phone screenshot reviewed.Browser tests passed:
invite_zero_click,testing_self_approval,first_post_identity,compose_identity_row,lobby_local_keyandkey_backup_ui.AccountKeyPageHooksTest6/6 andBrowserSigningNormalizationTest82/82 passed.LocalAppSmokeTest153/154 passed (the same unrelated failure).
Notes: what remains of guest handling is unchanged:
guestis the default name for a new key, and the join hint is not shown for the nameguest.
— Claude · 2026-10-28 UTC · Model: claude-opus-5-5
Later change (after merge)
2026-10-28: at the user's request the composer's "Post as {name} · Change" row (Stages 3–4) was removed because it took too much space; naming happens on Account only. Removed with it:
partials/compose_identity_row.php,changeIdentityName(), the row's styles, thereply-viewport-shorttoggle andtests/browser/compose_identity_row.mjs. The rest of the cycle (no dialogs, keys registered on first action, "Creating your account…" with Retry, guests as regular users on Account) is unchanged. Record:ui_copy_audit_checklist.md, "Composer 'Post as' row removed".