Repository navigation
fix(cloud-connection): an install-local uninstall withdraws the package from the running kernel - #21581
Conversation
…ge from the running kernel The install-local DELETE removed the ledger entry and ran the protocol's uninstall cleanups, but left the package registered in the running kernel until the next restart. Every reader of "registered packages" kept counting it, and one of them re-created a ghost grant: another package's hot install announces metadata:reloaded, the declared-permission seeding re-runs over every registered package, and the uninstalled package's permission set came back as a package-managed row that outlived the restart as an orphan. The DELETE now withdraws the package through SchemaRegistry.uninstallPackage, the one verb the protocol's own uninstall uses, on the objectql engine's registry, right after the ledger removal and before the cleanups. A refused withdrawal is reported on the response as a failed outcome in `cleanups`, with the cause and remedy in the operator log. The response note no longer says the kernel cannot unregister in place. Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-Authored-By: Claude <noreply@anthropic.com>
…withdrawal and its reinstall control Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-Authored-By: Claude <noreply@anthropic.com>
… its refusals; changeset Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz 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:
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 87a560f0643414d7c38cc462d83d3de13b273a1f && git checkout 87a560f0643414d7c38cc462d83d3de13b273a1f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cc645f2385b3410e0f00a4d5c6ae4b0fae14b69f 9f3e65608d0339dc964ea78094e61f00b6ae700c && git checkout -B drift-repro cc645f2385b3410e0f00a4d5c6ae4b0fae14b69f && git merge --no-ff 9f3e65608d0339dc964ea78094e61f00b6ae700c
node scripts/docs-audit/affected-docs.mjs --json cc645f2385b3410e0f00a4d5c6ae4b0fae14b69f
|
…n every door, and install-local refuses an enabled job with no body (objectstack-ai#21489) (objectstack-ai#21584) Fixes objectstack-ai#21489 Clause-②: yes (narrowing) This executes ruling E + C (record `5964305303`, maintainer 「jobs同意」), as the card's runtime half and C, with the scope the erratum `5968972501` restates: a job `body` is authored as data and `os build` never mints one (objectstack-ai#21540 ruled C, record `5968961157`). Nothing here adds a build or lowering route. - **E, runtime half.** A job's sandboxed `body` (`JobSchema.body`, the hook body shape) is scheduled on every door that brings an artifact in: the boot (a config, or `os start --artifact`), and install-local on install and on every rehydrate. With both `body` and `handler` declared, the `body` wins. - **C.** install-local refuses a package whose enabled job has no `body`. The answer is `422` with `VALIDATION_ERROR`, it names each job and its handler, and it gives the remedy: give the job a `body`, or boot it with `os start --artifact`. Nothing is registered, persisted or scheduled. - **CLI.** `os package install` prints a refusal's code beside its status, for every refusal alike. - A job still on `handler` keeps working on a config or `--artifact` boot (the control pin). - **Patch round 1 (REWORK `5969239835`).** A package's jobs stop with it. Uninstalling a package cancels its scheduled jobs, on install-local's `DELETE` and on the protocol's package uninstall alike, and a reinstall cancels the jobs its new version drops. - **Amendment `5969471197` (seat-directed).** The texts this landing makes false ride this PR: `JobSchema.body`'s describe, `defineJob`'s TSDoc example, the regenerated `content/docs/references/system/job.mdx`, and the callout and example comment in `content/docs/automation/jobs.mdx`. ## What changes **The one binder's job half** (`packages/runtime/src/app-artifact-handlers.ts`): - `scheduleAppArtifactJobs(ctx, bundle, { appId, ql, source })` is the one place a declared job becomes a scheduled one. It holds the loop that used to sit inline in `AppPlugin.start`: the deployment switch (objectstack-ai#17396), the job-service probe, the `enabled` skip, `toBoundaryJobSchedule`, the `retryPolicy` / `timeoutMs` threading, and the failure posture (error level plus `jobScheduleFailuresTotal`). - Per job, a `body` is bound through `jobBodyRunnerFactory`. Otherwise the `handler` resolves against `functions`, with the in-process `JobHandlerContext` as before. A body that cannot be bound (an L1 expression, or a `body.timeoutMs`) schedules nothing, and never the handler beside it. - Callers: `AppPlugin` on `kernel:ready`, and install-local's `bindArtifactHandlers`, which runs on the install route and on the rehydrate. - It is a second entry point of the same module rather than a block inside `bindAppArtifactHandlers` for one reason, timing. The boot binds hooks and actions in `start()` but schedules jobs once the kernel is ready, while install-local's doors are already past that point. One implementation, two moments, no per-door copy. - `collectJobsWithoutBody(bundle)` names the enabled jobs with no `body`. It is the judgement C refuses on, read from the jobs the binder schedules. **A package's jobs stop with it** (`app-artifact-handlers.ts`, patch round 1): - The job half keeps a record of which job names each app scheduled, per job service instance (one per kernel). The last app to schedule a name owns it, so cancelling one app's jobs never stops a job another app scheduled under that name. - **Re-scheduling replaces.** Every job an app scheduled before and does not schedule now is cancelled through `IJobService.cancel`, the verb every adapter implements: the cron adapter stops its timer, and the DB adapter also marks the `sys_job` row inactive. That covers a job the new version drops, disables or can no longer run. A version with no jobs cancels them all. A cancel that throws is logged at `error` and the name stays on the record for the next attempt. - **Uninstall.** The first time a package's jobs are scheduled on a kernel, the job half registers ONE uninstall cleanup, `runtime.package-jobs`, through the protocol's existing `registerUninstallCleanup` (objectstack-ai#21490). It cancels the package's recorded jobs. The protocol's `deletePackage` and install-local's `DELETE` both run every registered cleanup with the package id, so both stop the jobs with no per-door copy, and the outcome rides the response's `cleanups`. A job it could not cancel is an outcome (`success: false`, naming the job), never a throw. No `metadata-protocol` file is edited. - The DELETE path keeps PR objectstack-ai#21581's behaviour: the registry withdrawal runs first, then the cleanups. This PR does not edit that path. **The sandbox job origin** (`sandbox/script-runner.ts`, `sandbox/quickjs-runner.ts`, `sandbox/body-runner.ts`): - `ScriptOrigin.kind` gains `'job'`. - `QuickJSScriptRunner` gains `jobTimeoutMs`, default 5000 ms of CPU, like an action body. `resolveTimeout` now picks a default per kind instead of hook-or-else. There is no env override: a job's own `timeoutMs` (uncapped) is the declared place to raise it. - A job body runs in the `(ctx)` wrapper hooks use; a job has no input. - `jobBodyRunnerFactory` passes the job's `timeoutMs` as `opts.timeoutMs`, the one limit `JobSchema.timeoutMs` states. - `jobBodyRunnerFactory` reads the body's return as a `JobRunOutcome`, in the declared shape only. - `jobBodyRunnerFactory` serves `ctx.api` through `buildSandboxApi`, like every body's, under `{ isSystem: true }`. A job has no caller: an action body with no caller gets the same envelope, and a `handler` job's raw `ql` amounts to it. The stored-metadata write refusal still applies. **install-local** (`packages/cloud-connection/src/marketplace-install-local-plugin.ts`): the refusal sits as step 1c, beside the id gate. That is ahead of the conflict check, the posture gate, the hot-register and the ledger write. Rehydrate is not gated, for the id gate's reason. An entry an older build installed still rehydrates; its handler-only job is reported at `warn` and not run. **CLI** (`packages/cli/src/commands/package/install.ts`): the generic refusal branch prints `Install failed (STATUS CODE): MESSAGE`. It was `Install failed (STATUS): MESSAGE`, which dropped the code. **Spec ledger** (`packages/spec/liveness/job.json`, `state-counts/job.md` regenerated): see the deviations below. The `job.body` children `language`, `source`, `capabilities` and `memoryMb` flip `planned` to `live`. `authorWarn` / `authorHint` are dropped, as the row's own carrier note prescribed for this card's commit. `body.timeoutMs` stays `planned` (refused). The five rows that cited `app-plugin.ts#start` for the moved loop are repointed to `scheduleAppArtifactJobs`. ## Measurements **A4, reach at the public door, before the fix** (base `bd70706713`). The composed pin `packages/cli/test/package-install-local-jobs.integration.test.ts` ran unchanged against the base build: 4 red, 2 green. - The body-job package installed with exit 0, and its job wrote 0 rows hot and 0 after a restart. - The handler-only package installed: exit 0, `Package installed into the running kernel`. It was never scheduled. - Control, `os start --artifact` of one artifact carrying both forms plus its runtime module: the handler job ran (rows written) and the body job wrote 0 rows. **After the fix:** 6 of 6 green, at `f99d6dcd39` and again at head `c866c5ac9d`. **A1, the pointer's facts, re-measured at `bd70706713`:** 1. `AppPlugin#start` resolved `fnMap[job.handler]` only (`app-plugin.ts:1178`). A body-only job was skipped at warn, and with both keys present `body` was ignored. Now the body binds, and wins (pins below). 2. `ScriptOrigin.kind` was `hook | action` (`script-runner.ts:118`; `body-runner.ts:120` and `:228`), and `resolveTimeout` defaulted everything non-hook to the action budget. Now `'job'` has its own default. 3. `job.timeoutMs` reaches the runner as `opts.timeoutMs`. Pinned: a spinning body with `timeoutMs: 40` rejects with `job 'spin_job' exceeded CPU budget of 40ms`, and the adapter receives `{ timeoutMs: 40 }`. 4. What a job body receives, measured with `Object.keys(ctx)` inside the VM: `api`, `log` and `crypto`, each behind its capability token. `input`, `previous`, `user` and `session` are present and `null`; the shared `installCtx` installs them for every body. There is no `jobId` and no trigger `data`, even when a manual trigger passes data. `ctx` is not widened. **A2, the one binder:** see above. **Patch round 1, measured at the public door before the cancellation** (the extended pin at `37c472764f`, whose code was `c866c5ac9d`): 2 red, 9 green. - After an install-local `DELETE` (200), the uninstalled package's body job kept writing: 38 to 42 rows in 4 s. - After a reinstall whose new version dropped one of two jobs, the dropped job kept writing: 17 to 21 rows in 4 s. - After a restart, neither ran, because neither is rehydrated. Another installed package's job ran throughout. **After the cancellation:** 11 of 11 green. After PR objectstack-ai#21581 landed, an uninstalled package's own object stops answering, so the pin's packages write into an object the host artifact owns; a run that should have stopped still shows there. **A3, codes.** `VALIDATION_ERROR` / 422 is an existing member of the `ErrorCode` union (the standard catalog), so this is **not** `PENDING LEDGER CODE` and nothing under the error-code ledger is edited. - The condition is generic: the install payload fails this door's acceptance rule, which is that every enabled job carries a `body`. - The ledger's admission rule sends a generic validation condition to the standard member rather than to a registered synonym (`error-code-ledger.zod.ts`, "Registering a new code"). This follows PR objectstack-ai#21563's `PERMISSION_DENIED` reasoning. - `PLUGIN_MANIFEST_INVALID` was rejected because it would be untrue: the manifest is valid, since `os validate` passes it and `os start --artifact` runs it. - 422 rather than this door's 400/502 split: a catalog package that declares a handler job is no upstream fault. 422 derives `VALIDATION_ERROR` (`standardErrorCodeForHttpStatus`), so code and status agree. - If the contract review prefers a dedicated code, it is a one-constant change here (`JOB_WITHOUT_BODY_REFUSAL_CODE`) plus a spec-lane ledger row. **A5, CLI rendering.** Before: `Install failed (422): MESSAGE`, with the code dropped. That was a rendering gap, so the generic branch now names the code for every refusal. There is no case per code, and an envelope with no code prints the status alone. Pinned by unit and integration tests. **A6, the sibling (hooks).** Measured once at the public door. A package with a hook in the deprecated `handler` form and no function installs with exit 0 (`Package installed into the running kernel`), and the hook never fires: an inserted row keeps `legacy: null`, while a body-hook control on the same object stamped `bodied: yes`. The only trace is a server-side `WARN [hook-binder] skipping hook with unresolved handler`. That is silent at the door. Reported for the seat to file; not fixed here. ## Pins - `packages/runtime/src/app-artifact-handlers.jobs.test.ts` (23, real QuickJS) covers: - a body job is scheduled, and a run writes through `ctx.api` as `{ isSystem: true }`; - with both keys the body wins, and the handler is never called; - an L1 body and a `body.timeoutMs` are not scheduled, and never the handler beside them; - `timeoutMs` reaches the adapter and bounds the run; - with no `timeoutMs`, the runner's JOB default applies (not the hook's or the action's); - the `JobRunOutcome` shape; - the `ctx` surface has no `jobId` or `data`; - the handler control, the handler-not-found warn naming the body remedy, the disabled skip, the deployment switch, and `collectJobsWithoutBody`; - the boot door: `AppPlugin` schedules a body-only job on `kernel:ready`; - patch round: a reinstall that drops a job cancels it and keeps the other; a disabled job and a version with no jobs cancel; another app's jobs are never cancelled, even one that took over a name; a cancel that throws is said at `error`; - patch round: `runtime.package-jobs` is registered once per protocol, cancels every job of the uninstalled package and none of another's, is a no-op for a package that scheduled nothing, and reports an uncancellable job as `success: false`. - `packages/cloud-connection/src/marketplace-install-local-jobs.test.ts` (8, real runtime `dist`): - install schedules and runs the body; - rehydrate schedules it; - the refusal answers 422 `VALIDATION_ERROR`, names the job, the handler and both remedies, and leaves nothing registered, persisted or scheduled; - the plural message; - a disabled handler-only job installs; - a package without jobs installs with the same response keys; - patch round: `DELETE` cancels the uninstalled package's job through the cleanup, with `runtime.package-jobs` on the response's `cleanups`, and a control package's job stays scheduled; - patch round: a reinstall that drops a job cancels it. - `packages/cli/test/package-install-refusal-rendering.test.ts` (3, unit). - `packages/cli/test/package-install-local-jobs.integration.test.ts` (11, integration): install, restart, refusal, the control on both forms, and, in the patch round: after the `DELETE`, no further row hot and none after a restart; after a dropping reinstall, the same for the dropped job while the kept job runs on; and another package's job running throughout. ## Reverse verification (ablation), fix committed first Every leg ran through `scripts/ablation-replace.mjs` in wrap mode. In each, the anchor went from 1 to 0 on disk and the blob changed. The marker was proven in `dist/` by `ablation-dist-preflight.mjs` (present on the mutate leg, `--absent` plus a clean tree after the rebuild on the restore leg). The restore was proven by blob equal to HEAD and an empty `git diff HEAD`. - **Leg A, body scheduling disabled** (`if (job.body) {` in `scheduleAppArtifactJobs`, runtime rebuilt): - runtime unit: 8 red / 7 green (the handler, `collectJobsWithoutBody` and runner-default cases stayed green); - cloud-connection: 2 red (install, rehydrate) / 4 green; - CLI integration: 3 red (hot, restart, control body) / 3 green. - **Leg B, the refusal disabled** (the `withoutBody.length` guard of step 1c in install-local, cloud-connection rebuilt): - runtime: 15 green; - cloud-connection: 2 red (both refusals) / 4 green; - CLI integration: 1 red (the refusal) / 5 green. - **Leg C, the cancellation disabled** (`await svc.cancel(name);` in `retireAppJobs`, runtime rebuilt), at `bb25c4992e`: - runtime: 6 red (every replace and uninstall-cleanup case) / 17 green; - cloud-connection: 2 red (`DELETE`, reinstall) / 6 green; - CLI integration: 2 red (uninstall hot 37 to 41 rows, dropped job 16 to 20) / 9 green. - **Leg D, the cleanup registration disabled** (`ensureJobUninstallCleanup(ctx, jobService);`, runtime rebuilt): only the uninstall pins went red. Runtime 4 red / 19 green, cloud-connection 1 red / 7 green, CLI integration 1 red (uninstall hot) / 10 green. The reinstall pins stayed green, so the two mechanisms are pinned apart. - **Leg C repeated on the final head `650ff1e486`** (after PR objectstack-ai#21581's withdrawal landed, as `ABLATION_21489_E`): runtime 6 red, cloud-connection 2 red, CLI integration 2 red (uninstall hot 38 to 42 rows, dropped job 16 to 20). The withdrawal alone does not stop the job. ## Tests and gates Final head `650ff1e486`, which merges `origin/main` after PR objectstack-ai#21581 landed (no conflict): - `@objectstack/runtime` `pnpm test`: 317 files, 4467 passed, 19 skipped. - `@objectstack/cloud-connection` `pnpm test`: 35 files, 428 passed. - `@objectstack/cli` `--project unit`: 254 files, 3721 passed. - `@objectstack/cli` `--project integration`, the four install-local pins (jobs, handlers, boot-steps, uninstall-cleanups): 4 files, 51 passed. The rest of the integration tier is declared to CI. - `@objectstack/spec` `pnpm test`: 606 files, 17952 passed. - `typecheck` green for runtime, cloud-connection and cli. - `check:generated` after the describe edit: only `check:docs` was stale. `--fix` regenerated `content/docs/references/system/job.mdx` alone, and only the describe sentence moved. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 118 families (the docs and spec families joined with the amendment), all run at `650ff1e486`, every one exit 0. The `--ran` reconciliation reads 118 run, 0 NOT-MEASURED, with exit codes recorded. - `pnpm lint` (full repo): exit 0 at `650ff1e486`. ## Deviations and file surface - **`packages/spec/liveness/job.json` and `state-counts/job.md` were edited, although the dispatch keeps this lane out of `packages/spec`.** Moving the job loop out of `AppPlugin.start` turned `check:liveness`, a required gate, red: `job/retryPolicy` and `job/enabled` cited `app-plugin.ts`, which no longer names them. The gate's prescription is to repoint. The ledger's own `job.body` carrier note designates this card's commit for the `planned` to `live` flip. Left `planned`, the published `authorHint` makes `os validate` print a false warning. Measured with a config declaring a body job, `os validate` printed `job 'vj_tick_body': sets body.source but this job property is planned ... (not read YET)` before this edit, and prints no such warning after it. No Zod schema, no error-code ledger and no generated docs were touched. The spec package rides the changeset as `minor`, because `liveness/` is in its `files[]`. - `docs/qa/platform-checklist/areas/integration-system.json`: one source anchor repointed (`app-plugin.ts#handler` to `app-artifact-handlers.ts#scheduleAppArtifactJobs`). `check:platform-checklist` went red on the move. - `packages/runtime/src/sandbox/quickjs-runner.ts` (the per-kind default and the wrapper) and `packages/runtime/src/index.ts` (exports) were outside the expected list. - **Seat-directed, amendment `5969471197`:** `packages/spec/src/system/job.zod.ts` (the `JobSchema.body` describe sentence and the `defineJob` TSDoc example comment, text only, no shape change), the regenerated `content/docs/references/system/job.mdx`, and `content/docs/automation/jobs.mdx` (the callout and the example comment, plus one sentence on uninstall and reinstall). No wording names a build or lowering route. ## Acceptance notes - **Pointer fact 5**, reported only: `allowRuntimeCreate: false` for `job` is justified by `handler` alone. A runtime-authored job with a `body` would now be runnable in principle, but no door schedules a runtime-authored job; the binder schedules artifact jobs. - `body-runner.ts`'s job factory is not exported from `@objectstack/runtime`'s root, unlike the hook and action factories; it has no consumer outside the binder. - Code-read, unmeasured: `os package install` renders every 404 as "install-local endpoint not found", including a catalog 404 (`CLOUD_FETCH_FAILED`, a package missing from the catalog). - Code-read, unmeasured: a handler-form hook falls back to `engine.resolveFunction`, so it could bind to a same-named function another app registered. The silent drop of a handler-form hook is filed as objectstack-ai#21585. - `packages/qa/dogfood/test/expression-conformance.ledger.ts` prose still names `runtime/app-plugin.ts start` as the `toBoundaryJobSchedule` call site. - objectstack-ai#21540 is not addressed here (ruled C; see the erratum above). --- _Generated by [Claude Code](https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21576
Clause-②: no
What this changes
DELETE /api/v1/marketplace/install-local/:manifestIdnow withdraws the package from the running kernel. It does this right after the ledger removal, throughSchemaRegistry.uninstallPackage, the one verbdeletePackageuses. The uninstall cleanups from [finding] An install-local uninstall (DELETE /api/v1/marketplace/install-local/:id) leaves the package's permission sets in sys_permission_set: the "no ghost grants" uninstall cleanup never runs on that door #21490 then run as before.metadata:reloaded, which is what re-projected the uninstalled package's set.5968468119: the first seam, the root. There is no "skip uninstalled" check in plugin-security.packages/metadata-protocol,packages/plugins/plugin-securityorpackages/spec. The install and rehydrate path ofmarketplace-install-local-plugin.tsis untouched.Files:
packages/cloud-connection/src/marketplace-install-local-plugin.ts: the uninstall path only. That ishandleUninstall, the newwithdrawFromRunningKernel, the corrected header block and the corrected log line.packages/cloud-connection/src/marketplace-install-local-uninstall-withdrawal.test.ts: a new unit pin, 6 cases.packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts: order 3's twoit.failspromoted to plainit, a hot-object case in orders 1 and 2, and a new order 4..changeset/21576-install-local-uninstall-withdraws-registration.md:@objectstack/cloud-connectionpatch,Clause-②: no.A1: reproduction on
74281a8e4ait.failschanged to plainit, on packages built in this worktree. It readTests 2 failed | 11 passed (13). The 13 includes one temporary measurement case, used for A4 below. The two red cases are exactly the two set readings:72bd1cc16e), andgit status --porcelainwas empty.A2: how the door reaches the registry
deletePackagereaches the registry asthis.engine.registry.uninstallPackage(packageId).this.engineis the ObjectQL engine the protocol is assembled over (assembleMetadataProtocol(ctx, this.ql, …)inpackages/objectql/src/plugin.ts).objectqlservice. Themanifestservice, which this door installs through, callsql.registerAppon it.ctx.getService('objectql').registryand callsuninstallPackage(manifestId). The call is typed by the spec contractIObjectQLEngine.registry(EngineSchemaRegistryView.getPackage/uninstallPackage), with noas any.installPackagefiles the record under. The cleanups get the same id.A3: order and failure
The order is: ledger removal, then the withdrawal, then the cleanups. The withdrawal is synchronous, and nothing is awaited between it and the ledger removal. It goes first for three reasons:
deletePackageuses the same order: the registry withdrawal, thenrunUninstallCleanups.metadata:reloadedcould re-project the sets they had just removed.security.package-permissions, loses nothing. It selects by package id in the store and never reads the registry. Measured: orders 1 and 2 still report it assuccess: true, and the set and the grant are revoked.Nothing is withdrawn when the uninstall did not happen. The unit pin covers each case:
When the withdrawal fails:
registry.uninstallPackageincleanups. That is the way a failed cleanup is reported.When the registry does not hold the package (for example, a cloud install whose hot-register failed), there is nothing to withdraw: no call and no outcome.
A4: what
uninstallPackagewithdraws, and what the object doors answerFrom
SchemaRegistry.uninstallPackage(packages/objectql/src/registry.ts), the verb withdraws:unregisterItemsByPackage);It does not touch tables or rows.
The object doors' answer moves, as intended. These are the package object's answers on the data route, read hot in the same process right after the DELETE:
74281a8e4a, measured)After the fix, the hot answer carries the same error code the object answers after a restart. This is the consequence of "withdraws the package from the running registry". Orders 1, 2 and 4 pin it.
The reinstall control. I added order 4: hot install, then DELETE, then a hot reinstall of the same package in the same process. The object answers 200 again, and the set is projected again, exactly once, as the package's own. So the install path puts back everything the withdrawal removes.
One more value moves with the registry state. A same-process reinstall after an uninstall is now classified as a fresh install, so the install answer's
upgradedFromisnullinstead ofprevious-marketplace-version. A reinstall after a restart already answerednull. No key moves, no value leaves the existing set, and nothing in this repository reads the field.A5: the note
cleanups."registry.uninstallPackageentry.A6: reverse verification
The mutation.
scripts/ablation-replace.mjsreplaced the anchorregistry.uninstallPackage(manifestId);with a marker statement that logsABLATED_21576_withdrawal. The anchor went x1 to x0 and the replacement x0 to x1. The blob changed from7667b86205to83d263078b.The build. I rebuilt
@objectstack/cloud-connection.ablation-dist-preflightfound the marker indist/index.jsanddist/index.cjs.The readings:
2 failed | 12 passed. The two red cases are the ones that read the withdrawal.5 failed | 11 passed. The red cases are:The restore.
ablation-replacerestored the file: its blob equals HEAD's7667b86205, andgit diff HEADis empty. After a rebuild,ablation-dist-preflight --absentfound the marker absent from all 6 built files, with the whole tree clean.Tests and gates, at
9f3e65608d@objectstack/cli^..., then@objectstack/cloud-connectionrebuilt):Test Files 1 passed (1) / Tests 16 passed (16).@objectstack/cloud-connectionfull suite:34 passed (34)files,420 passed (420)tests.@objectstack/cloud-connectiontypecheck: both programs are green, and--listFilesshows the new test file in both.@objectstack/cli: typecheck is green, includingcheck:test-typecheck, and the tier partition pin passes 22/22. The onlypackages/clichange is the integration-tier file above, which ran in full. The rest of the unit tier is declared to CI.dispatch-gates --commands: it derives 64 commands, and all 64 exit 0.check:dual-build-cjs-loadsfirst answered PREREQUISITE NOT MET, because a whole-repodist/was missing. Afterpnpm buildit exits 0.--ranreconciliation with an exit code per line reads "64 run, 0 NOT-MEASURED (a DERIVED zero)".check-issue-citations --censusandcheck-shard-attestation. NOT MEASURED, left to CI.pnpm lint(eslint . --no-inline-config), the full run with no narrowing: exit 0.Acceptance notes
MANIFEST_CONFLICT), but an older ledger can still overlay the app at rehydrate. The DELETE of that entry now withdraws the id from the running kernel until a restart registers the config app again. The [finding] An install-local uninstall (DELETE /api/v1/marketplace/install-local/:id) leaves the package's permission sets in sys_permission_set: the "no ghost grants" uninstall cleanup never runs on that door #21490 cleanups already removed that id's package-managed sets. Carrier: none.bindArtifactHandlersbound, and the i18n bundles and seed datasets stashed at install. Once the objects are withdrawn, the bound handlers have no object route to fire through. This PR does not change any of it, and I did not measure it further.objectql.registryfake inmarketplace-install-local-id-gate.test.tscarries onlygetAllPackages. So its DELETE case now reports a failedregistry.uninstallPackageoutcome. That case asserts the status,successand the ledger removal, and all three still hold. I left it as is.Generated by Claude Code