Repository navigation
fix(cloud-connection): the install-local listing marks a package the rehydrate refused as not loaded (#21822) - #21833
Conversation
…ehydrate refused as not loaded
The kernel:ready rehydrate records each ledger entry it refuses under the
ADR-0087 D1 handshake, by manifest id and installedAt. GET /install-local
lists that entry with notLoaded { code, requiredRange } in place of
withSampleData, and reads no seed rows for it.
Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
Co-authored-by: Claude <noreply@anthropic.com>
…rker Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude <noreply@anthropic.com>
…and warnings Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude <noreply@anthropic.com>
… real boot across a restart Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude <noreply@anthropic.com>
… marker Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude <noreply@anthropic.com>
…t implement Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 15ef59c9dc4fa3cc5c4ad1225043cda837a52392 && git checkout 15ef59c9dc4fa3cc5c4ad1225043cda837a52392
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6fb71152ca5cc7c71723f15eaf33bb3f8f7138e8 3681793122c73909ef9bdb707e78b44fd16f3ede && git checkout -B drift-repro 6fb71152ca5cc7c71723f15eaf33bb3f8f7138e8 && git merge --no-ff 3681793122c73909ef9bdb707e78b44fd16f3ede
node scripts/docs-audit/affected-docs.mjs --json 6fb71152ca5cc7c71723f15eaf33bb3f8f7138e8
|
… load as not loaded (objectui#11645) (objectstack-ai#11759) Fixes objectstack-ai#11645 Clause-②: yes Installed Apps now reads an install-local entry the runtime refused to load at startup as **Not loaded**, names the reason in plain words, and keeps Uninstall on the row. The package's Details page no longer offers re-seed or purge for such an entry. A loaded entry renders as before, byte for byte (measured below). This is the console half of objectstack-ai/objectstack#21822. The server half is objectstack-ai/objectstack#21833 (`48297ad980`), carried by `@objectstack/*` 17.7.0, which objectui resolves since `c0862c1`. Written by the os-dev agent dispatched on claim comment `6029532484` (seat `domain:ui#3`, `mode:subagent`), session `https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8`. ## The server's wire shape, member by member Read at objectstack `48297ad980`: `handleList` and the `NotLoadedMarker` interface in `packages/cloud-connection/src/marketplace-install-local-plugin.ts`, and `ProtocolIncompatibleDiagnostic` in `packages/metadata-core/src/protocol-handshake.ts`. | Listing item member (server) | Present when | Console type (`LocalInstallEntry` in `marketplaceApi.ts`) | What the console does with it | |---|---|---|---| | `notLoaded.code` | the startup rehydrate refused this entry | `notLoaded?: LocalInstallNotLoaded`, member `code: string` | `OS_PROTOCOL_INCOMPATIBLE` selects `marketplace.notLoaded.protocolIncompatible`. Any other code selects `marketplace.notLoaded.otherReason`, which names the code. | | `notLoaded.requiredRange` | same | member `requiredRange: string` | interpolated into `marketplace.notLoaded.protocolIncompatible` | | `withSampleData` | loaded entries only (on a refused entry it is omitted, not `false`) | `withSampleData?: boolean`, unchanged | Details draws re-seed and purge only when `notLoaded` is absent | | `installedBy` | operator (`manage_metadata`) callers only | `installedBy?: string or null`, was required | unchanged rendering: the "by" line is drawn only when present | - `notLoaded` is closed on the server: exactly `code` and `requiredRange`. `LocalInstallNotLoaded` declares exactly those two members. - `code` is typed `string`, not the server's `'OS_PROTOCOL_INCOMPATIBLE'` literal. The server's own acceptance notes name other not-loaded states that are unmarked today. If a later server marks one, the console still reads the row as not loaded and names the code, instead of hiding the row the operator needs to uninstall. This is not a lenient alias: there is one key, `notLoaded`, read one way. - `installedBy` is now optional. The server omits it for a narrowed caller, and the narrowed-caller pin below needs a fixture without it. This is a bounded correction on the same interface: its only reader already guards with `&&`, so no reader changes. ### Public surface, measured on the BUILT declarations - **`LocalInstallEntry` and `LocalInstallNotLoaded` are not public.** After `pnpm --filter @object-ui/app-shell build`, a walk of the relative-import closure of `packages/app-shell/dist/index.d.ts` reaches 168 declaration files. None of them contains `LocalInstallEntry`, `LocalInstallNotLoaded`, `notLoaded` or `marketplaceApi`. Positive controls: `MarketplacePackagePage` is reached (2 files), and the emitted `dist/console/marketplace/marketplaceApi.d.ts` does carry `notLoaded`. The exports map has only `.` and `./styles.css`, so no deep import reaches the file. - **The five language-pack keys are public.** After `pnpm --filter @object-ui/i18n build`, `dist/locales/en.js`, `dist/locales/en.d.ts` and `dist/locales/zh.js` each carry them. So `Clause-②: yes` and the `minor` changeset stand. ## New language-pack keys, in all ten packs | Key | en value | Placeholders | Fed by | |---|---|---|---| | `marketplace.notLoaded.badge` | Not loaded | none | presence of `notLoaded` | | `marketplace.notLoaded.protocolIncompatible` | This runtime did not load this package: it targets protocol {{requiredRange}}, which this runtime does not support. | `requiredRange` | `notLoaded.requiredRange`, when `notLoaded.code` is `OS_PROTOCOL_INCOMPATIBLE` | | `marketplace.notLoaded.otherReason` | This runtime did not load this package ({{code}}). | `code` | `notLoaded.code`, for any other code | | `marketplace.uninstall.confirmNotLoaded` | Uninstall {{manifestId}} v{{version}} from this runtime? (blank line) The cached manifest will be removed. This runtime did not load the package, so none of it is running. | `manifestId`, `version` | the row's `manifestId` and `version` | | `marketplace.uninstall.successNotLoaded` | Removed {{manifestId}}. It was not loaded, so no restart is needed. | `manifestId` | the row's `manifestId` | - "targets protocol" is the server's own wording: the handshake message reads "package 'ID' targets protocol RANGE ... but this runtime is protocol VERSION". - No console phrase existed for the range. `git grep` for `OS_PROTOCOL_INCOMPATIBLE`, `requiredRange`, `protocol` and `incompatib` over `packages/i18n/src/locales/en.ts` and `packages/app-shell/src` found nothing relevant. Control: `versionBadge` is found in all ten packs. - The nine other packs are translated, not copied. The `untranslated-identity` pin refuses an English copy in zh, ja, ko, ru and ar, and it is green. - **One decision for the reviewer: the two `uninstall.*NotLoaded` keys.** The card asks for the row to keep Uninstall. The existing texts for that action say the package "will remain loaded in the running kernel until the next restart" (confirm) and "Restart the runtime to fully unload it from the running kernel" (result). Both contradict the row's own "Not loaded". If you read these as beyond the card, dropping them removes two keys and one ternary in each place. ## What changed - `InstalledListWidget.tsx`: a `destructive` "Not loaded" badge beside the version badge, and one reason line under the meta line. Uninstall stays and is enabled. The confirm and the result text are chosen by `notLoaded`. A row without `notLoaded` adds no node. - `MarketplacePackagePage.tsx`: the local menu's re-seed and purge items are not drawn for a `notLoaded` entry. Uninstall stays, and so does the primary Reinstall: that is the compatible re-install the server documents as reachable. Measured before this change: a refused entry's menu offered "Add sample data", enabled (the missing `withSampleData` read as no sample data), which posts a re-seed into objects the runtime never registered. It also showed purge, disabled. - `marketplaceApi.ts`: types only. - Locale packs: five keys in each of the ten packs. - `.changeset/11645-installed-not-loaded.md`: `minor` for `@object-ui/app-shell` and `@object-ui/i18n`. ## Tests - New `InstalledListWidget.notLoaded-11645.test.tsx`, 9 cases. `marketplaceApi` is not mocked. One stubbed `fetch` answers with the listing body the server landed, over a ledger that DELETE changes. The real `I18nProvider` renders in en and zh. Expected text is the pack's value, read from the pack and interpolated; no sentence is copied into the test. It pins: - the operator's refused entry: badge, reason naming `^16`, Uninstall enabled (en, zh); - the narrowed caller's entry, with no `installedBy`: badge and reason (en, zh); - a loaded entry: no badge and no reason, Uninstall enabled (en, zh); - Uninstall on the refused row: the not-loaded confirm, DELETE for its manifest id, the not-loaded result, and the re-read listing without the row; - a loaded row keeps the loaded texts; - an unknown code still reads not loaded, names the code, and keeps Uninstall. - New `MarketplacePackagePage.notLoadedMenu-11645.test.tsx`, 6 cases, run in the catalog view (marketplace on) and in the offline local view (objectui#11627). They pin: the refused entry's menu holds Uninstall alone; Uninstall still issues DELETE; a loaded entry still gets re-seed and purge. - `pnpm exec vitest run packages/app-shell/` at `79a84a7`: `Test Files 1054 passed | 1 skipped (1055)`, `Tests 10317 passed | 9 skipped (10326)`. The one later commit, `053d441`, edits one of the new test files only. Both new files re-ran at `053d441`: 15/15. - `pnpm exec vitest run packages/i18n/` at `79a84a7` (i18n has not changed since): `Test Files 81 passed (81)`, `Tests 1310 passed | 13 skipped (1323)`. - `pnpm --filter @object-ui/i18n type-check` and `pnpm --filter @object-ui/app-shell type-check` at `053d441`: exit 0. The script name echoes as `type-check`. `tsc -p tsconfig.test.json --listFiles` lists both new test files (2 of 5063 lines). Control: the existing `marketplaceDates.displayLocale-10331.test.tsx` is listed too. ## Reverse verification (one-off, not kept) - The fix was committed (`053d441`). Then the `c0862c1` blobs of `InstalledListWidget.tsx` and `MarketplacePackagePage.tsx` were checked out, under a trap that restores `HEAD`. - Proof that the change landed on disk: `git hash-object` equalled the base blobs `d2eaeb1d` and `e06fd029`, and the `notLoaded` count went from 10 to 0 (widget) and from 2 to 0 (page). - Proof of the restore: the hashes equal the head blobs `0e1e48ae` and `94d89725`, `git diff HEAD` is empty on both paths, the index is clean, and the count is back to 10. - Pre-fix: **8 failed, 9 passed (17)**. - The 8 that fail are every not-loaded pin: refused entry (en, zh), narrowed caller (en, zh), the Uninstall flow, the unknown code, and the Details menu in both views. - The 9 that pass on both sides are the preservation pins: loaded row (en, zh), the loaded row's Uninstall texts, Details DELETE in both views, and Details' loaded menu in both views. The other two are the markup probe below. - With the fix: 17 passed. - **A loaded row renders as before, byte for byte.** A one-off probe rendered Installed Apps over two loaded entries (one with `installedBy`, one without, one with a catalog id distinct from its manifest id) and hashed the container's `innerHTML`. Base and head are identical: en 6659 bytes, sha256 `fdfbc6d0…`; zh 6461 bytes, sha256 `250c012d…`. A `cmp` of the two dumps also reports them identical. ## Gates, at `053d441` Each line gives the exit code and the gate's own verdict. - `pnpm check:control-bytes`: 0, "check-control-bytes: OK (scanned 7750 tracked text file(s); skipped 85 binary)" - `pnpm check:test-path-roots`: 0, "check-test-path-roots: OK" - `pnpm check:changeset-claims`: 0, "No pending changeset names a file this change touches." - `pnpm check:pending-changeset-literals`: 0, "No test source names a pending changeset." - `pnpm check:i18n-keys`: 0, "Every in-scope call-site key resolves against the en pack (3291 keys)…" - `pnpm check:i18n-drift`: 0, "0 en value(s) changed (5 key(s) added, 0 removed …)" - `pnpm check:i18n-dead-keys`: 0 (report-only). None of the five new keys is in its candidate list. - `pnpm check:i18n-designer-parity`: 0, "Every en row has a zh row, and every shared row carries the same placeholders." - `pnpm check:new-line-citations`: 0, "0 new citation(s)" - `pnpm check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape`: 0, OK - `pnpm check:phantom-deps`: 0. `pnpm check:self-import`: 0 - `pnpm changeset:check`: 0, "No changeset declares a `major` bump." - `node scripts/check-changeset-presence.mjs`: 0, "15 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)" - `node scripts/check-governed-queue-guard.mjs --test` over the 16 paths: "NOT GOVERNED" - eslint, narrowed to the 15 touched `.ts`/`.tsx` files: exit 0, 0 errors, 49 warnings, 0 of them on a line this diff adds (mapped against the `git diff -U0` hunks). - Population: `isPathIgnored` is false for the touched files, and the `--format json` output has 15 results. - Invariance: the resolved config has no `parserOptions.project` or `projectService`, so linting is not type-aware and this diff cannot change the verdict on an untouched file. - The repo-wide `pnpm lint` is left to CI. - NOT MEASURED: `check:sdui-registration-pins`. It exited 2 with PREREQUISITE NOT MET, because it needs a console build (`apps/console/dist`). This diff does not touch the `ComponentRegistry.register` call. Left to CI. - NOT MEASURED: `check:eager-locale-catalogues`, which also needs a console build. The diff adds keys inside existing packs and no static import. Left to CI. ## Acceptance notes These were observed here and not changed. They are outside this card's surface, and no issues were filed for them. - The Details header still shows the green "Installed · vX" badge for a not-loaded entry. The card's Details step covers actions only, so the not-loaded marker is drawn on Installed Apps alone. - Details' own "Uninstall from this runtime" still uses `marketplace.uninstall.confirm` and `successInDetail`, which say the app stays loaded until a restart. Details' Uninstall is not an action that needs a loaded package, so it is outside the card's Details step. - The catalog page (`MarketplacePage`) badges a local install "Installed vX" whether or not it is loaded. - Docs: no page in `content/docs` or the app-shell README describes the Installed Apps rows, so no doc page changes. --- _Generated by [Claude Code](https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21822
Clause-②: no
What changed
After a restart whose
kernel:readyrehydrate refused a protocol-incompatible package (ADR-0087 D1),GET /api/v1/marketplace/install-locallisted it like any loaded package, and each GET logged awarnthat it could not read the package's seed rows. Now the rehydrate records each entry it refuses, and the listing serves that entry with anotLoadedmarker carrying the refusal's code and the declared range. The listing reads no seed rows for it, so the per-request warn is gone. This is the direction triage ruled (5988934231): a marker, not omission.packages/cloud-connection/src/marketplace-install-local-plugin.tsonly. ⛔ No change to the refusal itself (its line, level and wording), to DELETE, to the install route or tohandleReseed. Nopackages/specpath.The wire shape (for objectstack-ai/objectui#11645 to render)
A refused entry, as an operator (
manage_metadata) reads it:{ "packageId": "com.example.crm", "versionId": "…", "manifestId": "com.example.crm", "version": "…", "installedAt": "…", "notLoaded": { "code": "OS_PROTOCOL_INCOMPATIBLE", "requiredRange": "^16" }, "installedBy": "…" }notLoadedis CLOSED: exactlycodeandrequiredRange, the handshake diagnostic's own member names (the samerequiredRangethe install door's 422 carries inerror.details). It is not a spread of the diagnostic.notLoadedstands in place ofwithSampleData. The listing does not read the package's rows, so it makes no claim about them: the key is omitted, notfalse. This follows the same rule asinstalledByfor a narrowed caller.manage_metadata) gets the same item withoutinstalledBy. The marker is served to every authenticated caller.withSampleData, nonotLoadedkey.totalcounts marked entries.DELETEthe entry is gone from the ledger and the listing. Once a compatible version is installed over it, the entry is listed as loaded, with no marker.Where "not loaded" comes from (H3, measured on the code)
I compared two sources:
checkProtocolCompaton each ledger entry per request.I chose A. It records what the rehydrate did, so it agrees with it by construction. B is a second judgement, and it disagrees with what happened in reachable states:
rehydratereturns before the handshake loop when there is nomanifestservice ("nomanifestservice — rehydrate skipped"). B would mark entries whose refusal never happened.The record is keyed by manifest id and holds the refused entry's
installedAt, which says which entry was refused. The install door is the only writer of a new entry, it always stamps a freshinstalledAt, and it runs the same handshake first, so it can never write an incompatible entry. So a compatible re-install stops the marker without any write to the record from the install door or from DELETE.rehydrateclears the record when it starts.Pins (the ruling's, verbatim, plus the order's addition)
GET /install-locallists it with the marker and the code."withSampleDatawarn for the marked entry.Unit (
src/marketplace-install-local-listing-not-loaded.test.ts, 7 cases): a fresh plugin over a ledger with one refused and one loadable entry. The engine double answers only for registered objects, the way the real one answersObject '…' not found. The cases cover:Door (
packages/qa/dogfood/test/install-local-listing-not-loaded.dogfood.test.ts, 7 cases): real showcase boots over onedatabaseFileand one ledger.^(major-1), an install made for an older runtime.errorline.warnthrough the same logger, in the same capture window.Reverse verification (one-off, not kept)
The plugin file was restored to the merge base (
c4d57131, blob50a71c2f) with a trap that restoresHEAD. Each leg was proven on disk: therefusedAtRehydratecount went 6 → 0 → 6, the hash equalled the BASE blob, then theHEADblob411ad7a9, andgit diff HEADcame back empty. Both suites import the plugin fromsrc: the unit test by a relative path, and the dogfoodisolatedproject aliases@objectstack/cloud-connectionto../../cloud-connection/src/index.ts. So nodistleg applies.readsOfMarkedEntry: ["qa_old_account"]and the warningcom.example.qaold21822: the installed-apps listing could not read this package's seed rows (qa_old_account: Object 'qa_old_account' not found), so it answers withSampleData: false for it. With the fix: 7 passed.notLoadedisundefined, and the GET logsWARN [MarketplaceInstallLocal] com.example.crm: the installed-apps listing could not read this package's seed rows (crm_account: Object 'crm_account' not found; crm_contact: …; crm_opportunity: …; crm_lead: …; crm_activity: …). This is the card's own reading, reproduced. With the fix: 7 passed.The preservation pins (the loadable entry, DELETE, the compatible re-install, the preconditions and the capture control) pass on both sides, as preservation pins should.
Tests and gates
All readings are from
claude/issue-21822-listing-not-loaded-markerafter one merge oforigin/main(07bf21ff). Final head:36817931.@objectstack/cloud-connectionand@objectstack/dogfooddependency closures, rebuilt after the merge:turbo run build, 63/63 tasks.pnpm --filter @objectstack/cloud-connection test: 40 files / 492 tests passed (at6bb9f960). The one later commit,36817931, changes only the new test's engine double, and that file was re-run at36817931: 7/7.pnpm --filter @objectstack/cloud-connection typecheck: exit 0.tsc --listFilesincludes the new test file.pnpm --filter @objectstack/dogfood typecheck: exit 0.tsc --listFilesincludes the new dogfood file.vitest run --project isolatedoverinstall-local-listing-not-loaded,install-local-listing-sample-data,install-local-purge-sample-dataandinstall-local-no-active-organization: 4 files / 27 tests passed. This narrowing is proven, not assumed:git grep -l -E 'MarketplaceInstallLocalPlugin|marketplace/install-local' -- packages/qa/dogfood/testat HEAD names exactly these four files. The rest of the suite belongs to CI's Dogfood Regression Gate.dispatch-gates --commandsderives for this change set (the dispatch's 49 plus 18 more), all exit 0 at36817931.dispatch-gates --ran: "67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN".check:dual-build-cjs-loadsfirst answeredPREREQUISITE NOT MET: eight packages outside this closure had nodist. They were built and the gate re-ran green: "106 published require entry point(s) across 66 package(s) load".check:where-matchercaught the new engine double reading a combinator as a field name. The double now refuses what it does not implement: 461/461 conforming.pnpm lint(eslint . --no-inline-config, the whole repository): exit 0 at36817931.origin/maingained088428fb(fix(runtime): PATCH reaches a declared AI route through the /ai/* wildcards, and an undeclared method answers 405 #21823: the runtime dispatcher andscripts/check-route-envelope.mjs). Socheck:route-enveloperan on its pre-fix(runtime): PATCH reaches a declared AI route through the /ai/* wildcards, and an undeclared method answers 405 #21823 copy. CI runs on the merge ref.Acceptance notes
registerthrows ("rehydrate failed for …", aterror) is still listed withwithSampleData, and gets the per-request seed-row warning.manifestservice atkernel:ready("rehydrate skipped") lists every entry as installed.notLoaded.codeis its slot. Today the shape is closed to the one code.withSampleDatais install-wide, so after a purge in organization A,GET /install-localread as organization B answerswithSampleData: falsewhile B still holds its 28 seed rows #21775 changeset's "(for example, a package the runtime did not load)" stays true, for the first case above.error), same wording. Only its docblock now says the listing marks the entry.handleReseedin the same file. This PR touches only the rehydrate's refusal branch, the record field,handleListand a helper after it, so there is no overlapping region.Generated by Claude Code