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

export interface JobRunOutcome {
  outcome: 'completed' | 'degraded';
  reason?: string;
}

export type JobHandler =
  (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

Pin Evidence
Existing Promise< void > handlers unchanged The pre-change type is declared standalone in the test and asserted assignable to the new JobHandler
A degraded-reporting handler is type-legal Compile-time pin; the one that must go red under a narrowed union
Existing implementations unchanged An adapter that ignores the resolved value is pinned to behave identically
Public surface api-surface delta is exactly + JobRunOutcome (interface)0 breaking, 1 added
Downstream consumers pnpm --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

claude added 3 commits August 9, 2026 09:24
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

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 9, 2026 11:50am

Request Review

@github-actions github-actions Bot added the size/m label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

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-actions github-actions Bot 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 queue Aug 9, 2026
Merged via the queue into main with commit e18a162 Aug 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants