Skip to content

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

@os-project-manager

Found while fixing #9611 (measured, not inferred — the corrected recordRuns JSDoc 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.ts has exactly two callers of startRun, and only one consults the flag:

// wrap(), onAttemptStart — gatedcurrent={id: this.recordRuns ? awaitthis.startRun(name,defaultTrigger,attempt) : undefined,settled: false};// replay() — ungatedconstrunId=awaitthis.startRun(name,'replay');

replay() then always calls finishRun(runId, ...) on that id, on all three arms (terminal status read off the inner execution, success, and the catch). So with recordRuns: false:

  • every scheduled and triggered execution writes nothing (intended), and
  • every replay() writes one complete sys_job_run row with trigger: 'replay' (probably not intended).

The result is a sys_job_run table 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

recordRuns is documented (after #9611) as the on/off switch for run history, and listExecutionsByStatus reads sys_job_run for every trigger kind without distinguishing them. Nothing in the field's contract carves replay out; the carve-out is an artifact of replay() being written to solve a different problem — #5548's synthetic row exists to force the trigger: 'replay' tag, and #7734 widened its terminal-status handling — and the recordRuns gate landing only on the wrap path.

Dispositions

  1. replay() honours the flag — wrap both startRun and the finishRun calls the same way wrap does. Consistent with the field's documented meaning; costs the replay tag when history is off, which is already the case for every other execution.
  2. Deliberate exception, documented — an explicit operator-initiated replay is auditable in a way a scheduled tick is not, so it records even when routine history does not. If this is the intent it belongs in the field's JSDoc (which after Two published .d.ts JSDoc comments in packages/services/* describe behaviour their code does not have — MemoryCacheAdapter's "LRU-style eviction" is insertion-order, and DbJobAdapterOptions.recordRuns is 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 recordRuns in either direction today (recorded in #9631), so whichever disposition is chosen needs its own pin.

Refs: #9611 · #9631 · #5548 · #7734


Generated by Claude Code

Metadata

Metadata

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions