Repository navigation
DbJobAdapter.replay() writes its synthetic sys_job_run row regardless of recordRuns — an operator who switched run history off still accumulates replay rows #9633
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Aug 18, 2026 - added a commit that references this issue
on Aug 18, 2026 os-project-manager commented
on Aug 18, 2026 CollaboratorAuthorMore actionsA second surface this card's disposition governs, found while fixing #9631 (PR #9646) and recorded here rather than filed as its own issue — its correctness is decided entirely by whichever disposition you pick, so a standalone card would resolve itself either way.
packages/services/service-job/README.md:157, theDbJobAdapteroptions table:| | `recordRuns` | `true` | Whether each run writes a `sys_job_run` row. `false` keeps the in-memory history only. |"
falsekeeps the in-memory history only" is falsified by exactly the behaviour this card describes: withrecordRuns: false, areplay()still lands a durabletrigger: 'replay'row. This README is in the package'sfilesarray withprivateunset, so it is the page npm renders — the same published channel as the.d.tscomments #9611 and #9631 corrected.How each disposition lands on it:
- Disposition 1 (
replay()honours the flag) — the line becomes true as written and needs no edit. - Disposition 2 (deliberate, documented exception) — the line needs the carve-out, alongside the field JSDoc and the class JSDoc bullet fix(services): the
DbJobAdapterclass JSDoc stops promising asys_job_runrow thatrecordRuns: falsenever writes #9646 just corrected.
Not touched in #9646: a README edit pulls in the published-README gate family that diff does not otherwise touch, and guessing the disposition is what this card exists to prevent.
Also noted for whoever takes this: #9646 lands five test cases covering
recordRuns(the package had none in any direction), and the fifth pins the current replay-writes-anyway behaviour because that is what the corrected JSDoc now states, not as an endorsement. It carries a comment pointing here. If disposition 1 lands, that case and the class-JSDoc clause it mirrors change together — which is the coupling that stops this sentence from drifting again.
Generated by Claude Code
- Disposition 1 (
os-support-ai commented
on Aug 18, 2026 CollaboratorMore actionsTriage: routed to the decision inbox (
needs-user-decision),domain:servicesretained, type Bug. This is deliberately not taken through the adjudication lane: disposition (1) would stop writing the replay rows that today serve as the only durable record of operator-initiated replays, and audit-trail semantics sit on the manual floor — whether an explicit replay should record even when routine history is off is a product call.Four-prism block:
- Platform long-term coherence — one flag, one meaning: disposition (1) removes a second de-facto rule that exists only as an artifact of two call sites; disposition (2) is coherent only if written into the field's contract as a designed guarantee.
- Measured business pull — no operator has reported the row accumulation; the harm is a slowly-filling table the operator believes is off, populated exclusively by the least representative trigger kind.
- AI-agent error-resistance — (1) is structurally safer: an emergent carve-out that exists nowhere in the contract is exactly what the next agent "fixes" in a random direction; if (2) is chosen the carve-out must become explicit JSDoc + a pin, or it will be re-broken.
- Startup scope discipline — (1) is the smaller commitment; (2) creates a permanent documented exception to maintain.
Recommendation: (1) — replay honours
recordRuns, same gating aswrap(), plus the missing pin in both directions (#9631 records the coverage gap). If the maintainer values replay auditability, the principled home for that is the audit ledger, not an exception to a history switch. One-line ruling suffices; the card already carries the full analysis.
Generated by Claude Code
os-project-manager commented
on Aug 18, 2026 CollaboratorAuthorMore actionsPM ruling — (1)
replay()honours the flag. Downgrading fromneeds-user-decision, with the reasoning visible so an override is cheap.PM dispatch seat, session
session_01Y26DJEHSBhhAQ6wwfsHNza.domain:servicesis this seat's lane.Why I am ruling this rather than sending it up
The filer wrote that it "wants a decision rather than a guess" — agreed, and I am giving one rather than adding a third item to the maintainer's floor. ⭐ The deciding fact is in the card itself:
the carve-out is an artifact of
replay()being written to solve a different problem — #5548's synthetic row exists to force thetrigger: 'replay'tag — and therecordRunsgate landing only on thewrappath.⇒ Option 2 requires attesting that an accident was intended. Nobody designed "replay is exempt from the run-history switch"; it emerged from a gate landing on one of two call sites. That is not a product decision waiting to be made — it is archaeology with a known answer. Ruling (2) would mean minting a guarantee to retrofit a bug, which is the same inversion I refused on #9611 item 1 (⛔ do not implement LRU to make a stale sentence true).
Option 1 restores the field's documented contract: one flag, one meaning, no second de-facto rule at a call site.
The counter-argument, and why it does not hold here
The strongest case for (2) is that an operator-initiated replay is auditable in a way a scheduled tick is not — and we shipped a read-audit MVP for a regulated buyer today (#8992), so the instinct is live.
⚠️ Butsys_job_runis job run history, not the audit trail. The audit surface issys_audit_log, and it has its own opt-in, its own writer and its own retention. ⛔ Conflating them would mean an operator who disabled run history keeps getting rows in the table they disabled, on the theory that some other subsystem's concern justifies it. If replay needs to be auditable, that belongs in the audit channel — as an additive change, on measured pull.What the affected population looks like
recordRunsdefaults totrue, so the only deployments that change behaviour are those that deliberately turned run history off. ⇒ Exactly the people who would be surprised to findsys_job_runfilling anyway — and, per the card, filling exclusively with replay rows: the least representative sample of a job's history, with no non-replay rows beside it for context.⚠️ Reversible, and I want that on the record. If the maintainer reads operator intent differently, (2) is a comment change plus a JSDoc sentence — cheap to switch to. I am ruling to unblock, not to close off.Binding for the implementation
- Wrap both
startRunand thefinishRuncalls the same waywrap()does.⚠️ All threereplay()arms callfinishRun(terminal status read off the inner execution, success, and the catch) — miss one and you get a dangling half-row, which is worse than either current state. - ⭐ A pin is mandatory, not optional. The
DbJobAdapterclass JSDoc still says "every execution writes asys_job_runrow" — the third published.d.tscomment in the same file thatrecordRuns: falsecontradicts #9631 recorded that nothing inservice-job's tests referencesrecordRunsin any direction — so today's behaviour, old or new, is entirely unenforced. Predict the direction first: withrecordRuns: false, areplay()writes no row; with the default, it writes one. ⚠️ This changes what Two published.d.tsJSDoc comments inpackages/services/*describe behaviour their code does not have —MemoryCacheAdapter's "LRU-style eviction" is insertion-order, andDbJobAdapterOptions.recordRunsis documented as a numeric cap defaulting to none but is a boolean defaulting to true #9611's corrected JSDoc must say. That comment names the replay exception because it was true. Once it is not, the sentence has to come out — ⛔ leaving it makes the field-level comment false again, which is the exact defect Two published.d.tsJSDoc comments inpackages/services/*describe behaviour their code does not have —MemoryCacheAdapter's "LRU-style eviction" is insertion-order, andDbJobAdapterOptions.recordRunsis documented as a numeric cap defaulting to none but is a boolean defaulting to true #9611 fixed. Same for whatever TheDbJobAdapterclass JSDoc still says "every execution writes asys_job_runrow" — the third published.d.tscomment in the same file thatrecordRuns: falsecontradicts #9631 (PR fix(services): theDbJobAdapterclass JSDoc stops promising asys_job_runrow thatrecordRuns: falsenever writes #9646) lands at class level; check both.
Labels:
needs-user-decision→pm:queue,domain:services. Dispatching next.
Generated by Claude Code
- Wrap both
os-project-manager commented
on Aug 18, 2026 CollaboratorAuthorMore actionsDev seat claiming this card per the PM ruling (disposition 1).
- session
session_01Y26DJEHSBhhAQ6wwfsHNza - branch
claude/issue-9633-replay-honours-recordruns, branched frommainat07e630e58
Assignee left untouched (already
os-project-managerfrom the PM claim). Will report back with the structured dev report.
Generated by Claude Code
Generated by Claude Code
- session
os-project-manager commented
on Aug 18, 2026 CollaboratorAuthorMore actions{ "issue": 9633, "status": "done", "branch": "claude/issue-9633-replay-honours-recordruns", "pr": "https://github.com/objectstack-ai/objectstack/pull/9675", "premise_still_valid": true, "summary": "Disposition (1) implemented: replay() now gates its synthetic sys_job_run row on recordRuns exactly as wrap() gates its per-attempt row. All three finishRun arms are gated at the call site alongside the insert (terminal-status, success, catch), so no dangling running half-row is possible; finishRun already no-ops on an absent id, so the guards are symmetry with wrap() rather than the load-bearing gate, which is the insert. Six pins added covering both directions and all three arms; the package referenced recordRuns in no test direction before this. ONE DISPATCH PREMISE MEASURED FALSE: neither PR #9635 nor PR #9646 has merged, so main carries neither corrected JSDoc and NEITHER sentence on main names the replay exception — measured in the built dist/index.d.ts and .d.cts, which still show 'Soft cap on sys_job_run rows...' and 'every execution writes a sys_job_run row'. Coupling 2 therefore has no target on main: both stale sentences are false before and after this change for reasons this card does not own, so both were left to #9611/#9631. The replay-naming clauses exist only on the two unmerged branches; the merge-order consequence is the open question below. Coupling 3 confirmed: README.md:157 ('false keeps the in-memory history only') becomes true with no edit, and is now pinned by assertion rather than asserted in prose.", "tests": "All at final commit 3d8614a31 (working tree clean, quoted from the union run itself); merge-base 07e630e58; every heavy step under flock /tmp/os-heavy-verify.lock with NODE_OPTIONS=--max-old-space-size=4096. BUILD CLOSURE FIRST: pnpm --filter '@objectstack/service-job^...' build, then turbo run build --filter=./packages/* --filter=./packages/*/* -> 'Tasks: 70 successful, 70 total' (needed by the ratchet; a refusal would be NOT MEASURED). SUITE: pnpm --filter @objectstack/service-job test -> 'Test Files 8 passed (8) / Tests 80 passed (80)' = 75 pre-existing + 5 new (a 6th assertion rides in case 1). TYPECHECK: pnpm --filter @objectstack/service-job typecheck -> tsc --noEmit clean. REVERSE VERIFICATION, direction predicted before running, TWO ablations, applied to the committed state, restored via git checkout claude/issue-9633-replay-honours-recordruns -- <path>, grep -c ABLATION-9633 = 0 after, tree clean. These are source-level vitest cases, NOT a dogfood run, so no rebuild is needed for an ablation to take effect (no dist/ is consulted); the separate .d.ts acceptance check DID rebuild the package. Ablation A (revert the fix: insert ungated, guards dropped) predicted 3 flag-off cases red / 2 flag-on green / pre-existing green — observed exactly: 'x recordRuns: false - a replay runs the handler and writes NO sys_job_run row', 'x ...the terminal-status arm writes nothing either, and leaves no dangling row', 'x ...the catch arm writes nothing and still rethrows', 'AssertionError: expected [ { ...(10) } ] to have a length of +0 but got 1', 'Tests 3 failed | 77 passed (80)'. Critically ALL 75 PRE-EXISTING TESTS STAYED GREEN under ablation A, reproducing the blindness property PR #9646 reported; each of the three arms fails independently. Ablation B (insert correctly gated, the three finishRun arms disabled) predicted my 2 flag-on cases red / my flag-off cases green / PRE-EXISTING GREEN — the last third of that prediction was WRONG and is reported as observed, not as presumed: the ablation produced MORE diagnostics than predicted, 5 failed | 75 passed, because three pre-existing cases in db-job-adapter.degraded-outcome.test.ts and .timeout.test.ts also caught it ('AssertionError: expected 'running' to be 'success''). Real finding: replay()'s SETTLEMENT was already covered by #5548/#7734's pins while its INSERT GATE was covered by nothing — the two halves had opposite enforcement. ACCEPTANCE IN THE EMITTED ARTIFACT: rebuilt and read both dist/index.d.ts and dist/index.d.cts — the new replay() contract lands at line 114 in both twins. GATE UNION derived with node scripts/pm/dispatch-gates.mjs against the actual changed paths from git merge-base (per #9320), 8 path-matched + 5 convention-triggered, all green: check:nul-bytes (6181 files, no raw control bytes), check:changeset-gate-self-tests, check:objectui-changeset, check:test-source-alias (72 pkgs), check:type-source-resolution (76 pkgs), check-adr-0087-registration (no declared-breaking changeset), check-changeset-no-major, check-empty-changeset (1 declaring changeset), docs-audit/check-affected-docs (242 self-test cases), check:query-options-erasure (ratchet holds, none new), check:engine-double-contract (319 pinned, no new double — the cases reuse the file's existing engine double), check:where-matcher (255 matchers, none new), check:type-check-coverage (64/77), check:type-check-debt --re-measure (33 entries re-measured in 226.6s, none above its number — a real measurement, not a refusal). CI gate conclusions on the draft PR are in_progress and are the PM's to read.", "open_questions": [ { "question": "Merge order against PR #9635 and PR #9646, both still open. Three of their lines are falsified or contradicted by this fix, and none of them exists on main, so none could be corrected in this diff without either duplicating another card's work or conflicting with its hunk: (a) #9635's field JSDoc ends '...and DbJobAdapter.replay, which writes its synthetic trigger: replay row regardless of this flag' — false once this lands; (b) #9646's class JSDoc carries 'The one row it does not govern is DbJobAdapter.replay's synthetic trigger: replay row, written either way' — must come out; (c) #9646's fifth test case asserts expect(runs.map(r => r.trigger)).toEqual(['replay']) under recordRuns: false — superseded by cases 1 and 4 here. Note (c) makes the coupling MECHANICAL rather than a note someone must remember: that assertion and mine are contradictory, so whichever lands second turns CI red, and both describe blocks append to the same file so a conflict likely fires first. That is exactly what #9646's author designed the case for.", "options": [ "A — merge #9635 and #9646 first, then rebase this PR and resolve all three points inside it (one diff owns the corrected wording, and the contradictory test case is deleted in the same rebase that makes it wrong)", "B — merge this PR first, then fold the two JSDoc corrections and the test-case replacement into #9635 and #9646 before they land (touches two PRs instead of one, and both would sit red until amended)", "C — hold this PR until the two land, deciding nothing now" ], "recommendation": "A, because it keeps the correction in one diff and in the same hand: the two open PRs stay exactly as reviewed, and the rebase makes the falsified sentences and the superseded test case fail in front of the person fixing them rather than being remembered later. B has both sibling PRs going red the moment this merges, which converts a scheduling choice into two unplanned patch rounds. C costs nothing but decides nothing — A is C plus the ordering written down." } ], "out_of_scope_findings": [ "filed as #9673 (label `finding`, no `pm:queue`, unassigned): `packages/spec/src/contracts/job-service.ts` documents `IJobService.replay` as recording 'in the execution audit trail' — it calls sys_job_run the audit trail, which is precisely the conflation this card's ruling refused, and the sentence also now reads unconditional where the behaviour is conditional. Not fixed in place: condition 4 of the bounded in-place exemption fails, since editing packages/spec pulls in the spec gate families this diff does not otherwise touch." ] }
Generated by Claude Code
Generated by Claude Code
os-project-manager commented
on Aug 18, 2026 CollaboratorAuthorMore actionsSecond round — couplings resolved against merged
main, re-verified at the new head.{ "issue": 9633, "status": "done", "branch": "claude/issue-9633-replay-honours-recordruns", "pr": "https://github.com/objectstack-ai/objectstack/pull/9675", "premise_still_valid": true, "summary": "Option A executed. Both blockers confirmed on main (#9635 at c07d6e8b9, #9646 at 73010f180) and all three points resolved inside PR #9675 at merged head e3fed3e4d. (1) Field JSDoc: the replay-exception clause is gone, and NOT as a mechanical delete — removing it left 'Two things are unaffected either way — the counters' counting wrong, so the sentence is rewritten to state that false writes no rows at all, replay included, with the sys_job counters as the one exception. (2) Class JSDoc: the 'one row it does not govern' sentence is gone; the bullet keeps its single {@link DbJobAdapterOptions.recordRuns} pointer rather than paraphrasing the flag a second time, per the #9631 constraint, so the change is a four-word amendment ('...which writes none of them, {@link DbJobAdapter.replay}'s synthetic trigger: replay row included'). (3) #9646's fifth case is deleted, with a note left in its place so a reader of that block learns where it went; its four siblings are kept since they pin the wrap() path. ONE DEVIATION, FLAGGED: I MERGED rather than rebased. Rebasing an already-pushed branch requires a force-push, which dev ground rule 4 forbids unconditionally; the standing contract outranks the dispatch prompt on unconditional clauses, so I took the route that reaches the same place — current with main, all three points in one diff — without forcing history. See the open question. Test-file conflict was add/add (both describe blocks append at the end); both blocks survive.", "tests": "ALL RE-MEASURED at merged head e3fed3e4d, working tree clean, new merge-base 73010f180. Tree identity: the merge commit was amended for its MESSAGE only — git rev-parse HEAD^{tree} is 94c88ea20 both before and after — so every number below was measured on this exact tree. SUITE: pnpm --filter @objectstack/service-job test -> 'Test Files 8 passed (8) / Tests 84 passed (84)' = 75 base + 4 surviving from #9646 + 5 mine. TYPECHECK: tsc --noEmit clean. ABLATIONS RE-RUN, not carried over, because the suite changed underneath the branch. Ablation A (revert the fix: insert ungated, guards dropped) predicted at the merged head: my 3 flag-off cases red, everything else green INCLUDING #9646's four survivors since none touches replay(). Observed exactly: 'Tests 3 failed | 81 passed (84)', the three being 'x recordRuns: false - a replay runs the handler and writes NO sys_job_run row', '...the terminal-status arm writes nothing either, and leaves no dangling row', '...the catch arm writes nothing and still rethrows', all 'AssertionError: expected [ { ...(10) } ] to have a length of +0 but got 1'. SHARPER THAN THE FIRST ROUND'S MEASUREMENT: all 79 pre-existing stayed green under ablation A — #9646's four recordRuns cases included — so even the block written specifically to pin this flag does not cover the replay gate, because it pins the wrap() path. Ablation B (insert gated, the three finishRun arms disabled) — last round I predicted pre-existing would stay green and that was WRONG (it produced MORE diagnostics, not fewer); re-predicted this round with that correction carried forward and it held exactly: 'Tests 5 failed | 79 passed (84)', my 2 flag-on cases plus 3 pre-existing degraded-outcome/timeout cases, 'AssertionError: expected running to be success'. Both ablations applied to the committed state, restored via git checkout of the branch path, grep -c ABLATION-9633 = 0 after, porcelain empty, suite back to 84/84. Source-level vitest, not dogfood, so no rebuild is needed for an ablation to bite. EMITTED ARTIFACT re-checked in BOTH dist/index.d.ts and dist/index.d.cts: removed clauses 'regardless of this flag' = 0, 'does not govern' = 0, 'Two things are unaffected' = 0, stale 'Soft cap' = 0; kept/new 'row per attempt' = 1, 'The one thing unaffected' = 1, 'row included' = 1, new replay() contract = 1. GATE UNION RE-DERIVED off the NEW git merge-base (73010f180) with node scripts/pm/dispatch-gates.mjs — same 8 path-matched + 5 convention-triggered; main gained a 108th family which does not match these paths. All 13 green: check:nul-bytes (6187 files), check:changeset-gate-self-tests, check:objectui-changeset, check:test-source-alias (72 pkgs), check:type-source-resolution (76 pkgs), check-adr-0087-registration (no declared-breaking changeset), check-changeset-no-major (merge base 73010f180), check-empty-changeset (1 declaring changeset), docs-audit/check-affected-docs (242 self-test cases), check:query-options-erasure (baseline verified against 73010f1, none new), check:engine-double-contract (319 pinned, no new double), check:where-matcher (255 matchers, none new), check:type-check-coverage (64/77), check:type-check-debt --re-measure (33 entries re-measured in 244.1s, none above its number — real measurement, preceded by turbo build 70/70 successful, not a refusal). Every heavy step under flock /tmp/os-heavy-verify.lock; one 99 queue-timeout was waited out actively in-turn (holder was PID 14435, a service-datasource build with a live node child, progressing) and the interval spent on the lock-free gates. PR left as a DRAFT; I did not flip it.", "open_questions": [ { "question": "You said rebase; I merged. Rebasing an already-pushed branch requires a force-push, and dev ground rule 4 forbids force-push unconditionally — the standing contract says it outranks the dispatch prompt on unconditional clauses and that I should name the conflict rather than silently pick a side. So PR #9675 now carries merge commit e3fed3e4d instead of replayed commits. Content is identical either way; only the branch's shape differs.", "options": [ "A — accept the merge commit as-is; take linear history at merge time instead, via GitHub's Squash-and-merge or Rebase-and-merge button, which needs no force-push from a dev seat", "B — you or a maintainer rebase and force-push the branch, if a linear branch history before merge is actually required", "C — amend ground rule 4 to permit --force-with-lease on a dev seat's own claude/issue-* branch, which would make future rebase instructions executable as written" ], "recommendation": "A — the squash/rebase button gets linear history into main without any seat force-pushing, so the rule and the intent do not actually conflict at the point that matters. C is worth considering separately if rebase instructions are going to be routine: as written, ground rule 4 makes every 'rebase your branch' dispatch unexecutable by a dev seat, and this is the second-order cost of a rule that is otherwise correct (it exists to stop seats clobbering shared history). I would not change it just for this card." } ], "out_of_scope_findings": [ "filed as #9673 (label `finding`, no `pm:queue`, unassigned) in the first round and still standing: `packages/spec/src/contracts/job-service.ts` documents `IJobService.replay` as recording 'in the execution audit trail' — it calls sys_job_run the audit trail, precisely the conflation this card's ruling refused, and the sentence now reads unconditional where the behaviour is conditional. Not fixed in place: editing packages/spec would widen this diff's gate surface for a wording question." ] }
Generated by Claude Code
Generated by Claude Code
os-project-manager commented
on Aug 18, 2026 CollaboratorAuthorMore actionsLanded and verified on
main— PR #9675 merged as52182a69cat 15:58:32Z. This closes the three-card chain #9611 → #9631 → #9633.Probed the merged tree. Every one of the ordering points the rebase had to resolve is a separate anti-control, which is the only way to tell "resolved" from "merged without conflict":
probe expected observed 'regardless of this flag'— #9611's field-JSDoc replay clause0 0✅'does not govern'— #9631's class-JSDoc replay clause0 0✅'Two things are unaffected'— the sentence that counted wrong once the clause left0 0✅'row included'— the corrected replacement1 1✅toEqual(['replay'])indb-job-adapter.test.ts— #9631's fifth case0 0✅The third row is the one I care about most. Removing a named clause and stopping there would have left
Two things are unaffected either waycounting wrong — a sentence that is true about nothing, which reads as correct and survives review. It was rewritten rather than trimmed.What the chain measured, in one line each
- Two published
.d.tsJSDoc comments inpackages/services/*describe behaviour their code does not have —MemoryCacheAdapter's "LRU-style eviction" is insertion-order, andDbJobAdapterOptions.recordRunsis documented as a numeric cap defaulting to none but is a boolean defaulting to true #9611 — two published.d.tscomments described behaviour their code did not have. Ablation: a real LRU spliced in turned the three new discriminators red while both pre-existing eviction tests stayed green — they pass under FIFO and LRU alike, which is how a contradicting comment sat green. - The
DbJobAdapterclass JSDoc still says "every execution writes asys_job_runrow" — the third published.d.tscomment in the same file thatrecordRuns: falsecontradicts #9631 — the class JSDoc promised a rowrecordRuns: falsenever writes. All 75 pre-existing tests stayed green under the ablation that removed the gate entirely. DbJobAdapter.replay()writes its syntheticsys_job_runrow regardless ofrecordRuns— an operator who switched run history off still accumulates replay rows #9633 — the flag was honoured at one of its two call sites. Re-measured at the rebased head: all 79 stayed green — including TheDbJobAdapterclass JSDoc still says "every execution writes asys_job_runrow" — the third published.d.tscomment in the same file thatrecordRuns: falsecontradicts #9631's four survivingrecordRunscases. The test block written specifically to pin this flag does not cover the replay gate, because it pins thewrap()path.
⭐ That last number is the chain's real finding. The flag had a dedicated pin, and the defect still sat underneath it. Three cards' worth of careful documentation work on one file, and what actually made the behaviour enforceable was a test that discriminates the right call site — not more prose about the flag.
The ablation-B misprediction is worth keeping too: it produced more red than predicted, tracing to
replay()'s settlement being pinned by #5548/#7734 while its insert gate was pinned by nothing. Two halves of one method under opposite enforcement is a fair account of how a flag comes to be honoured at one call site out of two.Still open from this chain: #9673 —
packages/spec/src/contracts/job-service.tscallssys_job_run"the execution audit trail", the conflation this card's ruling refused, and now reads unconditional where the behaviour is conditional. Filed, unassigned.
Generated by Claude Code
- Two published
- added 4 commits that reference this issue
on Aug 23, 2026 - added a commit that references this issue
on Oct 7, 2026
Found while fixing #9611 (measured, not inferred — the corrected
recordRunsJSDoc had to name this exception in order to be true). ⛔ Filed rather than absorbed: #9611 is comment-only by ruling and this is a behavioural question needing a disposition.The inconsistency
packages/services/service-job/src/db-job-adapter.tshas exactly two callers ofstartRun, and only one consults the flag:replay()then always callsfinishRun(runId, ...)on that id, on all three arms (terminal status read off the inner execution, success, and the catch). So withrecordRuns: false:replay()writes one completesys_job_runrow withtrigger: 'replay'(probably not intended).The result is a
sys_job_runtable that an operator believes is switched off and which nonetheless fills, slowly and exclusively, with replay rows — the least representative sample of the job's history, and one with no non-replay rows beside it for context.Why the flag reads as intended to cover it
recordRunsis documented (after #9611) as the on/off switch for run history, andlistExecutionsByStatusreadssys_job_runfor every trigger kind without distinguishing them. Nothing in the field's contract carves replay out; the carve-out is an artifact ofreplay()being written to solve a different problem — #5548's synthetic row exists to force thetrigger: 'replay'tag, and #7734 widened its terminal-status handling — and therecordRunsgate landing only on thewrappath.Dispositions
replay()honours the flag — wrap bothstartRunand thefinishRuncalls the same waywrapdoes. Consistent with the field's documented meaning; costs the replay tag when history is off, which is already the case for every other execution..d.tsJSDoc comments inpackages/services/*describe behaviour their code does not have —MemoryCacheAdapter's "LRU-style eviction" is insertion-order, andDbJobAdapterOptions.recordRunsis documented as a numeric cap defaulting to none but is a boolean defaulting to true #9611 states it as observed behaviour, not as a designed guarantee) rather than being an emergent property of two call sites.Recommendation: (1), on the contract-first reading — one flag, one meaning, no second de-facto rule at a call site. But it is a real product call about what an operator is asking for when they disable run history, so it wants a decision rather than a guess.
Note there is no test coverage of
recordRunsin either direction today (recorded in #9631), so whichever disposition is chosen needs its own pin.Refs: #9611 · #9631 · #5548 · #7734
Generated by Claude Code