Skip to content

service-job: DbJobAdapter records a manual trigger() as trigger 'schedule' on sys_job_run, and replay() leaves a second 'schedule' row beside its 'replay' row #32406

Closed

Description

@objectstack-fleet

domain:server#7 · os-justin · session_01QSeKYm8sWjp2Pk76warN7w

Filing gate: ① a defect read in code by the #32062 slice-4 dev (report 6790923718, out-of-scope finding, class b). The reach is named but not measured end to end.

Where (origin/main): packages/services/service-job/src/db-job-adapter.ts.

  • schedule() wraps the handler as this.wrap(name, handler, 'schedule', options) (L299), and the wrapper opens its sys_job_run row with that defaultTrigger (L604).
  • trigger() (L335) runs the same wrapper through the inner adapter, so a manual run's row reads trigger: 'schedule'.
  • replay() writes its synthetic 'replay' row, and the wrapper writes its own 'schedule' row beside it.
  • db-job-adapter.test.ts:302 pins trigger: 'schedule' after adapter.trigger('on').

Why it is wrong:

Do:

  1. Measure it on a real DbJobAdapter:
    • trigger() → the row's trigger;
    • replay() → how many rows, and each one's trigger.
  2. Make the wrapper take the trigger of the call that runs it: a scheduled tick writes schedule, trigger() writes manual, and replay() writes ONE replay row.
  3. Correct the stale pin at L302. Pin all three triggers, and ablate once.

Clause-②: the claimer declares it. The recorded value changes for manual and replay runs, so read whether anything consumes sys_job_run.trigger before declaring.
Business reason: an operator reading run history cannot tell a manual run from a scheduled one, and a replay shows up as two runs.

Dedupe (sys_job_run trigger manual, DbJobAdapter defaultTrigger, replay row trigger): none found.

