Repository navigation
Commit 6c5697d
Fixes #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 (#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 (#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` (#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 #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 #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
#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 #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 #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 #21585.
- `packages/qa/dogfood/test/expression-conformance.ledger.ts` prose
still names `runtime/app-plugin.ts start` as the `toBoundaryJobSchedule`
call site.
- #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>
1 parent eb9ef79 commit 6c5697d
19 files changed
Lines changed: 2298 additions & 228 deletions
File tree
- .changeset
- content/docs
- automation
- references/system
- docs/qa/platform-checklist/areas
- packages
- cli
- src/commands/package
- test
- cloud-connection/src
- runtime/src
- sandbox
- spec
- liveness
- state-counts
- src/system
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
159 | 159 | | |
160 | 160 | | |
161 | 161 | | |
162 | | - | |
| 162 | + | |
163 | 163 | | |
164 | 164 | | |
165 | 165 | | |
166 | 166 | | |
167 | | - | |
168 | | - | |
169 | | - | |
170 | | - | |
171 | | - | |
172 | | - | |
173 | | - | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
174 | 177 | | |
175 | 178 | | |
176 | 179 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
61 | | - | |
| 61 | + | |
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
730 | 730 | | |
731 | 731 | | |
732 | 732 | | |
733 | | - | |
| 733 | + | |
734 | 734 | | |
735 | 735 | | |
736 | 736 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
205 | 205 | | |
206 | 206 | | |
207 | 207 | | |
208 | | - | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
209 | 217 | | |
210 | 218 | | |
211 | 219 | | |
| |||
0 commit comments