Note
Grok responding on behalf of Julius.
Triage
Thanks for the careful incident write-up and for opening this to agree on direction first, @flamboh. I could confirm on current main that a statev2.sqlite snapshot booted under a different T3 home can start provider turns and write to the source install's worktrees. The environment id lives only in the file at ServerConfig.environmentIdPath (ServerEnvironmentIdentity in apps/server/src/environment/ServerEnvironment.ts), and nothing in the database records which environment wrote it. 056_RemoveRedundantProjectionIndexes is the latest migration, so 057 is free.
What I found
The incident path. This one ignores continueThreadsAfterServerUpdate (default false in packages/contracts/src/settings.ts).
- Marking runs
interruptedmakes those children show up inProjectionStore.getRecoveryThreadIds("subagent-results")whenever the latest run is terminal and there's nosubagent_resulttransfer. Orchestrator.recoverDelegatedTasksthen callsfinalizeAppOwnedSubagent, which offers a parent wake.ProviderContinuationServicedispatches it asmessage.dispatchwithqueue_after_active, and since an interrupted parent has no active run, the wake starts a provider turn on the stored native session and absolute worktree path.continueRestartedRundoes re-check the setting and won't post "Continue where you left off." when it's off, but its finalizer still callsrecoverDelegatedTask. TheDelegatedCompletionDelivery"continuation declines to start" test currently locks that in.
The other automatic paths you listed are real too, and none consult an environment owner:
- A
waitingrun with a pending or runningcheckpoint.captureeffect is kept byProviderRuntimeRecoveryService. That effect is replay-safe (EffectOutbox.REPLAY_SAFE_EFFECT_TYPES_AFTER_PROCESS_LOSS), and when capture completes,handleTerminalRuncan finalize the child and wake the parent. provider-thread.rollbackreplays the same way.CheckpointRestoreSafetyonly checks other threads in the same database, so a copied thread can restore files at the originalcwd.PullRequestWatchReactorsweeps persisted watches and can dispatch a wake on any unsettled thread.ScheduledTaskService.runDueTasksfires from enabled state andnext_run_at, using the stored project and workspace binding.limitRecoveryCommandposts "Continue where you left off." from a persistedlimitRecovery.autoResumeeven when the booting server'sautoResumeLimitedThreadsis off.- Held queues are already safe, as you noted: startup reconcile sets
queueHeld, andstartNextQueuedRunwon't promote while a held run exists.
On AGENTS.md: the documented seed is vp run migrate-dev-db with the dev server stopped, not a raw VACUUM INTO into a fresh home. That command snapshots with VACUUM INTO, then drops the families recovery would touch (runtime, queued-runs, subagent-results, delegated-completions, a latest failed run or any limitRecovery) and deletes scheduled_tasks and orchestration_v2_effect_outbox. The incident used a raw copy, which skips that prune. The prune isn't a full ownership check either: an active PR watch on a stopped, unsettled thread survives it and can still wake.
On #12879: it was closed because the branch went stale, not because the problem was rejected. The earlier review note about not adding environmentId to the client OrchestrationV2Run contract still seems relevant; storing it on the run row without publishing it is one way around that.
Likely fix area
Some constraints that came out of the code, for whichever direction is chosen:
- Timing: the ownership check probably needs to run before startup recovery, delegated recovery, effect replay, and the background workers. Writing
orchestration_v2_runtime_owneronly after recovery finishes wouldn't protect the boot that writes it, which is the risky one. - Upgrades: a pre-existing
environment-idplus an unowned database could keep today's behaviour, while a nonempty database with a freshly created home identity looks like a copy. Copying an unowned database into a home that already has an identity file stays ambiguous, and suspending is the more cautious option there. - Scope: once an owner row exists, the paths above (including schedules and usage-limit resume) all go through the same unchecked admission, so they look like parts of the same bug.
migrate-dev-dbpruning seems worth keeping regardless.
Your questions about per-record vs. database-level ownership, legacy first-boot behaviour, and how suspended work surfaces in the UI are the open design choices here.
A maintainer will decide on the fix direction.