Skip to content

spec: optional "ran, but the work did not happen" report on JobHandler (#6617) - #7050

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-6617-jobhandler-degraded-outcome
Aug 9, 2026
Merged

spec: optional "ran, but the work did not happen" report on JobHandler (#6617)#7050
os-zhuang merged 3 commits into
mainfrom
claude/issue-6617-jobhandler-degraded-outcome

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#6617

The spec half of the maintainer's B-minimal ruling on #5548, split contract-first: packages/spec is the spec seat's lane, and the services half (#5548 — the DbJobAdapter mapping, the sys_job_run.status enum, bumpJob) is not implemented here.

The problem

JobHandler had exactly two states — it threw, or it did not — so a run that completed without accomplishing anything was recorded as success, indistinguishable from one that did the work. The motivating case is #5529's wait-wake handler: when its store is unavailable it fires the shot at nothing, returns normally, and sys_job_run says the wake succeeded.

The change

exportinterfaceJobRunOutcome{outcome: 'completed'|'degraded';reason?: string;}exporttypeJobHandler=(context: {jobId: string;data?: unknown})=>Promise<void|JobRunOutcome>;

Three states, of which the third is optional: throw means failed (and retries), resolve nothing means success exactly as today, resolve a degraded outcome means the run finished but its work did not happen.

⚠️degraded is not a failure and does not trigger a retry. Failure and retry remain driven exclusively by a rejected promise, so a resolved outcome never re-runs the job and never surfaces as an error. That separation is the point of the ruling — option A (make these handlers throw) was rejected precisely because it would change failure semantics that third-party IJobService implementations already build retry behaviour on.

Why the return-value shape

The API shape was delegated to the spec seat and ruled as a return-value union rather than a ctx.reportOutcome callback: the handler context is an inline anonymous object that each IJobService implementation constructs itself, so a context callback would force every third-party implementation to grow a new member — violating the ruling's additivity clause on the implementer side. A return-value union is additive on both sides.

The ruling, verbatim and untranslated:

B-minimal 采纳:给 handler 一个可选、可加的「跑完但没干成」回报方式;sys_job_run 得以表达第三态。

硬约束(裁决的可加性条款):existing Promise handler 逐字节不动、语义不变;不回报 = 现状(没抛错 ⇒ success)。

The issue's falsification clause required grepping for a contrary outcome-reporting precedent on a sibling handler contract, and stopping if one existed. None exists — report(Outcome|Progress|Result|Status|Degraded) has zero hits across packages/. The two nearest precedents both point the same way as the ruling:

  • packages/spec/src/automation/node-executor.zod.ts:346 — the sibling automation handler contract reports its out-of-band result by returning it: "an executor suspends by RETURNING suspend: true from execute()".
  • packages/spec/src/contracts/metadata-service.ts:292 — degraded reporting in the same contracts/ directory, also by return value: Promise< { data; degraded: boolean; errors } >.

Naming precedent for the vocabulary is integration/connector-descriptor.ts:104 (degradedReason); the ruling's own { outcome, reason } field names are kept, since the object is entirely about the outcome and self-describes.

Additivity — the acceptance criterion, pinned in both directions

PinEvidence
Existing Promise< void > handlers unchangedThe pre-change type is declared standalone in the test and asserted assignable to the new JobHandler
A degraded-reporting handler is type-legalCompile-time pin; the one that must go red under a narrowed union
Existing implementations unchangedAn adapter that ignores the resolved value is pinned to behave identically
Public surfaceapi-surface delta is exactly + JobRunOutcome (interface)0 breaking, 1 added
Downstream consumerspnpm --workspace-concurrency=2 --filter '...@objectstack/spec' typecheck — prefix direction, i.e. spec plus its dependents (74 of 78 projects), all green. Zero-change compilation of every existing consumer is the empirical additivity proof (#6218).

sys_job_run's status enum is untouched; extending it is #5548's half.

One consumer did need widening — reported rather than papered over

The consumption-radius sweep turned up a genuine exception to the "no consumer changes" story, and it is worth stating plainly: runWithPolicy in @objectstack/service-job typed its run as () => Promise< void >, which rejects a handler that may resolve an outcome. TypeScript's return-type void special case does not reach through Promise< void >, so the cron and interval adapters failed to build:

src/interval-job-adapter.ts(157,46): error TS2322:
Type 'Promise< void | JobRunOutcome >' is not assignable to type 'Promise< void >'.
src/cron-job-adapter.ts(158,46): error TS2322: (same)

Fixed at the wrapper rather than the call sites: runWithPolicy / withTimeout are now generic with T = void, so every existing caller still infers void and behaviour is unchanged — what changed is that the retry wrapper no longer erases what the run resolved to. Erasing it at the call sites instead would have put a value-discarding shim at exactly the seam #5548 needs to read, which is the consumer-side accommodation Prime Directive #12 rules out. Retry semantics are untouched, and there is a test pinning that a degraded report survives the wrapper and does not retry.

Reverse verification

Predicted before running: narrowing the union back to Promise< void > should turn the degraded pins red while every legacy pin stays green — the asymmetry is what distinguishes additive widening from replacement.

The first attempt aimed at the wrong program and is worth recording: tsc --noEmit reads packages/spec/tsconfig.json, which excludes **/*.test.ts, so it reported no errors under the narrowed union — the pins live in the test layer, which is checked by check:test-typecheck against the sibling tsconfig.test.json. Re-aimed at that gate, the prediction held exactly:

--- narrowed to Promise< void > ---
• src/contracts/job-service.test.ts: 9 type error(s) in a file the ledger does not cover.
--- restored ---
check:test-typecheck: OK

job-service.test.ts is not in test-typecheck-debt.json, so it must carry zero type errors — which is what makes these pins live rather than phantom.

Verification

67 gates enumerated one by one from .github/workflows/lint.yml (both jobs), plus check:adr-0087-registration --base origin/main (exit 0 — an additive minor needs no disposition). All green. check:generated reports every artifact current; the only regenerated artifact is the api-surface shard above.


Generated by Claude Code

JobHandler had two states — threw or did not — so a run that completed
without accomplishing its work was recorded as success. Widen the return
type to Promise<void | JobRunOutcome> so a handler can OPTIONALLY report
"ran to completion, work did not happen".
Additive on both sides: existing Promise<void> handlers are unchanged
byte for byte, and existing IJobService implementations are unchanged
because this widens a return type rather than adding a context member.
degraded is not a failure and does not retry — retry stays throw-driven.
Spec half of #5548's B-minimal ruling; the DbJobAdapter/sys_job_run
mapping is #5548's half and is deliberately not wired here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
The widened JobHandler may resolve a JobRunOutcome, and a wrapper typed
`() => Promise<void>` rejects that — TypeScript's return-type void
special case does not reach through Promise<void>, so both the cron and
interval adapters failed to build.
runWithPolicy/withTimeout are now generic with T = void: every existing
caller still infers void and behaviour is unchanged, but the wrapper no
longer erases what the run resolved to. Retry stays throw-driven, so a
resolved degraded report returns on the first attempt.
Also commits the regenerated api-surface shard (+ JobRunOutcome, the
only delta: 0 breaking, 1 added).
The sys_job_run mapping that CONSUMES the outcome remains #5548's half.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
)
Prime Directive #4 — @objectstack/spec subpaths, matching the sibling
adapters; JobHandler/JobRunOutcome are not on the package root.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
@vercel

vercelBot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 9, 2026 11:50am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-job, @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/service-job, @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/permissions/system-context.mdx(via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/service-job, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/apps.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/service-job, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 9, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 9, 2026 12:13
@os-zhuang
os-zhuang added this pull request to the merge queueAug 9, 2026
Merged via the queue into main with commit e18a162Aug 9, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6617-jobhandler-degraded-outcome branch August 9, 2026 12:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-zhuang@claude