Deployment start diagnostics
Every Yoke self-release run — the stage run over HTTPS and the serving-API
production self-deploy — prepares a detached checkout at the run's release
lineage before launching its engine. Locating that checkout reads only each
machine-config projects entry's checkout and project_id, so the installed
CLI finds a release that changes the machine-config contract. Preparation runs uv sync --frozen --no-dev
against that checkout's own uv.lock; the execute adapter and deploy watcher
both launch its .venv Python with pinned source imports. Ambient uv environment
selectors cannot redirect the sync into the installed tool environment.
Preparation is bounded to five minutes. A missing lock, uv executable, failed
sync, timeout, or missing interpreter refuses as DEPLOY-DRIVER-ENVIRONMENT,
with the pinned path, repair recipe, and same-run re-drive instructions. Watcher
failures remain in its raw and progress captures. Every other project's
delivery keeps the installed engine environment.
A driver connected straight to the control-plane database runs the
release's own code against it before that release has booted. Before its
first in-process call it applies boot's creation phase — tables, indexes and
additive columns, never the ordered migration history — so a release that
adds a column can start. It runs as the role that owns the database's tables,
so a table it creates stays convergeable by the serving build. The phase
reads the catalog first and runs only the statements whose object is missing,
so a release with no schema change takes no table locks; it commits one table
at a time, so it never holds one table's lock while waiting on another's.
Every run on one control plane converges the same database, so one driver
at a time holds the release-start schema lock; a second driver waits, then
finds every object present. A driver still waiting after two minutes refuses
as release_start_schema_lock_unavailable, naming the run that holds the
lock. A concurrent creation of the same object, such as by a booting server,
is retried; one that persists refuses as
release_start_schema_convergence_failed without blaming grants.
Each statement waits at most the run-lock timeout; a busy table is retried
with backoff and then refuses as release_start_schema_lock_unavailable,
naming the table, and any other failure refuses as
release_start_schema_convergence_failed. Each refusal names the re-drive
step and comes before anything about the run changes. A re-drive of a run that already
started skips the phase: its first start converged the schema and its
candidate is frozen. A driver relaying through HTTPS skips this step,
because the serving build answers every call.
A normal driver start builds its execution context once. Composition completes and validates membership before the driver reads it, and returns that pass’s enrolled references. Branch verification and QA seeding use those members. The driver reuses the context’s containment basis when starting the run. That basis includes merged open items and items finished since the latest succeeded release started on the same project and target environment; before the first success, it includes all recorded finishers. The same anchor covers bound projects and bound-source revalidation. Elapsed time alone never changes the question set; the Done band's 24-hour display window is separate.
The executing transition still locks stable membership, validates mutable item
bindings, freezes requirements and intent, verifies the containment digest,
and stamps execution in one transaction. A stale digest rolls that transaction
back. The driver refreshes only the basis and re-attests, with at most three
retries; other refusals stop immediately and name the repair and re-drive step.
deployment_runs.execution.containment_basis is the narrow registered read:
yoke deployment-runs execution containment-basis RUN-ID --json
It refuses while another session is the run's live driver. A serving build below its declared minimum version refuses by name; use the same-universe owner’s local authority when driving the control plane’s own upgrade. Ordinary projects use HTTPS.
Coverage resolves member completion flows as a set, with one project/workflow
default resolution per distinct pair. The flow and source record are shared
within a composition operation, and so are the repository reads: each bound
branch head resolves once, and each project's commit comparison against its
preceding release runs once however many readers ask. The execution context
records bound sources and validates composition on one connection, so its
validation reuses the heads the record resolved. A re-drive of a run whose
composition is frozen skips that validation. It only sets aside or refuses
members that left delivery readiness since the freeze, and refuses a run left
with no member its item-scoped QA can prove (timing step
context_frozen_composition). A comparison whose preceding
release changed is a different answer and is derived again. Catalog probes
share table and column answers only inside that operation and only for the
same connection. Exiting, including
on refusal, discards the scope; no process-global or cross-request cache exists.
Mutable member status/intent is read afresh. Freeze reuses one custody resolution
for both membership and completion-authority checks; unknown custody still refuses.
Create validates provisionally before the creation lock, rolls that transaction back, then validates again under the lock. The locked pass reuses each project's carried-work comparison from the provisional pass when its commits and preceding release are unchanged; custody, membership and readiness are read again under the lock. Each enrollment pass first pins members' completion flows, reads their outstanding intake together, and locks selected requirements in ID order. It reads attachments together, locks distinct plans in ID order, and reads their cases once. Every member still passes the existing ownership, waiver, retraction, attachment, project, retirement and case-chain checks. These mutable inputs belong only to that enrollment; the locked pass reads them again after provisional rollback. Membership and takeover still commit atomically, and derived selections are still derived again at composition freeze. Old runs continue to use their saved QA contracts.
After each member removal, the orphaned plan-group check reads the remaining members' live attachments. A group with no attaching member left is retracted; its passed rows stay. A removed member cannot keep a group alive through cached membership. The outer schema/source cache is a separate scope.
Completion publishes exact run/member/environment/candidate attribution in the existing delivery-stamp facts, in the same transaction as the item's done status. An independently completed member stays delivered even if its siblings or run later fail or cancel. Dependency and dashboard readers consume this durable answer. Accepted QA and a preflight stamp alone do not mean the item completed.
The driver raw capture contains Deployment start timing: JSON lines from
preflight pin reading onward. Each step has a UTC start/end timestamp, monotonic
elapsed milliseconds, outcome, run ID, source SHA, member count, and transport.
Failed steps also record reason_code: the refusal's code when present,
otherwise the exception type, so a planned retry remains distinguishable from
a defect. SHA and member count remain unset until their authoritative reads.
Pin reading, worktree retirement/ensure, child launch, context substeps, branch
verification, QA seed, containment attestation, freeze, and executing stamp are
named separately. HTTPS returns server substeps with its response for the driver
to flush; local execution collects them through the same contract. The watcher
preserves preflight lines when it appends child output. Records carry no secrets
and are never operational authority.
Each run-scoped driver scope (preflight, watched child, pinned re-exec, pipeline)
also stores its start/end records in deployment_run_timings through
deployment_runs.timings.record when it closes; the pipeline also flushes at
each stage, so a running run's earlier steps are readable. Any machine can read
them:
yoke deployment-runs timings RUN-ID [--json]
It prints per-step elapsed totals, then every record; --json returns each
record as the driver wrote it. A write that cannot land never fails the run: the
driver prints Deployment timing record refused: with the run, the named error
and the recovery, the capture still holds the lines, and the next flush retries
them without duplicating any that landed. An empty answer names
that no timings were recorded.
For a start-gap measurement, subtract the earliest preflight-start timestamp from the successful executing-transition end (both on the driver’s clock) in that same run’s records. Report the sum of context elapsed time, freeze elapsed time, and any refresh attempts alongside the total. Compare matched source/transport/member-count conditions; missing markers are incomplete evidence, never a zero-time result.
The read-only report computes it from the recorded timings:
python3 -m yoke_core.tools.deployment_start_report --run-id RUN-ID --baseline-seconds SECONDS
The report refuses incomplete markers or more than one context build, and prints the measured total and every step’s elapsed time; a slower start still reports its measured difference.
Source/provider and containment preparation precede item/run locks. The short
commit phase rereads membership, workflow, evidence, flow and source pins;
only an identical repository question reuses its prepared answer. Carried-work
attribution reads lane heads only for landed items, so workers committing in
open lanes during preparation ask no new question. A changed source question
(for example an item that landed meanwhile) rolls the transaction back, answers
only that question with every lock released (timing step start_source_refresh)
and retries the commit phase, up to eight preparations. After that the start
refuses as deployment_source_preparation_changed, naming the exact source
key; nothing was dispatched, so re-drive the same run.
Creation prepares
against the waiting run when its takeover would settle that run succeeded;
a cancelled takeover keeps the preceding succeeded release as its base. This
unlocked prediction moves unclosed members into provisional membership so
validation prepares their source questions too. Rollback restores the waiting
run without removal audits, notices or settlement; the locked pass rechecks
and applies the takeover. If creation exhausts source preparation, repeat the original
yoke deployment-runs create or yoke deployment-runs start-for-item command
with the same arguments and idempotency key, if supplied; no new run was
created.
QA occupancy
uses the first real blocker and shares peer-plan facts within that evaluation;
full diagnostic reads still report all blockers. Item/run/target lock contention
refuses as deployment_start_lock_busy after two seconds and names same-run
recovery. It is not a measured diagnosis of which older transaction held a lock.
An item-QA cursor followed by QA/approval gates and the declared final auto
completion step is eligible for takeover. Any remaining execution step is not.
Containment, same flow/target and driver ownership still apply. Recreating a run
alone cannot repair those facts. A delivered run can settle while its member
still owes manual acceptance or post-deploy QA on another named target; that
requirement stays unmet and the item stays at release until it is accepted.
Same-target or untargeted acceptance still blocks completion.
A mutation timeout or connection loss is deployment_outcome_unknown, not a
deployment failure. Bound-source preparation commits its exact request receipt
beside the consumed sources on the run; the driver reads that receipt before proceeding
after response loss. An absent, mismatched or unreadable receipt leaves the
attempt pending, with no retry, provider dispatch or failed transition. Inspect
yoke deployment-runs get RUN-ID and the existing driver's capture, and reconcile
the request or provider attempt before re-driving the same run; never use
--fresh to recover a lost response. The live attachment's phase and capture
show preparation even while run status remains created.
QA dispatch closes members within a time budget and recovers response timeouts over HTTPS using the existing request reservation and result.
A lost answer leaves the run in
its QA stage with status executing, so live QA executions keep writing, and
the next re-drive continues that stage. A genuine stage failure records run
status failed before the <stage>-failed stage. When that status write is
refused, for example qa_abort_restore_required while host QA work is in
flight, the driver reports run_failure_unrecorded with the refusal's recovery
and leaves both status and stage unchanged.
A scoped-QA dispatch whose database lock wait is cancelled or times out, for
example when an operator cancels a statement stuck behind a held run row, is
not a stage failure either. It answers qa_stage_dispatch_interrupted, naming
the SQLSTATE and the current deployment_runs lock holder. Members settled
before the interruption keep their result, the run stays executing at its QA
stage, and the driver re-dispatches that stage inside its bounded QA wait.
A member's QA plan configuration refusal is not a stage failure either. When
the gate refuses one member's saved plan, for example
qa_plan_source_snapshot_missing, the stage records a
qa_configuration_refused blocker for that member and keeps waiting on the
others. The driver does not exit for it. The run stays executing at its QA
stage and every recorded QA result is kept. Fix the named configuration, then
re-drive the same run with yoke watch deploy -- RUN-ID. A correction already
frozen on that member counts as selected, so attaching the same plan does not
raise qa_frozen_plan_snapshot_incomplete.
The scoped-QA wait polls a run fingerprint every 15 seconds and re-dispatches
only when QA facts change, plus once at the deadline. It announces its poll
and status cadence and deadline, then prints Still waiting for scoped QA with elapsed time and blocker count
every four polls (one minute by default). Each fingerprint read or re-dispatch
gets only the time left on that deadline: a read or dispatch still unanswered
then ends the wait with a named timeout. When the bounded scoped-QA wait ends with blockers, the driver returns
awaiting QA (exit code 5). If the following fresh diagnostic report read fails,
it explicitly reports that fresh QA details are unavailable and retains the
last known blocker diagnostic. That read failure does not change the established
awaiting-QA outcome, supply fresh counts, waive a gate or record success.
Inspect the existing driver's capture and scoped QA state, resolve the remaining
obligations, then re-drive the same run.
Whole-run reads project each stored field directly. Shell pipes, newlines and quotes in frozen member instructions remain part of the complete receipt; they cannot split its fields or displace source identities and timestamps.
Timing records distinguish start_source_preparation, start_admission_reads,
start_item_run_locks, start_admission_recheck, start_source_refresh,
target_occupancy, composition_freeze, bound_source_provider,
bound_source_composition, bound_source_enrollment, bound_source_judgement,
bound_source_item_run_locks, bound_source_recheck, bound_source_commit
and executing_stamp. The start_admission_reads, bound_source_composition,
bound_source_enrollment and bound_source_judgement steps read every member,
and judge the members enrollment would add, before the run row lock;
the two _recheck steps prove those reads current under it, and a changed
input repeats the reads while the lock is held. A lock timeout names the phase,
not an invented blocker; missing response/timing evidence stays unknown. Real-run verification must name
the exact deployed source and representative members; a small fixture is a
regression check, not proof of production startup time.
Fleet preparation reports the actual subphase under the declared deployment
stage: declarations, receipt authority, admin connection, roster, ownership,
extensions, copy dump/restore, boot convergence, invariants, product readers,
cleanup and receipt writing. Long subphases emit phase=running,
outcome=pending and elapsed milliseconds every thirty seconds. Restored-copy
reader diagnostics identify sessions, frontier, board, launch pages/details,
message attempts and items, with record_count and completed_count for each
record family. Every copied record still reaches its existing handler or
decoder. Bounded batches reuse the canonical session projection and item text
expressions, and fetch launch and attempt rows in sets; no check is sampled or
skipped. Batch data is discarded on exit and cannot cross copy authorities.
Connections opened under a bound copy still physically close.
Every timing line stays in the driver raw capture, which is where preparation
is identified before any provider dispatch. Outcome-only watchers do not
forward each line on its own: ordinary lines join the progress digest, at
most one digest line per flush window, which yoke watch tail holds until the
next urgent line or sentinel rather than waking on it; a line whose outcome is
failed is urgent and is forwarded immediately. Other watchers still emit timing lines
as they arrive. They carry run/source identity when supplied and no DSNs.
A missing end marker is unfinished evidence, never a successful receipt.
Validation reads member workflow pins and skipped-candidate status in sets,
interprets each distinct pinned workflow version once per set, and resolves
refusal and rework labels together. Mutable statuses are read again on the
locked pass; no validation result survives provisional rollback.
The nested steps validation_bound_sources, validation_custody,
validation_carried_work, validation_enrollment_plan and
validation_judgement show where validation spends its time.
Creation uses the same timing format for create_source,
create_provisional_composition, create_lock, create_locked_composition,
create_takeover, create_validation, enrollment_members,
create_commit, create_takeover_settlement and
create_response. Source refresh attempts have their own marker. Long phases
emit the same thirty-second pending heartbeat; member validation also reports
record/completed counts. The creation handler collects these diagnostics and returns them in
start_timings on both local and HTTPS transports. The CLI saves the timing
records to complete private evidence and prints its path beside the compact
creation receipt; it does not repeat the records as an inline table. Deployment
execution keeps its streamed driver timings. Nested phase durations overlap
and must not be added as independent wall time. A missing response or end marker
does not authorize another create; keep the request's idempotency key and
reconcile the existing attempt.
An unchanged fingerprint retains the previous blocker result. A dispatch reads gate facts
once across the run's members; acceptance writes re-check their own subject
under the run lock. The local driver resolves its actor at startup and reuses
that binding until it exits. A closed database connection, including a failed
actor lookup, reports control_plane_connection_lost; a lost write answer
leaves the run resumable with its pending receipt preserved. Restore the
control-plane connection, inspect the run and its driver capture, then re-drive
the same run using yoke watch deploy -- RUN-ID.
Before a dispatch begins its database writes, it resolves each project's
immutable release pin and published installer record with autocommit reads,
so Git, GitHub contents and HTTP work hold no database transaction open. The
answers are reused for that dispatch. A newly changed source input refuses as
deployment_qa_source_inputs_changed; re-drive the same QA stage to refresh
its inputs. If a connection drops while handling another error, the driver
reports both the original error and the rollback loss as a resumable connection
loss. An idle-in-transaction timeout is named as the original cause.