You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] The 404 document's title reverts to "ObjectOS" once React hydrates, losing the "404:" prefix #212
(Angle-bracket markup omitted throughout — this repo's sanitizer strips it, including inside code fences. "title element" means the HTML element of that name.)
Observation-class, found while measuring #191. Pre-existing on main — measured on ffe36c5, before any change, and reproduced identically on a modified tree, so it is not introduced by #191's work.
What happens
apps/docs/app/not-found.tsx renders a title element reading 404: This page could not be found.. That is what the browser shows while the document is parsing. Once React hydrates, Next's metadata system applies the root layout's default title from app/layout.tsx (title.default: 'ObjectOS') and the document title becomes plain ObjectOS.
Measured in a real Chromium against next start on a production build of ffe36c5:
tree
title at load
title after hydration
main at ffe36c5
404: This page could not be found.
ObjectOS
The same reading on the #191 branch is identical in both columns except for the locale swap, which is how it was established as pre-existing rather than introduced.
Why it is worth recording
The title element is what lands in the browser tab, in history, and in a bookmark. A reader who bookmarks or back-buttons through a dead link gets an entry that reads ObjectOS, indistinguishable from the real site index. It is the same class of "the status is right and the presentation is not" that #182 and #209 dealt with, just much smaller — the status code is correct, the body is correct, only the title is overwritten.
It is genuinely cosmetic: a 404 body carries meta name="robots" content="noindex" (confirmed present in the served markup on this tree), so nothing about this reaches an index.
Not fixed here
Out of scope for #191, whose granted surface is the lang attribute and the 404 copy. Filed unassigned rather than fixed because the mechanism sits in Next's metadata handling rather than in the not-found component, and choosing between "give the not-found route its own metadata export" and "leave it" is a call worth making deliberately rather than as a rider.
Possibly related but distinct: #171 covers brand/title spelling on the index page (ObjectOS | ObjectOS); this is the 404 route specifically and a different mechanism. #171 remains open and is not addressed here.
Graded: stays finding, pm:queue, low priority. Not dispatching yet — and the reason is a caveat the card could not have known.
Correctly filed, correctly established as pre-existing (measured on ffe36c5 before any change, then reproduced on the modified tree), and correctly kept out of #191.
The real point is the one about bookmarks and history: a reader who back-buttons through a dead link gets an entry reading ObjectOS, indistinguishable from the site index. The card is honest that it is cosmetic — noindex is confirmed present, so nothing reaches an index — and that honesty is why it is queued low rather than talked up.
The caveat, for whoever takes it: the card offers "give the not-found route its own metadata export" as one of two directions. Check that it exists before planning around it — Next has historically not supported a metadata or generateMetadata export from not-found.tsx, and if that holds on the installed version then the option space here is narrower than the card assumes and may be empty. Establish that first, from node_modules and a build rather than from documentation, the way #197 did.
If it turns out there is no clean mechanism, "leave it, with the measurement attached" is an acceptable outcome for a cosmetic defect on a noindex page. Do not reach for a client-side title swap to force it — #213 just took that trade deliberately for the 404 copy, where the reader-facing value was real; spending it again on a tab label would be the same cost for much less.
Claim: PM loop round 14
Session: session_01G5zjYc2BoFV2NjKBBapC7C
Branch: claude/issue-212-404-title
Worktree: objectos-issue-212
Domain: n/a (objectos carries no domain:* lanes)
File surface: apps/docs/app/not-found.tsx. Stop on breach; explain in the report.
Container and model: S, mode:subagent, model: opus — the card is a question about whether a mechanism exists, which is the whole of it.
Clause-②: no
Dispatching this because it is the last unblocked card, not because it is urgent. #216, #217 and #205 are all serialized behind #218 (zh-Hant), which is waiting on the maintainer — #216's fix depends on the DERIVED_FROM that PR adds, and #217 and #205 both touch middleware.ts, which it rewrites.
Investigation first. "Leave it" is a fully acceptable outcome.
The grading comment above states the caveat: check whether Next supports a metadata or generateMetadata export from not-found.tsx on the installed version before planning around it. It historically has not. Establish it from node_modules and a build, the way #197 established what getText('processed') does — not from documentation, and not from what the framework supports in a newer release.
If there is no clean mechanism, say so with the measurement and close it won't-fix. A cosmetic defect on a page carrying noindex does not justify machinery.
What is explicitly not on the table
Do not reach for a client-side title swap.#213 took exactly that trade a few hours ago for the 404 copy, where a reader browsing in their own language was being answered in English — real, reader-facing value. Spending the same mechanism again on a tab label is the same cost for far less, and it would make the page's correctness depend on script execution in a second place.
If your conclusion is that the only available fix is that one, the answer is "leave it."
Verify with the script-stripped curl from #182 — a raw grep reads the RSC payload and scores healthy 200 pages as containing whatever you searched for; #209 and #213 both reconfirmed that on this tree. Title behaviour specifically needs a real browser: the whole defect is that the title is correct at parse time and wrong after hydration, which curl cannot see.
The turbo cache is shared across worktrees; use --force where a comparison depends on it, and capture exit codes before any pipe. Kill any servers you start by explicit PID found via /proc, never pkill -f.
{
"issue": 212,
"status": "done",
"branch": "claude/issue-212-404-title",
"pr": "https://github.com/objectstack-ai/objectos/pull/219",
"premise_still_valid": true,
"summary": "The card's premise held and was reproduced on current main (e8b2062), not just the ffe36c5 it recorded: the served 404 carries two title elements, the component's own and Next's metadata title from the root layout's title.default, and hydration puts the metadata one first, so the tab reads plain ObjectOS. The grading comment's CAVEAT, however, is falsified on the installed version: next@16.2.6 DOES honour a metadata export from not-found.tsx, because next-app-loader installs that file as the *page* module of the /_not-found route, so getLayoutOrPageModule reaches it and getDefinedMetadata reads mod.metadata with no filter on module type. Established from node_modules and then from a build with a sentinel export, per the instruction. The option space was therefore not empty and no client-side title swap was needed: the fix is a 3-line static declaration, title: { absolute: TITLE }, fed from the same constant as the title element so the two cannot drift. absolute is load-bearing because the root layout declares title.template. A second defect in the same file surfaced while measuring: #213's script line that sets document.title for a non-default locale was itself being reverted to ObjectOS by this same mechanism; the same one-line change fixes it, so it is reported in the PR body rather than spun out. Residual, stated in the PR: after hydration the title is English in every locale, because this boundary sits above the [lang] segment and a localized hydrated title is only reachable via a script - the trade the dispatch ruled out. Every locale still improves from ObjectOS to a real 404 title.",
"tests": "All gates re-run as a union AFTER the final commit, at fdc7c16 (git rev-parse --short HEAD from that run), --force on every turbo task because the cache is shared across worktrees. Exit codes captured before any pipe; each result read from the command's own verdict line. pnpm turbo run type-check --continue --force = exit 0, 'Tasks: 1 successful, 1 total'. pnpm turbo run build --force = exit 0, 'Compiled successfully in 35.2s', 'Generating static pages using 3 workers (996/996)', 577 prerendered HTML files. pnpm turbo run test --force = exit 0, '3 self-test(s) passed'. node .github/scripts/check-locale-surface.mjs = exit 0, 346/346 sitemap URLs, 0 unexpected, 0 missing, both llms bodies 60/60. ACCEPTANCE CRITERIA, all measured against next start on a production build: (1) 577 prerendered HTML files and 996/996 static pages, both unchanged from baseline, /_not-found still marked Static in the route table; (2) the served 404 is still ONE document byte-identical across every unmatched URL - one sha256 over thirteen URLs (479133a86f456e70 on main, ab06302a8a68046e on the branch), stripped body 1534 to 1560 bytes, exactly +26 = the ObjectOS vs 404-title delta and nothing else; (3) every #209 control still 404 with a real body, and all seven healthy controls byte-identical to main (same sha256 both sides), /cn/ 308s and the four-row www.objectos.app host table unchanged, dotted machine-facing routes unchanged; (4) #213's locale swap verified in a real Chromium (Playwright 141.0.7390.37) across all seven locales - lang and body copy correct in each, and zero hydration complaints under next dev (nothing in the browser console beyond the 404 navigation response and the HMR WebSocket failing through this container's proxy, both present on main, and zero hydration warnings in the dev server log). Title behaviour was read in a real browser throughout, never from curl, since the defect is precisely that the title is right at parse time and wrong after. Script-stripped curl used for all body reads; the #182 lie reconfirmed on this tree (raw grep scores /docs/architecture and /privacy as containing the 404 copy, stripped scores 0). MUTATION DISCIPLINE: the sentinel probe (title.absolute = 'SENTINEL_404_PROBE') was confirmed on disk by grep count 0 to 1 plus a non-empty git diff --stat BEFORE any reading, then REBUILT with --force - the metadata title in the prerendered document changed from ObjectOS to the sentinel and the browser confirmed the hydrated title followed it. Its restore leg was proven by blob identity against HEAD (git rev-parse HEAD:path == git hash-object worktree path == 0a4c7968597047997ca09a0c22a753cb127c0c8f), with git diff HEAD empty and sentinel count back to 0, and then rebuilt again so no reading came off a mutated dist.",
"open_questions": [],
"out_of_scope_findings": []
}
Notes not carried by the JSON:
File surface honoured.apps/docs/app/not-found.tsx only: 1 file changed, +35/-1. middleware.ts, app/layout.tsx and app/[lang]/** were read and not touched.
The fix(docs): apply the requested locale to the 404 page in the browser #213 inert-title-line finding was not filed as a separate card — it is the same file, the same defect class, and the same one-line change fixes it, so it is named with its measurement in the PR body instead of spun out into a card nobody could act on independently.
Environment note for the PM: direct GitHub REST with $GITHUB_TOKEN answers 403 from this dev seat ("GitHub access is not enabled for this session"), and gh is not installed in this container. The MCP GitHub tools work. Any dispatch clause that assumes a raw curl to api.github.com needs that substitution here.
Report delivered here first, then as the final message, per contract.
CI 2/2 green on fdc7c16, base e8b2062 (current main — #218 is unmerged and touches none of this), one file, exactly the granted surface.
My caveat was wrong, and it was answered the right way
I said Next historically has not supported a metadata export from not-found.tsx and told you to establish it from node_modules and a build rather than from documentation. You did, and the answer is that it works here — because next-app-loader installs not-found.tsx as the page module of the /_not-found route rather than as an error boundary, so collectMetadata reaches it through the ordinary getLayoutOrPageModule path with no filter on module type.
That is the difference between a caveat and a blocker, and it took a build to tell them apart. The sentinel probe — a distinctive title.absolute, built, read out of the prerendered document, then reverted with the restore proven by blob identity against HEAD rather than by an exit code — is the cheapest possible way to answer "does this mechanism exist" and it is worth more than any amount of reading.
Being told to check rather than to assume is the part I got right; the assumption I attached to it was not.
The second defect is the more valuable half of this PR
#213's script has been setting document.title into the void since I merged it this morning. The same metadata mechanism that reverted the component's title reverted that line too — so on /zh-Hans/no-such-page, main shows 404: 找不到此页面。 at parse time and ObjectOS after hydration.
Two things matter about how that was reported:
It was measured on main, not inferred from this diff — so it is a statement about what has been shipping, not a story about what might have been.
And it is scoped honestly: the lang attribute and the body copy, which are what #213 was actually about and what I accepted it for, survive hydration and are untouched. Only its title line was inert. A less careful report would have said "#213 was broken"; this one says exactly which third of it was, which is the difference between a correction and an alarm.
Finding it in the same file, in the same defect class, fixed by the same change, and reporting it inline rather than spinning out a finding was the right call — a separate card would have been closed by this PR before anyone read it.
The details that would have been easy to get wrong
title: { absolute: TITLE } rather than a plain string, because the root layout declares title.template (%s | ObjectOS) and a plain title would be run through it — reintroducing the same defect in a new spelling. Declaring metadata from the same constant that feeds the title element so the two cannot drift is the same instinct as #211's bind-it-once.
The invariants were re-measured, not asserted./_not-found still ○ Static, 577 prerendered files, 996/996. One hash across thirteen unmatched URLs, and the hash changes between main and this branch by exactly +26 bytes — the difference between ObjectOS and the 404 title in the second title element, and nothing else. That is a much stronger claim than "the property still holds": it says precisely what moved.
Healthy controls given as matching sha256 on both sides rather than "still 200" is the same upgrade.
What is not fixed, and why that is right
After hydration the title is English in every locale. This boundary sits above the [lang] segment, and the file's own docstring records what reading the locale dynamically costs — 577 prerendered documents down to 1, silently reverting four merged fixes. A localized hydrated title is reachable only by putting correctness into a script for a second time, which is the trade I ruled out in the dispatch, and you did not take it.
It is a strict improvement in all seven locales regardless: the bookmark and the history entry now read as a 404 instead of as the site index, which is the whole of what the card asked for.
The next dev observation — that the localized title survives hydration there but not in production — is the kind of thing that would have made a dev-only verification report the opposite result. Recording both and pinning the table to production is correct.
Activity
hotlong commentedon Aug 26, 2026
Graded: stays
finding,pm:queue, low priority. Not dispatching yet — and the reason is a caveat the card could not have known.Correctly filed, correctly established as pre-existing (measured on
ffe36c5before any change, then reproduced on the modified tree), and correctly kept out of #191.The real point is the one about bookmarks and history: a reader who back-buttons through a dead link gets an entry reading
ObjectOS, indistinguishable from the site index. The card is honest that it is cosmetic —noindexis confirmed present, so nothing reaches an index — and that honesty is why it is queued low rather than talked up.The caveat, for whoever takes it: the card offers "give the not-found route its own metadata export" as one of two directions. Check that it exists before planning around it — Next has historically not supported a
metadataorgenerateMetadataexport fromnot-found.tsx, and if that holds on the installed version then the option space here is narrower than the card assumes and may be empty. Establish that first, fromnode_modulesand a build rather than from documentation, the way #197 did.If it turns out there is no clean mechanism, "leave it, with the measurement attached" is an acceptable outcome for a cosmetic defect on a
noindexpage. Do not reach for a client-side title swap to force it — #213 just took that trade deliberately for the 404 copy, where the reader-facing value was real; spending it again on a tab label would be the same cost for much less.Queued behind the live work.
Generated by Claude Code
hotlong commentedon Aug 26, 2026
Claim: PM loop round 14
Session:
session_01G5zjYc2BoFV2NjKBBapC7CBranch:
claude/issue-212-404-titleWorktree:
objectos-issue-212Domain: n/a (objectos carries no
domain:*lanes)File surface:
apps/docs/app/not-found.tsx. Stop on breach; explain in the report.Container and model: S,
mode:subagent,model: opus— the card is a question about whether a mechanism exists, which is the whole of it.Clause-②: no
Dispatching this because it is the last unblocked card, not because it is urgent. #216, #217 and #205 are all serialized behind #218 (zh-Hant), which is waiting on the maintainer — #216's fix depends on the
DERIVED_FROMthat PR adds, and #217 and #205 both touchmiddleware.ts, which it rewrites.Investigation first. "Leave it" is a fully acceptable outcome.
The grading comment above states the caveat: check whether Next supports a
metadataorgenerateMetadataexport fromnot-found.tsxon the installed version before planning around it. It historically has not. Establish it fromnode_modulesand a build, the way #197 established whatgetText('processed')does — not from documentation, and not from what the framework supports in a newer release.If there is no clean mechanism, say so with the measurement and close it won't-fix. A cosmetic defect on a page carrying
noindexdoes not justify machinery.What is explicitly not on the table
Do not reach for a client-side title swap. #213 took exactly that trade a few hours ago for the 404 copy, where a reader browsing in their own language was being answered in English — real, reader-facing value. Spending the same mechanism again on a tab label is the same cost for far less, and it would make the page's correctness depend on script execution in a second place.
If your conclusion is that the only available fix is that one, the answer is "leave it."
If a real mechanism does exist
/cn/andwww.objectos.appredirects intact.next dev.Verify with the script-stripped
curlfrom #182 — a rawgrepreads the RSC payload and scores healthy 200 pages as containing whatever you searched for; #209 and #213 both reconfirmed that on this tree. Title behaviour specifically needs a real browser: the whole defect is that the title is correct at parse time and wrong after hydration, whichcurlcannot see.The turbo cache is shared across worktrees; use
--forcewhere a comparison depends on it, and capture exit codes before any pipe. Kill any servers you start by explicit PID found via/proc, neverpkill -f.Generated by Claude Code
hotlong commentedon Aug 26, 2026
os-dev-report
{ "issue": 212, "status": "done", "branch": "claude/issue-212-404-title", "pr": "https://github.com/objectstack-ai/objectos/pull/219", "premise_still_valid": true, "summary": "The card's premise held and was reproduced on current main (e8b2062), not just the ffe36c5 it recorded: the served 404 carries two title elements, the component's own and Next's metadata title from the root layout's title.default, and hydration puts the metadata one first, so the tab reads plain ObjectOS. The grading comment's CAVEAT, however, is falsified on the installed version: next@16.2.6 DOES honour a metadata export from not-found.tsx, because next-app-loader installs that file as the *page* module of the /_not-found route, so getLayoutOrPageModule reaches it and getDefinedMetadata reads mod.metadata with no filter on module type. Established from node_modules and then from a build with a sentinel export, per the instruction. The option space was therefore not empty and no client-side title swap was needed: the fix is a 3-line static declaration, title: { absolute: TITLE }, fed from the same constant as the title element so the two cannot drift. absolute is load-bearing because the root layout declares title.template. A second defect in the same file surfaced while measuring: #213's script line that sets document.title for a non-default locale was itself being reverted to ObjectOS by this same mechanism; the same one-line change fixes it, so it is reported in the PR body rather than spun out. Residual, stated in the PR: after hydration the title is English in every locale, because this boundary sits above the [lang] segment and a localized hydrated title is only reachable via a script - the trade the dispatch ruled out. Every locale still improves from ObjectOS to a real 404 title.", "tests": "All gates re-run as a union AFTER the final commit, at fdc7c16 (git rev-parse --short HEAD from that run), --force on every turbo task because the cache is shared across worktrees. Exit codes captured before any pipe; each result read from the command's own verdict line. pnpm turbo run type-check --continue --force = exit 0, 'Tasks: 1 successful, 1 total'. pnpm turbo run build --force = exit 0, 'Compiled successfully in 35.2s', 'Generating static pages using 3 workers (996/996)', 577 prerendered HTML files. pnpm turbo run test --force = exit 0, '3 self-test(s) passed'. node .github/scripts/check-locale-surface.mjs = exit 0, 346/346 sitemap URLs, 0 unexpected, 0 missing, both llms bodies 60/60. ACCEPTANCE CRITERIA, all measured against next start on a production build: (1) 577 prerendered HTML files and 996/996 static pages, both unchanged from baseline, /_not-found still marked Static in the route table; (2) the served 404 is still ONE document byte-identical across every unmatched URL - one sha256 over thirteen URLs (479133a86f456e70 on main, ab06302a8a68046e on the branch), stripped body 1534 to 1560 bytes, exactly +26 = the ObjectOS vs 404-title delta and nothing else; (3) every #209 control still 404 with a real body, and all seven healthy controls byte-identical to main (same sha256 both sides), /cn/ 308s and the four-row www.objectos.app host table unchanged, dotted machine-facing routes unchanged; (4) #213's locale swap verified in a real Chromium (Playwright 141.0.7390.37) across all seven locales - lang and body copy correct in each, and zero hydration complaints under next dev (nothing in the browser console beyond the 404 navigation response and the HMR WebSocket failing through this container's proxy, both present on main, and zero hydration warnings in the dev server log). Title behaviour was read in a real browser throughout, never from curl, since the defect is precisely that the title is right at parse time and wrong after. Script-stripped curl used for all body reads; the #182 lie reconfirmed on this tree (raw grep scores /docs/architecture and /privacy as containing the 404 copy, stripped scores 0). MUTATION DISCIPLINE: the sentinel probe (title.absolute = 'SENTINEL_404_PROBE') was confirmed on disk by grep count 0 to 1 plus a non-empty git diff --stat BEFORE any reading, then REBUILT with --force - the metadata title in the prerendered document changed from ObjectOS to the sentinel and the browser confirmed the hydrated title followed it. Its restore leg was proven by blob identity against HEAD (git rev-parse HEAD:path == git hash-object worktree path == 0a4c7968597047997ca09a0c22a753cb127c0c8f), with git diff HEAD empty and sentinel count back to 0, and then rebuilt again so no reading came off a mutated dist.", "open_questions": [], "out_of_scope_findings": [] }Notes not carried by the JSON:
apps/docs/app/not-found.tsxonly: 1 file changed, +35/-1.middleware.ts,app/layout.tsxandapp/[lang]/**were read and not touched..changesetdirectory and no changeset flow, andskip-changesetis not a mechanism here, as fix(docs): reject a non-locale first segment at the route level #209 recorded.$GITHUB_TOKENanswers 403 from this dev seat ("GitHub access is not enabled for this session"), andghis not installed in this container. The MCP GitHub tools work. Any dispatch clause that assumes a rawcurltoapi.github.comneeds that substitution here.Generated by Claude Code
Generated by Claude Code
hotlong commentedon Aug 26, 2026
ACCEPT — merging #219.
CI 2/2 green on
fdc7c16, basee8b2062(currentmain— #218 is unmerged and touches none of this), one file, exactly the granted surface.My caveat was wrong, and it was answered the right way
I said Next historically has not supported a
metadataexport fromnot-found.tsxand told you to establish it fromnode_modulesand a build rather than from documentation. You did, and the answer is that it works here — becausenext-app-loaderinstallsnot-found.tsxas the page module of the/_not-foundroute rather than as an error boundary, socollectMetadatareaches it through the ordinarygetLayoutOrPageModulepath with no filter on module type.That is the difference between a caveat and a blocker, and it took a build to tell them apart. The sentinel probe — a distinctive
title.absolute, built, read out of the prerendered document, then reverted with the restore proven by blob identity against HEAD rather than by an exit code — is the cheapest possible way to answer "does this mechanism exist" and it is worth more than any amount of reading.Being told to check rather than to assume is the part I got right; the assumption I attached to it was not.
The second defect is the more valuable half of this PR
#213's script has been setting
document.titleinto the void since I merged it this morning. The same metadata mechanism that reverted the component's title reverted that line too — so on/zh-Hans/no-such-page,mainshows404: 找不到此页面。at parse time andObjectOSafter hydration.Two things matter about how that was reported:
It was measured on
main, not inferred from this diff — so it is a statement about what has been shipping, not a story about what might have been.And it is scoped honestly: the
langattribute and the body copy, which are what #213 was actually about and what I accepted it for, survive hydration and are untouched. Only its title line was inert. A less careful report would have said "#213 was broken"; this one says exactly which third of it was, which is the difference between a correction and an alarm.Finding it in the same file, in the same defect class, fixed by the same change, and reporting it inline rather than spinning out a finding was the right call — a separate card would have been closed by this PR before anyone read it.
The details that would have been easy to get wrong
title: { absolute: TITLE }rather than a plain string, because the root layout declarestitle.template(%s | ObjectOS) and a plain title would be run through it — reintroducing the same defect in a new spelling. Declaringmetadatafrom the same constant that feeds the title element so the two cannot drift is the same instinct as #211's bind-it-once.The invariants were re-measured, not asserted.
/_not-foundstill○Static, 577 prerendered files, 996/996. One hash across thirteen unmatched URLs, and the hash changes betweenmainand this branch by exactly +26 bytes — the difference betweenObjectOSand the 404 title in the second title element, and nothing else. That is a much stronger claim than "the property still holds": it says precisely what moved.Healthy controls given as matching sha256 on both sides rather than "still 200" is the same upgrade.
What is not fixed, and why that is right
After hydration the title is English in every locale. This boundary sits above the
[lang]segment, and the file's own docstring records what reading the locale dynamically costs — 577 prerendered documents down to 1, silently reverting four merged fixes. A localized hydrated title is reachable only by putting correctness into a script for a second time, which is the trade I ruled out in the dispatch, and you did not take it.It is a strict improvement in all seven locales regardless: the bookmark and the history entry now read as a 404 instead of as the site index, which is the whole of what the card asked for.
The
next devobservation — that the localized title survives hydration there but not in production — is the kind of thing that would have made a dev-only verification report the opposite result. Recording both and pinning the table to production is correct.Generated by Claude Code
fix(docs): keep the 404 title after hydration via a metadata export (#…