Skip to content

dl <path> dotfiles has no liveness pin, and the status call there is not a duplicate #408

Closed

Description

@blooop

Question

Build the one duplicate removal that The pre-up devpod status call costs 0.43-0.74s on every launch
found while refusing everything else. Read that ticket's resolution comment
first — it is the spec, and it is mostly a record of what not to do here.

flows/launch.rs::run_dotfiles re-asks devpod for state that Placement
already carries. At flows/launch.rs:2843 it calls
lifecycle::workspace_state(...).is_ok_and(ContainerState::is_running) where
placement.is_running() gives the same answer in all three arms. That is one
devpod status round trip — measured ~0.57s — spent asking a question this
launch has already answered.

The slice:

  1. Replace the workspace_state call in run_dotfiles with placement.is_running().
    Confirm the three arms genuinely agree before changing it; if they do not, that
    is a finding and the ticket stops rather than the behaviour changing.
  2. Add devpod status count pins per launch shape at the existing
    recorded-call seam (flows/launch.rs unit tests plus rust/dl/tests/launch.rs).
    The red test is dotfiles-warm = 1 before the change. Afterwards 0 is a
    failure for every warm shape
    — the point of the pin is that the launch must
    keep making exactly one live liveness observation, so a future change that
    removes the remaining status call gets caught rather than celebrated.
  3. CHANGELOG line.

No new module, no cache, no type change.

What this ticket must not do

#393 refused three cheaper-looking things, each on measurement, and this build
must not quietly reintroduce any of them:

  • No cache of container liveness. A container stopped by anything but devpod
    itself leaves $DEVPOD_HOME byte-identical, mtimes included. There is no key.
  • No "try the attach and fall back". devpod ssh <ws> --command against a
    Stopped workspace does not fail: it starts the container and exits 0.
    A trip can silently perform an up.
  • No always-up. 2.12-2.23s against status's 0.569s, and it rewrites the
    verdict anchor every launch.
  • No Remembered arm on ContainerState, and no stale/cached_at/Option
    beside it. The invariant is "one observation of liveness per launch, an
    observation not a memory"; Placement::Known { state } already enforces it.

The provider fast path (docker inspect, 0.018s) is not this ticket's — it
belongs to #390 and carries a
type constraint recorded there.

Notes for the builder

  • This clone may be behind origin/main; cut the branch from origin/main.
  • Other agents may be active — use a git worktree of your own.
  • Guards: cargo test --workspace, cargo clippy --locked --all-targets -- -D warnings,
    cargo fmt --check in rust/. No em or en dashes in README or docs/ pages
    (test/test_docs_prose.py).

