Skip to content

feat!: v8 audit remediation — robustness, unthrown, DX, and family consistency - #357

Merged
btravers merged 15 commits into
mainfrom
chore/v8-audit-remediation
Jul 31, 2026
Merged

feat!: v8 audit remediation — robustness, unthrown, DX, and family consistency#357
btravers merged 15 commits into
mainfrom
chore/v8-audit-remediation

Conversation

@btravers

Copy link
Copy Markdown
Collaborator

Second full-surface pass hardening robustness, the unthrown integration, developer experience, and btravstack-family consistency before the 8.0 major stabilises. Driven by a five-track audit (unthrown integration, API/DX, robustness, docs, cross-library consistency).

Verification: typecheck (12 tasks), lint, unit tests (all packages), build (10), publint + attw esm-only (14), knip, and the VitePress docs build all pass. Integration is green serially (worker 21, client 19, testing 3, sample-worker 8); the parallel test:integration can fail on Docker resource contention — run turbo run test:integration --concurrency=1.

Critical fix

  • Shared-activity clobber: an activity referenced from several contract scopes and implemented differently in each silently ran only one implementation. It now throws at declaration time unless the implementations are the same function reference or hoisted to the global activities map.

unthrown integration

  • qualifyFailure(type, { expected })expected is now required (error class / array / predicate / literal "any"). Unexpected causes ride the defect channel instead of being mislabelled a business error; a matched inner nonRetryable is inherited.
  • First-class client errors replace defect leaks: WorkflowCancelledError / WorkflowTerminatedError / WorkflowTimeoutError and UpdateFailedError / UpdateRejectedError / QueryFailedError.
  • Imperative assertNoDefect thunks → AsyncResult combinator chains; OkAsync/ErrAsync and @unthrown/vitest matchers adopted throughout.

Robustness

  • Typed-error wire marker (details[1] = { $tc: 1 }) closes a rehydration false positive for data-less errors; onRehydrationMiss diagnostic hook on degrade-to-generic.
  • rethrowCancellation helper + docs for the swallowed-cancellation hazard when an activity declares an errors map.
  • Async query/update schemas rejected at bind time; TypedWorker.create verifies workflow registration by default (verifyWorkflowRegistration: false to opt out); continueAsNew can't override its validated target; reserved-name and ms-duration validation at defineContract.
  • runActivityHandler (boundary-faithful activity testing) + a replay-determinism test.

DX & family consistency

  • Load-bearing worker types now exported (WorkflowContext, child-handle types, ActivityImplementationFor, …).
  • context.defineSignal/defineQuery/defineUpdatehandleSignal/handleQuery/handleUpdate.
  • Infer* helper naming and defineActivity({ activityOptions }) align with amqp-contract; P-composable tag bundles + tagPatterns collapse repetitive match arms.
  • ESM-only everywhere (CJS dropped from client/worker); @temporalio/* peers tightened to ^1.16.0; internals moved to a private /internal subpath.

Docs

  • Fixed the systemic non-compiling isErr()-then-.value samples across every page, plus behavioral bugs (heartbeat() never throws, parentClosePolicy default is TERMINATE, the tutorial's unbounded-retry hang, the bogus continueAsNew retry option).
  • Every page reflects the new API; new reference/testing-surface.md; typedoc entry points for worker.ts and contract errors.ts; a complete migration section added to the v8 upgrade guide.

A changeset (all four packages, major) documents the full set. All changes are breaking and intended for the 8.0 major.

btravers added 10 commits July 30, 2026 22:47
…/ErrAsync

Family-consistency audit against amqp-contract/unthrown conventions:

- New TypedWorker class with a static create(options) factory (the org's
  Typed*.create() shape, sibling of TypedClient.create) returning
  AsyncResult<TypedWorker, never>; infrastructure failures stay
  TechnicalError-caused defects. run() returns AsyncResult<void, never>
  (never-rejecting, mid-run crash = defect), shutdown() delegates, and the
  raw Temporal Worker is exposed via .raw (runUntil/getState). The free
  createWorker is removed (beta window).
- createContractTest's worker fixture now exposes the TypedWorker and drops
  the unhandled-rejection guard (run()'s promise never rejects).
- Pre-lifted async results are built with OkAsync(v)/ErrAsync(e) (canonical
  since unthrown 4.1) instead of Ok(v).toAsync()/Err(e).toAsync() across
  source, specs, TSDoc, docs, examples, and agent rules.
- Docs/README/upgrade-guide sweep + changeset (fixed group, major).
…e contract boundary

- SignalNamesOf/QueryNamesOf/UpdateNamesOf/DeclaredErrorsOf renamed to the Infer* prefix family
- defineActivity defaultOptions renamed to activityOptions
- typed-error wire envelope marker (details[1]); data-less rehydration now requires the marker
- onRehydrationMiss diagnostic hook for degrade-to-generic rehydration
- strict ms-grammar duration validation at defineContract time
- Temporal-reserved name rejection (__temporal_* prefix, __stack_trace, __enhanced_stack_trace)
- activity-only contracts allowed (workflows may be empty when global activities exist)
- exported error tag constants (CONTRACT_ERROR_TAG, TECHNICAL_ERROR_TAG)
…close the qualify defect hole

- shared activity implemented twice with different functions now throws at declaration time
- context.defineSignal/defineQuery/defineUpdate renamed to handleSignal/handleQuery/handleUpdate
- export WorkflowContext, DeclareWorkflowOptions, child handle types, ActivityImplementationFor
- qualifyFailure requires an expected-cause discriminator; unexpected causes ride the defect
  channel; inner nonRetryable inherited by default; expected: "any" is the explicit escape hatch
- rethrowCancellation helper; swallowed-cancellation warnings on cancellation error classes
- async query/update schemas rejected at bind time with ContractMisuseError
- continueAsNew can no longer override workflowType/taskQueue via options spread
- ChildWorkflowError carries workflowName; validation errors carry a direction field
- worker error tag constants; TypedWorker.create verifies workflow registration by default
…combinator refactor

- WorkflowCancelledError/WorkflowTerminatedError/WorkflowTimeoutError classified from result paths
- UpdateFailedError/UpdateRejectedError/QueryFailedError replace defect-channel leaks
- error tag constants + P-composable tag bundles (tagPatterns helper)
- handle.raw escape hatch; ContractClient/TypedScheduleClient constructors privatized
- ContractClient exposes contract and taskQueue; DATETIME rejects invalid Date
- interceptors can patch only input/signalInput, never identity fields
- imperative assertNoDefect thunks replaced with AsyncResult combinator chains
- schedule update() validates args against the contract when workflowType is declared
…, replay and skew coverage

- createContractTest({ contract, ... }) and runActivity(definition, options) per family convention
- runActivityHandler routes through the real declareActivitiesHandler wrapping and wire round-trip
- @unthrown/vitest wired in; raw boolean asserts migrated to matchers across worker/testing specs
- replay-history spec proves the Result machinery replays deterministically
- e2e specs: contract-skew degrade + onRehydrationMiss, marker false-positive regression,
  activityOptionsByName taskQueue routing (Docker)
- testcontainers moved to an optional peer dependency with a lazy import and install-hint error
… off public subpaths

- client/worker drop CJS output and legacy main/module/types fields; attw esm-only on all packages
- @temporalio/* peer ranges tightened to ^1.16.0 (schedule API + search-attribute imports)
- _internal_* symbols move to @temporal-contract/contract/internal; /result-async subpath removed
- @standard-schema/spec regular-dep exception documented in dependencies rule
…owing, match folds

- client matchers use exported tag constants and tagPatterns bundles
- ContractError discrimination via the { errorName } object pattern
- workflow error handling via match({ ok, errCases, defect }) with rethrow-on-defect
- fix README link to the examples overview
Copilot AI review requested due to automatic review settings July 31, 2026 14:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is a broad v8 “audit remediation” pass across the Temporal contract ecosystem, tightening correctness and robustness around unthrown integration, typed error wire format + rehydration, workflow/worker safety checks, and aligning naming/API shapes across the btravstack family as the 8.0 major stabilizes.

Changes:

  • Strengthens typed error handling: introduces a wire marker for contract errors, adds rehydration diagnostics, and adds _tag constant exports/bundles for matcher ergonomics.
  • Improves worker/runtime robustness: adds bind-time schema synchronicity probing for query/update-input schemas, adds workflow registration branding, and hardens activity option merging / continue-as-new routing invariants.
  • DX + packaging alignment: ESM-only outputs, catalog-managed doc dependencies, optional testcontainers in testing, and widespread docs/tests updates to new APIs (TypedWorker, OkAsync/ErrAsync, handle*).

Reviewed changes

Copilot reviewed 131 out of 132 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
turbo.json Adds lint/format tasks + dependencies.
pnpm-workspace.yaml Adds mermaid/vitepress deps to catalog.
pnpm-lock.yaml Lockfile updates for catalog + dev dep moves.
packages/worker/typedoc.json Adds worker entrypoint to typedoc.
packages/worker/src/workflow-proxy.spec.ts Renames defaultOptions→activityOptions in tests.
packages/worker/src/workflow-errors.spec.ts Updates typed error wire marker expectations.
packages/worker/src/workflow-brand.ts New workflow branding helper for registration checks.
packages/worker/src/wire-format.spec.ts Switches to @unthrown/vitest matchers.
packages/worker/src/types.ts Updates docs/comments for handle* renames.
packages/worker/src/internal.ts Internal import path moves + option merge / child error shaping.
packages/worker/src/handlers.ts Adds sync-schema probing for query/update binding.
packages/worker/src/errors.spec.ts Adds coverage for tags, structured fields, cancellation rethrow helper.
packages/worker/src/error-tags.ts New worker error-tag constants module.
packages/worker/src/contract-errors.ts Adds wire marker slotting to ApplicationFailure details.
packages/worker/src/continue-as-new.spec.ts Tests validated target wins over smuggled overrides.
packages/worker/src/child-workflow.ts Uses InferSignalNames + carries workflowName structured field.
packages/worker/src/child-workflow.spec.ts Verifies structured workflowName on ChildWorkflowError.
packages/worker/src/activities-proxy.ts Moves rehydration helper import to contract/internal.
packages/worker/src/activities-proxy.spec.ts Matcher updates + typed tag assertions.
packages/worker/src/tests/worker.spec.ts Migrates to TypedWorker + OkAsync/ErrAsync patterns.
packages/worker/src/tests/time-skipping.inprocess.spec.ts Migrates to TypedWorker + adds cancellation outcome test.
packages/worker/src/tests/test.workflows.ts Updates to handleSignal/handleQuery/handleUpdate.
packages/worker/src/tests/routing.workflows.ts Adds routed activityOptionsByName workflow for routing spec.
packages/worker/src/tests/routing.spec.ts Adds end-to-end routing integration spec.
packages/worker/src/tests/routing.contract.ts Contract for routing spec (activityOptions + routing queues).
packages/worker/src/tests/replay.inprocess.spec.ts Adds replay determinism coverage for unthrown machinery.
packages/worker/src/tests/rehydration.workflows.ts Adds workflows for rehydration audit gaps.
packages/worker/src/tests/rehydration.inprocess.spec.ts Adds in-process rehydration audit-gap integration tests.
packages/worker/src/tests/rehydration.contract.ts Defines skewed worker/client contracts for rehydration tests.
packages/worker/src/tests/registration.contract.ts Contract for workflow registration check tests.
packages/worker/src/tests/registration-raw.workflows.ts Adds “raw workflow function” scenario.
packages/worker/src/tests/registration-missing.workflows.ts Adds “missing exported workflow” scenario.
packages/worker/src/tests/registration-mismatch.workflows.ts Adds “export name mismatch” scenario.
packages/worker/src/tests/registration-complete.workflows.ts Adds “registration complete” happy path scenario.
packages/worker/src/tests/inprocess.workflows.ts Adds waitForever workflow for cancellation outcome coverage.
packages/worker/src/tests/inprocess.contract.ts Adds waitForever workflow + activityOptions rename.
packages/worker/README.md Updates docs to TypedWorker + run().get() semantics.
packages/worker/package.json Drops CJS exports; tightens @temporalio peer ranges; esm-only attw.
packages/testing/vitest.config.ts Adds vitest setupFiles; updates alias to contract/internal.
packages/testing/tsconfig.json Updates TS path alias to contract/internal.
packages/testing/src/vitest.setup.ts Registers @unthrown/vitest matchers globally.
packages/testing/src/time-skipping.ts Updates examples to TypedWorker + raw.runUntil.
packages/testing/src/global-setup.ts Lazily imports optional testcontainers with helpful error.
packages/testing/src/contract.ts createContractTest moves to options bag + TypedWorker fixture.
packages/testing/src/tests/test.contract.ts Updates activityOptions rename.
packages/testing/src/tests/contract-test.spec.ts Updates OkAsync + matcher usage + options bag API.
packages/testing/package.json Makes testcontainers optional peer; adds @unthrown/vitest.
packages/contract/typedoc.json Adds errors.ts to typedoc entrypoints.
packages/contract/src/types.ts Renames type helpers; defaultOptions→activityOptions.
packages/contract/src/types-inference.spec.ts Updates to new Infer* helper names + adds activity-only contract test.
packages/contract/src/internal.ts Establishes contract/internal entry + re-exports internal error helpers.
packages/contract/src/internal.spec.ts Updates internal helper import path.
packages/contract/src/index.ts Re-exports tag constants + renamed helper types.
packages/contract/src/errors.spec.ts Adds marker + onRehydrationMiss coverage + tag constant checks.
packages/contract/src/error-tags.ts New contract tag constants module.
packages/contract/package.json Replaces /result-async with /internal subpath.
packages/client/src/types.ts Widens query/update error unions with new first-class errors.
packages/client/src/types-inference.spec.ts Pins new unions; ensures clients aren’t publicly constructible.
packages/client/src/schedule.spec.ts Hardens schedule update semantics + validation coverage.
packages/client/src/interceptors.ts Restricts interceptor patches to payload keys only.
packages/client/src/index.ts Re-exports new errors + tag bundles + TagPatterns type.
packages/client/src/error-tags.ts Adds client tag constants + grouped tag bundles.
packages/client/src/tests/client.spec.ts Uses tag bundles + tagPatterns helper in matcher arms.
packages/client/package.json Drops CJS output + sets attw esm-only profile + tightens peers.
knip.json Updates Knip schema version + adds worker workflow entrypoints.
examples/order-processing-worker/src/application/worker.ts Migrates to TypedWorker + defect handling + run().get().
examples/order-processing-worker/src/application/activities.ts Requires qualifyFailure.expected + OkAsync/ErrAsync usage.
examples/order-processing-worker/README.md Updates docs links (examples overview).
docs/tutorial/your-first-workflow.md Updates TypedWorker, retry cap, tag bundles, qualifyFailure.expected.
docs/tutorial/adding-signals-and-queries.md Updates handle* APIs + sync-schema rules explanation.
docs/reference/glossary.md Clarifies qualification as “expected-only” mapping.
docs/reference/errors.md Updates TypedWorker.create naming and tables.
docs/reference/contract-surface.md Updates activityOptions rename + reserved names/durations rules + marker docs.
docs/package.json Moves docs deps to catalog specifiers.
docs/index.md Updates qualifyFailure.expected + tag bundle matching.
docs/how-to/use-signals-queries-and-updates.md Updates contract example + handle* + getOrThrow guidance.
docs/how-to/tune-activity-options.md Updates defaultOptions→activityOptions + duration validation docs.
docs/how-to/troubleshoot.md Updates TypedWorker naming + extractor guidance + qualifyFailure.expected docs.
docs/how-to/schedule-workflows.md Documents schedule client reachability + update validation + extractor usage.
docs/how-to/run-child-workflows.md Clarifies child signal arg behavior + parentClosePolicy default.
docs/how-to/model-domain-errors.md Updates qualifyFailure.expected + CONTRACT_ERROR_TAG + marker explanation.
docs/how-to/migrate-from-neverthrow.md Corrects historical neverthrow removal + updates unthrown idioms.
docs/how-to/intercept-client-calls.md Documents patch-key restrictions + correct tap/tapFailure patterns.
docs/how-to/install.md Tightens peer floors + documents optional peers.
docs/how-to/index-workflows-with-search-attributes.md Fixes Result narrowing (Ok vs Err vs Defect).
docs/how-to/implement-activities.md Requires qualifyFailure.expected + updates ErrAsync/OkAsync guidance.
docs/how-to/handle-cancellation.md Adds rethrowCancellation guidance + corrects heartbeat behavior.
docs/how-to/define-a-contract.md Updates collision semantics + activityOptions validation + activity-only contracts.
docs/how-to/continue-as-new.md Fixes cursor advancement + continueAsNew option semantics.
docs/how-to/add-activity-middleware.md Updates ErrAsync usage + qualifyFailure.expected examples.
docs/explanation/workflow-determinism.md Updates handleSignal naming in examples.
docs/explanation/validation-boundaries.md Updates contract shape rules + reserved names + activityOptions.
docs/explanation/the-result-model.md Updates TypedWorker naming.
docs/explanation/nexus.md Requires qualifyFailure.expected in example.
docs/explanation/architecture.md Updates TypedWorker naming in examples.
docs/examples/index.md Corrects integration test command.
docs/api/index.md Adds testing surface ref + clarifies entrypoint-based docs.
docs/.vitepress/config.ts Adds Testing surface to sidebar.
AGENTS.md Updates agent rules to prefer OkAsync/ErrAsync and TypedWorker wording.
.changeset/v8-audit-remediation.md Major changeset documenting full audit remediation surface.
.changeset/family-consistency-typed-worker.md Major changeset documenting TypedWorker migration + idiom sweep.
.agents/rules/handlers.md Updates worker/setup and qualifyFailure.expected guidance.
.agents/rules/dependencies.md Tightens peer floors + documents optional peers and spec exception.
.agents/rules/contract-patterns.md Adds verb-tier naming policy + options-bag signature rule.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment thread packages/worker/src/handlers.ts
Comment thread packages/worker/src/workflow-brand.ts
Benoit Travers and others added 2 commits July 31, 2026 18:25
Addresses both review comments on #357.

`bindQueryHandler` / `bindUpdateHandler`'s per-call sync guards used
`instanceof Promise` while the bind-time probe used `isThenable`. Standard
Schema types the async branch as `Promise<Result>`, but an implementation
may hand back any `PromiseLike` — that slipped past `instanceof Promise`,
and the helper then read `.issues` (undefined, i.e. "valid") and `.value`
(undefined) straight off the thenable, handing the handler an unvalidated
`undefined` instead of tripping `ContractMisuseError`. Both call sites now
share an `isAsyncValidation` helper that matches the probe's check and
detaches the thenable's settlement.

`_internal_declaredWorkflowName` read a symbol property off an arbitrary
module export, so a `Proxy` with a throwing `get` trap (or a throwing
getter) aborted the whole workflow-registration check with an unrelated
exception. The read is now guarded — anything that fails to yield a string
brand is simply "not a declared workflow".

Also fixes the red Integration Tests job: turbo ran the four
`test:integration` tasks in parallel, each spinning up its own Temporal +
Postgres testcontainer pair, and the contention pushed the client suite's
fixture setup past Vitest's 10s `hookTimeout` default. Serialize the task
(the workaround already documented in the PR description) and give the
integration projects a `hookTimeout` sized for fixture setup that opens
Temporal connections and builds a workflow bundle per worker.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The reusable CI workflow already invokes the root script with
`--concurrency 3`, so hard-coding `--concurrency=1` inside it produced
`turbo run … --concurrency=1 --concurrency 3`, which turbo rejects with
"the argument '--concurrency <CONCURRENCY>' cannot be used multiple times".

The `hookTimeout` bump on the integration projects is the fix that actually
addresses the original failure (a 10s hook timeout during fixture setup);
serializing the task stays available as the documented local workaround.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@btravers btravers left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff, with focus on the worker/client runtime surface. The
architecture holds up well — the triage philosophy behind qualifyFailure's
required expected, the shared-activity clobber guard, continueAsNew's
validated-target-wins ordering, the interceptor patch-key restriction, and the
outcome-error classification all read as sound and are properly tested.

Findings below, plus notes on what I pushed for the two Copilot comments and
the red Integration job.

Pushed in this branch

Both Copilot comments are addressed (351b56b).

The instanceof PromiseisThenable one is worth calling out as a real
correctness bug, not just an inconsistency. Standard Schema types the async
branch as Promise<Result>, but an implementation may legally return any
PromiseLike. Such a value slipped past instanceof Promise, and the code
then read .issues off the thenable (undefined → "no issues, valid") and
.value (undefined) — handing the handler an unvalidated undefined
instead of tripping ContractMisuseError. All four per-call guards now share
an isAsyncValidation helper that matches the bind-time probe's check and
detaches the thenable's settlement for the same reason the probe does.

The regression test was verified to fail against the pre-fix code before being
kept. _internal_declaredWorkflowName's guarded read has its own new
workflow-brand.spec.ts covering the throwing-getter and throwing-Proxy cases.

Integration Tests was failing on Hook timed out in 10000ms in the client
suite's fixture setup, which opens three Temporal connections and
webpack-bundles the workflows per worker — Vitest's 10s hookTimeout default
was never sized for that, and CI runs three integration tasks concurrently,
each with its own Temporal + Postgres containers. Fixed with
hookTimeout: 60_000 on the integration projects; the test body is still
bounded by testTimeout.

(For the record: I first also hard-coded --concurrency=1 into the root
test:integration script, which broke CI — the reusable workflow already
passes --concurrency 3 and turbo rejects a duplicate flag. Reverted in
c970308; serializing stays available as the manual workaround this PR's
description documents.)

Worth addressing before merge

packages/client/src/schedule.ts:338update loses conflict safety

To get validation in before anything persists, the wrapper now calls
describe() itself and hands handle.update() a precomputed object:

handle.update(() => updated)

Temporal's ConditionFailed retry loop re-invokes that callback — but it now
returns the same options, built from a description that may since have gone
stale. So a concurrent modification landing between describe and update is
silently overwritten, where the SDK's own contract (re-run updateFn against a
fresh description) is precisely what makes concurrent updates safe.

The JSDoc states the mechanism ("the same computed options are retried") but
not the consequence. Either say last-writer-wins outright so callers can plan
for it, or re-describe → re-run updateFn → re-validate on conflict.

Minor

  • packages/contract/src/builder.tsassertDuration ordering. The guard
    is !MS_DURATION_PATTERN.test(value) || value.length > 100. The regex is
    linear so this isn't exploitable, but the cheap length cap belongs first.

  • Wire-marker rolling upgrades. A data-less contract error emitted by a
    pre-8.0 worker carries no marker, so an 8.0 client won't rehydrate it and it
    degrades to a generic failure. Correct behavior for a major, and
    onRehydrationMiss makes the window observable — but the upgrade guide should
    state the deploy ordering explicitly, since the degrade is silent to anyone
    who hasn't registered the hook.

  • ActivityDefinition.activityOptions is still typed ActivityDefaultOptions
    (packages/contract/src/types.ts:102). The property was renamed as part of the
    defaultOptionsactivityOptions sweep; the type name wasn't.

  • The integration fixtures are per-test. 19 tests each build two workers, so
    the suite pays the bundle cost ~38 times. The hookTimeout bump treats the
    symptom; file-scoped fixtures would cut wall clock several-fold and remove the
    sensitivity to runner contention entirely. Good follow-up — too invasive to
    fold into this PR.

Comment thread examples/order-processing-worker/src/application/activities.ts Outdated
…schedule update concurrency

Review remediation on the v8 audit branch.

- `ActivityDefaultOptions` -> `ContractActivityOptions`. The property was
  renamed to `activityOptions` in the earlier sweep but the type name was
  missed; the new name also keeps the contract-level, portable subset
  distinct from Temporal's own `ActivityOptions`, which is what the
  worker-side `activityOptionsByName` overrides take.

- `assertDuration` checks the cheap length cap before running the regex.
  The pattern is linear so this was never exploitable, just the wrong order.

- `TypedScheduleHandle.update`'s JSDoc claimed Temporal retries `updateFn`
  on a server-side conflict. It does not: in SDK 1.20.3 `ScheduleHandle.update`
  describes once, calls `updateFn` once, and `_updateSchedule` sends no
  conflict token, so `UpdateSchedule` is unconditional. Concurrent updates are
  last-writer-wins in the raw SDK too. Documented plainly, including the one
  thing the wrapper does change (an extra `describe`, slightly widening the
  window) and the one guarantee it keeps (validated options == persisted
  options).
The example used `expected: Error` at every `qualifyFailure` site, which
matches essentially every throw — the pre-v8 catch-all spelled differently.
It was accurate (the domain only threw plain `Error`s) but it neutralized the
feature the flagship example is meant to teach, and a reader landing on one of
the bare sites had nothing to go on.

Adds `domain/errors.ts` with one class per port under a shared
`OrderProcessingError` base, and throws those from the use cases and the
payment adapter. Each activity now names the failure it actually anticipates
(`expected: ShippingError`, `expected: PaymentError`, ...), so the triage is
visible: a provider fault is a modeled activity failure, a `TypeError` from a
bug stays a defect and re-throws at the edge with its original stack.
- The v8 upgrade guide now has a rolling-upgrade warning for the typed-error
  wire marker. A data-less contract error emitted by a 7.x worker carries no
  marker, so an 8.0 reader degrades it to a generic failure — silently, unless
  `onRehydrationMiss` is registered. Documents workers-before-callers ordering,
  the hook, and that schema-bearing errors are unaffected. Added to the
  checklist.
- `ActivityDefaultOptions` -> `ContractActivityOptions` in the rename table,
  the checklist, the contract-surface reference, and the changeset.
@btravers

Copy link
Copy Markdown
Collaborator Author

Review-body findings — resolved

Follow-up to the four items from the self-review that weren't inline comments. Pushed in 6785391 (packages) and 8e6ad46 (docs).

schedule.tsupdate conflict safety

The finding doesn't hold as written, so I documented rather than restructured. There is no ConditionFailed retry loop to preserve. In @temporalio/client 1.20.3, ScheduleHandle.update describes once, calls updateFn once, and updates once — the "updateFn might be invoked multiple times" note in its JSDoc is a forward-looking caveat, not current behavior. More decisively, _updateSchedule sends no conflict token (it is only ever returned from _createSchedule), so UpdateSchedule is unconditional: concurrent updates are last-writer-wins in the raw SDK too.

So the wrapper doesn't lose safety the SDK never provided. The residual real issue is narrower — the extra describe widens an already-existing window, and the JSDoc asserted retry semantics that don't exist. Rewritten to state last-writer-wins outright, name the one thing the wrapper changes (the extra describe) and the one guarantee it keeps (validated options == persisted options), and tell callers to serialize if two writers can race.

Implementing a re-describe → re-run → re-validate loop was the other option, but it would have to be built on a conflict signal the server interaction doesn't currently expose, and it would break the validated-equals-persisted guarantee for an impure updateFn.

assertDuration ordering

Swapped — cheap length cap first, regex second.

Wire-marker rolling upgrades

Added an explicit warning block to the v8 upgrade guide: workers before callers, drain in-flight executions before cutting callers over, and register onRehydrationMiss to make the window observable. It states plainly that the degrade is silent by default and that schema-bearing errors are unaffected (they rehydrate on validation, marker or not). Added to the checklist.

ActivityDefaultOptions

Renamed to ContractActivityOptions, rather than ActivityOptions — that name is already Temporal's own, and it is exactly what the worker-side activityOptionsByName overrides take, so reusing it for the contract-level portable subset would collide at the point of maximum confusion. Rename table, checklist, contract-surface reference, and changeset updated.

Integration fixtures

Left alone, per your "good follow-up — too invasive to fold into this PR."


All 12 checks green on 8e6ad46, Integration Tests included. Docker wasn't available in my environment, so the example's integration suite was verified by CI rather than locally; its spec asserts nothing on the failure types the domain-error change touches.

@btravers
btravers merged commit d5ddb13 into main Jul 31, 2026
12 checks passed
btravers pushed a commit that referenced this pull request Jul 31, 2026
Addresses both review comments on #357.

`bindQueryHandler` / `bindUpdateHandler`'s per-call sync guards used
`instanceof Promise` while the bind-time probe used `isThenable`. Standard
Schema types the async branch as `Promise<Result>`, but an implementation
may hand back any `PromiseLike` — that slipped past `instanceof Promise`,
and the helper then read `.issues` (undefined, i.e. "valid") and `.value`
(undefined) straight off the thenable, handing the handler an unvalidated
`undefined` instead of tripping `ContractMisuseError`. Both call sites now
share an `isAsyncValidation` helper that matches the probe's check and
detaches the thenable's settlement.

`_internal_declaredWorkflowName` read a symbol property off an arbitrary
module export, so a `Proxy` with a throwing `get` trap (or a throwing
getter) aborted the whole workflow-registration check with an unrelated
exception. The read is now guarded — anything that fails to yield a string
brand is simply "not a declared workflow".

Also fixes the red Integration Tests job: turbo ran the four
`test:integration` tasks in parallel, each spinning up its own Temporal +
Postgres testcontainer pair, and the contention pushed the client suite's
fixture setup past Vitest's 10s `hookTimeout` default. Serialize the task
(the workaround already documented in the PR description) and give the
integration projects a `hookTimeout` sized for fixture setup that opens
Temporal connections and builds a workflow bundle per worker.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@btravers
btravers deleted the chore/v8-audit-remediation branch July 31, 2026 22:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants