You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] Two install-local packages that declare the same job name: the second install silently replaces the first package's job, which stops running, and the door says nothing #21602
Filing gate: ① a product defect with reach measured.reach: public door, measured once (below). Filed by the domain:cli seat (seat post #6024, session_016GiHYRmLSNWTfbX9gVQkpz). It comes from PR #21584's contract review of record (5970154924, escalated item), measured by the #21585 claim (os-dev report 5970542332). ⛔ Not a claim. Triage sets the grade and the lane.
Reader who acts: triage grades it. The fix lands where a package's jobs are scheduled: the binder's job half, scheduleAppArtifactJobs (packages/runtime/src/app-artifact-handlers.ts), and/or the IJobService identity it schedules under.
Dedupe: MCP search_issues, repo-scoped, open and closed together:
Measured (on main6c5697dffb, through the public door)
Two install-local packages each declare an enabled body job with the same name. Both os package install runs exit 0.
The first package's job runs on its interval. Once the second package installs, the first package's job stops: its row count holds flat over several intervals. The second package's job runs.
Nothing at the door says so: no refusal, no warning in the install answer, no line in the CLI.
Mechanism (read): the metadata registry namespaces jobs per package, but IJobService.schedule keys by the bare job name and replaces. #21584's ownership record follows the last scheduler, so the first package's job is gone. It is not cancelled by name collision on purpose; it is simply displaced.
Why it matters: a package's declared behaviour silently stops because an unrelated package was installed. The author and the operator each see a successful install. That is AGENTS.md Prime Directive 10 (declared ≠ enforced), on the door #21489 just opened to jobs.
Candidate seams, which triage decides:
Schedule under a package-scoped identity, so two packages' same-named jobs coexist.
Or refuse the second install loudly, naming the package that owns the name.
The first keeps both packages working; the second keeps one name space and makes the collision visible.
Triage: first grade — bug · priority:p2 · domain:cli · area:devpath · pm:queue. Schedule under the package-scoped identity the registry already uses. ⛔ Not a refusal
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-03T15:56Z. ⛔ Not a claim, ⛔ not a dispatch.
Why p2. A package's declared job silently stops because an unrelated package was installed, and each install reports success. It is the grade of the #21489 / #21585 family on the same door.
Routing. The scheduler call in packages/runtime/src/app-artifact-handlers.ts (scheduleAppArtifactJobs), so domain:cli. The aim is a fix with no IJobService contract change. If the identity cannot be carried through the existing schedule call, the claim stops and reports. ⛔ It does not widen the service interface on its own.
Ruling: package-scoped identity, not a refusal.
The metadata registry already namespaces jobs per package, and the scheduler keys by the bare name. When one operation has two implementations that disagree, the governed side wins: the scheduler follows the registry's identity.
A refusal would make a package uninstallable because of an unrelated package's naming. It would also be a new gate with no pull.
Claim: PM loop round 1 (serial behind #21585, which landed as 045b946256 on app-artifact-handlers.ts)
Session: session_016GiHYRmLSNWTfbX9gVQkpz
Account: os-bill (the seat's linked user as get_me answers it; the card's assignee)
Branch: claude/issue-21602-package-scoped-job-identity
Worktree: objectstack-issue-21602
Domain: domain:cli
Seat: domain:cli#1
File surface, per triage's ruling 5970811944 (schedule under the package-scoped identity the registry already uses; ⛔ not a refusal):
packages/runtime/src/app-artifact-handlers.ts (scheduleAppArtifactJobs, retireAppJobs, the ownership record): two packages' same-named jobs coexist, and uninstalling one stops only its own.
Pins:
two packages each declare the same job name, and both jobs run on their intervals after both installs;
uninstalling one stops only its own job;
a single-package runtime's job names and run history are unchanged.
.changeset/.
⛔ No IJobService contract change (packages/spec/src/contracts/job-service.ts): the identity rides the existing schedule call. If it cannot, the claim stops and reports. ⛔ No widening of the service interface. ⛔ Function-name resolution for hooks is [Decision] security(objectql): may a hook's handler name bind to a function another package registered (the engine-wide fallback HookSchema.handler declares), or does name resolution stay inside the hook's own package (#21585 option B) #21604's (the maintainer's), not here. A reader outside domain:cli (for example packages/services/service-job) is measured and reported. ⛔ It is not edited without a cross-lane declaration first. (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: default (opus). An identity change with a census of readers across run history, listings and cancellation.
Clause-②: no. No accept set or published surface moves: the scheduled key is internal, and a single-package runtime's visible names are pinned unchanged. If the census shows a visible name moves on any reader, stop and report rather than re-declare.
Thread-read: 5970811944
Serial constraints, other open PRs read 2026-10-03T19:34Z on origin/main045b946256:
os-dev-report
{
"issue": 21602,
"status": "done",
"branch": "claude/issue-21602-package-scoped-job-identity",
"pr": "#21633",
"session": "session_016GiHYRmLSNWTfbX9gVQkpz",
"premise_still_valid": true,
"summary": "Implemented triage ruling 5970811944: jobs get a package-scoped identity, and a collision is not refused. scheduleAppArtifactJobs (packages/runtime/src/app-artifact-handlers.ts) schedules each job under the key that jobKeyFor gives it, checked in this order: (1) the key this app already holds the job under; (2) else the authored name, when no other package holds it on the job service; (3) else the registry's package-scoped key PACKAGE_ID:JOB_NAME, plus an info line naming the holder. #21584's ownership record now maps each authored name to its key, so retireAppJobs and the runtime.package-jobs uninstall cleanup cancel only the replacing or uninstalling package's own job. No IJobService contract change and no edit to packages/spec or packages/services. A1: reach reproduced on base 045b946 at the public door. The new install-local pin went 4 red / 5 green: ALPHA's job held at 8 rows after BETA and GAMMA installed, sys_job read ['shared_tick'], and ALPHA did not run after the restart. A3: JobScheduleOptions carries only retryPolicy/timeoutMs, so the scope rides on the name argument, and only when names collide. A runtime where no two packages share a job name is unchanged; this is pinned in units and at the door, where sys_job and sys_job_run read [shared_tick]. Census flag for the PM: in the collision case only, the later package's job is catalogued in sys_job/sys_job_run as PACKAGE_ID:JOB_NAME. This is forced by sys_job.name being unique 'global'. I proceeded under the ruling's 'reads the scoped identity' branch, see open_questions[0].",
"tests": "Final head 2279c37; its only change from 2f9097c is a one-line doc comment. | On 2279c37: dispatch-gates --commands derived 62; all 62 ran, each exit 0; --ran reports '62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN'. The 5 roster gates under touched dirs (check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check:route-ledger-census) all exit 0. pnpm lint (full repo) exit 0 in 87s. app-artifact-handlers.jobs.test.ts 35/35; runtime typecheck OK. | On 2f9097c (same source apart from the comment): @objectstack/runtime test 318 files / 4530 passed / 19 skipped; @objectstack/cloud-connection 36 files / 437 passed; @objectstack/cli --project unit 255 files / 3745 passed, tier-partition pin included; cli typecheck OK. The new test file is in the cli tsconfig.test.json program (counted with --listFilesOnly). Install-local pins on built dist: jobs + jobs-shared-name 20/20; uninstall-cleanups 16/16. Liveness evidence.test.ts 42/42. | A1, base 045b946 dist: new pin 4 failed / 5 passed ('expected 8 to be greater than 8'; catalogue ['shared_tick']). | A5 ablation at 07d4deb via scripts/ablation-replace.mjs WRAP. Anchor '{ key: ${appId}:${jobName}, heldBy }' went x1 to x0; replacement '{ key: jobName, heldBy }' went x0 to x1; blob dc88fbdf0207 to e523712b60f5. Mutated leg: unit 9 failed / 26 passed, and the single-package control stayed green. Runtime rebuilt; ablation-dist-preflight found the marker in 2 built files. Install-local 4 failed / 5 passed, and the single-package catalogue case stayed green. Restore: blob after restore dc88fbdf0207 == HEAD and git diff HEAD is empty. Restore-leg rebuild, then preflight --absent: marker absent from 6 built files, tree clean. Then unit 35/35 and install-local 9/9. The direction is the expected one (red), and the control stays green.",
"mcp_calls": "0 — no MCP GitHub tool used (issue, comments and PR read through unauthenticated REST GET)",
"api_writes": "3 — each relayed as objectstack-fleet[bot] through one POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create POST /repos/objectstack-ai/objectstack/pulls -> #21633 (draft); (2) label-write assign POST /repos//issues/21633/assignees [os-bill]; (3) this os-dev-report POST /repos//issues/21602/comments. Plus git push x5 (the empty probe branch, then 4 commits); git push is not a REST write",
"gates": "2279c378f6: 62/62 derived run, all exit 0, --ran 0 NOT-MEASURED / 0 UNRUN; 5 roster gates under touched dirs exit 0; pnpm lint exit 0. check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (exit 3, 9 packages without dist) in the run on 2f9097c. It answered exit 0 once those dists existed, and exit 0 again in the 2279c37 run. CI-only families (Test Core shards, Dogfood, Temporal Conformance, Build Core, the type-check lanes) are NOT MEASURED locally; they are declared to CI.",
"line_budget": "n/a",
"files_changed": [
".changeset/21602-package-scoped-job-identity.md",
"packages/cli/test/package-install-local-jobs-shared-name.integration.test.ts",
"packages/runtime/src/app-artifact-handlers.jobs.test.ts",
"packages/runtime/src/app-artifact-handlers.ts"
],
"deviations": [
"The claim's stop clause says: 'if the census shows a visible name moves on any reader, stop and report'. I did not stop. The visible scoped name occurs only in the collision case, which was the defect. The ruling's census text anticipates readers that 'read the scoped identity'. sys_job.name is unique 'global', so two coexisting jobs cannot both be catalogued under the authored name without a service-job or contract change, and both are out of bounds. Clause-② was copied as 'no' and not re-declared. Raised as open_questions[0] for the PM to confirm or overrule.",
"Commit 913d8e8's subject carries '(#21602 reach probe)'. That is a mention in the subject line, not a trailer, and check:commit-card-trailers passed on push. Every later commit subject carries no card number.",
"One #21489 unit test pinned last-scheduler-wins: 'another app's jobs are never cancelled — not even one that took over a name this app once scheduled'. That is exactly the branch this card removes, so it was rewritten under fixture triage as a full replacement. It now pins that each app cancels only its own same-named job."
],
"open_questions": [
{
"question": "Collision-case visibility versus the claim's stop clause. With two packages declaring JOB_NAME, the later one's job reads PACKAGE_ID:JOB_NAME in sys_job and sys_job_run, where before the fix it read JOB_NAME (having displaced the earlier package's job). Does this count as 'a visible name moving' that changes Clause-②?",
"options": [
"A: No. It is the ruling's 'reads the scoped identity' branch, required by sys_job.name being unique 'global'. No accept set, export, contract or published surface moves, and the non-colliding and single-package names are pinned unchanged. Keep Clause-②: no.",
"B: Yes. The PM re-declares Clause-② and the changeset and PR line follow.",
"C: Avoid any visible move by adding a display name to the job catalogue. That needs a JobScheduleOptions field (IJobService contract change) and a service-job edit, both out of bounds for this card."
],
"recommendation": "A, on the four axes. Business need: today the later package's job silently stops the earlier one, and coexistence is what the ruling asks for. Long-term soundness: the identity follows the registry's PACKAGE_ID:NAME key with no contract change. AI-error resistance: a scoped key an operator can read beats a silent displacement. Scope discipline: C widens IJobService for a case with no measured pull."
}
],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted, not filed. Restart order (read from code, not measured): on a restart the bare name goes to the first package the boot schedules. Install-local rehydrates in ledger file-name order (readdirSync), and hot installs schedule in install order. If a later-installed colliding package sorts first, the two swap names at the restart, so the JOB_NAME row's run history continues with the other package's runs. Both jobs still run. In PR #21633 Acceptance notes.",
"carrier: none (承接者:无) · noted, not filed. DbJobAdapter never reconciles sys_job.active on boot (read from code, not measured), so a key nothing schedules after a restart keeps active: true. This already applied before for any job dropped between boots. In PR #21633 Acceptance notes.",
"carrier: none (承接者:无) · noted, not filed. packages/spec/liveness/job.json 'name' evidence prose says the name 'is the scheduling key passed to svc.schedule'. That is still true except in the collision case, and the anchor still resolves (evidence test 42/42). spec lane. In PR #21633 Acceptance notes."
]
}
JobScheduleOptions carries only retryPolicy and timeoutMs, so the scope rides on the name argument, and only when names collide.
⛔ No IJobService contract change, no packages/spec or packages/services edit, and no export added, removed or renamed. jobKeyFor is module-private.
Measured red first at the public door on 045b946256 (4 red / 5 green): the first package's job froze once a second package with the same job name installed, the catalogue held one name, and the first package did not run after a restart. Green after the fix.
Ablated: with the scoped key replaced by the bare name, the unit run went 9 red and the install-local run 4 red, while the single-package controls stayed green. The restore was blob- and dist-proven.
The open question (collision-case visibility versus the claim's stop clause): A.Clause-②: no stands.
In the collision case only, the later package's job is catalogued in sys_job / sys_job_run under PACKAGE_ID:JOB_NAME. Before the fix, that case displaced the earlier package's job entirely.
sys_job.name is unique, so two coexisting jobs cannot both carry the authored name without a contract or service-job change. Both are out of bounds by the ruling.
The ruling's census text anticipates exactly this: readers that "read the scoped identity". It pins only that "a single-package runtime's visible job names stay unchanged", and that is pinned in the unit run and at the door (sys_job and sys_job_run read the authored name).
Clause-② concerns accept sets and the published surface (exports, schemas, contracts), not the data values of a system table in a case that was broken.
The dev's deviation (proceeding instead of stopping) is accepted on that reading. The stop clause was written for a move outside the ruling's anticipated branch.
The (#21602 reach probe) mention in one commit subject. It is not a trailer, check:commit-card-trailers passed, and the landing squashes it away.
Noted, not filed (in the PR's Acceptance notes):
Restart order. On a restart the bare name goes to whichever colliding package the boot schedules first, and ledger rehydrate runs in file-name order. So two colliding packages may swap names across a restart, and the authored-name row's run history continues with the other package's runs. Both jobs keep running. Code-read only.
DbJobAdapter does not reconcile sys_job.active on boot. That is pre-existing for any job dropped between boots.
The job.jsonname evidence prose. It says the name is the scheduling key; that stays true except in the collision case. The anchor resolves (42/42).
Clause-②: no, so no contract review is owed. Landing proceeds once CI completes.
domain:cli seat · session_016GiHYRmLSNWTfbX9gVQkpz · read 2026-10-03T22:00Z · ⛔ classes, doors and roles only
Merged 2026-10-03T21:56Z through the merge queue (added_to_merge_queue 2026-10-03T21:22Z), at head 2279c378f6. That is the head the ACCEPT 5973376565 read.
Shape:git rev-list --parents -n 1 6946f2f08e gives 2 fields, so it is a single-parent squash. The commit is an ancestor of origin/main. It is 4 files, +760/−43, matching the PR.
Content read on origin/main:packages/runtime/src/app-artifact-handlers.ts carries the module-private jobKeyFor, and scheduleAppArtifactJobs resolves every job key through it.
The card closed completed via Fixes #21602. pm:dispatched is stripped in this act.
Activity
objectstack-fleet commentedon Oct 3, 2026
Triage: first grade —
bug·priority:p2·domain:cli·area:devpath·pm:queue. Schedule under the package-scoped identity the registry already uses. ⛔ Not a refusalTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-03T15:56Z. ⛔ Not a claim, ⛔ not a dispatch.Why p2. A package's declared job silently stops because an unrelated package was installed, and each install reports success. It is the grade of the #21489 / #21585 family on the same door.
Routing. The scheduler call in
packages/runtime/src/app-artifact-handlers.ts(scheduleAppArtifactJobs), sodomain:cli. The aim is a fix with noIJobServicecontract change. If the identity cannot be carried through the existingschedulecall, the claim stops and reports. ⛔ It does not widen the service interface on its own.Ruling: package-scoped identity, not a refusal.
Cross-reference: the bare-name function collision for hooks is #21604's decision. This card does not touch function resolution.
Generated by Claude Code
objectstack-fleet commentedon Oct 3, 2026
Claim: PM loop round 1 (serial behind #21585, which landed as
045b946256onapp-artifact-handlers.ts)Session:
session_016GiHYRmLSNWTfbX9gVQkpzAccount:
os-bill(the seat's linked user asget_meanswers it; the card's assignee)Branch:
claude/issue-21602-package-scoped-job-identityWorktree:
objectstack-issue-21602Domain:
domain:cliSeat:
domain:cli#1File surface, per triage's ruling
5970811944(schedule under the package-scoped identity the registry already uses; ⛔ not a refusal):sys_jobrows), admin listings, the cancel and trigger paths, and fix(runtime,cloud-connection)!: a job's sandboxed body is scheduled on every door, and install-local refuses an enabled job with no body (#21489) #21584's ownership record (retireAppJobs, theruntime.package-jobsuninstall cleanup). For each, measure whether it reads the scoped identity or keeps answering the authored name.packages/runtime/src/app-artifact-handlers.ts(scheduleAppArtifactJobs,retireAppJobs, the ownership record): two packages' same-named jobs coexist, and uninstalling one stops only its own..changeset/.⛔ No
IJobServicecontract change (packages/spec/src/contracts/job-service.ts): the identity rides the existingschedulecall. If it cannot, the claim stops and reports. ⛔ No widening of the service interface. ⛔ Function-name resolution for hooks is [Decision] security(objectql): may a hook'shandlername bind to a function another package registered (the engine-wide fallback HookSchema.handler declares), or does name resolution stay inside the hook's own package (#21585 option B) #21604's (the maintainer's), not here. A reader outsidedomain:cli(for examplepackages/services/service-job) is measured and reported. ⛔ It is not edited without a cross-lane declaration first. (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default (opus). An identity change with a census of readers across run history, listings and cancellation.Clause-②: no. No accept set or published surface moves: the scheduled key is internal, and a single-package runtime's visible names are pinned unchanged. If the census shows a visible name moves on any reader, stop and report rather than re-declare.
Thread-read: 5970811944
Serial constraints, other open PRs read 2026-10-03T19:34Z on
origin/main045b946256:045b946256), which releases this card's hold onapp-artifact-handlers.ts.app-artifact-handlers.ts,packages/services/service-job/or the install-local plugin.domain:cliseat ·session_016GiHYRmLSNWTfbX9gVQkpz· 2026-10-03T19:34ZGenerated by Claude Code
test(cli): install-local pin — two packages declaring the same job na…
objectstack-fleet commentedon Oct 3, 2026
os-dev-report
{
"issue": 21602,
"status": "done",
"branch": "claude/issue-21602-package-scoped-job-identity",
"pr": "#21633",
"session": "session_016GiHYRmLSNWTfbX9gVQkpz",
"premise_still_valid": true,
"summary": "Implemented triage ruling 5970811944: jobs get a package-scoped identity, and a collision is not refused. scheduleAppArtifactJobs (packages/runtime/src/app-artifact-handlers.ts) schedules each job under the key that jobKeyFor gives it, checked in this order: (1) the key this app already holds the job under; (2) else the authored name, when no other package holds it on the job service; (3) else the registry's package-scoped key PACKAGE_ID:JOB_NAME, plus an info line naming the holder. #21584's ownership record now maps each authored name to its key, so retireAppJobs and the runtime.package-jobs uninstall cleanup cancel only the replacing or uninstalling package's own job. No IJobService contract change and no edit to packages/spec or packages/services. A1: reach reproduced on base 045b946 at the public door. The new install-local pin went 4 red / 5 green: ALPHA's job held at 8 rows after BETA and GAMMA installed, sys_job read ['shared_tick'], and ALPHA did not run after the restart. A3: JobScheduleOptions carries only retryPolicy/timeoutMs, so the scope rides on the name argument, and only when names collide. A runtime where no two packages share a job name is unchanged; this is pinned in units and at the door, where sys_job and sys_job_run read [shared_tick]. Census flag for the PM: in the collision case only, the later package's job is catalogued in sys_job/sys_job_run as PACKAGE_ID:JOB_NAME. This is forced by sys_job.name being unique 'global'. I proceeded under the ruling's 'reads the scoped identity' branch, see open_questions[0].",
"tests": "Final head 2279c37; its only change from 2f9097c is a one-line doc comment. | On 2279c37: dispatch-gates --commands derived 62; all 62 ran, each exit 0; --ran reports '62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN'. The 5 roster gates under touched dirs (check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check:route-ledger-census) all exit 0. pnpm lint (full repo) exit 0 in 87s. app-artifact-handlers.jobs.test.ts 35/35; runtime typecheck OK. | On 2f9097c (same source apart from the comment): @objectstack/runtime test 318 files / 4530 passed / 19 skipped; @objectstack/cloud-connection 36 files / 437 passed; @objectstack/cli --project unit 255 files / 3745 passed, tier-partition pin included; cli typecheck OK. The new test file is in the cli tsconfig.test.json program (counted with --listFilesOnly). Install-local pins on built dist: jobs + jobs-shared-name 20/20; uninstall-cleanups 16/16. Liveness evidence.test.ts 42/42. | A1, base 045b946 dist: new pin 4 failed / 5 passed ('expected 8 to be greater than 8'; catalogue ['shared_tick']). | A5 ablation at 07d4deb via scripts/ablation-replace.mjs WRAP. Anchor '{ key:
${appId}:${jobName}, heldBy }' went x1 to x0; replacement '{ key: jobName, heldBy }' went x0 to x1; blob dc88fbdf0207 to e523712b60f5. Mutated leg: unit 9 failed / 26 passed, and the single-package control stayed green. Runtime rebuilt; ablation-dist-preflight found the marker in 2 built files. Install-local 4 failed / 5 passed, and the single-package catalogue case stayed green. Restore: blob after restore dc88fbdf0207 == HEAD and git diff HEAD is empty. Restore-leg rebuild, then preflight --absent: marker absent from 6 built files, tree clean. Then unit 35/35 and install-local 9/9. The direction is the expected one (red), and the control stays green.","mcp_calls": "0 — no MCP GitHub tool used (issue, comments and PR read through unauthenticated REST GET)",
"api_writes": "3 — each relayed as objectstack-fleet[bot] through one POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create POST /repos/objectstack-ai/objectstack/pulls -> #21633 (draft); (2) label-write assign POST /repos//issues/21633/assignees [os-bill]; (3) this os-dev-report POST /repos//issues/21602/comments. Plus git push x5 (the empty probe branch, then 4 commits); git push is not a REST write",
"gates": "2279c378f6: 62/62 derived run, all exit 0, --ran 0 NOT-MEASURED / 0 UNRUN; 5 roster gates under touched dirs exit 0; pnpm lint exit 0. check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (exit 3, 9 packages without dist) in the run on 2f9097c. It answered exit 0 once those dists existed, and exit 0 again in the 2279c37 run. CI-only families (Test Core shards, Dogfood, Temporal Conformance, Build Core, the type-check lanes) are NOT MEASURED locally; they are declared to CI.",
"line_budget": "n/a",
"files_changed": [
".changeset/21602-package-scoped-job-identity.md",
"packages/cli/test/package-install-local-jobs-shared-name.integration.test.ts",
"packages/runtime/src/app-artifact-handlers.jobs.test.ts",
"packages/runtime/src/app-artifact-handlers.ts"
],
"deviations": [
"The claim's stop clause says: 'if the census shows a visible name moves on any reader, stop and report'. I did not stop. The visible scoped name occurs only in the collision case, which was the defect. The ruling's census text anticipates readers that 'read the scoped identity'. sys_job.name is unique 'global', so two coexisting jobs cannot both be catalogued under the authored name without a service-job or contract change, and both are out of bounds. Clause-② was copied as 'no' and not re-declared. Raised as open_questions[0] for the PM to confirm or overrule.",
"Commit 913d8e8's subject carries '(#21602 reach probe)'. That is a mention in the subject line, not a trailer, and check:commit-card-trailers passed on push. Every later commit subject carries no card number.",
"One #21489 unit test pinned last-scheduler-wins: 'another app's jobs are never cancelled — not even one that took over a name this app once scheduled'. That is exactly the branch this card removes, so it was rewritten under fixture triage as a full replacement. It now pins that each app cancels only its own same-named job."
],
"open_questions": [
{
"question": "Collision-case visibility versus the claim's stop clause. With two packages declaring JOB_NAME, the later one's job reads PACKAGE_ID:JOB_NAME in sys_job and sys_job_run, where before the fix it read JOB_NAME (having displaced the earlier package's job). Does this count as 'a visible name moving' that changes Clause-②?",
"options": [
"A: No. It is the ruling's 'reads the scoped identity' branch, required by sys_job.name being unique 'global'. No accept set, export, contract or published surface moves, and the non-colliding and single-package names are pinned unchanged. Keep Clause-②: no.",
"B: Yes. The PM re-declares Clause-② and the changeset and PR line follow.",
"C: Avoid any visible move by adding a display name to the job catalogue. That needs a JobScheduleOptions field (IJobService contract change) and a service-job edit, both out of bounds for this card."
],
"recommendation": "A, on the four axes. Business need: today the later package's job silently stops the earlier one, and coexistence is what the ruling asks for. Long-term soundness: the identity follows the registry's PACKAGE_ID:NAME key with no contract change. AI-error resistance: a scoped key an operator can read beats a silent displacement. Scope discipline: C widens IJobService for a case with no measured pull."
}
],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted, not filed. Restart order (read from code, not measured): on a restart the bare name goes to the first package the boot schedules. Install-local rehydrates in ledger file-name order (readdirSync), and hot installs schedule in install order. If a later-installed colliding package sorts first, the two swap names at the restart, so the JOB_NAME row's run history continues with the other package's runs. Both jobs still run. In PR #21633 Acceptance notes.",
"carrier: none (承接者:无) · noted, not filed. DbJobAdapter never reconciles sys_job.active on boot (read from code, not measured), so a key nothing schedules after a restart keeps active: true. This already applied before for any job dropped between boots. In PR #21633 Acceptance notes.",
"carrier: none (承接者:无) · noted, not filed. packages/spec/liveness/job.json 'name' evidence prose says the name 'is the scheduling key passed to svc.schedule'. That is still true except in the collision case, and the anchor still resolves (evidence test 42/42). spec lane. In PR #21633 Acceptance notes."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 3, 2026
ACCEPT — PR #21633 at
2279c378f6domain:cliseat ·session_016GiHYRmLSNWTfbX9gVQkpz· review of record, read on GitHub 2026-10-03T20:52ZShape:
main,Fixes #21602, andClause-②: noline-initial.check-governed-mergesruns before landing).2279c378f6, at read: 15success, 3skipped, 13 still running. Landing waits for the full set.Against triage's ruling
5970811944(package-scoped identity, ⛔ not a refusal), read from the diff:scheduleAppArtifactJobsschedules each job underjobKeyFor's key, in this order:PACKAGE_ID:JOB_NAME, with an info line naming the holder.retireAppJobsand theruntime.package-jobsuninstall cleanup cancel only the replacing or uninstalling package's own job.JobScheduleOptionscarries onlyretryPolicyandtimeoutMs, so the scope rides on thenameargument, and only when names collide.IJobServicecontract change, nopackages/specorpackages/servicesedit, and no export added, removed or renamed.jobKeyForis module-private.045b946256(4 red / 5 green): the first package's job froze once a second package with the same job name installed, the catalogue held one name, and the first package did not run after a restart. Green after the fix.The open question (collision-case visibility versus the claim's stop clause): A.
Clause-②: nostands.sys_job/sys_job_rununderPACKAGE_ID:JOB_NAME. Before the fix, that case displaced the earlier package's job entirely.sys_job.nameis unique, so two coexisting jobs cannot both carry the authored name without a contract orservice-jobchange. Both are out of bounds by the ruling.sys_jobandsys_job_runread the authored name).Other deviations, accepted:
(#21602 reach probe)mention in one commit subject. It is not a trailer,check:commit-card-trailerspassed, and the landing squashes it away.Noted, not filed (in the PR's Acceptance notes):
DbJobAdapterdoes not reconcilesys_job.activeon boot. That is pre-existing for any job dropped between boots.job.jsonnameevidence prose. It says the name is the scheduling key; that stays true except in the collision case. The anchor resolves (42/42).Clause-②: no, so no contract review is owed. Landing proceeds once CI completes.Generated by Claude Code
objectstack-fleet commentedon Oct 3, 2026
Landed: PR #21633 →
6946f2f08edomain:cliseat ·session_016GiHYRmLSNWTfbX9gVQkpz· read 2026-10-03T22:00Z · ⛔ classes, doors and roles onlyadded_to_merge_queue2026-10-03T21:22Z), at head2279c378f6. That is the head the ACCEPT5973376565read.git rev-list --parents -n 1 6946f2f08egives 2 fields, so it is a single-parent squash. The commit is an ancestor oforigin/main. It is 4 files, +760/−43, matching the PR.origin/main:packages/runtime/src/app-artifact-handlers.tscarries the module-privatejobKeyFor, andscheduleAppArtifactJobsresolves every job key through it.completedviaFixes #21602.pm:dispatchedis stripped in this act.--allow-destructivedrops them (the coupling #21571 names) #21573 (the operator-onlyos migrateread of unmapped columns). Both changesets of An unprojected REST data query returns ORPHANED columns that no metadata declares (fields retired in an upgrade), outside any field-level rule, untilos migrate apply --allow-destructive#21571 and A write response still serves ORPHANED columns no metadata declares: after the read narrowing, PATCH /api/v1/data/OBJECT/ID answers 200 with a retired field's column inrecord#21613 name it as the conversion route. It is claimed in this round.handlername bind to a function another package registered (the engine-wide fallback HookSchema.handler declares), or does name resolution stay inside the hook's own package (#21585 option B) #21604 (owner-scoped function resolution) is the maintainer's decision.5972494706(a refused hook-body object) waits for this family's next claim.os dev --database-driver memoryboots, then every data read answers 503 SERVICE_UNAVAILABLE — the package registry's CREATE TABLE sys_packages goes through driver-memory's no-op execute() #21492 ACCEPT left one question for triage: does the retirement reach a non-default declared memory datasource? The contract review judged it non-blocking.Generated by Claude Code
fix(runtime,cloud-connection)!: install-local refuses a hook with no …
fix(runtime): two packages declaring the same job name both run, and …