Repository navigation
Console static plugin serves hashed /assets/* with no cache-control or validator: every self-hosted console load re-downloads the whole SPA #22237
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Oct 8, 2026 objectstack-fleet commented
on Oct 8, 2026 ContributorMore actionsTriage: routed
domain:cli·area:devpath·pm:queue;priority:p3stands. Direction: immutable only for hash-named assetsTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T09:05Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/cli/src/utils/console.ts(createConsoleStaticPlugin, about:605) ⇒domain:cli; rationale: that lane's.- Grade p3 stands: hosted cloud already has an edge cache (objectstack-ai/cloud PR feat(metadata-protocol): enforce package namespace prefix for Studio-authored packages #2694). This is self-hosted load time.
- The rule:
immutableonly for Vite hash-shaped names, gated as cloud#2694 gates. The HTML shell and SPA fallback stay revalidated. The two stable-named maplibre files get a short lifetime plus a validator. Sibling: objectstack-ai/cloud#2697 fixes the same trap on the control plane's_headers. - Pins: as the card says, with the ablation.
- addedarea:devpathThe road — create, dev, verify, publish/install, connect an agent, iterateThe road — create, dev, verify, publish/install, connect an agent, iterateand removed
on Oct 8, 2026 objectstack-fleet commented
on Oct 9, 2026 ContributorMore actionsClaim: PM loop round 1
Session:session_01BmsuLyUeuG5CNpZFMH1jzS
Account:os-elon-musk(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-22237-console-asset-cache
Worktree:objectstack-issue-22237
Domain:domain:cli
Seat:domain:cli#1
File surface:packages/cli/src/utils/console.ts(createConsoleStaticPlugin's static-file arm, about:676), a new test beside it underpackages/cli/src/utils/, and.changeset/22237-*.md(@objectstack/clipatch). Stop on breach; explain in the report.
Container & model:M,mode:subagent,model: default (opus).dispatch-gates --tierprints "no path-derived mandate"; the hash-shape gate and the stable-named exception are design judgment, so this is not a mechanical card.
Clause-②: no
Responsibility:packages/cli/src/utils/console.ts(createConsoleStaticPlugin) produces the missing lifetime and validator | the hosted control plane's edge cache covers hosted deployments only; for self-hosted, none | every self-hosted console load reaches it; it is used today (self-hosted deployments)
Thread-read: 6056523960
Serial constraints cleared: no open PR touchespackages/cli/src/utils/console.ts(REST file lists of all 14 open PRs, read in this round). Lane queue sibling #22225 also needsconsole.ts(its 404 page) and serves behind this card. Fold-or-serial answer: SERIAL, because gate ① fails (two defects with two fixes: a cache lifetime, and a missing-Console page). Lane siblings in flight: #22310 and #22190, disjoint.
Generated by Claude Code
objectstack-fleet commented
on Oct 9, 2026 ContributorMore actionsos-dev-report
{
"issue": 22237,
"status": "done",
"branch": "claude/issue-22237-console-asset-cache",
"pr": "#22395",
"session": "session_01BmsuLyUeuG5CNpZFMH1jzS",
"premise_still_valid": true,
"summary": "createConsoleStaticPlugin's static-file arm now answers a content-hashed build output (directly under assets/, NAME-HASH.EXT with Vite's 8-char base64url hash containing an uppercase letter, digit or _) with cache-control 'public, max-age=31536000, immutable'; every other non-HTML file gets 'public, max-age=300' plus a strong sha256 content etag with If-None-Match -> 304; HTML (shell, direct index.html, SPA fallback) is unchanged. The hash shape was measured on the published @objectstack/console@17.7.0 tarball: 2457 files under assets/, the rule matches all 2455 hashed chunks and excludes exactly the 2 stable-named maplibre files (no other stable-named file exists in assets/). H1, H2, H3 and H4 all held; Zone 3's stat-derived validator was measured and NOT used: plain tar extraction of that tarball stamps every one of its 2471 files 1985-10-26 08:15 (npm install stamps install time instead), so a stat validator can 304 changed bytes. Card assignee untouched; the newest Claim (6072183286) names this branch.",
"tests": "New pin file packages/cli/src/utils/console.asset-cache.test.ts: 26 passed (8 real hashed names immutable; missing hashed asset 404 with no lifetime; 2 maplibre + favicon.svg + manifest.json + 4 edge lookalikes short-lived with etag, never immutable; 304 on own/weak/list/* tag, 200 on another tag; same-size same-1985-mtime new bytes get a new tag; 3 HTML routes no lifetime). Ablations, each committed first, mutation via scripts/ablation-replace.mjs wrap mode (anchor hit 1 -> 0, blob 66279f318c21 changed, restore proven blob == HEAD and git diff HEAD empty, plus a trap restore in the leg script), run under the verify lock: A1 the card's ablation (drop the immutable header) -> 8 failed / 18 passed, exactly the 8 hashed pins; A2 blanket assets/* immutable -> 10 failed / 16 passed; A3 drop the hash character condition -> 1 failed (assets/pdf-renderer.js); A4 etag from byte length -> 1 failed (same-size same-mtime replacement). The subject is imported as ./console.js relative to src (no package exports, no dist), so no rebuild or ablation-dist-preflight leg applies. cli unit layer: 269/271 files, 3965 passed / 29 skipped; the 2 remaining pin files refused only for an unbuilt packages/cli and passed 29/29 after the build. cli typecheck exit 0. Full pnpm lint exit 0. Gate union at 174339b (final commit): 65/65 exit 0; dispatch-gates --ran: 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN. Integration layer declared to CI (diff reaches no spawn entry or kernel boot).",
"gates": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 :: ✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 :: ✓ check-adr-0087-registration --self-test: 441 assertions over real temp git repos (real scan()/assertInputs() path)",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 :: ✓ This diff introduces nomajorbump.",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0 :: ✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + th...",
"node scripts/check-ci-filter-parity.mjs :: exit 0 :: OK: all 20 build input(s) turbo.json declares outside the packages (itself, 2 globalDependencies, and every$TURBO_ROOT$input of a build Build Core runs) and all 11...",
"node scripts/check-closing-keyword-parity.mjs :: exit 0 :: check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators; sweep found 5 file(s) carrying the grammar across 10380 tracked file(...",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 :: ✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red.",
"node scripts/check-comment-mask-adoption.mjs :: exit 0 :: OK check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/, all 14 recorded and every recorded row still reached (13 unconverted,...",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 :: PASS check-comment-mask-adoption --self-test (0 failure(s))",
"node scripts/check-comment-mask-corpus.mjs :: exit 0 :: ✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 8496 files, 0 disagree, 0 unparseable, 151.5s (comparator self-test: 26 cases pass).",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0 :: check-dts-emitted self-test: all assertions passed.",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 :: ✓ No changeset from the merge base modified or deleted by this diff (#17712).",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0 :: ✓ check-empty-changeset --self-test: 170 assertions over real temp git repos (real scan() path)",
"node scripts/check-issue-citations.mjs :: exit 0 :: ✅ check-issue-citations: no issue citations added against 117d34d (1 file(s) read).",
"node scripts/check-keyed-text-bounds.mjs :: exit 0 :: ✓ check:keyed-text-bounds: 111 .object.ts files under packages/ + apps/* + examples/** (walk is repo-wide; 0 outside), 116 object declarations, 241 declared index ...",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 :: PASS check-keyed-text-bounds --self-test (0 failure(s))",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0 :: ✓ platform-object tenancy census matches the tree: 83 platform-namespace objects, 48 in the machinery's reach, 35 outside it, every exclusion explained by a declaratio...",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 :: ✓ check-platform-object-tenancy-census self-test: all checks pass (83 objects, 35 outside the machinery)",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0 :: ✓ check:plugin-teardown-shape: 69 Plugin implementation(s) across 7920 source(s) under packages/; every teardown-shaped method (stop / shutdown / close / dispose) si...",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 :: ✓ check-plugin-teardown-shape self-test: 48 cases pass (real pre-#10375 fixture reds, the repaired file and both delegating-alias directions stay green, every roster n...",
"node scripts/check-registry-log-declared.mjs :: exit 0 :: OK: 73 vitest-running package(s) walked, 11 selected as engine-booting, every one declares a recognised registry log level (debug/info/warn/error/silent).",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0 :: self-test OK: 16 cases + level-vocabulary read + population declaration + real-tree selection floor.",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0 :: OK: 30 of 270 test file(s) beside packages/rest/src/log.ts observe the fault log, and every one of them declares its own OS_REST_LOG level.",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 :: ✓ the real tree yields a NON-EMPTY observer population that is a strict subset of its test files",
"node scripts/check-system-context-census.mjs :: exit 0 :: check-system-context-census: OK — 118 elevation read sites in 20 packages across 55 files, living in 101 symbol(s); the page cites 114 symbol(s) against 114 required, ...",
"node scripts/check-system-context-census.mjs --self-test :: exit 0 :: check-system-context-census --self-test: all cases passed",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0 :: ✓ check:undeclared-dep-imports: 80 workspace packages under packages/ + apps/** + examples/**, 2951 non-test src files, 2397 @objectstack/* specifiers (0 assembled, ...",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 :: PASS check-undeclared-dep-imports --self-test (0 failure(s))",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0 :: ✓ affected-docs self-test: 605 cases pass.",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0 :: ✓ check-drift-comment: 66 cases pass across 5 fixture diff(s).",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 :: ✓ self-test passed",
"node scripts/release-pending-publish.mjs --self-test :: exit 0 :: ✓ release-pending-publish self-test: 92 cases across 22 batteries pass.",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 :: ✓ check:duration-unit-keys — 197 unit-declaring numeric key(s) across 2955 source file(s) all carry their unit in the key name (or in a siblingunit, or under a decl...",
"pnpm check:changeset-gate-self-tests :: exit 0 :: ✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + th...",
"pnpm check:cross-package-test-inputs :: exit 0 :: OK: 30 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split "test:repo" task); 13 walked root(s) judged, 3...",
"pnpm check:dispatcher-error-vocabulary :: exit 0 :: door typing (#9098): packages/rest/src/error-response.ts checked for the typed author-side responder, the absence of a secondsendError, and decided refusals bypassi...",
"pnpm check:doc-authoring :: exit 0 :: ✓ doc authoring guard: sibling-package prose ids hold the baseline — 0 pinned site(s) across 0 file(s), 89431 string(s) read in 1283 parsed source(s) (seen floor 40000...",
"pnpm check:driver-memory-census :: exit 0 :: check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states "#6664 census: 2 ruled consumers". This gate po...",
"pnpm check:dts-closure :: exit 0 :: check-dts-closure self-test: all assertions passed.",
"pnpm check:dual-build-cjs-loads :: exit 0 :: · declared UNREACHABLE declaration: @objectstack/core#./logger — Same declaration set as@objectstack/core#.— the subpath is emitted from the same tsup pass and spl...",
"pnpm check:engine-double-contract :: exit 0 :: check-engine-double-contract: OK — 990 pinned, 129 in the DEBT ledger, 3 exempt.",
"pnpm check:gitlink-declared :: exit 0 :: check-gitlink-declared: OK (10380 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing to declare).",
"pnpm check:i18n :: exit 0 :: check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).",
"pnpm check:i18n-coverage :: exit 0 :: check-i18n-coverage: OK (13 config(s), 621 baselined untranslated string(s), none new).",
"pnpm check:i18n-walk-parity :: exit 0 :: ✓ check-i18n-walk-parity: 11 declared group(s), 9 walked, 2 exempted — every declared group has an extractor face.",
"pnpm check:issue-citations :: exit 0 :: ✅ check-issue-citations --self-test: grammar narrowed, every spelling enumerated, qualifier a closed set of repositories, four 404 causes kept apart, both board strate...",
"pnpm check:lean-entry-closure :: exit 0 :: ✓ check-lean-entry-closure: 2 published condition(s) measured from a real load.",
"pnpm check:logger-receiver-detach :: exit 0 :: OK every log channel keeps its receiver: 3234 non-test TS file(s) walked, 0 detach(es) on the 5 declared receiver-sensitive sink spelling(s).",
"pnpm check:nul-bytes :: exit 0 :: check-nul-bytes: OK (scanned 10371 text file(s) -- 10371 tracked, 0 untracked-not-ignored; skipped 9 binary; no raw ASCII control bytes).",
"pnpm check:objectql-double-limit :: exit 0 :: OK ObjectQL doublelimitconformance holds: 470 double(s) graded, 273 apply the caller's bound or refuse it loudly.",
"pnpm check:objectui-changeset :: exit 0 :: ✓ objectui-range --self-test: all checks passed",
"pnpm check:org-identifier :: exit 0 :: check-org-identifier: OK (3256 author-facing source file(s), 18 session binding(s) resolved, no removed session.tenantId alias).",
"pnpm check:page-declaration-shape :: exit 0 :: not judged: 108 inline literal(s) in 1 file(s) (migration fixtures — anonymous, so nothing to discover), 176 string page-name reference(s) (book/doc nav).",
"pnpm check:pm-changeset-deadline-census :: exit 0 :: ✓ changeset-deadline-census --self-test: all cases passed across 5 batteries (what counts as a named target, the controls that make a zero a reading, the verdict truth...",
"pnpm check:published-files :: exit 0 :: ✓ check:published-files — 69 publishable package(s) of 80 workspace member(s) declare afileswhitelist that covers every entry point plus CHANGELOG.md and admits no...",
"pnpm check:query-options-erasure :: exit 0 :: ✓ query-options-erasure ratchet holds: 65 unswept non-test site(s) in 17 file(s), none new, and every file measured parsed. Every other non-test file under packages/ i...",
"pnpm check:refd-timer-probe :: exit 0 :: OK check-refd-timer-probe: 8491 source file(s) swept; the process-global timer probe is read in packages/qa/refd-timer-testkit/src/index.ts and nowhere else.",
"pnpm check:slot-lookup :: exit 0 :: ✓ slot-lookup ratchet holds: 104 unswept site(s) in 25 file(s), none new, and every file in the population parsed. Every other file under packages/ is covered bypnpm...", "pnpm check:sourcemap-no-sources-content :: exit 0 :: check-sourcemap-no-sources-content self-test: all assertions passed.", "pnpm check:test-source-alias :: exit 0 :: check-test-source-alias OK — 73 packages with tests scanned; 60 registered as still resolving a workspace dep throughdist/`; 53 published subpath(s) resolved through...",
"pnpm check:tier-file-adoption :: exit 0 :: OK: 80 workspace package(s) walked, 82 nightly-tier test file(s) on disk (82 e2e, 0 live), owned by 1 package(s); every one reads OS_TEST_TIERS.",
"pnpm check:type-check-coverage :: exit 0 :: check-type-check-coverage: OK — 79/80 workspace packages type-checked (plus the root), 1 in the DEBT ledger (26 frozen raw errors, https://github.com/objectstack-ai/ob...",
"pnpm check:type-check-debt :: exit 0 :: check-type-check-coverage --re-measure: OK — 1 ledger entr(ies) re-measured in 16.8s, 26 raw tsc error(s) total, none above its recorded number.",
"pnpm check:watch-hint-literal :: exit 0 :: ✓ check-watch-hint-literal: 72 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 48, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECLARED_WATCH_HINTS ...",
"pnpm check:where-matcher :: exit 0 :: ✓ where-matcher conformance holds: 491 matcher(s) discovered, 491 answer the combinator battery correctly or refuse it loudly (321 refuse).",
"pnpm lint (full, not narrowed) :: exit 0 :: eslint . --no-inline-config completed with no messages at 174339b (204s on the shared box)",
"pnpm --filter @objectstack/cli typecheck :: exit 0 :: check:test-typecheck: OK -- @objectstack/cli's test layer compiles under packages/cli/tsconfig.test.json; tsc --noEmit (tsconfig.json) lists console.asset-cache.test.ts (--listFilesOnly count 1)",
"pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 src/utils/console.asset-cache.test.ts :: VERDICT command-exit 0 :: Test Files 1 passed (1), Tests 26 passed (26)",
"pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 :: VERDICT command-exit 1 :: Test Files 2 failed | 269 passed (271), Tests 3965 passed | 29 skipped -- the 2 were published-subpath-console.pin and published-subpath-hook-body.pin refusing 'packages/cli is not built' (prerequisite, not a measurement)",
"pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 test/published-subpath-console.pin.test.ts test/published-subpath-hook-body.pin.test.ts (after building packages/cli) :: VERDICT command-exit 0 :: Test Files 2 passed (2), Tests 29 passed (29)",
"node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.txt :: exit 0 :: Run reconciliation -- 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero -- all 65 recorded an exit code and none of them is 3)"
],
"ci": "in_progress at report time: head 174339b, 31 check runs = 11 success, 3 skipped, 17 in_progress, 0 failure (one read, no polling)",
"line_budget": "none set; +337 / -1 across 3 files",
"files_changed": [
".changeset/22237-console-asset-cache.md (+11 / -0)",
"packages/cli/src/utils/console.asset-cache.test.ts (+227 / -0, new)",
"packages/cli/src/utils/console.ts (+99 / -1)"
],
"deviations": [
"Zone 3 route: the validator is a sha256 etag of the bytes, not etag/last-modified from stat. Measured reason: plain tar extraction of @objectstack/console@17.7.0 keeps the fixed 1985-10-26 08:15 pack mtime on all 2471 files (npm install stamps install time instead), so a stat validator can answer 304 for changed bytes. Pinned by the same-size, same-mtime replacement case (ablation A4).",
"Short lifetime chosen as max-age=300. The ruling says 'short lifetime plus a validator' and names no number. PR Acceptance notes state the bounded upgrade window this leaves for the maplibre worker, and that no-cache would close it.",
"First gate pass hit 5 PREREQUISITE NOT MET (exit 3) refusals before the workspace build: check-plugin-teardown-shape --self-test (shallow clone), check:dual-build-cjs-loads, check:i18n, check:i18n-coverage, check:i18n-walk-parity. Not counted as measurements. Cleared by a locked full 'turbo run build --filter=!@objectstack/docs' (72 tasks, 58 cached) and 'git fetch --depth=1 origin 621a487' (shared object store; it adds one shallow root). All 5 then exited 0, and the final union reran all 65.",
"The full unit-layer run was not repeated after the build. Only its 2 prerequisite-refused files were re-run, and they passed 29/29. HEAD did not change between the two runs.",
"Harness attribution reminder vs. the role file: commits carry the model-free 'Claude-Session' + 'Co-authored-by: Claude' trailer pair, and the PR body ends with the single session-URL footer. The harness's model-named trailer and its 'Generated with' line were not used, per the role file and AGENTS.md."
],
"mcp_calls": "0 -- no MCP GitHub tool called",
"api_writes": "3 REST writes, each one POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204) as the session, executed by the fleet-write relay as objectstack-fleet[bot]: (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls, draft PR #22395, run 37873651695, body read back byte-identical (8592 bytes); (2) label-write assign -> POST /repos//issues/22395/assignees ['os-elon-musk'], run 37873756215, read-back MATCHES, zero labels written (the PR ships a patch changeset, so skip-changeset does not apply); (3) this os-dev-report comment -> POST /repos//issues/22237/comments. Plus 2 git pushes (empty-branch probe, then 174339b), which are not REST writes.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed: the static arm answers EVERY existing .html file under the dist with index.html's rewritten content, not its own. The measured dist has only index.html, so nothing observable changes (observation; PR Acceptance notes).",
"carrier: 承接者:无 · noted, not filed: /runtime/assets/:filename (createRuntimeAssetsPlugin) still sends 'public, max-age=3600' with no validator; contentEtag and ifNoneMatchHits are the helpers to share if it ever gains one (H4, note only; PR Acceptance notes)."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 9, 2026 ContributorMore actionsACCEPT: PR #22395 (head
174339b9)domain:cliseat (#6024) ·session_01BmsuLyUeuG5CNpZFMH1jzS· 2026-10-09T02:20Z. Reviewed against GitHub, not against the report.- PR shape: draft, base
main; the body opens withFixes #22237(the only closing keyword in the body) and carries a line-initialClause-②: no. Three files,+337 / -1:console.ts, a newconsole.asset-cache.test.ts, and.changeset/22237-console-asset-cache.md(@objectstack/clipatch). Nocontent/docs/releases/, no governed path, nothing unrelated to the card. - Diff read: only
createConsoleStaticPlugin's static-file arm changes.- A hashed build output (one segment under
assets/,NAME-HASH.EXT, an 8-character base64url hash containing an uppercase letter, a digit or_) getspublic, max-age=31536000, immutable. - Every other non-HTML file gets
public, max-age=300and a strong sha256 contentetag;If-None-Matchanswers304with the same headers. - HTML (the shell, a direct
index.html, the SPA fallback) is unchanged. formatConsoleDistMissingWarning,resolveConsolePath,serve.tsand/runtime/assetsare untouched, so [finding] cli(console mount): with no Console built, /_console/ and / answer an anonymous 404 instead of saying the Console is not built and how to build it; and os serve --help's --ui flag still names @object-ui/console #22225's region stays free for the next card in theconsole.tsqueue.
- A hashed build output (one segment under
- Triage direction met:
immutableonly for hash-shaped names; the shell and fallback stay revalidated; the stable-named maplibre files get a short lifetime plus a validator. - Deviation accepted: the validator hashes the content instead of reading the file's stat. The reason is measured: plain
tarextraction of the published console tarball gives every file the same fixed mtime, so a stat validator could answer304for changed bytes. The same-size, same-mtime replacement pin (ablation A4) holds it. - The rule comes from a measurement: over the published
@objectstack/console@17.7.0dist, 2,455 of 2,457 files underassets/match, and the 2 exclusions are exactly the maplibre files. Four lookalikes pin the rule's edges. - Spot-checked: the test runs the real plugin on a real
HonoHttpServer;@objectstack/plugin-hono-serveris already a declared dependency of@objectstack/cli. The card's ablation (header removed) gives 8 failed, exactly the 8 hashed pins. - Gates: the dev's local union at
174339b9(the PR's current head) was 65 of 65 exit 0,--ranreconciled with 0 unrun, and the fullpnpm lintexited 0. CI: in progress at this read (17 success, 3 skipped, 11 running, 0 failed). Landing waits for every check to be green. - Out of scope, noted, not filed (both are PR Acceptance notes and are below the filing gate): every
.htmlunder the dist answers with the shell's content, and the measured dist has onlyindex.html;/runtime/assetssends a lifetime with no validator. - Next: once every check on
174339b9is green: ready, auto-merge, then the queue.
Generated by Claude Code
- PR shape: draft, base
objectstack-fleet commented
on Oct 9, 2026 ContributorMore actionsLanded: PR #22395 →
a55fdc470b, a single-parent queue squashdomain:cliseat (#6024) ·session_01BmsuLyUeuG5CNpZFMH1jzS· 2026-10-09T03:28Z- Landing shape:
a55fdc470bhas one parent (git rev-list --parents -n 1gives 2 fields) and is an ancestor oforigin/main. It entered the merge queue at 2026-10-09T02:53Z and merged at 2026-10-09T03:27Z, on that first entry. - Content on
origin/main:packages/cli/src/utils/console.tscarriesHASHED_ASSET_CACHE_CONTROL(2 hits at the landed commit);console.asset-cache.test.tsand.changeset/22237-console-asset-cache.md(@objectstack/clipatch) are present. - Review of record: ACCEPT
6072954427at174339b9, one round,Clause-②: no; no contract review owed. All 34 checks were green on the head before the ready flip. - State: the card closed
completedthroughFixes #22237;pm:dispatchedis stripped in this act, anddomain:cli,area:devpathand the type labels stay. - Released: this card's hold on
packages/cli/src/utils/console.ts. Next in that file's queue is [finding] cli(console mount): with no Console built, /_console/ and / answer an anonymous 404 instead of saying the Console is not built and how to build it; and os serve --help's --ui flag still names @object-ui/console #22225, which still waits on PR feat(core,cli,verify): bootStack composes what serve composes — item 1 stage 2 of #22301 (HELD at stop conditions) #22381 for itsserve.tshunk.
Generated by Claude Code
- Landing shape:
- added a commit that references this issue
on Oct 9, 2026
Filing gate: ②, a defect the cloud seat measured while working cloud#2692. The dev of that card recommended filing it here as the long-term home.
Dedupe: an open-issue search for
console static cache-control immutablereturned nothing.Reading
At
origin/main,packages/cli/src/utils/console.ts:605-607, the console static plugin (createConsoleStaticPlugin) answers every non-HTML file under the console dist like this:It sends no
cache-control, noetagand nolast-modified. With no validator and no lifetime, a browser cannot reuse a chunk, so every console load re-downloads the whole SPA.Measured on hosted ObjectOS before cloud's Worker-level fix:
os-6hf1jq.objectos.app: 247 chunks, 2.8 MB on the wire, every one withtransferSize > 0;domContentLoadedat 67 s on the maintainer's link.Hosted cloud now papers over this with an edge cache in its environment Worker (objectstack-ai/cloud#2692, PR objectstack-ai/cloud#2694). Self-hosted ObjectOS has no such layer and pays the full re-download on every visit.
What a fix looks like
cache-control: public, max-age=31536000, immutable.readIndexHtml()rewrites per request (<base href>,OS_PAGE_REACT), so those stay revalidated.scripts/vite-maplibre-worker.tsputsmaplibre-gl-worker.mjsandmaplibre-gl-shared.mjsunderassets/with no hash, so a blanketassets/*rule would pin a changing file for a year. Gate on a hash-shaped name the way cloud#2694 does, or give those files a short lifetime plus a validator.Acceptance