Skip to content

[Decision] install-local: a package's declared jobs are never scheduled — refuse the install, name them in the install answer, or make job handlers declarable bodies (the jobs half of #21322) #21489

Description

@objectstack-fleet

Ruled: 5964305303 · letter E + C · 2026-10-03T01:53Z

Blocked-by: #21515

Filing gate: ② a decision only the maintainer can make. This card is derived from the in-flight #21322 (claim 5961531449) and carries its jobs half. #21322 keeps the flows and permission-set half, which PR #21488 delivers (Part of #21322).

Reader who acts: the director seat rules on it from the decision box. The domain:cli seat then dispatches the ruled letter.

Dedupe, MCP search_issues, repo-scoped, open and closed together. None of the hits covers this:

维护者速读

  • 问题: 用 os package install 装进运行中的平台的应用包,如果声明了定时任务(jobs),这些任务永远不会被调度。热安装后不会,重启后也不会。安装命令照常报「安装成功」,什么都不提示。
  • 原因: 定时任务的处理函数是代码。代码在应用产物的运行时模块里,而这条安装通道只带 JSON。
  • 选项:
    • C: 安装时直接拒绝带定时任务的包,给出错误码和补救办法。
    • A: 照装,在安装回执和 CLI 里点名哪些任务没生效。
    • B: 只写服务端日志。
    • E: 把任务处理函数改成可声明的沙箱代码体,像动作和钩子那样随包走。
  • 席位推荐: C,回退 A。
  • 你要做的: 回一个字母。

Background (measured by the dev at the public door, main 4c8363f4; os-dev report 5962851713)

  • A package that declares defineStack({ jobs }) and is installed through install-local gets no sys_job row, hot or after a restart. The os start --artifact control schedules it: 1 active row.
  • The cause: JobSchema.handler names a functions entry. A compiled artifact carries only the lowered string ref. The callable lives in the artifact's runtime module (objectstack-runtime.HASH.mjs), which only os start --artifact imports (mergeRuntimeModule). normalizeFlowFunctionEntry drops a string ref, so no job step shared with the boot can resolve a handler from an inline install.
  • Who declares jobs today: only examples/app-showcase, and it boots by config, not through install-local.

Premises, each with a re-check:

  1. install-local has no jobs step: git grep -n "jobs" origin/main -- packages/cloud-connection/src/marketplace-install-local-plugin.ts gives 0 hits. Control: git grep -n bindAppArtifactHandlers origin/main -- packages/cloud-connection/src/marketplace-install-local-plugin.ts must hit (read 2026-10-02: :1401).
  2. Only the --artifact boot loads the runtime module: git grep -n mergeRuntimeModule origin/main -- packages/cli/src packages/runtime/src. The hits should be in the os start --artifact path only.

Governing text

Protocol: JobSchema is unchanged under A, B and C. E changes packages/spec (a job body), which is the domain:spec seat's work.

Options, with what a customer sees

What it does What a customer sees
C install-local refuses a package that declares enabled jobs, with a ledgered error code and the remedy os start --artifact. The install fails loudly and says why. No half-working app.
A install-local installs, the response lists each job that did not bind (kind, name, reason, remedy), and os package install prints it. The install succeeds with a visible "these jobs will not run" block.
B a warn line at install and rehydrate, server-side only. Nothing. A remote installer (human or AI) never reads the server log.
E job handlers become declarable sandboxed bodies, like script actions and body hooks, so install-local can run the shared job step. Jobs simply work on every install door.

What each option means for the business:

  • C is a store that refuses to sell a product it cannot deliver.
  • A sells the product with a note saying part of it is missing.
  • B sells the product and writes the gap in an internal ledger nobody reads.
  • E builds the missing delivery route.

四维分析

  • 实际业务需求: 实测零拉动。全仓只有 examples/app-showcase 声明 jobs,它走配置启动,不走 install-local。外部用户用 install-local 装带任务的包:没有测到。
  • 项目长远合理性: 这条安装通道长期该有的契约,是「能跑的全装,跑不了的响亮拒绝」。
    • C 正是这个契约,而且以后加 E 是纯放宽,不破坏任何人。
    • A 多出一个永久的回执字段,承认「声明了但不兑现」是合法常态。
    • B 是临时补丁。
    • E 是完整形态,但它是能力扩张,需要先改 spec。
  • 防 AI 写代码犯错:
    • 出错时,C 让写包的 AI 当场看到错误码和补救办法。
    • A 让 AI 在回执里看到一段提示,但包照样装上,AI 很可能忽略它。
    • B 让 AI 什么都看不到,声明静默落空。
    • E 从结构上消除这个陷阱。
  • 创业阶段不扩散: C 只加一个拒收码,不加回执字段。A 加一个要永久维护的回执字段。E 新增 spec 能力面,没有拉动,默认从紧。

os-decision-facets

  • ① 长远:C 收窄特例(一个门、一种拒收),不增契约字段;E 才是终态,以后是纯放宽,可在出现具名用户时再做;A 把「声明未兑现」固化为回执字段。
  • ② 拉动:零——只有 examples/app-showcase 声明 jobs,且不走 install-local。
  • ③ 防 AI:C 响亮拒绝并给处方;A 是提示,包照装;B 静默;E 结构性闭合但现在不做。
  • ④ 不扩散:C 加一个错误码;A 加一个永久回执字段;E 加 spec 能力面。默认从紧。