Activity

  1. self-assigned this
    on Aug 24, 2026
  2. blooop commented on Aug 24, 2026

    @blooop
    OwnerAuthor

    finding — the ticket stops at step 1's caveat: the three arms do not agree, and the arm
    that disagrees makes the change a regression rather than a saving.

    placement.is_running() and lifecycle::workspace_state(...).is_ok_and(ContainerState::is_running)
    give different answers for Placement::Creating, and the difference is not in the arm's
    own reading. It is in which of the two ways a Creating is built:

    how the Creating was built devpod asked during placement? is_running() the workspace_state re-ask
    place_triple's cold path (prepare) yes, and it denied the id false denies again — agrees
    Plan::Creatable (a path or a git source) no. nothing at all false asks for the first time, and can answer Running — disagrees

    Plan::Creatable's own doc comment is where the trap is written down:

    A path or a git source devpod clones itself. devpod is given source and --id workspace_id,
    and nothing is asked of devpod about it — that is Python's behaviour rather than an
    oversight: everything creatable goes through up, which is idempotent for a workspace
    devpod already has.

    So for dl <path> dotfiles the workspace_state call in run_dotfiles is not a duplicate.
    It is the only liveness observation the command makes, and #393's invariant — one
    observation of liveness per launch, an observation not a memory — is what it is satisfying.
    Placement::Creating carries no memory of liveness to reuse, because nothing ever looked.

    Reproduced at the binary boundary

    rust/dl/tests/launch.rs, --warm world, spec = the path of the running workspace's own
    clone so the derived leaf is the id devpod knows (devlaunch-main-zovomobo):

    dl {ROOT}/cache/devlaunch/repos/blooop/devlaunch/devlaunch-main-zovomobo dotfiles
    

    On origin/main (57955a3), one status and no up:

    devpod status devlaunch-main-zovomobo --output json
    devpod context options --output json
    devpod ssh devlaunch-main-zovomobo --command bash -lc 'if command -v …    # the refresh
    

    With the slice applied (if !placement.is_running()):

    devpod context options --output json
    devpod up {ROOT}/cache/devlaunch/r… --id devlaunch-main-zovomobo --ide none --init-env DEVLAUNCH_WORKSPACE_ID=d… --mount … --workspace-env … --dotfiles-script-env …
    devpod ssh devlaunch-main-zovomobo --command bash -lc 'if sudo hostna…   # setup pass, trip 1
    devpod ssh devlaunch-main-zovomobo --command bash -lc 'set -u…           # setup pass, trip 2
    devpod ssh devlaunch-main-zovomobo --command bash -lc 'if command -v …   # the refresh
    

    stderr gains Starting workspace devlaunch-main-zovomobo... for a workspace that was
    already running.

    Three things wrong with that, in the ticket's own terms

    1. devpod status goes to 0 on a warm shape. The pre-up devpod status call costs 0.43-0.74s on every launch #393 named that row itself: 0 is a
      failure for every warm shape
      . The pin the ticket asks for, written honestly, fails on
      the change it was written to make green.
    2. It is refusal 3 (always-up) reintroduced, on exactly the shape where the status
      call was load-bearing. Arithmetic with The pre-up devpod status call costs 0.43-0.74s on every launch #393's and Re-baseline launch latency on the Rust build #388's own figures: minus one status
      (0.569s), plus one up-on-running (2.12-2.23s), plus two ssh trips (2.07-2.33s each) —
      about +5.7s to +6.4s on this shape, and a workspace_result.json rewrite that moves
      verdict_cache's anchor on every dl <path> dotfiles.
    3. The up it starts making is a fully identified one — --init-env DEVLAUNCH_WORKSPACE_ID, launch lock, tools — where run_dotfiles's doc comment says
      this verb's up passes no identity and dotfiles_starts_a_stopped_workspace_first_and_says_so
      pins devpod up {MAIN} --ide none {PIXI} for it. That pin still passes, because it uses
      a named spec; the path-spec shape has no pin at all, which is why this was invisible.

    The other two arms do agree

    • Known { state } — the state came from a devpod status during placement, so the
      re-ask agrees modulo the ~0.6s window The pre-up devpod status call costs 0.43-0.74s on every launch #393 already accepts.
    • Listed — is_running() is false; the re-ask is a second workspace_state against
      the devpod whose first one just failed, so it fails again and reads false. Agrees except
      under a devpod that answers differently between two calls seconds apart, which is the same
      window as above.

    What this leaves for #393's slice

    The duplicate is real for the named-spec shape (dl <ws> dotfiles, dl owner/repo dotfiles),
    which is the shape #393 measured: there Placement::Known carries the observation and
    run_dotfiles asks again for nothing, and the existing golden
    dotfiles_refreshes_a_running_workspace_without_bringing_anything_up pins the two status
    calls with a comment explaining them. It is not real for the path-and-git-source shape,
    where the Creating arm carries no observation because none was ever made.

    So the slice as written is not one substitution. It needs a decision this build stage should
    not make alone, and the shapes are:

    • Narrow the substitution to where the observation exists. Read the Placement when it
      has one and keep the live ask when it does not — which is a match on the arm rather than
      a call to is_running(), since is_running()'s false is deliberately overloaded
      ("nothing said it was running, which is not the same as saying it is not"). No new type,
      no new arm; the honest reading is that Creating cannot answer this question and the
      caller has to ask.
    • Or ask once, earlier, for the creatable shape too — which is a change to place,
      not to run_dotfiles, and puts a status round trip on the dl <path> attach path that
      deliberately has none today.
    • Or drop the slice. The pre-up devpod status call costs 0.43-0.74s on every launch #393 already says this ticket does not move the headline number;
      if the win is only the named-spec shape, it is 0.57s off a verb nobody waits on in a
      loop, against a call site whose overloaded false is easy to get wrong twice.

    The pin table also needs a row it does not have: dl <path> dotfiles, warm = 1, and
    that 1 is run_dotfiles's own call. Any pin table without that row cannot catch this.

    Behaviour is unchanged and nothing is pushed: the probe test and the substitution were both
    reverted, and the worktree is byte-identical to origin/main at 57955a3. Guards there:
    cargo fmt --check and cargo clippy --locked --all-targets -- -D warnings clean;
    cargo test --workspace green apart from one pre-existing flake, aid's
    a_pasted_multi_line_prompt_arrives_whole_rather_than_leaking, which fails only when the
    full workspace run loads the machine and passes on its own and with its whole target
    (cargo test -p aid --test interactive, 7 passed). It is a real pty timing race on
    origin/main and belongs to nothing here.

  3. changed the title [-]dl <ws> dotfiles reads the state it was given, not devpod again[/-] [+]dl <path> dotfiles has no liveness pin, and the status call there is not a duplicate[/+] on Aug 24, 2026
  4. removed their assignment
    on Aug 24, 2026
  5. blooop commented on Aug 24, 2026

    @blooop
    OwnerAuthor

    breadcrumb: retyped wayfinder:build to wayfinder:grilling by the manager, and the title changed with it. The build attempt above did the right thing by stopping: what looked like a slice was a question wearing a slice's clothes, and the answer decides whether there is any code here at all.

    The question this ticket now asks, from the finding above:

    1. Which of the three ways forward — narrow the substitution to a match on the arm (Known/Listed only, keeping the ask for Creating), ask once earlier for the creatable shape, or drop the slice entirely. Note what the original slice's whole justification was: one duplicate removed, worth 0.57s. Any option that adds a match arm to save 0.57s owes principle 2 an argument it did not have to make when it was a deletion.
    2. The missing pin row is worth having whichever way (1) goes, and may be the only thing worth shipping: the path-spec dotfiles shape has no liveness pin, which is exactly why this was invisible. The pre-up devpod status call costs 0.43-0.74s on every launch #393's pin table needs that row.

    Map #139's entry for #393 has been corrected to record the falsification. The refusals #393 established stand unchanged; only its "one duplicate" removal does not.

  6. blooop commented on Aug 24, 2026

    @blooop
    OwnerAuthor

    breadcrumb: the arm's doc comment is false for half of its values, and that is the defect rather than a consequence of it.

    Placement::Creating (flows/launch.rs:2134) says, of itself:

    This launch may have to create it: source is what devpod is given and
    workspace_id is the --id. Nothing has been asked of devpod about it.

    True of the value place builds at launch.rs:2576 from Plan::Creatable. False of the value prepare builds at launch.rs:2341, which is reachable only through Resolution::Cold, which is reachable only when lifecycle::resolve_known_workspace asked devpod status for the derived id — and, where a record named a different one, for that too — and devpod denied it (KnownWorkspace::Unknown, lifecycle.rs:653).

    So one sentence, one arm, both true and false depending on which of two lines built the value. #393 read the true half of it and wrote a decision on that reading. Nothing downstream can tell the halves apart, because the type does not spell the difference.

  7. blooop commented on Aug 24, 2026

    @blooop
    OwnerAuthor

    breadcrumb: two more shapes measured at the seam the finding used — rust/dl/tests/launch.rs, --warm world, the devpod_shim.py call log. Probes reverted; the worktree is byte-identical to origin/main at 57955a3.

    dl blooop/devlaunch@cold dotfiles — a cold triple, so the Creating that prepare built:

    devpod status devlaunch-cold-sadetohe --output json
    devpod status devlaunch-cold-sadetohe --output json
    devpod context options --output json
    devpod up {ROOT}/cache/devlaunch/r… --id devlaunch-cold-sadetohe --ide none --init-env DEVLAUNCH_WORKSPACE_ID=d… …
    devpod ssh … 'if sudo hostna…    # setup pass, trip 1
    devpod ssh … 'set -u…            # setup pass, trip 2
    devpod ssh … 'if command -v …    # the refresh
    

    Two status calls, both denials of the same id, the second bought and thrown away. So the finding's "agrees" row understates this arm: for a Creating that prepare built the re-ask is not merely consistent, it is redundant, and removable on its own evidence — devpod denied the id moments earlier, and the one case where that could have changed underneath is a sibling create, which up_under_stage re-asks about anyway on the other side of its wait (launch.rs:1091-1104) for a Naming::Create request, which is what this verb passes for this arm.

    dl {ROOT}/cache/devlaunch/repos/blooop/devlaunch/devlaunch-main-zovomobo -- true — a path spec whose leaf is the id the shim knows, workspace Running:

    devpod context options --output json
    devpod up {ROOT}/cache/devlaunch/r… --id devlaunch-main-zovomobo --ide none --init-env DEVLAUNCH_WORKSPACE_ID=d… --mount … --workspace-env … --dotfiles-script-env …
    devpod ssh devlaunch-main-zovomobo --command bash -lc 'if sudo hostna…
    devpod ssh devlaunch-main-zovomobo --command bash -lc 'set -u…
    devpod ssh devlaunch-main-zovomobo --command bash -lc true
    

    Zero status calls, and a fully identified up against a container that is already running, plus both setup trips. That is refusal 3 — always-up — shipping today, unpinned, on the shape this map is named after. It is the same defect as the dotfiles one seen from the other side: the arm cannot say "running", so this shape never asks and always ups.

  8. blooop commented on Aug 24, 2026

    @blooop
    OwnerAuthor

    agent-decided (1):

    The answer

    Question 1: none of the three as written. The substitution is narrowed to where an observation exists — that much of shape one is right — but it is narrowed by splitting the arm that carries two meanings, not by matching around it at one call site. run_dotfiles then matches a Placement whose arms each mean one thing, and the special case disappears because it stops being special: an arm that never asked devpod anything is an arm whose reader has to ask.

    Question 2: yes, and it ships first. The pin rows are the guard whose absence hid this, they are green on origin/main as written, and they land before anything moves.

    Principle 1 decided it, over principle 2, on the evidence of this ticket: principle 3 supplies the shape, and the reason the shape is worth its arm is that the one-arm version has already cost a falsified decision and a reverted build.

    Question 1: the options

    Option A — narrow at the call site: match in run_dotfiles, keep the ask for Creating

    The finding's own first shape. It is correct behaviour and it is three lines. What it leaves standing is the sentence in the first breadcrumb: Placement::Creating's doc comment is true of one construction and false of the other, and is_running()'s false still means two things with a comment to explain which. The next reader of either gets what #393 got. Principle 1 outranks principle 2, and the cost is not hypothetical — it is this issue.

    It also leaves the redundancy the second breadcrumb measured: a Creating that prepare built asks devpod a question devpod answered moments ago, twice on the cold dotfiles shape, and Option A pays that forever because it cannot tell that arm from the other one.

    Option B — ask once, earlier, for the creatable shape

    Not refused. Unblocked, and ticketed separately — see the last section. Two reasons it is not this ticket's answer:

    • On its own it saves nothing. The dl <path> dotfiles shape makes exactly one status call today (measured: 1). Moving it from run_dotfiles to place still makes one, and puts 0.57s on every other path-spec verb that has none. Strictly worse.
    • It only pays if the fast attach comes with it, and that is a behaviour change on the dl <path> launch path with an open question of its own (the derived id is a path leaf). Its upside is ten times this ticket's, which is exactly why it should not ride along inside it.

    Option C — drop the slice, ship the pin

    Honest, and it was the safe answer before the arm was measured. It forgoes 0.57s on three shapes for nothing, and — worse — it leaves the type saying something false, which is the thing that actually cost this map a cycle.

    Option D — split the arm. Winner.

    Placement::Creating becomes two arms, because it is two states:

    • asked, and devpod denied the id — prepare's value, reached only through Resolution::Cold. Never warm, and the denial is the reason rather than an assumption.
    • nothing asked — Plan::Creatable's value. Not "not running": unobserved. devpod may well already have this id; up is idempotent and is what settles it today.

    Then run_dotfiles matches all four arms and the Creatable arm is the only one that asks — which is not a special case but the arm's meaning. And the arm that prepare built stops paying for its second denial.

    The type shape

    pub enum Placement {
        /// devpod knows this workspace and reported this state.
        Known { workspace_id: String, state: ContainerState },
        /// devpod lists it and could not describe it.
        Listed { workspace_id: String },
        /// devpod was asked for this id and denied it, and the host has prepared the
        /// clone: this launch is the create.
        Creating { workspace_id: String, source: String },
        /// A path or a git source. **Nothing has been asked of devpod**, so this
        /// launch does not know whether devpod already has `workspace_id`. Not "not
        /// running" — unobserved.
        Creatable { workspace_id: String, source: String },
    }

    workspace_id(), source() and naming() treat the new arm exactly as they treat Creating (Naming::Create, source positional), so nothing about what devpod is handed changes.

    is_running() goes, and is replaced by the positive question it was the only honest answer to:

    /// Whether this launch may attach without an `up`.
    ///
    /// Only `Known` can say yes. The other three have no positive answer to give —
    /// devpod could not describe it, devpod denied it, or nothing has asked — and
    /// this is the question where all three of those correctly mean no.
    pub(crate) fn attachable(&self) -> bool {
        match self {
            Self::Known { state, .. } => state.is_running(),
            Self::Listed { .. } | Self::Creating { .. } | Self::Creatable { .. } => false,
        }
    }

    The rename is the load-bearing half. is_running() promised to answer both directions and could only answer one; the comment apologising for that ("Nothing said it was running, which is not the same as saying it is not") is the smell, and a predicate named for the question it actually answers does not need it. run_attach and run_up_verb read attachable() unchanged in meaning — for both, "no positive answer" and "not running" lead to the same up.

    run_dotfiles is the one site where they do not lead to the same place, so it does not get a projection at all:

    let running = match placement {
        Placement::Known { state, .. } => state.is_running(),
        // devpod could not describe it, or denied this id moments ago: `up` either way.
        Placement::Listed { .. } | Placement::Creating { .. } => false,
        // Nothing has asked. This is the launch's one liveness observation, not a
        // second one.
        Placement::Creatable { workspace_id, .. } => {
            lifecycle::workspace_state(self.context.runner(), workspace_id)
                .as_ref()
                .is_ok_and(ContainerState::is_running)
        }
    };

    What is still not added: no cache, no TTL, no cached_at, no Option<ContainerState>, and no Remembered — #393's deliberate absence stands. Creatable is the opposite of Remembered: Remembered would carry a liveness reading from the past, and Creatable carries none at all and says so. ContainerState is untouched, five arms, Unknown(String) kept.

    Placement is pub but is not in devlaunch_core's prelude (lib.rs:127 exports Launch and LaunchVerb only) and appears nowhere outside flows/launch.rs — 14 sites, one file, all compiler-flagged. The public-API snapshot is unaffected.

    What it costs

    • One more arm, 4 where there were 3, to save 0.57s on shapes nobody waits on in a loop. That is the trade principle 2 objects to, and the answer is that the arm is not bought with the 0.57s: the two states already exist in the program and were both spelled Creating. Naming a distinction the program already makes is what principle 3 is for, and principle 1 is what says a type that states something false is worth an arm to fix.
    • dl <path> dotfiles keeps its 0.57s, and correctly: that call is the command's only liveness observation.
    • This still does not move the headline number. The pre-up devpod status call costs 0.43-0.74s on every launch #393 said so about itself and it is true again. What moves the headline number is the ticket named at the end, which this makes a change to place alone.

    Removed round trips, all measured at the shim seam: one off dl <ws> dotfiles, one off dl owner/repo@<cold> dotfiles, one off the Listed dotfiles shape (by inspection — the second status is against the devpod whose first one just failed). No shape gains one.

    Question 2: the pins, and the rule they enforce

    #393's table, extended with the three rows whose absence hid this. Measured columns are from the --warm world at the recorded-call seam.

    shape devpod status today after
    warm attach dl <ws>, dl <ws> -- cmd 1 1
    dl <ws> up, running 1 1
    cold dl owner/repo@<new> 1 (2 in the #88 recorded-id case) unchanged
    warm attach that waited out a sibling 2 2
    dl <ws> dotfiles, warm 2 1
    dl owner/repo@<cold> dotfiles (cold triple) 2 (measured) 1
    dl <path> dotfiles, warm 1 (measured) 1 — and that 1 is run_dotfiles's own
    dl <path> -- cmd, warm 0 (measured) 0, with a full identified up

    That last row is why #393's rule needs sharpening rather than repeating. "0 is a failure for every warm shape" was written when every shape in the table fast-attached; the path-spec attach is a warm shape whose 0 is correct today, because it pays a devpod up instead of asking. The rule that is total over all four arms, and is what #393 meant:

    A launch that attaches without an up has made exactly one liveness observation. A launch that has made none must up.

    So 0 status calls is a failure for any shape that skips the up, and correct for any shape that does not — and the two halves of the path-spec attach row can only move together, which is exactly the guard the follow-on ticket needs.

    Pins that must exist, at rust/dl/tests/launch.rs's exact-call-sequence seam (the path spec is {ROOT}/cache/devlaunch/repos/blooop/devlaunch/devlaunch-main-zovomobo, whose leaf is the id launch_scenario.py gives the shim, so the shape is reachable with no new fixture):

    1. dl <path> dotfiles in --warm: one status, no up, then context options and the refresh trip.
    2. dl <path> -- true in --warm: no status, the identified up, both setup trips, the command trip. The before-picture of the next ticket, and the pin that makes it visible at all.
    3. dl owner/repo@cold dotfiles: two status today, one after slice 3.
    4. The sharpened rule as prose beside them, since it is the thing a future cache or a future substitution trips over.

    Per #145's lesson, prove they can fail: flip run_dotfiles to placement.is_running() locally and pin 1 goes red on the spot. That is the test #393's table could not write.

    Build slices, in dependency order

    Slice 1 — the pins that were missing. No production change. Add rows 1-3 above plus the rule as a comment; all three are green on origin/main, and row 1's ability to fail is demonstrated by the local flip described above. CHANGELOG: none.

    Slice 2 — split the arm. Depends on 1. Placement gains Creatable; place's Plan::Creatable branch builds it and prepare keeps building Creating; Creating's doc comment is rewritten to say what it now means (devpod was asked and denied this id); workspace_id()/source()/naming() gain the arm identically to Creating; is_running() becomes attachable() with the doc above; run_attach, run_up_verb and the unit test at flows/launch.rs:5359-5380 follow the compiler. No behaviour change and no call-count change — every pin from slice 1 stays green, which is the slice's whole acceptance test. CHANGELOG: none (internal).

    Slice 3 — run_dotfiles reads the placement where the placement knows. Depends on 2. The exhaustive match above. Red first: dl <ws> dotfiles 2 → 1 and the cold-triple row 2 → 1, with dl <path> dotfiles staying at 1 and no up appearing anywhere. CHANGELOG: one devpod round trip off dl <ws> dotfiles and off a cold dotfiles.

    Slices 2 and 3 could be one PR; they are separate because slice 2's acceptance criterion is nothing changed and slice 3's is two counts changed, and a reviewer reading one diff cannot check both.

    Next ticket, and it is a decision rather than a slice

    dl <path> and dl <git-url> pay a full up against an already-running workspace. The argv is in the second breadcrumb: zero status calls, a fully identified up, both setup trips. Arithmetic on #388's and #393's own figures — up-on-running 2.12-2.23s, an empty ssh trip 2.07-2.33s, status 0.569s — puts it at -5.7s to -6.4s on dl <path> -- cmd warm, which is the shape this map is named after and the largest number left on it outside the ssh-multiplexing work.

    Slice 2 is what makes it a change to place alone: promote Creatable to Known by asking, and every consumer downstream already handles it. It reintroduces none of #393's four refusals — it is one observation per launch, an observation and not a memory, and it removes an always-up rather than adding one — and the precedent is already in the divergence table, row 21: the no-argument selector was routed through the same probe on exactly this argument, "the extra probe is the fast-attach contract every other entry already pays." The path-spec shape is the last entry not paying it.

    What that ticket must settle and this one deliberately does not:

    • The derived id is a path leaf (path_leaf, divergence row 20), so ~/a/foo and ~/b/foo are one id. devpod status carries no source; devpod list --output json does, at 0.404s. Whether fast-attach needs a source check or merely inherits a hazard up --id foo <other-source> already has depends on what real devpod does with that command, which is a measurement nobody here has taken.
    • Whether the CreateRecord::NeverCompleted guard run_attach applies to Known is sufficient for a workspace this launch did not create.
    • Whether git-URL specs come along or stay behind.
    • A new divergence row, since Python always upped for a path spec.

    Falsification conditions

    #393's two stand unchanged. One is added, for the decision above:

    • Reopen the split if a third construction of either creatable arm appears, or if attachable() ever needs a caller that wants the negative answer — at which point the honest shape is a three-valued liveness() returning observed / undescribable / unobserved, and the two-projection compromise above stops paying.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions