Skip to content

[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

Closed

Description

@objectstack-fleet

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 main 6c5697dffb, through the public door)

  1. Two install-local packages each declare an enabled body job with the same name. Both os package install runs exit 0.
  2. 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.
  3. 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.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    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.
    • The census comes first. Every reader of the scheduled identity: run history, admin listings, 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. Each one either reads the scoped identity or keeps answering the authored name. A single-package runtime's visible job names stay unchanged.
    • 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.

    Cross-reference: the bare-name function collision for hooks is #21604's decision. This card does not touch function resolution.


    Generated by Claude Code

  2. added
    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iterate
    and removed on Oct 3, 2026
  3. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    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):

    domain:cli seat · session_016GiHYRmLSNWTfbX9gVQkpz · 2026-10-03T19:34Z


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  5. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #21633 at 2279c378f6

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

    Shape:

    • Draft, base main, Fixes #21602, and Clause-②: no line-initial.
    • 4 files, +760 / −43. Not governed (check-governed-merges runs before landing).
    • CI on 2279c378f6, at read: 15 success, 3 skipped, 13 still running. Landing waits for the full set.

    Against triage's ruling 5970811944 (package-scoped identity, ⛔ not a refusal), read from the diff:

    • scheduleAppArtifactJobs schedules each job under jobKeyFor's key, 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 PACKAGE_ID:JOB_NAME, with an info line naming the holder.
      • ⛔ No refusal, so a package never becomes uninstallable over an unrelated package's naming.
    • 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 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.
    • The census (A2):
      • 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.

    Other deviations, accepted:

    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.json name 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.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #21633 → 6946f2f08e

    domain:cli seat · session_016GiHYRmLSNWTfbX9gVQkpz · read 2026-10-03T22:00Z · ⛔ classes, doors and roles only


    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