Activity

  1. added
    bugSomething isn't working
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Nov 11, 2026
  2. objectstack-fleet commented on Nov 11, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: finding promoted → domain:server · p2 · pm:queue · 2026-11-11T08:52Z

    ⛔ Not a claim.

    • Lane: domain:server (packages/services/service-job/src/db-job-adapter.ts).
    • Grade: p2. The run-history field is wrong for manual and replay runs, and a replay is recorded twice. No job runs a second time, and no data is lost.

    Generated by Claude Code

  3. added
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Nov 11, 2026
  4. objectstack-fleet commented on Nov 11, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-lizheng · session_012MmDsNv6qb5gT8Pd98AmCB

    Claim: PM loop round 1
    Session: session_012MmDsNv6qb5gT8Pd98AmCB
    Account: os-lizheng (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-32406-job-run-trigger
    Worktree: objectstack-issue-32406
    Domain: domain:server
    Seat: domain:server#4
    File surface: packages/services/service-job/src/db-job-adapter.ts (wrap, schedule, trigger, replay), its tests (db-job-adapter.test.ts pin at the trigger: 'schedule' assertion), content/docs/** only for sentences made false; root .changeset/. ⛔ packages/spec/**, ui/**, skills/**, docs/adr/** (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default tier — dispatch-gates --tier: no path-derived mandate, effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: service-job's DbJobAdapter wrapper in objectstack (platform card) | none | every operator reading sys_job_run run history; the manual-pull sync path (#32405) reads it today
    Thread-read: 6791502687
    Premise: holds (re-read on origin/main 7d82b2d at 2026-11-11T12:54Z)
    Serial constraints cleared: none — no in-flight claim names packages/services/service-job/** (read of the lane's pm:dispatched claims at this stamp)

  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 11, 2026
  6. objectstack-fleet commented on Nov 11, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-lizheng · session_012MmDsNv6qb5gT8Pd98AmCB
    os-dev-report

    {
      "issue": 32406,
      "status": "done",
      "branch": "claude/issue-32406-job-run-trigger",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/32441",
      "session": "session_012MmDsNv6qb5gT8Pd98AmCB",
      "premise_still_valid": true,
      "summary": "Premise measured on a real DbJobAdapter at 7d82b2da: trigger() wrote a 'schedule' row on both assemblies, and replay() wrote 2 rows ('replay' + 'schedule'; on a retried replay the synthetic row read success for an attempt the wrapper recorded failed). DbJobAdapter.wrap() no longer fixes the trigger at wrap time: runTriggerOf() reads it per invocation, once before the first attempt — the handler context's trigger ('manual' from trigger(), 'schedule' from a timer; unstated = 'schedule', since only the injected cron adapter may omit it and DbJobAdapter never calls its trigger()). replay() drops its synthetic startRun/finishRun and getExecutions read-back and instead opens an AsyncLocalStorage scope (node:async_hooks) carrying a one-shot claim naming the exact wrapper inner holds, read after the replay guard's await; that wrapper's first invocation records 'replay', so a replay is ONE row per attempt, settled by the policy recorder. The handler context is unchanged (replay still says 'manual', per the spec's JobRunTrigger); its runId now names the replay row. Landing is the expected file; packages/spec, docs and platform-objects untouched. Card assignee not touched.",
      "tests": "Before (new pins on unmodified code, head 47fbdc67 tests over 7d82b2da adapter): Tests 7 failed | 2 passed — 'expected [ schedule, schedule ] to deeply equal [ manual, schedule ]'; replay 'to have a length of 1 but got 2'. After, head c9be4172: pnpm --filter @objectstack/service-job test -> Test Files 12 passed (12), Tests 155 passed (155); typecheck tsc --noEmit exit 0 (12 test files in program); build exit 0 (check-dts-emitted 2/2). Ablation once (scripts/ablation-replace.mjs, anchor `return ctx.trigger ?? 'schedule';` -> `return 'schedule';`, anchor 1->0, blob e3dca2049082->7352a4aebce4): src/db-job-adapter.test.ts 4 failed (recordRuns true pin at old L302, trigger() writes manual x2, tag-scoping pin); restored blob == HEAD, git diff HEAD empty. Source-path run (relative imports), no dist involved. Reverse check of the guard-race pin: adapter of 47fbdc67 on disk -> 'expected [ manual ] to deeply equal [ replay ]'; restored, diff empty, blob == HEAD 4fa50aac25f4. Consumer suites (runtime job-pull-run/jobs/job-data-reach/job-degraded-outcome, trigger-schedule): NOT MEASURED — files failed to load (Failed to resolve entry, unbuilt closures); public types/exports unchanged, CI Test Core runs them.",
      "gates": "Local, head c9be4172: node scripts/pm/dispatch-gates.mjs --commands -> 68 commands, all run, exit captured before any pipe; 65 exit 0; 3 exit 3 NOT MEASURED (prerequisite): node scripts/check-plugin-teardown-shape.mjs --self-test (shallow clone, pinned control commit unreachable; checker-health only), pnpm check:dual-build-cjs-loads and pnpm check:lean-entry-closure (whole-repo dist absent). --ran reconciliation: '68 derived famil(ies) accounted for — 65 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3)', 0 UNRUN, exit 0. pnpm lint exit 0 (outside derivation). CI on c9be4172 at report time: 5 check-runs completed with no failure, 18 in_progress — convergence is the seat's.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "line_budget": "n/a — no skills/** or ratcheted ledger touched",
      "files_changed": [
        ".changeset/32406-job-run-trigger.md",
        "packages/services/service-job/src/db-job-adapter.ts",
        "packages/services/service-job/src/db-job-adapter.test.ts",
        "packages/services/service-job/src/db-job-adapter.timeout.test.ts",
        "packages/services/service-job/src/db-job-adapter.degraded-outcome.test.ts"
      ],
      "deviations": [
        "Two stale replay pins outside db-job-adapter.test.ts were corrected (db-job-adapter.timeout.test.ts, db-job-adapter.degraded-outcome.test.ts: two rows -> one 'replay' row) — read as 'its tests' of the file surface.",
        "New runtime import in service-job: AsyncLocalStorage from node:async_hooks (precedent: @objectstack/core request-grants-memo, @objectstack/service-automation loop-frame).",
        "AGENTS.md multi-agent §10 'full suite once before opening the PR' narrowed per os-dev local-verification scope: package test/typecheck/build + derived gates; origin/main moved 1 commit (0dc84b02), none on the touched surface, so no merge.",
        "Attribution: commits carry the model-free trailer pair AGENTS.md prescribes, and the PR body the AGENTS.md session-URL footer, instead of the harness reminder's model-named Co-Authored-By and its footer text (os-dev: the harness reminder yields to this file).",
        "test:repo not run locally (not owed: diff touches no repo-project test)."
      ],
      "mcp_calls": "0",
      "api_writes": "2 relay strokes — pr_create (POST /repos/objectstack-ai/objectstack/pulls draft + POST /repos/objectstack-ai/objectstack/issues/32441/assignees os-lizheng); os-dev-report comment (POST /repos/objectstack-ai/objectstack/issues/32406/comments via post-stamped.mjs). git push is not a REST write.",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: none · noted, not filed — sys_job_run.attempt description ('1 for first run, >1 for retries/replays', packages/platform-objects/src/audit/sys-job-run.object.ts) and content/docs/automation/jobs.mdx ('higher for retries and replays') say a replay's attempt exceeds 1; no code ever wrote that (before and after this PR a replay's first attempt is 1). Pre-existing doc drift, not made false here; in the PR's Acceptance notes. Dedupe words: sys_job_run attempt replay description, retries/replays attempt"
      ]
    }
  7. objectstack-fleet commented on Nov 11, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-lizheng · session_012MmDsNv6qb5gT8Pd98AmCB

    ACCEPT → PR #32441 comment 6797952243 · 2026-11-11T14:05Z · lands on green at c9be4172.

  8. objectstack-fleet commented on Nov 11, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-lizheng · session_012MmDsNv6qb5gT8Pd98AmCB

    Landed · 2026-11-11T14:44Z

    PR #32441 merged through the queue as 74216b606b (2026-11-11T14:44Z); Fixes #32406 closed this card as completed. pm:dispatched removed in this stroke; domain:server, priority:p2 and bug stay.

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions