SRD-083 — Ad-Hoc Sub-Process checkpoint fidelity¶
| Field | Value |
|---|---|
| Status | Accepted (2026-08-08) |
| Date | 2026-08-08 |
| Owner | Ruslan Gabitov |
| Implements | ADR-033 v.4 §2.10 (composite constructs capture faithfully), ADR-035 v.1 (the Router-driven routing model whose position this persists) |
| Upstream | ADR-023 v.3 §2.4 (the scope lifecycle an ad-hoc container rides) |
| Related | SRD-082 (the adoption machinery this extends; schema 4, which this bumps to 5), SRD-074 (the runtime this makes durable) |
| Tracking | #307 |
The Ad-Hoc Sub-Process is the one composite whose position the checkpoint still does not carry — and unlike the pre-SRD-082 constructs, it is not guarded either: an in-flight container is captured today, and the restored instance is silently wrong. This SRD records the routing state (schema 5, additive), rebuilds it on restore, and makes a pre-fidelity document with an open ad-hoc scope refuse loudly instead of resuming corrupt.
§1 Background (verified)¶
- The routing state is loop-owned and never captured. An open ad-hoc
scope's progress lives in
adHocProgress—completedandrunningcounts, the manual-modeofferedcandidates, thestoppedflag andstopReason(internal/instance/adhoc.go:50-70) — attached to the scope entry (scope_runtime.go:85).captureDocumentwalks scopes, ledgers and tracks (checkpoint_capture.go:150-200) and records none of it;TrackRecord(checkpoint/document.go:148-169) does not carry the track'sadHocActivityassignment (track.go:200) either. - No guard defers the capture. SRD-082 FR-8 retired the capture
deferral guards entirely (
checkpoint_capture.go:145-148) — so an in-flight ad-hoc container checkpoints without complaint, with its routing state absent from the document. - The restored instance is silently corrupt, four ways.
adoptRestoredScopesrebuilds a generic scope entry withadHoc: nil(scope_runtime.go:399-405), and: - a restored inner activity's settle skips
settleAdHocon the nil guard (scope_runtime.go:598) — the Router is never consulted again, so the container drains and completes after the in-flight activities finish, skipping all remaining routing; - the
completed/runningcounts are gone, so even a Router that did run would decide over a false progress picture; - a manual container's pending
offeredset is gone — nothing is left to activate and no settle will ever drive the drain: the container hangs; - a container whose completion condition had fired (
stopped) loses the flag — a settle after restore would re-route work past completion. - The adoption machinery to extend is proven. SRD-082 landed the
pattern this SRD reuses verbatim: a per-construct record adopted
before the initial spawns (
adoptRestoredGroups,scope_runtime.go:417), restored-state plumbing on the instance (restore.go:129-130), and loud refusals when a record names state the track table does not carry. - The schema is at 4.
Document.Marshalstamps 4 andUnmarshalrefuses schemas beyond it with "schema 1..4" (SRD-082 FR-1); the bump to 5 follows the same additive discipline.
§2 Requirements¶
§2.1 Functional¶
- FR-1 — schema 5, additive. The document gains
AdHoc []AdHocRecord(one per open ad-hoc scope) andTrackRecordgainsAdHocActivity string(the inner activity a routed track runs; empty for every other track).Marshalstamps 5;Unmarshalaccepts 1..5 and refuses 6+ loudly. Absent records rebuild nothing — every schema ≤ 4 document without an open ad-hoc scope restores exactly as today. - FR-2 — capture records the routing state. For every scope entry
with a non-nil
adHoc, capture emits anAdHocRecord: the host track id, the scope path, thecompletedcounts, theofferedcandidate ids (manual mode),stoppedandstopReason. Therunningcounts are derived, not recorded (ADR-033 v.4 §2.1 minimality): each live routed track already records itsAdHocActivity, and the counts rebuild from the track table. - FR-3 — restore rebuilds the progress. An adoption pass
(
adoptRestoredAdHoc, running with the SRD-082 adoption passes before the initial spawns) rebuildsentry.adHocon the adopted scope entry: restoredcompletedcounts,offeredresolved against the container's inner nodes,stopped/stopReasonreinstated, andrunningcounted from the restored tracks of that scope carrying anAdHocActivity. Restored routed tracks get theiradHocActivityfield back from the track record. - FR-4 — routing resumes at the recorded position. After restore:
a settled inner activity consults the Router with the restored
progress (
Completed/Runningtrue to the pre-kill state,Last= the settled activity); astoppedcontainer runs no further routing and completes when its live activities drain; a manual container's restored offer is visible throughAdHocViewand consumable throughActivateAdHoc; the sequential-ordering invariant (one live activity) holds across the restore. Astoppedcontainer captured in its cancel window — the condition fired andcancelScoperan, but the canceled tracks settled before the capture — restores with nothing live to drive the drain, so it completes at adoption (theseedAdHocempty-first-answer rule, applied post-spawn). - FR-5 — refusals are loud and classified. A restore refuses, naming
the cause, when an
AdHocRecordnames a host track the track table does not carry; when the named host's node is not an ad-hoc container; or when an offered id resolves to none of the container's inner nodes. - FR-6 — a pre-fidelity document refuses. When scope adoption finds
an open scope whose host node is an ad-hoc container (
adHocOfis non-nil) but the document carries no matchingAdHocRecord(any schema ≤ 4 capture of an in-flight container), the restore refuses, naming the scope — loud beats the silent corruption of §1. Documents without in-flight ad-hoc work are unaffected.
§2.2 Non-functional¶
- NFR-1 — the capture stays loop-serialized: all recorded state is
loop-owned (
adHocProgressandtrack.adHocActivityare written on the loop goroutine pre-spawn), so no new synchronization appears. - NFR-2 — race-clean under
-race; diff-coverage ≥ 95% (aim 100%) on touched files.
§3 Models¶
// checkpoint/document.go — Schema 5 (SRD-083)
type AdHocRecord struct {
HostTrack string `json:"host_track"`
ScopePath string `json:"scope_path"`
Completed map[string]int `json:"completed,omitempty"`
Offered []string `json:"offered,omitempty"`
StopReason string `json:"stop_reason,omitempty"`
Stopped bool `json:"stopped,omitempty"`
}
type TrackRecord struct {
// ... unchanged fields ...
AdHocActivity string `json:"adhoc_activity,omitempty"` // SRD-083
}
Worked trace. An automatic container triage (inner activities
a,b,c; Router: run a, then b+c in parallel, stop when all
three completed once) is killed after a completed and while b and
c run. The capture holds: the scope record for /p/triage; two track
records at b and c with AdHocActivity: "b"/"c"; one
AdHocRecord{HostTrack: <host>, ScopePath: "/p/triage",
Completed: {a:1}}. Restore adopts the scope entry, rebuilds
adHoc.completed={a:1}, counts running={b:1,c:1} from the two
tracks, and re-parks the host. b settles → settleAdHoc consults the
Router with Completed{a:1,b:1}, Running{c:1}, Last:"b" — exactly the
pre-kill contract. When c settles the Router answers empty
(router-empty), the scope drains, and the container completes with
the same terminal fact stream as an unkilled run.
§4 Analysis & decisions¶
- Record the offer; do not re-ask the Router at restore. A manual
container's pending offer is restored as recorded rather than
re-derived. Re-asking would emit a second
Offeredfact for the same decision and could answer differently against restored data — the offer the host saw is the offer that must stand. (Rejected: re-route on restore.) - Derive
running, recordcompleted. Live work is exactly what the track table already carries — recording it twice invites the tables to disagree. Completed counts exist nowhere else once the tracks are gone, so they are recorded. (Rejected: recording both; rejected: derivingcompletedfrom ledger-like replay — the ad-hoc container has no ledger.) - Refuse a pre-fidelity in-flight document (FR-6) rather than restore it as today. Today's behavior is the silent corruption of §1 — the only honest options are refusal or fidelity, and a schema ≤ 4 document cannot offer fidelity. The refusal names the scope so an operator knows which instance to re-run. (Rejected: preserving today's silent restore for compatibility — compatibility with corruption is not a contract worth keeping.)
- No new persist points. Routing decisions happen synchronously on
the loop inside settle handling; every existing capture point sees a
consistent
adHocProgress. (Rejected: a capture-on-route persist point — nothing waits between routing and the next persist point that could lose more than the standard at-least-once window.)
§5 API deltas¶
None public. The checkpoint schema (internal) gains the §3 records; no engine, model or thresher surface changes.
§6 Test scenarios¶
| # | Test | Verifies |
|---|---|---|
| T-1 | schema-5 round-trip (internal/instance/checkpoint) |
FR-1: records marshal/unmarshal; schema stamps 5; future refusal names 1..5 |
| T-2 | automatic container mid-flight kill/restore (internal/instance) |
FR-2/FR-3/FR-4: restored counts feed the Router; routing resumes; the container completes with the unkilled run's outcome |
| T-3 | manual offer kill/restore (internal/instance) |
FR-3/FR-4: the restored offer is visible in AdHocView and consumable by ActivateAdHoc; nothing re-routes |
| T-4 | stopped container kill/restore (internal/instance) |
FR-4: no routing after restore; the container completes on drain |
| T-5 | refusal table (internal/instance) |
FR-5/FR-6: missing host track, non-ad-hoc host node, unresolvable offered id, pre-fidelity in-flight document — each refuses with its cause |
| T-6 | schema-4 document without ad-hoc work (internal/instance) |
FR-1/FR-6: restores exactly as today |
| T-7 | e2e kill-and-resume through the engine (pkg/thresher) |
the §3 worked trace end-to-end over a real Repository |
§7 Milestones¶
- M1 — schema 5 + capture. FR-1, FR-2; T-1.
feat(instance): schema-5 ad-hoc routing records; capture emits them (SRD-083 M1). - M2 — restore adoption + refusals. FR-3, FR-4, FR-5, FR-6;
T-2..T-6.
feat(instance): an in-flight ad-hoc container restores at its position (SRD-083 M2). - M3 — the proof + docs. T-7; guides (
operating/persistence.mddrops the ad-hoc line from "Current limits",subprocesses/adhoc.mdgains its restart note), CHANGELOG, the roadmap §5 map files #305–#308.feat(thresher): the ad-hoc kill-and-resume e2e; docs (SRD-083 M3).
§8 Cross-doc¶
- Implements ADR-033 v.4 §2.10 and ADR-035 v.1 (no ADR change: the routing model is untouched; only its durability lands).
- Upstream: ADR-023 v.3 §2.4.
- Related: SRD-082 (adoption machinery, schema 4 → 5), SRD-074 (the runtime made durable; frozen one-shot, not retro-edited).
- #307: closes it.
§9 Definition of Done¶
- [x] FR-1…FR-6 implemented; every §6 test exists and passes.
- [x]
make cigreen; diff-coverage ≥95% (aim 100%); touched functions ≥80%. - [x] The persistence guide's "Current limits" no longer lists ad-hoc routing state; the ad-hoc guide carries the restart note.
- [x] §10 filled.
§10 Implementation summary¶
Landed on feat/composite-followups in three milestones plus the
audit round — doc 926a4c5, M1 a8de260 (schema 5 + capture; four
pre-existing tests re-pinned from the literal schema 4 to
checkpoint.CurrentSchema), M2 d3a36e2 (adoption + refusals; the
cancel-window completion edge surfaced here and was folded into FR-4),
M3 0566575 (the kill-and-resume e2e + guides/CHANGELOG/roadmap), and
the /check-srd close-out commit (the FR-4 sequential-invariant test;
the README composite-fidelity paragraph gained the ad-hoc clause — and
its RU twin turned out to be missing the whole paragraph since
SRD-082, fixed under "no pre-existing errors").
Verification: make ci green end to end (exit 0 — the gate aborts on
the first failing step), with diff-coverage 98.1% of 103 changed
coverable lines (min 95%; adhoc.go, loop.go, restore.go at
100%); all suites race-clean; golangci-lint incl. tests 0 issues.
Deviations from the plan: none in scope. The roadmap §5 map now files the #277-review follow-ups: #305 → E1, #306 → S1, #307 → E2 (this landing), #308 → S6.
Open questions¶
None — §4 records the resolved design points (offer restored as recorded, derived running counts, the pre-fidelity refusal, no new persist points).