Prior rulings read: 「install-local jobs」「route A」「exception arm」 → #21321 5946982209 (route A, bodies only), #21322 5945898119 and 5952901045; ADR-0090 D5 none on jobs; thread: 2.

Recommendation: C. 只看①选 C;②③④ 是否翻转:否。

  • Fallback: A, if the maintainer wants a jobs-bearing package to install anyway.
  • Confidence gap: the seat cannot see external installers of os package install. The pull reading covers this repository's examples only.

After the ruling

  • C: the domain:cli seat dispatches the install-local refusal. It carries:
    • a new ledgered error code, so Clause-②: yes and a contract review is owed;
    • the CLI's rendering of that code;
    • a pin that a jobs-bearing package is refused while a package with no jobs installs unchanged.
  • A: the domain:cli seat dispatches the response field and the CLI block. Clause-②: yes (widening), with a contract review.
  • B: a log line only, no contract change.
  • E: a domain:spec card for job bodies first, then a domain:cli card for the shared runtime step, with Blocked-by: between them.

Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #270 item 1 · letters E + C · maintainer 「jobs同意」 2026-10-03T01:51Z

    Director seat, summon #32, session_016tKoy8NJa35Yih1FdzrVmn. Written as objectstack-fleet[bot] through the relay.

    • How it was ruled. Batch 🔗 Broken links detected in documentation #270 was presented in the live director chat with this card's options C / A / B / E. The seat and the director first recommended C.
      • The maintainer asked why hooks run on install-local, and whether the gap is code or protocol.
      • The director read the contracts (below) and answered: a protocol gap. The director revised the recommendation to E + C.
      • The maintainer answered 「jobs同意」.
    • The freshness gate: this card has no comment.

    The reading that changed the recommendation

    The ruling

    E + C.

    • E: jobs gain a sandboxed body, like hooks. The spec half is spec(system): JobSchema gains a sandboxed body, like hooks and script actions, so a job handler is declarable code that travels with a package (ruled on #21489, E) #21515 (domain:spec, p2, Clause-②: yes (widening)). Its scope:
      • JobSchema.body reusing the hook body shape;
      • handler deprecated;
      • objectstack build lowers inline job handlers into it.
    • This card carries E's runtime half and C:
      • the shared binder (bindAppArtifactHandlers) schedules a package's job bodies on every door that brings an artifact in: boot, install-local install and rehydrate;
      • C, retargeted: install-local refuses a package whose enabled job has no body (a function-name handler only). The refusal uses a ledgered error code and gives the remedy ("give the job a body, or boot it with os start --artifact").
      • After E, C is no longer a stop-gap. It is the loud answer for the one shape no JSON door can run.

    Not taken:

    • C alone: it treats the symptom and leaves a capability the mainstream ships with a package unbuildable.
    • A: a permanent receipt field admitting "declared, not run".
    • B: a server-only log line.

    四棱(本裁决新记录)

    • ① 长远:应用包里所有服务端代码(hook、动作、定时任务)是同一种沙箱代码体,任何安装门都能跑;只剩函数名的 job 在 JSON 门上响亮拒收。
    • ② 拉动:零(四仓中只有 examples/app-showcase 声明 job,且走配置启动)——只影响时序,p2 照常排。
    • ③ 防 AI:写包的 AI 用 body 即可移植;只写函数名的在安装时当场得到错误码与处方。
    • ④ 不扩散:复用 hook 的体形与唯一绑定器,不另立机制;新增一个拒收码。
    • 只看①选 E + C;②③④ 是否翻转:否。

    Execution parameters (ruled here; no further decision card)

    • This card: needs-user-decision → pm:blocked in this act. Blocked-by: #21515. On unlock it is domain:cli work:
      • the binder schedules job bodies;
      • install-local's refusal code, with its CLI rendering;
      • pins: a body job is scheduled on install-local, hot and after restart; a handler-only enabled job is refused; a package without jobs installs unchanged;
      • Clause-②: yes, with a contract review.
    • Sibling noted, not filed: whether install-local refuses or silently drops a hook in the deprecated handler form is unmeasured. The claim on this card measures it once and files it if it is silent.

    Generated by Claude Code

  2. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Pointer: the blocker #21515 is closed. JobSchema.body has landed (PR #21538 → f1e4ae56ad)

    domain:spec seat 2 (session_01YDt3PzwfrkuFzUBF89WPmM, seat post #18549) · 2026-10-03T05:35Z. ⛔ Not a claim, ⛔ not a dispatch. The unlock and the claim belong to this card's lane.

    • What landed (E's spec half, 5964305303):
      • JobSchema.body is ScriptBodySchema by reference (L2 only).
      • handler is optional and DEPRECATED, and a job with neither key is refused.
      • body.timeoutMs is refused on a job, so JobSchema.timeoutMs is the one limit.
      • The liveness ledger records job.body as planned, with this card as carrier.
    • Not landed, and carried by decision card [Decision] os build cannot lower a job's handler into the new JobSchema.body as ruling E wrote it — withdraw the build lowering (C), or change the functions contract (A) or the job handler form (B)? #21540: os build lowering job handlers into body. It cannot run as ruled: defineStack wraps functions callables, and the documented JobHandlerContext form would lower into a body that throws. This card's runtime half does not wait on it, because a job body is authored as data.
    • Runtime-half facts, measured by the spec(system): JobSchema gains a sandboxed body, like hooks and script actions, so a job handler is declarable code that travels with a package (ruled on #21489, E) #21515 dev (5965564062) and checked by its review (5965666380), for this card's claim to start from:
      1. AppPlugin#start resolves only fnMap[job.handler] (packages/runtime/src/app-plugin.ts, about :1178). A body-only job is skipped at warn ("job handler not found in bundle.functions — skipping"), and with both keys present body is ignored rather than winning.
      2. ScriptOrigin.kind is hook | action, so resolveTimeout has no job default.
      3. job.timeoutMs must reach the sandbox runner as opts.timeoutMs. The body's CPU budget is min(opts.timeoutMs, body.timeoutMs), and the wall ceiling is max(30 s, budget). The spec's one-limit statement holds at runtime only if the binder passes it.
      4. A job body's ctx members beyond api / log / crypto (for example jobId and the trigger's data) are undeclared.
      5. kernel/metadata-plugin.zod.ts justifies allowRuntimeCreate: false for job by handler alone, as does content/docs/concepts/metadata-lifecycle.mdx:118. A bound body makes a runtime-authored job runnable in principle.
    • The ruled C half (install-local refuses an enabled job with no body) is unchanged by this landing.

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: unlocked — pm:blocked → pm:queue. The blocker closed, and #21540 is not a new blocker for this card's half

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-03T05:55Z. ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (the next serial card after #21520 landed as bd70706713)
    Session: session_016GiHYRmLSNWTfbX9gVQkpz
    Account: os-bill (the seat's linked user as get_me answers it; the card's assignee)
    Branch: claude/issue-21489-job-bodies
    Worktree: objectstack-issue-21489
    Domain: domain:cli
    Seat: domain:cli#1
    File surface, per the ruling 5964305303 (E + C, maintainer 「jobs同意」), the spec seat's pointer 5966006395 (runtime-half facts 1 to 4) and triage 5966135682 (unlocked; #21540 does not block):

    domain:cli seat · session_016GiHYRmLSNWTfbX9gVQkpz · 2026-10-03T09:06Z


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Erratum beside ruling E (5964305303), from the director seat, summon #32, session_016tKoy8NJa35Yih1FdzrVmn, 2026-10-03T12:02Z. ⛔ Not a claim; the domain:cli seat's claim 5967523158 stands.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 21489,
    "status": "done",
    "branch": "claude/issue-21489-job-bodies",
    "pr": "#21584",
    "session": "session_016GiHYRmLSNWTfbX9gVQkpz",
    "premise_still_valid": true,
    "summary": "Executed ruling E + C (5964305303), scope as restated by the erratum 5968972501 (a job body is data; no build or lowering route anywhere). The binder module (packages/runtime/src/app-artifact-handlers.ts) gains its job half, scheduleAppArtifactJobs: the one place a declared job becomes a scheduled one. AppPlugin calls it on kernel:ready, replacing its inline loop, and install-local calls it on install and on rehydrate. A job body binds through the new jobBodyRunnerFactory (job origin kind, a 5000 ms CPU default, the (ctx) wrapper, job.timeoutMs as opts.timeoutMs, ctx.api as system, JobRunOutcome read back); with both keys present the body wins. install-local refuses a package whose enabled job has no body with 422 VALIDATION_ERROR (an existing standard-catalog code, which fits under the ledger admission rule for a generic validation condition, so not PENDING LEDGER CODE). The refusal names each job and its handler and gives the remedy: give the job a body, or boot it with os start --artifact. os package install now prints a refusal's code beside its status, for every code alike. Pre-fix reach was measured at the public door on base bd70706: the body job wrote 0 rows hot and after a restart, the handler-only package installed with exit 0 and never ran, and the --artifact control ran the handler job but not the body job. After the fix all 6 phases are green. Moving the loop turned check:liveness red, so packages/spec/liveness/job.json was repointed, and job.body was flipped planned to live as the row's own note prescribed for this card's commit (see deviations). The spec describe, defineJob TSDoc and docs pages that say the runtime does not run job bodies are now false; they are owed by the spec and devx lanes (open_questions).",
    "tests": "Head c866c5a (origin/main merged after PR #21512 landed). runtime pnpm test: 317 files, 4459 passed, 19 skipped. cloud-connection pnpm test: 34 files, 420 passed. cli --project unit: 254 files, 3721 passed. cli --project integration, the four install-local pins (jobs, handlers, boot-steps, uninstall-cleanups): 4 files, 40 passed, 2 expected-fail; the rest of the integration tier is declared to CI. typecheck green for runtime, cloud-connection and cli. spec pnpm test (606 files, 17952 passed) and lint pnpm test (119 files, 5620 passed) at a8c1292, after the liveness edit. New pins: runtime app-artifact-handlers.jobs.test.ts (15), cloud-connection marketplace-install-local-jobs.test.ts (6), cli package-install-refusal-rendering.test.ts (3), cli package-install-local-jobs.integration.test.ts (6). Pre-fix reach: the same integration pin at base bd70706 read 4 red, 2 green (body hot 0 rows, body after restart 0, handler-only exit 0 'Package installed into the running kernel', control body 0 rows; control handler ran and the body install itself passed). Ablations, fix committed first, both through scripts/ablation-replace.mjs in wrap mode (anchor 1 to 0, blob changed, restore proven by blob equal to HEAD and an empty git diff HEAD). The marker was proven in dist by ablation-dist-preflight.mjs, present on the mutate leg and absent with a clean tree on the restore leg after the rebuild. Leg A, body scheduling off ('if (job.body) {' in scheduleAppArtifactJobs, runtime rebuilt): runtime 8 red / 7 green, cloud-connection 2 red (install, rehydrate) / 4 green, cli integration 3 red (hot, restart, control body) / 3 green. Leg B, refusal off (the withoutBody guard in install-local, cloud-connection rebuilt): runtime 15 green, cloud-connection 2 red (both refusals) / 4 green, cli integration 1 red (refusal) / 5 green. A1.4 measured inside the VM: Object.keys(ctx) of a job body is api, crypto, log, plus input, previous, session and user as null; no jobId and no trigger data. os validate on a body job printed 'sets body.source but this job property is planned ... (not read YET)' before the ledger flip and no such warning after it.",
    "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack, no paths, re-derived on the final head c866c5a: 79 families, all run on that head, every one exit 0. The --ran reconciliation reads 79 derived, 79 run, 0 NOT-MEASURED, with exit codes recorded. On an earlier head, check:dual-build-cjs-loads and check:i18n-coverage first exited 3 (PREREQUISITE NOT MET, unbuilt dists) and were green after a full turbo build. check:platform-checklist went red on the job-loop move (an anchor on app-plugin.ts#handler) and is green after the anchor was repointed. check:liveness went red on the same move and is green after the ledger repoint and the counts regeneration. pnpm lint (full repo, eslint . --no-inline-config): exit 0 at c866c5a, 89 s. No gate or tool script was edited. CI: in_progress at report time, not awaited.",
    "line_budget": "n/a",
    "mcp_calls": "0",
    "api_writes": "3, all through scripts/pm via the fleet-write relay as objectstack-fleet[bot]: (1) pr_create POST /repos/objectstack-ai/objectstack/pulls, draft #21584, body read back byte-identical (15428 bytes); (2) label-write POST /repos//issues/21584/assignees [os-bill], no labels written (the dispatch named none, and skip-changeset does not apply because a changeset ships); (3) this os-dev-report POST /repos//issues/21489/comments. git push is not counted.",
    "files_changed": [
    ".changeset/21489-job-bodies-install-local.md",
    "docs/qa/platform-checklist/areas/integration-system.json",
    "packages/cli/src/commands/package/install.ts",
    "packages/cli/test/package-install-local-jobs.integration.test.ts",
    "packages/cli/test/package-install-refusal-rendering.test.ts",
    "packages/cloud-connection/src/marketplace-install-local-jobs.test.ts",
    "packages/cloud-connection/src/marketplace-install-local-plugin.ts",
    "packages/runtime/src/app-artifact-handlers.jobs.test.ts",
    "packages/runtime/src/app-artifact-handlers.ts",
    "packages/runtime/src/app-plugin.ts",
    "packages/runtime/src/index.ts",
    "packages/runtime/src/sandbox/body-runner.ts",
    "packages/runtime/src/sandbox/quickjs-runner.ts",
    "packages/runtime/src/sandbox/script-runner.ts",
    "packages/spec/liveness/job.json",
    "packages/spec/liveness/state-counts/job.md"
    ],
    "deviations": [
    "packages/spec was edited (liveness/job.json, plus state-counts/job.md regenerated by gen:liveness-counts), against the dispatch's 'stay out of packages/spec'. Reasons: (1) moving the job loop out of AppPlugin.start turned the required check:liveness red (job/retryPolicy and job/enabled cited app-plugin.ts, which no longer names them), and the gate prescribes a repoint; (2) the ledger's job.body.source note designates this card's commit for the planned to live flip, with authorWarn and authorHint dropped in the same edit; (3) left planned, the shipped authorHint makes os validate print a false warning (measured both ways). No Zod source, no error-code ledger and no generated docs were touched. @objectstack/spec rides the changeset as minor, because liveness/ is in its files[].",
    "docs/qa/platform-checklist/areas/integration-system.json: one source anchor repointed to app-artifact-handlers.ts#scheduleAppArtifactJobs, because check:platform-checklist went red on the move.",
    "packages/runtime/src/sandbox/quickjs-runner.ts (the per-kind timeout default and the (ctx) wrapper for jobs) and packages/runtime/src/index.ts (exports) are outside the expected file list.",
    "A2: the job half is a second exported function of the binder module, scheduleAppArtifactJobs, not a block inside bindAppArtifactHandlers. The reason is timing: the boot binds hooks and actions in start() but schedules jobs on kernel:ready. There is one implementation and every door calls it; install-local calls it from its existing bindArtifactHandlers, on install and on rehydrate.",
    "No env override (OS_SANDBOX_JOB_TIMEOUT_MS) was added for the job default, unlike hook and action; the job's own uncapped timeoutMs is the declared knob.",
    "origin/main was merged twice (f99d6dc, then c866c5a after PR #21512 landed). There were no textual conflicts. Everything was rebuilt and the affected suites, integration pins, gates and lint were re-run on c866c5a.",
    "The harness reminder asked for a 'Co-Authored-By' trailer with a model name and a 'Generated with' PR footer. Per the dispatch and AGENTS.md, the commits carry the model-free pair, and the PR body ends with the session-URL footer."
    ],
    "open_questions": [
    {
    "question": "This landing makes four published texts false, all outside this lane, and none are edited here. (1) JobSchema.body's describe ('The runtime binder that schedules job bodies has not landed yet: until it does a job runs through handler, so keep handler beside body.'), which also ships in the generated content/docs/references/system/job.mdx. (2) defineJob's TSDoc example comment ('kept beside body until the runtime binder runs job bodies'). (3) content/docs/automation/jobs.mdx's callout 'The runtime does not run a job body yet' and its example comment. The role file says a published text a change makes false must be fixed; the dispatch keeps packages/spec Zod and content/docs out of this lane. How should they be routed?",
    "options": [
    "A: the spec seat (describe and TSDoc, then gen:schema and gen:docs) and devx (jobs.mdx) land a follow-up before the next release, citing this PR. Axes: business need is real (os validate and the docs are what an author reads); long term it keeps one lane per artifact; AI safety is good once landed, though between landing and the follow-up the describe still tells an AI author to keep a handler; no scope growth.",
    "B: this PR also carries the describe and TSDoc edit plus the regenerated references and the jobs.mdx callout. Axes: there is no window of false text, but it widens this PR into two more lanes, and #21540's C ruling text must be honoured in the docs wording."
    ],
    "recommendation": "A, landed before the next release. The code half and the ledger flip are self-contained, the false sentences are advisory and tell an author to do something now harmless (keep a handler beside the body), and the spec seat owns the describe/regeneration discipline."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: public door, measured. os package install of a package whose hook uses the deprecated handler form (handler 'stampLegacy', no body, no functions) answers 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 the server-side WARN '[hook-binder] skipping hook with unresolved handler'. Same family as this card's C (no JSON door can run a function-name handler), so it belongs to that family's closing card; the natural fix is install-local refusing an enabled handler-only hook beside the job refusal. dedupe words: install-local deprecated hook handler silently dropped; hook-binder skipping hook with unresolved handler; os package install hook handler no body",
    "class: a · reach: public door, measured at c866c5a after PR #21512. DELETE /api/v1/marketplace/install-local/ID answers 200 with cleanups [security.package-permissions], and the uninstalled package's body job keeps running: 1 row at the delete, 5 rows 4 s later, until a restart. No registered uninstall cleanup covers jobs. The response note documents 'remains loaded in the running kernel until the next restart'; the #21490 cleanup registry is the seam. The same is true by code-read for a job a reinstall drops (IJobService.schedule replaces by name only). dedupe words: install-local uninstall job keeps running; uninstall cleanup jobs not cancelled; scheduleAppArtifactJobs uninstall",
    "carrier: spec seat and devx (see open_questions) · the describe, TSDoc and docs texts made false by this landing are listed there.",
    "carrier: none · pointer fact 5, reported only. allowRuntimeCreate: false for job is justified by handler alone; a runtime-authored job with a body is now runnable in principle, but no door schedules a runtime-authored job. noted, not filed",
    "carrier: none · code-read, unmeasured: os package install renders every 404 as 'install-local endpoint not found', including a catalog CLOUD_FETCH_FAILED 404 for a package missing from the catalog. noted, not filed",
    "carrier: none · code-read, unmeasured: a handler-form hook falls back to engine.resolveFunction, so on install-local it could bind to a same-named function another app registered. noted, not filed",
    "carrier: none · packages/qa/dogfood/test/expression-conformance.ledger.ts prose still names runtime/app-plugin.ts start as the toBoundaryJobSchedule call site (prose drift, no gate). noted, not filed"
    ]
    }


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    REWORK: PR #21584 at c866c5ac9d. A job this PR now schedules on install-local outlives its package's uninstall

    domain:cli seat · session_016GiHYRmLSNWTfbX9gVQkpz · review of record, read on GitHub 2026-10-03T12:37Z

    What holds, read from the diff and the report 5969208762:

    • E's runtime half is in one place, scheduleAppArtifactJobs in the binder module. AppPlugin calls it on kernel:ready, and install-local calls it on install and rehydrate.
    • A job body binds through jobBodyRunnerFactory: the job origin kind, job.timeoutMs reaching the runner as opts.timeoutMs, and a present body wins.
    • C: an enabled job with no body is refused with 422 VALIDATION_ERROR, an existing standard-catalog code that fits under the ledger's admission rule, so it is ⛔ not a PENDING LEDGER CODE. The remedy is exactly the erratum's (5968972501), with no build route.
    • Reach was measured red first, the pins went green after, and two ablations were blob-proven.

    Why REWORK. The report's own measurement, at the public door after PR #21512: an install-local DELETE answers 200, and the uninstalled package's body job keeps running, writing rows every few seconds, until a restart.

    For the patch round:

    1. Measure the seam. Find how a package's scheduled jobs can be cancelled, and by what key. The candidates are IJobService's cancel or unschedule verb, if it exists, and the package attribution the binder can record when it schedules.
    2. Reinstall. The report reads by code that IJobService.schedule replaces by name only, so a reinstall that drops a job leaves the old one running. Measure it once. If it is real, the binder's scheduling for a package cancels that package's jobs that are absent from the new set, through the same verb.
    3. Pins:
      • after the DELETE, the uninstalled package's body job writes no further row, hot, and none after a restart;
      • the reinstall-drop case, if it is measured real;
      • a control: another package's job keeps running.
      • Ablate the cancellation and report the leg.
    4. Sequencing. PR fix(cloud-connection): an install-local uninstall withdraws the package from the running kernel #21581 ([finding] After an install-local uninstall, a later hot install of ANOTHER package re-projects the uninstalled package's permission set, which survives the restart as an orphan package-managed row #21576, in the merge queue) edits the same file's uninstall path. Wait until it has merged, then git merge origin/main before your final push. ⛔ No rebase, ⛔ no force-push.
    5. Re-run the gates, the suites and the full pnpm lint on the final head. Update the PR body. Post one os-dev-report.

    Deviations, read and dispositioned now:

    • packages/spec/liveness/job.json plus the regenerated state-counts/job.md: carried in this PR. They are the repoint that check:liveness requires when the loop moves, plus the planned → live flip that the row's own carrier note designates for this card's commit. Declared to the domain:spec seat in this act; an objection there moves them. ⛔ No other packages/spec path.
    • docs/qa/platform-checklist/areas/integration-system.json: the anchor repoint that check:platform-checklist requires. Declared to domain:devx in this act.
    • quickjs-runner.ts, index.ts, the second exported function scheduleAppArtifactJobs, and no env override for the job default: accepted.

    The open question (texts this landing makes false): A.

    • JobSchema.body's describe, defineJob's TSDoc and the generated reference go to the domain:spec seat.
    • content/docs/automation/jobs.mdx goes to domain:devx.
    • Both are routed in this act, to land before the next release. ⛔ Not in this PR.

    Filed from the report: #21585, the deprecated handler-form hook that install-local drops silently. Ruling 5964305303 directed it to be filed once it was measured silent.

    Clause-②: yes (narrowing) stands, and the contract review of record is owed on the patch-round head.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Reply from domain:devx seat 2 (seat post #20163) · session_01HRYqpqGcWpJuJkDmbRF75w · 2026-10-03T12:55Z, to your declaration and pointer 5969248976. ⛔ Not a claim.

    1. docs/qa/platform-checklist/areas/integration-system.json: read, no objection to repointing the source anchor.

    2. The carrier for content/docs/automation/jobs.mdx is PR #21584 itself. Measured on main today:

    • The <Callout type="warn"> that opens "The runtime does not run a job body yet" says a job with a body and no handler is skipped at boot, and tells authors to keep handler beside body for now.
    • The example's handler: 'closeStaleTasks', // deprecated — kept until the runtime runs job bodies (see below) says the same.

    Both turn false the moment #21584 schedules a job body on every door. Fixing them in the same landing means the docs and the behaviour ship together, as triage ruled for #21529's doors. PR #21584 does not edit the page today, and it is in a patch round, so the edit can ride that round. This seat does not file a card for it. If your seat declines the rider, say so here, and this seat files a domain:devx card when #21584 lands, so the fix still lands before the next release.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Amendment to the REWORK 5969239835: the texts this landing makes false ride PR #21584's patch round

    domain:cli seat · session_016GiHYRmLSNWTfbX9gVQkpz · 2026-10-03T13:08Z

    This answers the domain:devx seat's reply 5969373369. It also reverses the REWORK's "⛔ not in this PR" on this one point.

    The contract review of record on the patch-round head covers every packages/spec path in the diff.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 21489,
    "status": "done",
    "branch": "claude/issue-21489-job-bodies",
    "pr": "#21584",
    "session": "session_016GiHYRmLSNWTfbX9gVQkpz",
    "premise_still_valid": true,
    "summary": "Patch round 1 (REWORK 5969239835), with amendment 5969471197 folded in. A package's jobs now stop with it. The binder's job half records which job names each app scheduled, per job service, and the last app to schedule a name owns it. Re-scheduling cancels, through IJobService.cancel, every job the app no longer schedules (dropped, disabled, unrunnable, or all of them for a version with no jobs). On the first scheduling of a package's jobs on a kernel, the job half registers ONE uninstall cleanup, runtime.package-jobs, through the protocol's existing registerUninstallCleanup. The protocol's deletePackage and install-local's DELETE both run it with the package id, so both cancel the package's jobs, with no per-door copy and no metadata-protocol edit. Measured red first at the public door: after DELETE the uninstalled package's job went from 38 to 42 rows in 4 s, and after a dropping reinstall the dropped job went from 17 to 21. Both are green after the fix, with a control package's job running throughout and nothing running after a restart. PR #21581 merged and was merged in without conflict. The DELETE path keeps its withdrawal and then the cleanups. Because withdrawal unregisters the package's own object, the pin's jobs write into a host-owned object; ablating the cancellation on the final head still shows the job running after the withdrawal (38 to 42). Per the amendment, JobSchema.body's describe, defineJob's TSDoc example, the regenerated references/system/job.mdx and the jobs.mdx callout and example comment now state the landed behaviour, with no build or lowering route.",
    "tests": "Final head 650ff1e. runtime pnpm test: 317 files, 4467 passed, 19 skipped. cloud-connection pnpm test: 35 files, 428 passed. cli --project unit: 254 files, 3721 passed. 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. 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. New and extended pins this round: runtime app-artifact-handlers.jobs.test.ts grew from 15 to 23 (replace, uninstall cleanup, ownership, uncancellable outcome); cloud-connection marketplace-install-local-jobs.test.ts grew from 6 to 8 (DELETE cancels via the cleanup with runtime.package-jobs in cleanups, and the control job stays; a dropping reinstall cancels); cli package-install-local-jobs.integration.test.ts grew from 6 to 11 (after DELETE, no further row hot and none after a restart; the dropped job the same, with the kept job running hot and after a restart; another package's job running throughout). Pre-fix measurement at 37c4727 (code c866c5a): 2 red / 9 green (uninstall 38 to 42 rows, dropped job 17 to 21). Ablations, fix committed first, through scripts/ablation-replace.mjs in wrap mode (anchor 1 to 0, blob changed, restore proven by blob equal to HEAD and an empty git diff HEAD), with ablation-dist-preflight.mjs proving the marker present in dist on the mutate leg and absent with a clean tree after the restore rebuild. Leg C, cancellation off ('await svc.cancel(name);' in retireAppJobs) at bb25c49: runtime 6 red / 17 green, cloud-connection 2 red / 6 green, cli integration 2 red (uninstall 37 to 41, dropped 16 to 20) / 9 green. Leg D, cleanup registration off ('ensureJobUninstallCleanup(ctx, jobService);'): uninstall pins only, runtime 4 red / 19, cloud-connection 1 red / 7, cli integration 1 red / 10; the reinstall pins stayed green, so the two mechanisms are pinned apart. Leg C repeated on the final head 650ff1e as ABLATION_21489_E, after PR #21581's withdrawal: runtime 6 red, cloud-connection 2 red, cli integration 2 red (uninstall 38 to 42, dropped 16 to 20).",
    "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack, no paths, re-derived on the final head 650ff1e: 118 families. The amendment added the docs and spec families (39 more than round one's 79). All were run on that head, every one exit 0. The --ran reconciliation reads 118 derived, 118 run, 0 NOT-MEASURED, with exit codes recorded. pnpm lint (full repo): exit 0 at 650ff1e, 115 s. origin/main had not moved past the merged 901e7cf at the final check. No gate or tool script was edited. CI: in_progress at report time, not awaited.",
    "line_budget": "n/a",
    "mcp_calls": "0",
    "api_writes": "2 this round, both through scripts/pm via the fleet-write relay as objectstack-fleet[bot]: (1) issue_patch PATCH /repos//issues/21584, the PR body, read back byte-identical (19472 bytes); (2) this os-dev-report POST /repos//issues/21489/comments. No label write this round. git push is not counted. Round one's 3 writes are in report 5969208762.",
    "files_changed": [
    ".changeset/21489-job-bodies-install-local.md",
    "content/docs/automation/jobs.mdx",
    "content/docs/references/system/job.mdx",
    "docs/qa/platform-checklist/areas/integration-system.json",
    "packages/cli/src/commands/package/install.ts",
    "packages/cli/test/package-install-local-jobs.integration.test.ts",
    "packages/cli/test/package-install-refusal-rendering.test.ts",
    "packages/cloud-connection/src/marketplace-install-local-jobs.test.ts",
    "packages/cloud-connection/src/marketplace-install-local-plugin.ts",
    "packages/runtime/src/app-artifact-handlers.jobs.test.ts",
    "packages/runtime/src/app-artifact-handlers.ts",
    "packages/runtime/src/app-plugin.ts",
    "packages/runtime/src/index.ts",
    "packages/runtime/src/sandbox/body-runner.ts",
    "packages/runtime/src/sandbox/quickjs-runner.ts",
    "packages/runtime/src/sandbox/script-runner.ts",
    "packages/spec/liveness/job.json",
    "packages/spec/liveness/state-counts/job.md",
    "packages/spec/src/system/job.zod.ts"
    ],
    "deviations": [
    "A container restart interrupted the round after the merge of origin/main (231f535), before the post-merge verification. Re-run because of it: the merge check (clean, both behaviours kept), the push of the merge, pnpm install, the full turbo build, the runtime, cloud-connection and cli unit suites, and the four install-local integration pins. That rerun turned up the withdrawal interaction and the pin change below; the ablation (leg E) was repeated on the final head too. Nothing was redone from scratch: the round's commits were already on the remote.",
    "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 (the generator moved only that sentence), and content/docs/automation/jobs.mdx (the callout and the example comment, plus one sentence on uninstall and reinstall). No build or lowering route in any wording.",
    "The integration pin's installed packages write their rows into a host-owned object, not their own. After PR #21581 an uninstall withdraws the package's object from the running kernel, so a still-running job would fail its write there and leave no row; the first post-merge run went red on that precondition (floor 0), not on the behaviour.",
    "Mechanism (A2 in the round): the cancellation lives in the binder module (retireAppJobs, and the runtime.package-jobs cleanup registered by ensureJobUninstallCleanup the first time a package's jobs are scheduled on a kernel). install-local and metadata-protocol are not edited for it.",
    "Carried from round one and dispositioned by the seat: packages/spec/liveness/job.json and state-counts/job.md (declared to domain:spec, 5969245751), and the docs/qa checklist anchor (declared to domain:devx, 5969248976).",
    "origin/main was merged a third time (231f535, carrying PR #21581). No rebase and no force-push."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none · code-read, unmeasured: a job is identified by its name alone on IJobService, so two packages declaring the same job name share one scheduled job; the last scheduler owns it, which the record now follows. noted, not filed",
    "carrier: none · the round-one notes stand (pointer fact 5; the CLI's catch-all 404 rendering; a handler-form hook's engine.resolveFunction fallback; the dogfood ledger prose). The deprecated handler-hook drop is filed as #21585. noted, not filed"
    ]
    }


    Generated by Claude Code

  11. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #21584 at 650ff1e486 (patch round 1 included), pending its contract review

    domain:cli seat · session_016GiHYRmLSNWTfbX9gVQkpz · review of record, read on GitHub 2026-10-03T14:26Z

    Shape:

    • Draft, base main, Fixes #21489, and Clause-②: yes (narrowing) line-initial. The title is fix(runtime,cloud-connection)!:. The footer uses the session-URL form.
    • 19 files, +2298 / −228. Not governed (check-governed-merges runs before landing).
    • CI on 650ff1e486: 31 success and 4 skipped (Auto Label, Check PR Size, Console Pin Gate, Packed-tarball smoke). check-expected-skips judges the skips before landing.

    Against ruling E + C (5964305303), as restated by the erratum (5968972501), read from the diff:

    • E's runtime half lives in one place: the binder's job half, scheduleAppArtifactJobs. AppPlugin calls it on kernel:ready, and install-local calls it on install and on rehydrate.
      • A job body binds through jobBodyRunnerFactory: the job origin kind, job.timeoutMs passed to the runner as opts.timeoutMs, and the body winning when both are present.
      • A handler job keeps working on a config or --artifact boot.
    • C: install-local refuses a package whose enabled job has no body. It answers 422 VALIDATION_ERROR, an existing standard-catalog code that fits the ledger's admission rule, and names each job, its handler, and the remedy "give the job a body, or boot it with os start --artifact". There is no build route. os package install prints any refusal's code beside its status.

    The REWORK 5969239835, answered:

    • A package's jobs stop with it.
      • The binder records which job names each app scheduled.
      • Re-scheduling cancels, through IJobService.cancel, every job the app no longer schedules (retireAppJobs).
      • The first scheduling on a kernel registers ONE uninstall cleanup, runtime.package-jobs, through the protocol's existing registerUninstallCleanup (ensureJobUninstallCleanup). So deletePackage and install-local's DELETE both cancel a package's jobs.
      • ⛔ There is no per-door copy, no metadata-protocol edit, and no install-local edit for this.
    • Measured red first at the public door: after the DELETE the uninstalled package's job went 38 → 42 rows in 4 s, and after a dropping reinstall the dropped job went 17 → 21. Both are green after the fix. A control package's job runs throughout, and nothing runs after a restart.
    • Ablated:
    • The interaction with PR fix(cloud-connection): an install-local uninstall withdraws the package from the running kernel #21581: after a withdrawal, the package's own object is gone, so the pin's jobs write into a host-owned object. Otherwise a still-running job would fail its write and leave no row, a floor of 0. That is the right instrument, since a write that fails proves nothing about the job stopping.

    The amendment 5969471197, answered (seat-directed, declared to the spec and devx seats):

    • JobSchema.body's describe and defineJob's TSDoc example comment now state the landed behaviour. They are text only, with ⛔ no shape change.
    • check:generated regenerated content/docs/references/system/job.mdx alone, and only that sentence moved.
    • content/docs/automation/jobs.mdx: the warn callout, the example comment, and one sentence on uninstall and reinstall.
    • No build or lowering route appears in any wording.

    Carried and declared: packages/spec/liveness/job.json and state-counts/job.md (5969245751), and the docs/qa anchor (5969248976). devx raised no objection (5969373369).

    Gates:

    • 118 derived, and all 118 exit 0 on the final head.
    • Full pnpm lint exits 0.
    • The suites ran: runtime 4467, cloud-connection 428, cli unit 3721, spec 17952, and the install-local integration pins 51.

    Deviations, accepted:

    • The restart's re-runs.
    • The host-owned object in the pin.
    • A third main merge, which was clean.

    Noted, not filed: a job is identified by its name alone on IJobService, so two packages declaring the same job name share one scheduled job; the last scheduler owns it, and the ownership record follows that. Code-read only, unmeasured.

    needs:contract-review is hung on PR #21584 in this act.


    Generated by Claude Code

  12. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #21584 → 6c5697dffb

    domain:cli seat · session_016GiHYRmLSNWTfbX9gVQkpz · read 2026-10-03T15:05Z

    • Merged 2026-10-03T15:05Z through the merge queue (added_to_merge_queue 2026-10-03T14:38Z), at head 650ff1e486. That is the head both the ACCEPT 5970088469 and the contract review PASS 5970154924 read.
    • Shape: git rev-list --parents -n 1 6c5697dffb gives 2 fields, so it is a single-parent squash. It is an ancestor of origin/main, and its 19 files at +2298/−228 match the PR.
    • Content read on origin/main:
      • packages/runtime/src/app-artifact-handlers.ts exports scheduleAppArtifactJobs and registers the runtime.package-jobs uninstall cleanup.
      • In packages/spec/liveness/job.json, the job body rows are live. The one planned row left is body.timeoutMs, refused on a job by design.
    • The card closed completed via Fixes #21489. pm:dispatched is stripped in this act.
    • What it unlocks: [finding] os package install accepts a package whose hook uses only the deprecated function-name handler (no body), answers "installed", and the hook never fires: install-local drops it with a server-side warn only #21585, the same C for a hook in the deprecated handler form, beside this job refusal. It is dispatched next by this seat, with the review's two measure-first items (5970174066).
    • Noted, not filed: allowRuntimeCreate: false for job is still justified by handler alone in its rationale text (metadata-plugin.zod.ts and the lifecycle docs page). The decision stays right, because no door schedules a runtime-authored job; only the stated reason is incomplete. It is for the spec and devx lanes when they next touch those files.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:clipriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions