Skip to content

[Experiment] Integrate scheduler readability stack with server main - #64

Draft
chaptersix wants to merge 140 commits into
sch-readable-execution-phasesfrom
experiment/scheduler-integration-base
Draft

[Experiment] Integrate scheduler readability stack with server main#64
chaptersix wants to merge 140 commits into
sch-readable-execution-phasesfrom
experiment/scheduler-integration-base

Conversation

@chaptersix

@chaptersix chaptersix commented Sep 5, 2026

Copy link
Copy Markdown
Owner

The scheduler experiment was validated on a merge of the readability stack (#40) and server main at ad7b229. This PR exposes that existing integration baseline so the readability and experimental scheduler layers form one GitHub stack.

The merge commit is 95d50ed. Its diff includes the intervening main changes and the existing scheduler conflict resolutions. No branch history is rewritten by this stack repair.

Dependency order: #40 → this baseline → #59#60#61#62. All layers remain draft and unmerged.

tsurdilo and others added 30 commits August 18, 2026 20:56
# What changed?

`tdbg` now registers the **archival** task category (`5`) in its
task-category registry (`tools/tdbg/app.go`), so `tdbg dlq
list/merge/purge --dlq-type 5` works. Previously `merge`/`purge`
rejected it with `unknown dlq category 5`. Because the `--dlq-type` help
text is generated from the registry, archival now appears there too.
Adds a regression test to `TestDLQCommand_V2`.

## Why?

`tdbg` built its registry with `tasks.NewDefaultTaskCategoryRegistry()`,
which omits `CategoryArchival`. The server registers archival
conditionally in `TaskCategoryRegistryProvider` (`temporal/fx.go`) when
archival is enabled, but `tdbg` is a client tool with no access to the
cluster's archival config, so it rejected `--dlq-type 5` client-side —
even though archival tasks are written to the history task DLQ and the
server's `AdminService.MergeDLQTasks` accepts the category. That left
archival DLQ tasks unrecoverable via the standard tool (operators had to
call the RPC directly). Registering the category unconditionally in
`tdbg` is safe: with archival disabled there is simply no archival DLQ
to operate on.

Fixes temporalio#11586.

## How did you test it?
- [x] built
- [ ] run locally and tested manually
- [ ] covered by existing tests
- [x] added new unit test(s)
- [ ] added new functional test(s)

---------

Signed-off-by: Tihomir Surdilovic <tihomir@temporal.io>
Co-authored-by: Prathyush PV <prathyush.pv@temporal.io>
Add nil check for `request.History` before accessing
`request.History.Data` in `serializeAppendRawHistoryNodesRequest` to
prevent a panic when the caller passes a nil DataBlob.

Co-authored-by: Prathyush PV <prathyush.pv@temporal.io>
…11177)

## Motivation

`TimerSequenceID` (~48 bytes) is heap-allocated and returned as a
`*TimerSequenceID` from 5 getter functions: `getUserTimerTimeout`,
`getActivityScheduleToStartTimeout`,
`getActivityScheduleToCloseTimeout`, `getActivityStartToCloseTimeout`,
and `getActivityHeartbeatTimeout`. These are called for every pending
timer/activity during `LoadAndSortUserTimers()` and
`LoadAndSortActivityTimers()` — a hot path in every workflow task.

Callers were already dereferencing the pointer before appending to the
value-type `[]TimerSequenceID` slice, so the existing code was
allocating on the heap only to immediately copy to the stack.

## Changes

- `TimerSequenceID` is now returned by value with a `(TimerSequenceID,
bool)` tuple
- Idiomatic Go pattern (same as map access) replaces nil-check sentinel
- All 5 getter methods updated + call sites and tests adapted

## Impact

Eliminates one heap allocation per getter call on the timer-sorting hot
path.

## Tests

- `service/history/workflow` (1075 tests): ✅ passed

---------

Co-authored-by: Prathyush PV <prathyush.pv@temporal.io>
## What changed?
Instrumented local frontend, CHASM callback, and external Nexus
operation HTTP clients with the shared OpenTelemetry transport.

Legacy HSM callbacks and cross-cluster forwarding are intentionally out
of scope.

## Why?
Outbound Nexus HTTP calls need to carry trace context so callbacks and
internal frontend calls remain connected to their originating spans.

## How did you test it?
- [x] built
- [ ] run locally and tested manually
- [ ] covered by existing tests
- [ ] added new unit test(s)
- [x] added new functional test(s)

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## What changed?

- Adds `phase=error` to the existing `replication_lifecycle` wide event;
no new event type or table.
- Covers state-based replication (`SyncVersionedTransition`,
`VerifyVersionedTransition`, and `SyncWorkflowState`) plus standby
transfer, timer, and outbound queue failures.
- Captures sender, passive execution/apply, verification,
recovery/refetch, namespace refresh, Nack, DLQ, and history-branch
cleanup boundaries.
- Uses one shared error builder with small sender, executable-task, NDC,
and standby-queue adapters.
- Records workflow identity, source task identity, target context,
operation, error, attempt/priority, disposition/recovery, and extensible
diagnostics in `details`.
- Identifies apply provenance as `apply_artifact_source=task_payload` or
`sync_state_refetch`.
- Remains gated by `history.emitReplicationLifecycleEvents` (default
off).

## Why?

Replication failures span the sender, passive executor, recovery loop,
and apply layer. Recording these boundaries in the existing lifecycle
event makes the path of a workflow or replication task directly
traceable without adding another event schema.

## Example traces

The examples below are abridged records captured from the two-cluster
XDC test. Events for the same task correlate on `source_cluster`,
`source_shard`, and `source_task_id`; workflow identity is present on
every record.

A state task that fails on the passive cluster and is written to the
DLQ:

```json
{"phase":"sent","task_type":"sync_versioned_transition","workflow_id":"example-workflow","source_cluster":"active","source_shard":1,"source_task_id":1048581,"priority":"High"}
{"phase":"error","task_type":"sync_versioned_transition","workflow_id":"example-workflow","source_cluster":"active","source_shard":1,"source_task_id":1048581,"details":{"operation":"passive_task_execution","error":"failed to apply replication task","error_type":"serviceerror.InvalidArgument","apply_artifact_source":"task_payload","attempt":1,"priority":"High","target_cluster":"standby"}}
{"phase":"error","task_type":"sync_versioned_transition","workflow_id":"example-workflow","source_cluster":"active","source_shard":1,"source_task_id":1048581,"details":{"operation":"task_execution","error":"failed to apply replication task","terminal":true,"priority":"High","target_cluster":"standby"}}
{"phase":"error","task_type":"sync_versioned_transition","workflow_id":"example-workflow","source_cluster":"active","source_shard":1,"source_task_id":1048581,"details":{"operation":"dlq_write","disposition":"dlq","terminal":true,"priority":"High","target_cluster":"standby","target_shard":1}}
```

A verification task that detects missing state, refetches it, and then
verifies successfully:

```json
{"phase":"sent","task_type":"verify_versioned_transition","workflow_id":"example-workflow","source_cluster":"active","source_shard":1,"source_task_id":1048595,"priority":"High"}
{"phase":"executing","task_type":"verify_versioned_transition","workflow_id":"example-workflow","source_cluster":"active","source_shard":1,"source_task_id":1048595,"attempt":1}
{"phase":"applied","task_type":"verify_versioned_transition","workflow_id":"example-workflow","source_cluster":"active","source_shard":1,"source_task_id":1048595,"outcome":"resend_needed"}
{"phase":"error","task_type":"verify_versioned_transition","workflow_id":"example-workflow","source_cluster":"active","source_shard":1,"source_task_id":1048595,"details":{"operation":"standby_verification","error":"missing mutable state, resend","error_type":"serviceerror.SyncState","recovery_action":"sync_state","priority":"High","target_cluster":"standby"}}
{"phase":"applied","task_type":"sync_versioned_transition","workflow_id":"example-workflow","source_cluster":"active","source_shard":1,"source_task_id":1048595,"outcome":"applied","details":{"apply_artifact_source":"sync_state_refetch"}}
{"phase":"executing","task_type":"verify_versioned_transition","workflow_id":"example-workflow","source_cluster":"active","source_shard":1,"source_task_id":1048595,"attempt":1}
{"phase":"applied","task_type":"verify_versioned_transition","workflow_id":"example-workflow","source_cluster":"active","source_shard":1,"source_task_id":1048595,"outcome":"verified"}
```

## How was it tested?

- `go test -tags test_dep ./common/wideevents
./service/history/replication ./service/history/ndc ./service/history
./tests/testcore`
- Changed-lines `golangci-lint`: 0 issues.
- Temporary, uncommitted two-cluster XDC tests forced passive task
failures, DLQ handling, standby verification, and SyncState
refetch/recovery with lifecycle events both enabled and disabled.
- A tiered-processing run confirmed concrete `High` priority on sent and
error records.

## Risks

- Enabling the dynamic config increases event volume; retries and
recovery can produce several error phases for one source task.
- `details.operation`, `details.disposition`, and
`details.recovery_action` distinguish those boundaries.
- Emission is best effort and does not change replication error
propagation, retry, or recovery behavior.
…e status (temporalio#11636)

## What

Adds a `--schedule-id` flag to `tdbg schedule migrate status`. When set,
it looks up one specific schedule instead of the namespace-wide V1-vs-V2
counts, and reports whether that schedule is currently V1
(workflow-backed), V2 (CHASM), or caught in a migration sentinel state.

## Why

During schedule migration triage, an operator often needs to know the
status of *one specific* schedule, not aggregate counts. There was no
way to answer that without manually poking `execution describe` and
reasoning about CHASM node internals by hand.

## How it works

It always probes **both** sides — the V1 workflow ID
(`temporal-sys-scheduler:<id>`) and the V2/CHASM business ID (`<id>`) —
via the same `DescribeMutableState` RPC that `tdbg execution describe`
uses, regardless of which ID form was passed in. It deliberately does
not short-circuit based on the input's shape (e.g. its prefix) without
confirming against the server — an unconfirmed inference is
confusing/untrustworthy in a diagnostic tool.

This lets it flag both kinds of migration sentinel:
- a **CHASM-side sentinel** `Scheduler` component reserving the ID
during a V1→V2 migration
- a **V1-side `DummyWorkflow`** reserving the workflow ID during a V2→V1
rollback

Output leads with a plain-language headline describing the schedule's
current, authoritative form (written for someone with no prior knowledge
of the V1/V2 migration internals), followed by a note about any sentinel
found on the other side, a details table of both sides' raw status, and
the exact `execution describe` invocations to inspect each side further.

### Example output (V2→V1 rollback in flight)

```
Schedule "foo" is a V2 (CHASM) schedule.
Additionally, a placeholder ("sentinel") V1 workflow exists at workflow ID "temporal-sys-scheduler:foo",
reserving that ID while a V2→V1 rollback is in progress. This is expected during rollback and requires no action.

Details:
  V1 (workflow-backed)  [workflow ID temporal-sys-scheduler:foo]: sentinel (V2→V1 rollback placeholder)
  V2 (CHASM)            [business ID foo]: genuine

Inspect further:
  V1: tdbg execution describe --workflow-id temporal-sys-scheduler:foo -n <namespace>
  V2: tdbg execution describe --workflow-id foo --archetype scheduler.scheduler -n <namespace>
```

## Testing

- `go test ./tools/tdbg/... -run TestScheduleStatus -v` — new tests
cover: prefixed input, genuine V1, genuine V2, V1 genuine + CHASM
sentinel, V1 sentinel + genuine V2, not-found on both sides, and an
unexpected/inconsistent-state fallback. Existing aggregate-count tests
are unaffected.
- `go build ./tools/...`, `go vet ./tools/tdbg/...`
- Manually checked `tdbg schedule migrate status --help` output.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
)

## What changed?

- Removed blanket timestamp wrapping
- cron/retry/start child wf use virtual time directly

## Why?
there was a double-shifting for CaN but not for other cases
it shall be fixed unifying the clock used by all virtual time
propagating cases, and this PR chooses to unify in a way that all cases
propagate use virtual time

## How did you test it?
- [x] built
- [ ] run locally and tested manually
- [x] covered by existing tests
- [x] added new unit test(s)
- [x] added new functional test(s)
## What changed?
Wrapped the frontend Nexus dispatch routes with the shared OpenTelemetry
HTTP handler.

## Why?
Nexus HTTP requests need an inbound server span to connect the caller
trace.

## How did you test it?
- [x] built
- [ ] run locally and tested manually
- [ ] covered by existing tests
- [x] added new unit test(s)
- [ ] added new functional test(s)
## What changed?

1. perf improvement: no time-skipping task regen on full refresh
2. new functional test: claim Nexus HSM timers are part of in-fight
nexus operation, and won’t be skipped, add functional test
3. new functional test: add a functional test to verify time skipping
won't change retention time
4. trivial bug fix: time-skipping: task regen didn’t read
EnableWorkflowExecutionTimeoutTimer

## Why?
- correctness related No. 4 though it is an edgy case
- perf related No.1 
- test coverage No.2,3

## How did you test it?
- [x] built
- [ ] run locally and tested manually
- [x] covered by existing tests
- [x] added new unit test(s)
- [x] added new functional test(s)
temporalio#11618)

## What
- Don't send scale down signal when task is matched from backlog even if
the poll wait time is high.
- Do not send scale up signal when task queue is rate limited.

## Why
- When a task comes from DB backlog, the poll wait time reflects DB read
path latency, not excess pollers — the -1 is not appropriate. Instead we
want to apply the normal scale up check.

- Similarly, when dispatch is bottlenecked by a task queue rate limit,
scaling up pollers won't help.

## How did you test it?
Unit tests

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## What changed?
Batch operations now target Paused executions in addition to Running
ones. The filter auto-appended by adjustQueryBatchTypeEnum changed from
ExecutionStatus='Running' to ExecutionStatus='Running' OR
ExecutionStatus='Paused', affecting all workflow batch types
(terminate/signal/cancel/update-options) and activity batch types
(unpause/update-options/reset/terminate/cancel).

## Why?
A paused execution is still non-terminal. Users issuing a batch
terminate/signal/cancel reasonably expect it to apply to paused targets,
but the old Running-only filter silently skipped them.

## How did you test it?
- [X] built
- [ ] run locally and tested manually
- [X] covered by existing tests
- [X] added new unit test(s)
- [X] added new functional test(s)

## Potential risks
- The filter now references ExecutionStatus='Paused' on every batch
operation. This is ok as it's an additive clause to the visibilty query
- This would be changing behavior for batch callers as paused activities
are now affected.
…eleasing workflow lock (temporalio#11602)

## What changed?
Captured `ExecutionState.Status` before calling `GetReleaseFn()(nil)` in
`updateWithStart.Invoke` to avoid a data race with concurrent goroutines
that may acquire the lock and modify `ExecutionState` after it is
released.

Added a regression test that confirms the race under `-race`.

Fixes temporalio#11600

## Why?
`Invoke` in `service/history/api/multioperation/api.go` releases the
workflow lock at line 196 via `workflowLease.GetReleaseFn()(nil)`, then
reads `workflowLease.GetMutableState().GetExecutionState().Status` at
line 201 — after the lock is released. Any concurrent goroutine waiting
on `Lock()` for the same workflow (e.g. a signal, terminate, or another
update) can acquire the lock and modify `ExecutionState` between lines
196 and 201, creating a data race.

This matches the pattern noted in the `Updater` struct itself:

> WARNING: any references to mutable state data *have to* be copied to
avoid data races when used outside the workflow lease.

## How did you test it?
- [x] added new unit test(s)

The test spawns a concurrent writer that modifies
`ExecutionState.Status` after the lock is released.

**Before fix:**
```
go test -race -tags test_dep -count=1 \
  -run TestUpdateWithStartSuite/TestInvoke_CompletedUpdate_StatusCapturedBeforeRelease \
  ./service/history/api/multioperation/

WARNING: DATA RACE
Read at ... api.go:201
--- FAIL
```

**After fix:**
```
ok  go.temporal.io/server/service/history/api/multioperation
```

## Potential risks
Minimal - single line moved before the release call. No API or
persistence behavior change.
## What changed?
1. Binds httpCaller after setting httpClient
2. Uses TransitionStarted.Possible for complete-before-start
3. Records request time for nexus operation cancel

## Why?
1. Binds httpCaller after setting httpClient
When clusterID isn't set, a non-nil httpCaller with a nil receiver gets
passed to `nexusrpc.NewHTTPClient`. Since the httpCaller is non-nil, it
will skip the check that sets the nil httpCaller to the default caller.

2. Uses TransitionStarted.Possible for complete-before-start
Matches HSM, for if a start response is lost, and a completion lands
while while the operation is in BACKING_OFF.

3. Records request time for nexus operation cancel
This just seems like it wasn't being recorded.

## How did you test it?
- [x] built
- [ ] run locally and tested manually
- [ ] covered by existing tests
- [x] added new unit test(s)
- [ ] added new functional test(s)
…oralio#11617)

## What changed?
Validate the retry delay is a valid proto duration.

## Why?
Prevent malformed retry delays from poisoning Nexus completions.

## How did you test it?
- [ ] built
- [ ] run locally and tested manually
- [ ] covered by existing tests
- [x] added new unit test(s)
- [x] added new functional test(s)

## Potential risks
Potentially users could have been sending an invalid proto duration,
unclear how exactly, and now we would fail their request.
…tion (temporalio#11373)

## What changed and why?

1. Edge case: If fast-forward completes during a workflow transaction
but when time skipping checks at close transaction this fast-forward
time is before ms.Now(), time skipping should still get disabled. Now we
don't look at time points that are in the past and it is a bug.

2. Time-skipping propagation: Always propagate the time-skipping
configuration and fast-forward state to the next run and other
executions, regardless of whether time skipping is currently active.
This ensures that read APIs (for example, Describe and PollFastForward)
continue to return meaningful information instead of nil.
- Otherwise, in an edge case where fast-forward completes in the first
run and the user polls after the second run has become the current run
of the workflow execution, the poll API would return a NotFound error
instead of a completed poll result.
- Similarly, the Describe API would return a nil configuration instead
of the original configuration that should have been propagated.

3. Trivial changes:
    - simiplify parameter of `propagateTimeSkippingToNextRun`
    - unify UT names of timeskipping_test.go
…1646)

## The bug

`applyUpdatesToSubStateMachine` shadowed the variable it meant to
populate:

```go
var existing V                                  // outer: never assigned
if existing, ok := pendingInfos[key]; ok {       // := declares a NEW existing, scoped to the if
    ...
    ms.approximateSize -= existing.Size() + getSizeOfKey(key)   // inner one — correct here
}
val := updated
if sanitizeFn != nil {
    val = common.CloneProto(updated)
    sanitizeFn(existing, val)                   // outer one — always nil
}
```

`:=` only requires *one* new variable on the left (`ok`), and the `if`
init statement is its own scope, so `existing` is redeclared there
rather than assigned. It compiles because both variables are used, and
`go vet` does not check shadowing by default.

`sanitizeFn` has therefore always received `current == nil`.

## What this activates

Two of the five `sanitizeFn` implementations depend on `current`; the
other three (timers, request cancels, signals) ignore it or are `nil`,
so they are unaffected.

**Activities.** `getActivityTimerTaskStatus` short-circuits to
`TimerTaskStatusNone` when `current` is nil, so every applied activity
update cleared the *entire* timer task mask rather than only the bits
whose deadline moved. The deadline comparison added in temporalio#11565 has never
taken effect — that PR is currently a no-op.

**Child executions.** `if current != nil { incoming.Clock =
current.Clock }` never ran. `Clock` is a local shard vector clock that
`sanitizeChildExecutionInfo` strips before replicating, so the incoming
copy always arrives nil — the guard exists precisely to restore the
local value. Without it, every replicated update to an existing child
execution erased it.

## Risk

Both are restorations of intended behavior, not new behavior, and both
previously failed in the safe direction:

- An over-cleared activity mask regenerates a timer task that is already
pending. Duplicates are dropped at execution
(`processSingleActivityTimeoutTask` re-derives the sequence and fires
what expired), so the symptom was extra timer-queue writes, not missed
timeouts.
- A nil child clock is tolerated by callers — see the comment in
`recordchildworkflowcompleted/api.go`: *"it should be fine e.g. that
ci.Clock is nil"*.

The child execution change is a **no-op unless the local cluster
recorded a clock by starting the child itself**, which for a passive
cluster means after a failover. On a standby that never started the
child, both sides are nil.

For activities, preserved bits suppress recreation of a timer task, so
it is worth being explicit about why that is safe on the passive side:
an expired-but-unresolved standby timer returns `ErrTaskRetry` and stays
in the queue rather than being consumed, so there is nothing to
recreate. The one path that does drop a task, past
`StandbyTaskMissingEventsDiscardDelay`, logs a warning and increments
`task_errors_discarded`, so it cannot happen silently.

## Tests

Two new tests at the `applyUpdatesToSubStateMachines` level, one per
activated `sanitizeFn`. **Both fail with the shadowing restored**
(verified: `expected: 13, actual: 0` for the mask; `local child
execution clock was not carried over` for the clock).

The gap they close: the existing `getActivityTimerTaskStatus` tests call
the decision function directly with a non-nil `current`, so no unit test
could observe the *caller* passing nil. Nine passing tests, zero
coverage of the wiring.

Two pre-existing tests needed updating, both informative:

- `TestApplyMutation` / `TestApplySnapshot` failed with *"Unexpected
call to IsVersionFromSameCluster"* — that mock was never needed because
the nil short-circuit made the cluster check unreachable. The gap is
itself evidence the path was dead.
- `verifyActivityInfos` asserted `s.Equal(int32(TimerTaskStatusNone),
actual.TimerTaskStatus)`, encoding the bug and blocking any correct fix.
Replaced with the invariant that actually matters:

  ```go
s.Zero(actual.TimerTaskStatus&^originStatus, "TimerTaskStatus gained a
bit that was not set locally")
  ```

Applying an update may carry over or drop locally set bits, but must
never introduce one that was not already set — claiming a timer task
exists when none does is the direction that loses timers. That holds
regardless of which bits a given case preserves, so it will not need
rewriting as the mask logic evolves.

## Related, tracked separately

While auditing whether the mask can be trusted, one path was found that
moves a deadline without invalidating the mask: unpausing an activity.
Paused activities are excluded from the timer sequence, so a timeout
task firing during a long pause is dropped as an invalid task, yet
`unpauseActivityInfo` leaves the bit set. That is pre-existing and
active-side, independent of this PR, and is being tracked as its own
change.

It is worth noting here because this PR removes the passive side's
accidental repair for that class of stale bit: today's blanket wipe
clears any stale bit on the next replicated activity update.

---

`go build ./service/...`, `go vet`, and golangci-lint (repo config,
`--new-from-rev=origin/main`) clean; `./service/history/workflow/...
./service/history/ndc/...` pass across 3 consecutive runs.

Two pre-existing flakes are skipped in those runs and unrelated to this
change (both reproduce on unmodified `main`):
`TestTaskRefresherSuite/TestRefreshSubStateMachineTasks`
(nanosecond-differing HSM deadlines, map iteration order — fails 7/12 on
main) and
`TestMutableStateSuite/*/TestApplyWorkflowExecutionOptionsUpdatedEvent_TimeSkippingConfig`
(wall-clock resolution, ~1/30).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
## Summary
- Generator execution clamps its processed time range to
`LastProcessedTime`, but `computeFutureActionTimes` (used by both
`Describe` and `UpdateFutureActionTimes`) only floored its starting time
at `max(now, UpdateTime)`, ignoring the watermark.
- This let Describe/`FutureActionTimes` advertise occurrences at or
before `LastProcessedTime` that the generator had already processed or
silently skipped.
- Floors the starting time at `LastProcessedTime` as well, so every
advertised future action is guaranteed to be strictly after the
generator's high water mark.
- Reuses `common/util.MaxTime` for this floor and for the existing
`UpdateTime`-vs-`LastProcessedTime` clamp in
`GeneratorTaskHandler.Execute`, instead of hand-rolled `if X.After(Y) {
Y = X }` checks.

## Test plan
- Added
`TestGeneratorTask_FutureActionTimesRespectLastProcessedTimeWatermark`
in `chasm/lib/scheduler/generator_tasks_test.go`, which pushes
`LastProcessedTime` ahead of "now" and asserts all advertised future
times are strictly after it. Verified it fails without the fix and
passes with it.
- Built on a new scheduler-specific `chasmtest` helper
(`newSchedulerTestEngine` + `updateScheduler`/`readScheduler` in
`helper_test.go`), adapted from PR 0 of the `sch-readable` stack, rather
than the `newTestEnv` rapid harness, so the watermark mutation and task
execution cross the same transaction/read boundaries as production. Left
out the parts of that PR not applicable here (frontend-client plumbing,
the generic side-effect-task firing helper), since
`GeneratorTaskHandler` is a pure task handler that doesn't touch the
frontend client.
- `go test -tags test_dep ./chasm/lib/scheduler/...` passes.
## What changed

Adds a few Nexus-specific log tags to the handler-side frontend logger.

## Why

Mainly for the request ID to debug Nexus calls across namespaces better.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## What changed

Adds logs tags for failures on the Nexus frontend path.

## Why

Have more details to correlate issues with requests.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## What changed?

- Added a separate `namespace_replication_lifecycle` wide event with
`created`, `received`, `processed`, and `dlqed` phases.
- Included namespace/task identity, source and target clusters, source
task ID, retry attempt count, deterministic task fingerprint, and the
serialized namespace replication task.
- Included the successful `CreateNamespaceRequest` or resolved
`UpdateNamespaceRequest` as `persistence_request` on `processed`;
duplicate, stale, and skipped tasks omit it.
- Passed receiver-side diagnostic metadata through a typed context so
the existing `TaskExecutor.Execute` and create/update handler signatures
remain unchanged.
- Added the dedicated, default-off
`system.emitNamespaceReplicationLifecycleEvents` dynamic-config gate,
checked explicitly at both the processor and processed-event emitter.
- Preserved the namespace replication queue message ID as
`source_task_id` when reading tasks.

## Why?

Namespace CRUD events describe user-visible namespace mutations, but do
not show whether the resulting namespace replication task was queued,
received, applied, retried, or sent to the DLQ. These events provide
that transport and processing audit trail without additional persistence
reads.

## How did you test it?

- [x] built
- [x] run locally and tested manually
- [x] covered by existing tests
- [x] added new unit test(s)
- [ ] added new functional test(s)

Commands:

```text
go test -tags test_dep ./common/wideevents ./common/namespace/nsreplication ./service/worker/replicator ./service/frontend
make GOLANGCI_LINT_FIX=false GOLANGCI_LINT_BASE_REV=origin/main lint-code
```

Local two-cluster testing covered create, update, and failover after
rebasing onto `origin/main`. Each task produced linked `created ->
received -> processed` events, and `processed` contained the expected
persistence request. With the dynamic-config flag off, namespace
replication still completed and neither cluster emitted a matching
lifecycle event.
### What changed
Sets gobreaker's OnStateChange hook on the outbound queue circuit
breaker pool, logging every transition.

### Why
Obtain more details for debugging curcuit breaker in production.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…oralio#10878)

## What changed?
A blocked `ChasmEngine.pollComponent` now returns a
`ShardOwnershipLostError` as soon as its shard moves off this host — by
adding a `select` case on the shard's lifecycle context — instead of
blocking until the request context deadline.

## Why?
Follow-up to temporalio#10860 (requested in review): `pollComponent` had the same
gap as the `GetWorkflowExecutionHistory` long poll — nothing in its
`select` was tied to shard lifecycle, so a poll in flight when its shard
moved stalled until timeout. This lets the caller redirect to the new
owner immediately.

## How did you test it?
- [x] built
- [ ] run locally and tested manually
- [x] covered by existing tests
- [x] added new unit test(s)
- [ ] added new functional test(s)
Go 1.27 prerequisite that upgrades golangci-lint, goimports, and
stringer to versions compatible with the new toolchain.
…ter closure (temporalio#11628)

## What changed?

Makes Standalone Activity conflict updates idempotent by recording the
`requestID` when attaching callbacks or links and recognizing duplicate
request IDs. I needed to add a dedicated CHASM error for

## Why?
This prevents a successful attachment whose response was lost from
failing on retry or duplicating/replacing callbacks and links.

## How did you test it?
- [ ] built
- [ ] run locally and tested manually
- [ ] covered by existing tests
- [x] added new unit test(s)
- [x] added new functional test(s)
Go 1.27 prerequisite that applies the `atomictypes` Go fixer to use
typed atomic values.
## What changed?
Adds `namespace_lifecycle` start and finish events for the namespace
handover, force replication, and catchup system workflows.

The events carry workflow identity and the core operation inputs.
Finished events classify the result as succeeded, canceled, or failed.
Force replication reports its cumulative verified workflow count and
emits only one start and one finish across a continue-as-new chain.

Emission uses one shared activity, the existing
`system.emitNamespaceLifecycleEvents` gate, disconnected cleanup for
cancellation, and workflow versioning for replay compatibility. Existing
shard handover events are unchanged.

## Why?
These system workflows currently have no consistent operation-level
event pair, which makes it difficult to correlate a namespace migration
request with its final outcome.

## How did you test it?
- [x] covered by existing tests
- [x] added new unit test(s)

`go test -tags test_dep ./common/wideevents ./service/worker/migration`

`make fmt-imports`

`make lint-code` reports no issues introduced by this change; the
repository-wide target still reports existing findings on current
`main`.

## Potential risks
The terminal event is best effort and cannot run after server-side
workflow termination or workflow run timeout because those outcomes do
not execute workflow cleanup.
## What changed?
- Clear an activity’s timer-task status when it is unpaused so timeout
tasks are regenerated.
- Make ResetActivity with keepPaused=false fully unpause both scheduled
and running activities, including clearing pause metadata.
- Add and strengthen unit and functional coverage for unpause,
reset-unpause, timer regeneration, and keepPaused=true.

## Why?
Timeout tasks can fire while an activity is paused and be discarded.
Previously, the activity’s timer-task status still indicated that those
tasks existed, preventing them from being recreated after unpause and
potentially making the timeout ineffective.
ResetActivity also bypassed normal unpause handling, and running
activities returned early without clearing their paused state.

## How did you test it?
- [X] built
- [ ] run locally and tested manually
- [X] covered by existing tests
- [X] added new unit test(s)
- [X] added new functional test(s)

## Potential risks
Unpausing now invalidates existing timeout tasks and recreates the next
applicable timer during transaction close. This is correct, but a
behavioral change. Stale queued tasks may still be processed and
discarded through the existing stamp validation.
## What changed?

- Emit `remote_cluster_lifecycle` wide events for successful and failed
`AddOrUpdateRemoteCluster` and `RemoveRemoteCluster` calls through both
Operator and Admin APIs.
- Capture the request, request fingerprint, caller/auth context when
available, remote response, persistence request, authoritative
pre-mutation persistence state for upserts, cached pre-mutation state
for removals, requested connection/replication transitions, mutation,
outcome, and terminal error details.
- Preserve the `NamespaceLifecyclePayload` envelope and place
remote-cluster-specific fields in `details`.
- Gate emission with the existing `system.emitNamespaceLifecycleEvents`
dynamic config.

## Why?

Remote-cluster connection, replication, and removal changes need an
auditable record that shows what was requested, what state existed
before the mutation, and whether the request succeeded or failed.

## How did you test it?

- [x] built
- [x] run locally and tested manually
- [x] added new unit test(s)
- [ ] added new functional test(s)

Commands:

```text
go test -tags test_dep ./service/frontend ./common/wideevents
make lint-code
```

Local E2E against `development-cluster-a.yaml` covered:

- Operator: create enabled, update disabled, remove.
- Admin: create disabled, update enabled, remove.
- Operator removal rejected while an intact global namespace referenced
both the local and remote clusters.

<details>
<summary>Successful Operator update: connection and replication
disabled</summary>

```json
{
  "details": {
    "api": "operator",
    "call_origin": "AddOrUpdateRemoteCluster",
    "caller_type": "api",
    "local_cluster": "cluster-a",
    "mutation": "updated",
    "outcome": "succeeded",
    "persisted_before": {
      "cluster_address": "127.0.0.1:9433",
      "cluster_id": "5234378e-93a3-42e7-9ef0-b0a68ec20a4f",
      "cluster_name": "cluster-remote-lifecycle-update-e2e",
      "failover_version_increment": 100,
      "history_shard_count": 32,
      "http_address": "127.0.0.1:9443",
      "index_search_attributes": null,
      "initial_failover_version": 21,
      "is_connection_enabled": true,
      "is_global_namespace_enabled": true,
      "is_replication_enabled": true,
      "tags": {
        "environment": "local-e2e",
        "purpose": "remote-cluster-lifecycle-update-wide-events"
      },
      "use_cluster_id_membership": false,
      "version": 1,
      "version_info": null
    },
    "persistence_request": {
      "cluster_metadata": {
        "cluster_address": "127.0.0.1:9433",
        "cluster_id": "5234378e-93a3-42e7-9ef0-b0a68ec20a4f",
        "cluster_name": "cluster-remote-lifecycle-update-e2e",
        "failover_version_increment": 100,
        "history_shard_count": 32,
        "http_address": "127.0.0.1:9443",
        "index_search_attributes": null,
        "initial_failover_version": 21,
        "is_connection_enabled": false,
        "is_global_namespace_enabled": true,
        "is_replication_enabled": false,
        "tags": {
          "environment": "local-e2e",
          "purpose": "remote-cluster-lifecycle-update-wide-events"
        },
        "use_cluster_id_membership": false,
        "version_info": null
      },
      "version": 1
    },
    "remote_cluster": "cluster-remote-lifecycle-update-e2e",
    "remote_cluster_id": "5234378e-93a3-42e7-9ef0-b0a68ec20a4f",
    "request": {
      "enable_remote_cluster_connection": false,
      "enable_replication": false,
      "frontend_address": "127.0.0.1:9433",
      "frontend_http_address": "127.0.0.1:9443"
    },
    "request_fingerprint": "e356fbe885b9f750f5e9a35ec01c29dfaf8f82f135bbfefd93304b50dcdbcb89",
    "requested_connection_transition": "disabled",
    "requested_replication_transition": "disabled"
  },
  "event_name": "remote_cluster_lifecycle",
  "instrumentation_scope": "go.temporal.io/server/common/wideevents",
  "namespace": "N/A",
  "namespace_id": "N/A",
  "phase": "remote_cluster_upsert"
}
```

</details>

<details>
<summary>Failed Operator removal: cluster still referenced by global
namespace</summary>

```json
{
  "details": {
    "api": "operator",
    "cached_before": {
      "cluster_id": "992aa482-40a4-46ad-a4ec-c15d853cb37c",
      "cluster_name": "cluster-guard-e2e",
      "http_address": "127.0.0.1:9543",
      "initial_failover_version": 31,
      "is_connection_enabled": true,
      "is_replication_enabled": true,
      "rpc_address": "127.0.0.1:9533",
      "shard_count": 32,
      "tags": {
        "environment": "local-e2e",
        "purpose": "cluster-removal-namespace-guard"
      }
    },
    "call_origin": "RemoveRemoteCluster",
    "caller_type": "api",
    "error": "cannot remove cluster \"cluster-guard-e2e\": still referenced by namespace \"cluster-guard-e2e-ns\"",
    "error_code": "FailedPrecondition",
    "error_type": "*serviceerror.FailedPrecondition",
    "local_cluster": "cluster-a",
    "mutation": "unknown",
    "outcome": "failed",
    "remote_cluster": "cluster-guard-e2e",
    "remote_cluster_id": "992aa482-40a4-46ad-a4ec-c15d853cb37c",
    "request": {
      "cluster_name": "cluster-guard-e2e"
    },
    "request_fingerprint": "9992050714897282663c1cc8a216ad99eff3aee746c24ac1c2a510a5aa9fd23e"
  },
  "event_name": "remote_cluster_lifecycle",
  "instrumentation_scope": "go.temporal.io/server/common/wideevents",
  "namespace": "N/A",
  "namespace_id": "N/A",
  "phase": "remote_cluster_remove"
}
```

</details>
## What changed?

Adds `testcontext.EnsureRemaining` and has `await` use it so long await
calls can request additional test-scoped context time while still
respecting the test context cap.

## Why?

Await calls can need more time than the default test context has left
(esp after the environment setup). Extending the test timeout in this
way allows for (1) stuck tests to fail earlier than the default test
timeout and (2) legitimately longer running tests to pass without
manually tweaking the test timeout.

---------

Co-authored-by: Sean Kane <sean.kane@temporal.io>
## Summary

- capture post-transaction mutable state for fresh `NotFound` snapshots
and `IsFirstSync` creation
- report successful verify history repairs as `outcome=backfilled`
- include the repaired event range and `new_run_id` in the verify
applied event
- add regression coverage for fresh zombie applies and
non-current-branch backfills

## Testing

- `go test ./service/history/ndc ./service/history/replication
./common/wideevents -count=1`
rkannan82 and others added 29 commits September 2, 2026 05:01
…io#11851)

## What

Stop retrying worker command dispatch when the failure is a poller
timeout (UpstreamTimeout). Transport errors (gRPC unreachable,
connection refused) are still retried.

## Why

When a worker is gone, each `DispatchNexusTask` attempt blocks a
goroutine for the full 10s dispatch timeout waiting for a poller that
will never arrive. With 3 max attempts, that's ~32s of blocked resources
per command.

At scale this adds up: if many workers go away simultaneously (e.g.,
deployment rollout), each dead worker can accumulate pending cancel
commands. With per-destination concurrency of 100, this means up to 100
goroutines × N dead workers × 10s per attempt × 3 attempts — thousands
of goroutines blocked on matching RPCs, causing memory pressure and
connection buildup.

Since worker commands are best-effort, there's no value in retrying
after the first timeout. If no poller appeared in 10s, the worker is
likely gone.

## How did you test it?

- Unit tests: updated `TestExecute_UpstreamTimeout` and
`TestHandleError_UpstreamTimeout_ReturnNil` to verify no retry on
timeout while transport errors still retry.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…o#11853)

## What changed?
Workflow start task refresh now uses the workflow start versioned
transition instead of the latest execution-state transition. This limits
workflow start task regeneration to replication ranges that include the
initial workflow transition.

## Why?
Later execution-state changes can advance LastUpdateVersionedTransition
while the workflow remains running. Partial task refresh then
incorrectly treats the workflow as newly started and creates another
WorkflowRunTimeoutTask.

## How did you test it?
- [ ] built
- [x] run locally and tested manually
- [ ] covered by existing tests
- [x] added new unit test(s)
- [x] added new functional test(s)

Ran the focused TaskRefresher unit test and the single-cluster passive
replication workflow and parallel-activity scenarios with exact
active/passive task comparison enabled.

## Potential risks
This relies on workflow start being transition count 1, matching
transition-history initialization. Full refresh still includes the
initial transition and continues regenerating required workflow start
tasks.
…#11854)

## What changed?
The task refresher no longer creates an UpsertExecutionVisibilityTask
for the initial visibility transition, which is already represented by
StartExecutionVisibilityTask. Internal workflow state changes now mark
visibility as updated only when the externally visible workflow status
also changes.

## Why?
Scheduling the first workflow task changes the internal execution state
from CREATED to RUNNING while its public status remains RUNNING. That
advanced VisibilityLastUpdateVersionedTransition without producing an
active visibility task, causing passive refresh to create a passive-only
visibility upsert.

## How did you test it?
- [ ] built
- [x] run locally and tested manually
- [ ] covered by existing tests
- [x] added new unit test(s)
- [x] added new functional test(s)

Ran focused mutable-state and TaskRefresher unit tests and the
single-cluster passive replication workflow and parallel-activity
scenarios with exact active/passive task comparison enabled.

## Potential risks
Internal execution-state-only changes no longer advance the visibility
transition marker. Public workflow status changes still do, and continue
to generate the corresponding visibility work.
…11893)

I'm trying to reduce the size of a monstrous PR I'm working on, this was
one of the changes that could be teased out independently.

## Problem

Four places independently unwrapped a `*status.Status` from an error
with the same 12-line dance: `common.IsRetryableRPCError`,
`commonnexus.ConvertGRPCError`, and a private `isRetryableRPCResponse`
in both `chasm/lib/callback` and `service/history/hsm/callbacks`.

## Fix

Extract it as `common.GetRPCStatus(err) (*status.Status, bool)` and
delete the copies.

No behavior change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

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

## What changed?

Add a new unit test for `common/callbacks.ValidatorConfig`.

## Why?

While working on a PR that extended `ValidatorConfig` and adding new
fields, some bugs crept in because the new fields weren't checked in the
`Validate` function. This unit test will now catch those automatically,
by running `Validate` on an empty `ValidatorConfig`, and then confirming
that every field of the type is found in the error message. (Reporting
that it is uninitialized.)

This PR also allows me to remove this tiny change from an otherwise
monstrous PR I am trying to slim down.

## How did you test it?
- [x] built
- [x] run locally and tested manually
- [ ] covered by existing tests
- [x] added new unit test(s)
- [ ] added new functional test(s)

## Potential risks

None

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…poralio#11668)

## What changed?
`AdminRebuildMutableState`, invoked by `tdbg workflow rebuild`, no
longer replaces the mutable state
start time with the current time. It reuses the run's recorded start
time, falling back to
`ExecutionInfo.StartTime`. Only if both `ExecutionState.StartTime` and
`ExecutionInfo.StartTime` are
empty, the current time will be used.

The rebuild now also refreshes the timeout timer tasks so the run and
execution deadlines are anchoraed at the time of the call, which is what
reset already does. Without it, preserving the start time would make a
workflow rebuild after its deadline timeout immediately. It also records
ExecutionInfo.MutableStateRebuildTime so that a repaired run could be
identify by operator.

## Why?
Moving the start time to now, often results in workflow with a negative
duration, since the end time is kept as is. Downstream systems may
handle this incorrectly.

## How did you test it?
- [x] built
- [ ] run locally and tested manually
- [x] covered by existing tests
- [x] added new unit test(s)
- [ ] added new functional test(s)

## Potential risks
1) Some downstream systems may prefer the workflow start time to move.
2) Admin may intent for the workflow to have a new timeout instead of
getting a timeout
## What changed?

- Reuse one capture handler across each dropped-task metric table test.
- Install the handler before any task queue partition is created.

## Why?

Each table case replaced `matchingEngine.metricsHandler` while a prior
case's task queue could still be initializing asynchronously. The queue
goroutine reads the handler while emitting physical task queue gauges,
producing the race reported in
https://github.com/temporalio/temporal/actions/runs/33111032921/job/98654644755.

The capture handler already synchronizes capture sessions, so keeping
the engine handler stable removes the unsafe write without changing
production matching behavior.

## How did you test it?

- [x] Priority and fairness variants: all four affected tests passed 50
iterations under the race detector.
- [x] Classic variant: no race was reported; a repeated batch hit an
unrelated task-queue-close teardown log, and the required isolated rerun
passed.
- [x] Adversarial review completed with no findings.

## Potential risks

Low. This only changes test setup and introduces no imports,
dependencies, or production behavior.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Test-only changes to metric capture setup; no production matching
behavior.
> 
> **Overview**
> Fixes a **data race** in matching engine dropped-task metric tests by
keeping `matchingEngine.metricsHandler` stable for the whole
table-driven test instead of swapping it in every subtest.
> 
> `captureDroppedOnEngine` is now documented and enforced to run
**once** before any task queue partition exists
(`getTaskQueuePartitions` must be empty). The four `tasks_dropped`
poll-path tests install a single `metricstest.CaptureHandler` at the
start and reuse it across subtests; each subtest only starts/stops a
capture session (`defer StopCapture`). That avoids concurrent reads of
the handler from async partition initialization while another subtest
overwrites it.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
a2f545f. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
## What changed?
- Fix parsing ExecutionStatus in tuple (eg: `ExecutionStatus IN
('Running', 'Completed')`)
- Fix parsing negative double values (eg: `CustomDouble > -1.5` was
returning an error)
- Return error when comparing non-bool types with boolean value (eg:
`StartTime = true` was not returning an error).
- Return error when comparing Text type search attribute with an empty
string/no tokens (eg: `CustomText = ' '`). It was already returning an
error with PostgreSQL and SQLite, now also returning an error with MySQL
and Elasticsearch.
- Added unit tests covering all Visibility stores to make sure they all
behave the same. One exception: `ExecutionStatus STARTS_WITH` works with
Elasticsearch, but not with SQL. That's because in Elasticsearch we
store as string, while in SQL we store as int.

## Why?
Bug fixes, and comprehensive unit tests to ensure uniform behavior.

## How did you test it?
- [x] built
- [ ] run locally and tested manually
- [x] covered by existing tests
- [x] added new unit test(s)
- [ ] added new functional test(s)

## Potential risks
## What changed?

Previously you could Terminate a standalone Nexus operation that was
already in the Canceled state. This PR closes that gap.

## Why?

That behavior was never intended, and is a bug. Canceled is already a
terminal state.

Not only does terminating an already canceled operation not make sense,
it also opens up weird issues where we would mutate a resource that
should be in a terminal state.

## How did you test it?
- [x] built
- [x] run locally and tested manually
- [ ] covered by existing tests
- [x] added new unit test(s)
- [ ] added new functional test(s)

## Potential risks

This is a behavior change, and could break a test or something that was
relying on this behavior earlier.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…failover (temporalio#11815)

## What changed?

- Added request-only `OrphanedChildReplacementInfo`, carrying the
parent’s current Version History as branch evidence.
- When enabled, a retried child start may atomically terminate an
orphaned conflicting run and create a replacement.
- Replacement is allowed only when the conflicting child:
  - belongs to the same parent run;
- was initiated on a losing parent branch, while the incoming initiation
is on the current branch;
  - is the first run in its execution chain;
  - contains only `WorkflowExecutionStarted`;
  - has no pending in-memory Update.
- Existing request-ID deduplication remains ahead of replacement
handling.
- Added outcome metrics, a post-commit success log, and the
disabled-by-default `history.enableOrphanedChildWorkflowReplacement`
setting.

## Why?

After force failover, a child created from a losing parent branch may
conflict with the same child start reissued by the winning branch.
Normal workflow-ID conflict handling records `WORKFLOW_ALREADY_EXISTS`,
leaving the parent unable to make progress and the original child
orphaned.

Re-linking the existing child is unsafe because its parent coordinates
are stored in immutable history. This change instead replaces it only
when the new active cluster sees no progress beyond
`WorkflowExecutionStarted`.

Ownership and progress are rechecked while holding the child lock, and
termination plus replacement creation are committed atomically.

## How did you test it?

- [x] built
- [ ] run locally and tested manually
- [x] covered by existing tests
- [x] added new unit test(s)
- [x] added new functional test(s)

Validated with:

- `go test -tags test_dep ./service/history/api ./service/history
-count=1`
- Focused parent-child XDC integration test
- `make proto`
- `make lint-code`

## Potential risks

- The decision uses the new active cluster’s locally visible state.
During a network partition, it cannot know whether the previous active
advanced the child beyond the replicated state.
- Replacement terminates an existing run. The feature is disabled by
default and fails closed unless every ownership, branch, and progress
check passes under the child lock.
- The request carries the parent’s full current `VersionHistoryItems`
list. An unusually long version history may increase RPC size and could
exceed internal gRPC limits; oversized requests fail instead of
recovering the child.
- All history hosts processing child starts in an enabled cluster must
understand the request field. An older host may ignore it and record
`WORKFLOW_ALREADY_EXISTS`.
…io#11868)

## What changed?

When an active Parent `StartChildExecution` task regenerated from late
replication finds that the Child is already closed:

- Resolve the Child’s current run by Workflow ID using
`GetMutableState`.
- Verify that the current run belongs to the expected execution chain
using `FirstExecutionRunId`.
- Refresh only a final current run (`Completed`, `Failed`, `Canceled`,
`Terminated`, or `TimedOut`) to regenerate its `CloseExecution` task.
- Allow the regenerated task to record Child completion in the Parent.

Recovery is restricted to the path where the Parent still has a pending,
started `ChildExecutionInfo`. If completion has already been recorded,
that entry has been removed and recovery is not triggered.

The change also adds:

- A namespace-level `history.enableChildWorkflowCompletionRecovery` kill
switch.
- A `child_workflow_completion_recovery_attempts` metric.
- Logging for terminal recovery attempts and Workflow ID reuse
mismatches.
- Unit coverage for terminal recovery, Workflow ID reuse, disabled
recovery, deleted current runs, skipped running/paused successors, and
retryable `GetMutableState`/Refresh failures.
- XDC coverage for:
  - A normally completed Child.
  - A Child that Continued-As-New before its final run completed.
  - A reset Child whose current reset run completed.
- A namespace-scoped, test-only source replication conversion
interceptor. The XDC harness uses it to pause Parent raw-task conversion
and deterministically reproduce source-side replication backlog without
disabling replication, changing cluster metadata, or
acknowledging/dropping tasks.

## Why?

With cross-shard replication ordering, the Child can arrive and close on
the new active cluster before the Parent arrives. The Child’s original
`CloseExecution` task cannot notify the missing Parent, and the
resulting `NotFound` is acknowledged.

When Parent replication arrives later, task refresh regenerates its
`StartChildExecution` task. That task finds that the Child has already
closed, but previously did not recreate the lost completion
notification. Consequently, the Parent could permanently retain the
pending Child without recording `ChildWorkflowExecutionCompleted`.

The regenerated Parent task now resolves the Child’s current execution
chain and refreshes its final current run. This recreates the Child’s
`CloseExecution` task, allowing completion recording to converge.

`GetMutableState` is used instead of `DescribeWorkflowExecution` because
recovery only needs the current execution, workflow status, and first
execution run ID. It avoids constructing unrelated Describe data such as
pending operations, memo, search attributes, callbacks, and Nexus state.

## How did you test it?

- [ ] built
- [ ] run locally and tested manually
- [x] covered by existing tests
- [x] added new unit test(s)
- [x] added new functional test(s)

Commands run:

- Targeted `TransferQueueActiveTaskExecutorSuite` recovery unit tests
with `-tags test_dep`.
- Normal, Continue-As-New, and reset XDC recovery tests with `-tags
test_dep`.
- Full `TestParentChildXDCTestSuite`.
- `make lint-code-fast` with zero issues.
- CI-equivalent `govulncheck v1.7.0 -json ./...` with exit code 0.

Not run:

- Full `saas-temporal` dependency-injection and service-startup
compatibility validation.

## Potential risks

- `RefreshWorkflowTasks` performs a full task refresh rather than
regenerating only `CloseExecution`. It invalidates previously generated
tasks and may also regenerate visibility, retention, archival, pending
Child, and HSM/CHASM tasks.
- In the narrow case where an existing `CloseExecution` task has
`DeleteAfterClose=true`, full refresh regenerates it with
`DeleteAfterClose=false`. If the original task is invalidated before
processing, deletion may not complete.
- If the current Child run has already been deleted before Parent
replication arrives, there is no execution to refresh and completion
still cannot be recovered. A TODO records this remaining gap.
## Summary 

We held back several fixes in the V1 schedule workflow and bundled them
into a single atomic commit with the intention of avoiding having to
perform multiple increments of the workflow versioning. The changes are
broadly: Migration v1->v2 fixes + CGS version changes.

## Risk 

This touches V1 schedules workflow, and a mistake risks
nondeterminitism. This is relatively high impact and has quite a bit of
subtlety. This is therefore a commit we need to merge with care

## Testing and validation

1. Changes have been manually tested on individual PRs already
2. Changes have been manually run through CGS's validation suite (cc
@liam-lowe)

Our intention is to a) make the 163 release cut and b) continue to
manually testing a few operational scenarios manually while this is
being merged in. If we see problems we can hotfix this. Manually doing
some scenario testing will take time so we intend to do this in
parallel.

Some of the scenarios we will test manually are: 

1. Deploy and rollback to an earlier version (probably with LLM assist,
but running locally)
2. Manually lifting the the version of a schedule via dynamic config
(manually) ensuring there's no nondeterminitism risk
3. V2->v1 Rollback works as expected

Some of the tests we've already run and manually validated(cc
@liam-lowe)
1. Version floor works as expected 
2. That this change fixes v1->v2 migration 
3. That this change avoids the problematic history entries which broke a
customer using the coinbase Ruby SDK

## LLM Summary

This PR stacks a series of changes to the V1 (legacy) scheduler
workflow: two migration-correctness fixes, and dynamic-config levers to
safely roll the V1 workflow version forward/backward across a
multi-cluster deployment without requiring continue-as-new. Summary of
each PR in the stack, in order:

### temporalio#11462 — V1→V2 migration-eligibility fix and migrated-start ID
(David Porter)
Combines two previously separate fixes under one shared version bump
(`v13`), avoiding two separate version-bump deploys for the same version
number:
- `RefreshBeforeMigrationCheck`: fixes a bug that was preventing V1→V2
migration from ever succeeding under default configuration.
- `PreserveMigratedStartIDs`: preserves the request IDs workflows were
originally started with, in case of a rollback.
- Adds a guard against late migrations caused by a transient error
bouncing a migration attempt and the schedule re-attempting it after
waking again.
- Follows the existing two-phase-rollout pattern: this PR only teaches
the scheduler to *understand* v13 (gated behind `hasMinVersion(13)`);
`CurrentTweakablePolicies.Version` itself stays at v12 pending a
follow-up activation deploy.

### temporalio#11588 — Fix schedule action delay after refresh (Alex Stanfield)
- `processWatcherResult` only recorded `DesiredTime` on the long-poll
path; when a refresh instead discovered the prior action had completed,
`DesiredTime` stayed unset, inflating the reported `ScheduleActionDelay`
for back-to-back buffered actions.
- Backdates `DesiredTime` on the refresh path too, gated behind a new
version, `RefreshCompletionDesiredTime` (v14).
- Handles two follow-on correctness gaps surfaced in review: `ALLOW_ALL`
starts (never blocked by a running workflow, so shouldn't be backdated
to an unrelated close time) and multiple tracked executions in one
refresh pass (must only move the recorded close time forward, never
backward).
- Extracts the decision into a pure, unit-testable
`shouldBackdateDesiredTime` function, and a shared
`IgnoresRunningWorkflow` helper so `ProcessBuffer` and the new backdate
logic can't drift apart on what "waits for a running workflow" means.

### temporalio#11827 — Sort BufferedStarts by due time on CHASM-to-V1 rollback
(Alex Stanfield)
- `CHASMToLegacyStartScheduleArgs` (the CHASM→V1 rollback conversion)
appended trigger-derived `BufferedStarts` after the regular pending ones
unconditionally, without sorting by due time.
- V1's buffer-processing code assumes `BufferedStarts[0]` is always the
earliest-due pending start — an invariant not guaranteed across a
rollback, since manual triggers are built by iterating a Go map
(randomized order) and appended regardless of their own due time.
- Fix: sort the combined list by `ActualTime` after appending, mirroring
the sort already applied to `RecentActions` a few lines above.

### temporalio#11831 — Re-evaluate V1 version ceiling per iteration (Alex
Stanfield)
- The base PR (temporalio#10817, by liam-lowe) added
`worker.schedulerV1VersionCeiling` but only applied it on the first
`tweakables` evaluation, so raising/removing a ceiling left an in-flight
workflow stuck at the capped version until continue-as-new.
- Re-reads the ceiling on every evaluation: `next version = max(recorded
version, min(binary default, current ceiling))`. The recorded version
stays monotonic — a newly-lowered ceiling never downgrades a version
already recorded in the current run, but a raised/removed ceiling lets
the workflow advance at its next wakeup instead of waiting for
continue-as-new.
- Recorded via the existing `MutableSideEffect`, so replay consumes
history rather than re-evaluating live dynamic config.

### temporalio#11856 — Add worker.schedulerV1VersionOverride (Alex Stanfield)
- temporalio#11831 makes the ceiling dynamic, but a ceiling can only restrict a
binary default, never promote past it. Adds the namespace-level override
needed to activate a newer version already supported by the binary.
- `requested version = valid override, otherwise binary default`; `next
version = max(recorded version, min(requested version, current
ceiling))`. Default `-1` retains the binary default; values below the
binary default or above `LatestSchedulerWorkflowVersion` are ignored.
- The frontend uses the same override for initial schedule
memo/list-info construction, so version-dependent metadata agrees with
the first worker task.

Base of the stack: temporalio#10817 (liam-lowe) introduced the original
(static-per-run) `worker.schedulerV1VersionCeiling` dynamic config that
temporalio#11831/temporalio#11856 build on.

## Why?
To support safe, gradual rollout/rollback of V1 scheduler workflow
version bumps (migration fixes, action-delay-after-refresh fix) in a
cross-version multi-cluster topology, without requiring continue-as-new
to pick up config changes.

## How did you test it?
- [x] built
- [x] covered by existing tests
- [x] added new unit test(s)

---------

Co-authored-by: liam-lowe <56076876+liam-lowe@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: alex.stanfield <13949480+chaptersix@users.noreply.github.com>
Co-authored-by: Stephan Behnke <stephanos@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: michaely520 <michaely520@users.noreply.github.com>
Co-authored-by: Feiyang Xie <feiyang3cat@outlook.com>
Co-authored-by: Kannan <rkannan82@users.noreply.github.com>
Co-authored-by: Fred Tzeng <41805201+fretz12@users.noreply.github.com>
Co-authored-by: Lakshay <54310363+Lakshaymiddha@users.noreply.github.com>
Co-authored-by: samm <sam.mathis@temporal.io>
Co-authored-by: Quinn Klassen <klassenq@gmail.com>
Co-authored-by: Will Duan <xinw.duan@gmail.com>
Co-authored-by: Qian Chen <qyc5937@gmail.com>
Co-authored-by: Prathyush PV <prathyush.pv@temporal.io>
Co-authored-by: Sean Kane <sean.kane@temporal.io>
Co-authored-by: mavemuri <74267563+mavemuri@users.noreply.github.com>
Co-authored-by: Rodrigo Zhou <rodrigo.zhou@temporal.io>
Co-authored-by: Brian VanLoo <brian.vanloo@gmail.com>
Co-authored-by: akbala <akbala@gmail.com>
Co-authored-by: Dan Davison <dan.davison@temporal.io>
Co-authored-by: Chris Smith <chrsmith@users.noreply.github.com>
## What changed?
- Corrected standalone activity dispatch-reason classification for
resets and retries.
- Added a dispatch hook that runs after CHASM validation and before
sending the activity to Matching.
  - Added coverage for retry, reset, and pause/unpause transitions.

## Why?
Consumers need a reliable way to classify and intercept standalone
activity dispatches. Reset attempt 1 was previously classified as a
retry, and processing before CHASM validation could include stale tasks
replaced by reset, update, pause, or unpause operations.
These changes ensure dispatch reasons reflect the actual attempt and
allow integrations to act only on valid dispatches.

## How did you test it?
- [X] built
- [ ] run locally and tested manually
- [X] covered by existing tests
- [X] added new unit test(s)
- [ ] added new functional test(s)

## Potential risks
- An incorrectly implemented dispatch hook could block, drop, or
duplicate an activity dispatch.
- Hook errors now propagate through task processing and may cause the
dispatch task to retry.
- Consumers relying on the previous reset dispatch classification may
observe reset attempt 1 changing from RETRY to IMMEDIATE.

  When no hook is configured, dispatch behavior remains unchanged.
## What changed?

Adds a new metric, `task_alertable_attempt`, alongside the existing
`task_attempt` histogram in
`service/history/queues/executable.go`, to distinguish system-caused
retry loops from
namespace/customer-caused ones.

- New predicate, counting: any `ResourceExhausted` error with `Scope !=
NAMESPACE` (system-caused
throttling, including causes that never set `Scope` at all, e.g.
`CIRCUIT_BREAKER_OPEN`), plus a
deliberate `BUSY_WORKFLOW` carve-out; and everything else not explicitly
excluded.
Also excludes `NamespaceNotActive`, `ErrDependencyTaskNotCompleted`,
`ErrTaskRetry`, `ErrNamespaceHandover`.
- Two emission sites, mirroring `task_attempt`'s: a terminal sample in
`Ack()` (unconditional)
  and an in-flight sample gated by the `task_attempt` 30-attempt limit.
- Two tags on both sites: `last_attempt_cause` (the specific
`ResourceExhausted` cause, or the Go
error type for the catchall case) and `stage` (`terminal` vs.
`in_flight`).

## Why?

`task_attempt` and its alert can't tell a system-caused retry storm
apart from a
namespace/customer-caused one — e.g. a task throttled by its own
namespace's APS limit climbs into
the same 100+ attempt range as a genuinely stuck task, firing the same
cluster-wide stuck-task alert
and turning it into noise. Full investigation and design rationale in
`task-attempt-metric-noise-v2.md`.

## How did you test it?

- [x] built
- [x] run locally and tested manually
- [x] covered by existing tests
- [x] added new unit test(s)
- [ ] added new functional test(s)
> ⚠️ I'm a little out of my depth when it comes to the System Nexus
Endpoint, and the current requirements around "system payloads". So
please review this carefully to keep me honest.
>
> I'm not sure if this is the right way we want to address this problem,
of if instead we should just relax the check in the SDK. (@stuart-wells
opened temporalio/api-go#309, which we may also
want to take. Independently of this.)

## Context

The Temporal server was labeling _everything_ that came through the
History Service's `StartNexusOperation` as a system payload.
(temporalio#10948) However, when we
landed support for visiting nested payloads in the Golang SDK
(temporalio/api-go#297) it asserts that all
system payloads have BOTH a `"encoding":"binary/protobuf"` AND
`"messageType"` metadata key.

That's a problem.

Now, if the Temporal server were to pick up the latest `api-go` bits, it
will introduce test failures. Because we have tests that return payloads
encoded with `plain/json` that will fail when ran through the SDK's
payload visitor at runtime.

## What changed?

~~This PR makes the requirements surrounding a system payload clearer,
and ONLY flags a System Nexus Endpoint payload as a "system payload" IFF
if is a properly labeled protobuf message. Otherwise, it doesn't set the
system payload tag at all.~~

This PR now adds a check that the payload sent to the System Nexus
Endpoint is a protobuf with message type available. It also updates a
testcase that was sending `plain/json` responses to the SNE and added a
new testcase.

## How did you test it?
- [x] built
- [x] run locally and tested manually
- [ ] covered by existing tests
- [x] added new unit test(s)
- [ ] added new functional test(s)

## Potential risks

We've already shipped code that includes the "label non-Protobuf
payloads as system payloads". So it's possible that we've persisted
those somewhere, and updating the `api-go` dependency (unless patched
there) would cause problems.
## What

Change default dynamic config values for worker command dispatch:
- `WorkerCommandsDispatchTimeout`: 10s → 5s
- `WorkerCommandsMaxAttempts`: 3 → 30

## Why

- **Shorter timeout (5s):** Each dispatch attempt holds an outbound
executor thread waiting for a poller. If the worker is gone, that thread
is blocked for nothing. Cutting from 10s to 5s means we detect missing
workers faster and free up the thread sooner.

- **More attempts (30):** The main reason for retrying is to survive
matching server restarts — during a rolling restart, dispatch RPCs can
fail with transient transport errors until the new pod is ready. With
only 3 attempts, commands could be dropped permanently during a routine
restart. 30 attempts with the default backoff (initial=1s,
coefficient=1.1) spreads retries over ~2 minutes, enough to ride out a
restart. Transport errors fail fast (no blocking wait), so more attempts
are cheap.


🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…g shutdown (temporalio#11841)

## What changed?

Register worker polls before checking the shutdown cache.

## Why?

Prevent a shutdown request from missing a poll that is still starting.
The previous implementation handled polls that arrived after shutdown
had populated the cache. It did not handle the case where the poll and
shutdown calls are interleaved.

  - The poll checks the cache and sees the worker as active.
- Shutdown records the worker and cancels registered polls, but this
poll is not registered yet.
  - The poll registers afterward and remains outstanding.

## How did you test it?

Tried adding a unit test to verify this exact race; but this gets into
the guts of the impl and makes it hard to read.
- [x] existing unit test(s)
## What changed?
Removed the extra UpdateCount increment when replicated UpdateInfo
entries are inserted into mutable state. Extended mutation and snapshot
replication tests to cover a newly replicated workflow update and verify
the source count is preserved.

## Why?
UpdateCount is already synchronized from the source ExecutionInfo.
Incrementing it again while applying UpdatedUpdateInfos caused passive
mutable state to report 2 updates when the active mutable state reported
1.

## How did you test it?
- [x] run locally and tested manually
- [x] added new unit test(s)
- [x] added new functional test(s)

Ran the focused MutableState ApplyMutation, ApplySnapshot, and
UpdateInfos tests. Also ran snapshot-only passive-path replication
against the OMES throughput_stress scenario with Workflow Updates,
Nexus, child workflows, and continue-as-new; 283 passive applies
completed without task or mutable-state differences.

## Potential risks
Low. The change only stops passive replication from incrementing a
cumulative counter that is already copied from the source artifact.
This changes the weekly CI report to show deltas to help clearly see if
sth improved or got worse.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Fix a test data race caused by passing the address of `sql.ErrNoRows` to
`assert.ErrorAs`.

Since `errors.As` writes the matching error into its target, the
assertion could mutate the package-level sentinel while parallel
persistence tests read it.

Use `require.ErrorIs` to verify sentinel identity without mutating
global state.
## What changed?
- assign priority key 1 to Worker Deployment and Worker Deployment
Version workflow starts
- cover the shared start-request builder with a unit test

## Why?
Worker versioning workflows run on the per-namespace worker task queue
alongside other system workflows. Giving their initial workflow tasks
higher-than-default priority prevents versioning operations from being
delayed behind default-priority work.

## How did you test it?
- `go test -tags test_dep ./service/worker/workerdeployment -run
TestMakeStartRequestSetsHighPriority`
- `go vet -tags test_dep ./service/worker/workerdeployment`
## What changed?
- Fix parsing negative double values.
- Consolidated the function to resolve alias (there were two, and not
behaving the same way, the legacy was missing some cases).
- Added more unit tests to verify the legacy query converter behaves the
same as in the new one except for the expected differences.

## Why?
Fixes to legacy visibility query converter for SQL and Elasticsearch.

## How did you test it?
- [x] built
- [ ] run locally and tested manually
- [x] covered by existing tests
- [x] added new unit test(s)
- [ ] added new functional test(s)

## Potential risks
This PR replaces the two existing scaling signals for poller autoscaling
with improved versions (on a opt-in basis):

* Backlog age signal: Instead of checking if backlog age is >200ms
(default threshold) to scale pollers, check if overall dispatch latency
is greater than the threshold
* Ratio signal: Instead of checking if addRate/dispatchRate ratio > 1.2
(default threshold), check if addRate/syncMatchRate > the threshold.
This will let us add more pollers if the sync match rate is too low.

Both improved signals are only enabled if the
`matching.useSignalsV2ForPollerScaling` flag is true - set to `false` by
default.

## How we tested it

- added unit tests
- ran benchmarks in a test cell, with multiple omes workloads (see
below)

---------

Co-authored-by: Kannan Rajah <kannan.rajah@temporal.io>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
… estimate overall add task rate (temporalio#11699)

## What changed?
- use probability of AddTask to root partition to estimate overall add
task rate, instead of assuming uniform distribution
- to enable this, change the AddTask loadbalancing to ensure that there
is at least 1% chance of hitting the root, regardless of how much
backlog the root has

## Why?
A previous PR changed AddTask load-balancing from uniform random to
backlog-aware

## How did you test it?
- [x] built
- [x] run locally and tested manually
- [ ] covered by existing tests
- [x] added new unit test(s)
- [ ] added new functional test(s)

## Potential risks
Changes AddTask computation and AddTask load balancing, but improves the
estimation of the former, and we are ok with the tradeoff for the latter


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches AddTask write routing and dynamic partition scaling inputs;
behavior changes when backlog-aware routing heavily favors child
partitions, though a 1% root floor and fallback estimates limit
worst-case drift.
> 
> **Overview**
> Backlog-aware write routing no longer assumes one root add implies
uniform load on every partition. The matching client now returns an
**estimated tasks across all partitions** when load-balancing picks the
**root** partition, sends it on gRPC metadata (`etap`), and the root
partition scaler uses that instead of multiplying by write partition
count.
> 
> **Write load balancing** enforces at least a **1% chance** of routing
to the root (`writePartitionRootProbabilityFloor`) so sampling stays
viable when the root backlog is full. When the root is chosen, the
estimate is derived from gap-weighted routing
(`randomRound(total/gap0)`); non-root picks carry zero estimate.
Fallback paths (uniform random, forced partition via test hooks) still
return `partitionCount` on root samples.
> 
> **Scale manager** batches and wakes the partition scaler on
**estimated** queue-wide task volume
(`AddedTasks(estimatedTasksAllPartitions)`), with threshold `batchSize ×
currentWrite` once write count is known.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
1ee95df. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## What changed?

Marks three `Predicate` oneof fields, and the messages they hold,
`[deprecated = true]` in `predicates.proto`, with a comment on each
pointing at what replaced them: `TaskTypePredicateAttributes`,
`DestinationPredicateAttributes`, and
`OutboundTaskGroupPredicateAttributes`. Regenerated
`api/persistence/v1/predicates.pb.go`
accordingly.

On top of the annotation, also removes the dead code those three types
left behind:

- The
`*TypePredicate`/`*DestinationPredicate`/`*OutboundTaskGroupPredicate`
case blocks in `AndPredicates`/`OrPredicates`
(`service/history/tasks/predicates.go`). Combining two of these now
falls through to the existing generic `predicates.And`/`predicates.Or`
wrapper instead of a type-specific simplification.
- `// Deprecated: ...` GoDoc comments on `NewTypePredicate`,
`NewDestinationPredicate`, and `NewOutboundTaskGroupPredicate`, so
`staticcheck` flags any new caller.
- Test fixtures that used one of these three types incidentally are
swapped to a still-supported type. The tests that cover these types are
untouched, so that old stored shards work correctly if they ever do
happen to be reloaded.

## Why?

These three types are never constructed anywhere in the server today.
The only place any of them is referenced outside their own message
definitions and unit tests is the read-path switch in
`service/history/queues/convert.go`, which exists to deserialize a
variant if it happens to be stored in a very old shard.

- `TaskTypePredicateAttributes` was added speculatively to be able to
create task-keyed predicates.
- `DestinationPredicateAttributes` and
`OutboundTaskGroupPredicateAttributes` both were introduced during the
outbounb queue's development. Two months later they were unified into
one composite predicate,
`OutboundTaskPredicateAttributes.Group{task_group, namespace_id,
destination}`

Removing their `AndPredicates`/`OrPredicates` case blocks is safe
because both functions already fall back to a generic, correct wrapper
(`predicates.And`/`predicates.Or`) for any pair they don't specifically
simplify. Losing a type's branch only costs simplification precision and
not correctness.

## How did you test it?

- [x] built
- [ ] run locally and tested manually
- [x] covered by existing tests
- [ ] added new unit test(s)
- [ ] added new functional test(s)

`make proto` regenerated only `predicates.pb.go`. `go build ./...` and
`go test ./service/history/tasks/... ./service/history/queues/...
./api/persistence/...` all pass.
)

## What changed?
Enable the new Visibility unified query converter introduced in v1.30.0
by default.

## Why?
The unified query converter is stable, and ready to replace the old one.

## How did you test it?
- [ ] built
- [ ] run locally and tested manually
- [ ] covered by existing tests
- [ ] added new unit test(s)
- [ ] added new functional test(s)

## Potential risks
@chaptersix chaptersix changed the title experiment/scheduler integration base [Experiment] Integrate scheduler readability stack with server main Sep 5, 2026
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.