Skip to content

Prototype D: #116's declarative engine with retry removed#121

Open
kyleve wants to merge 2 commits into
typed-lifecyclekitfrom
typed-lifecyclekit-no-retry
Open

Prototype D: #116's declarative engine with retry removed#121
kyleve wants to merge 2 commits into
typed-lifecyclekitfrom
typed-lifecyclekit-no-retry

Conversation

@kyleve

@kyleve kyleve commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Problem

A prototype for comparison, branched off #116 (the typed LaunchPlan — the declarative combinator engine). It answers the same question we explored for the function-style engine in #120, now for the engine you'd actually keep with two more apps coming: #116 minus retry, keeping the declarative API intact. LaunchPlan, the then/thenKeeping/gate/detached combinators, the LifecycleStep/LifecycleGate protocols, nodeIDs, and the compile-checked data flow are all untouched — this only strips retry from the runner beneath them.

Changes

Retry was the only thing that resumed a drive, so removing it deletes a cluster of runner machinery, not just a method:

  • Deleted: the FailedResume struct + failedResume var (its only job was picking the resume site/index), the from startIndex parameter threaded through drive/runLaunchPlan/walk (a re-drive now always walks from 0), the walk's site param (it only tagged FailedResume), the RetainedTeardown struct + retention (teardown runs inline once), and the injectFailureForTesting SPI.
  • Finding-1's disjointness precondition is gone. assertDisjointFromLaunchPlan existed because the shared memo was walked live by both launch and teardown (a retry could re-walk teardown against a populated launch memo). Without retry, teardown clears the memo at its start — the launch attempt is over — so a teardown node may freely reuse a launch ID with no collision. This is demonstrated by a new teardownNodesMayReuseLaunchNodeIDs test, replacing what used to be an untestable death-test precondition.
  • The memo stays — it's promotion's requirement (an enterForeground() re-walk must skip completed work), not retry's. That was the point worth confirming: the declarative API and its safety rest on promotion + the compile-checked plan, not on retry.
  • .failed is terminal: LifecycleFailureView loses its button (and the failure.launch.retry string), the container's failure closure drops its retry action, and LifecycleProxy + LifecycleDriving drop retry().
  • Teardown-failure safety preserved: a thrown erase never reaches the session drop, so relaunching the app returns to the working app rather than a half-erased one.

What it buys, precisely

#116 (combinators) This branch
Swift LOC baseline −243 (121(+) / 364(−))
Runner state memo + RetainedTeardown + failedResume + from/site threading just memo
Teardown retained, re-runnable, released-on-success inline, fire-once
Cross-plan ID collision fail-fast precondition (finding 1) non-issue (teardown clears memo first) — legal, tested
Failure retryable surface + proxy/seam plumbing terminal surface
Declarative API (LaunchPlan, combinators, nodeIDs, compile-checked flow) unchanged
Fuzz suites 200 model-parity + 120 retry-drain 200 model-parity

The parallel to #120 (the function-style, no-retry prototype) is exact: removing retry shaves ~250 lines and a quarter of the runner's bookkeeping in both engines, but leaves the engine's conceptual weight (the memo, the walk, promotion) intact — because that weight is promotion's, not retry's. The difference is that here the declarative combinator surface — the mark of #116 — is fully preserved.

Testing

Dropped the retry/resume unit tests, the injected-failure test, and the 120-seed retry-drain fuzz suite; kept the 200-seed model-parity fuzz. Added a terminal-failure test (a second run() after a thrown node doesn't re-drive), the teardownNodesMayReuseLaunchNodeIDs test, and reshaped the promotion run-once test off retry. UI/proxy tests updated for the terminal failure surface and enterForeground forwarding. Full Stuff-iOS-Tests scheme green (against a freshly-erased, pre-booted iPhone 17 / iOS 26.2 sim by UDID — the shared runner host's "preflight Busy" / "server died" flakes needed a clean boot of the exact target); ./swiftformat --lint clean.

Recommendation context

With two more apps planned, this is the concrete form of my standing recommendation: the reusable declarative engine of #116, with retry removed — same compile-checked LaunchPlan API app authors would learn once, terminal failure, ~250 fewer lines and the finding-1 precondition retired. #120 is the same simplification applied to the function-style engine (#117); this is it applied to the combinator engine. Pick the rung by whether you value the combinator API's static introspection (nodeIDs, parity tests) over the function style's readability — retry is off the table either way.

Open in Web Open in Cursor 

kyleve added 2 commits July 22, 2026 15:25
…ative API

Explores #116 (the typed LaunchPlan) minus retry, keeping the
declarative surface that is the mark of that PR — LaunchPlan, the
then/thenKeeping/gate/detached combinators, the LifecycleStep/
LifecycleGate protocols, nodeIDs, and the producing-step modes
precondition are all untouched.

Retry was the only thing that resumed a drive, so removing it deletes
a cluster of runner machinery, not just a method:
- Deleted the FailedResume struct + failedResume var (its only job was
  picking the resume site/index), the from-startIndex parameter
  threaded through drive/runLaunchPlan/walk (a re-drive now always
  walks from 0), the RetainedTeardown struct + retention (teardown runs
  inline once), and the injectFailureForTesting SPI.
- The walk's site param goes too — it only tagged FailedResume;
  failures now just park .failed at the node's id.
- The finding-1 cross-plan disjointness precondition
  (assertDisjointFromLaunchPlan) is gone: teardown clears the memo at
  its start (the launch attempt is over), so a teardown node may freely
  reuse a launch id — no shared-live-memo collision, because there is
  no retry re-walk to consult it. The single memo stays; it's
  promotion's requirement.
- .failed is terminal: LifecycleFailureView loses its button (and the
  failure.launch.retry string), the container's failure closure drops
  its retry action, and LifecycleProxy + LifecycleDriving drop retry().
- Teardown-failure safety is preserved without retry: a thrown erase
  never reaches the session drop, so relaunching returns to the working
  app rather than a half-erased one.

Tests: dropped the retry/resume unit tests, the injected-failure test,
and the 120-seed retry-drain fuzz; kept the 200-seed model-parity fuzz.
Added a terminal-failure test (a second run() doesn't re-drive), a
teardown-reuses-launch-ids test (proving the precondition is
unnecessary now), and reshaped the promotion run-once test off retry.
UI/proxy tests updated for the terminal surface + enterForeground
forwarding. Full Stuff-iOS-Tests scheme green; swiftformat --lint
clean.
LifecycleKit README/AGENTS: retry removed from the API and lifecycle;
the memo is reframed as promotion's requirement; failure (launch and
teardown) is terminal; teardown starts from an empty run-once set so it
may reuse launch node IDs (the finding-1 disjointness precondition is
gone). LifecycleKitUI README/AGENTS: terminal failure surface,
LifecycleFailureView loses its button, the proxy forwards only
enterForeground/teardown. ./sync-agents run.
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.

1 participant