Skip to content

decisions: read operations stay "proposed" forever, and abandoned proposals are never superseded #26

Closed
1 of 1 issue completed

Description

@romamo

What happens

Every operations propose writes a decision record, which is right for an audit trail. Two things make the trail noisy:

  1. A risk: read operation has already run when it is recorded, but its status stays proposed forever
  2. A mutating proposal the agent abandons, or proposes again, stays proposed with nothing marking it superseded or stale

One disk-full incident handled by an agent on a single host produced 47 decision records for cloudfall why --host hz1, most of them read runs and repeated proposals of the same operation.

Reproduce

$ cloudfall operations propose disk-report --target hz1        # read
$ cloudfall operations propose shop-logrotate --target hz1     # mutating
$ cloudfall operations propose shop-logrotate --target hz1     # again
$ cloudfall decisions list
# three records, all "proposed"; the read one ran, the first logrotate one is superseded

Expected

  • read operations get a terminal status (for example ran) with their exit code
  • a newer proposal of the same operation and targets marks the older open one superseded
  • open proposals older than their evidence basis can be marked stale, and decisions list / why can filter on status

The staleness check is already noted as a future improvement in the M8/M9 proving runs.

Depends on #97

Activity

  1. added
    enhancementNew feature or request
    needs-decisionA shipmill session asked a question here; waits for a reply
    on Oct 8, 2026
  2. shipmill-romamo commented on Oct 8, 2026

    @shipmill-romamo
    Contributor

    Triage: implement parts 1 and 2 and the status filter; postpone part 3 (stale). The implement part waits on your decision below.

    Confirmed on main (38fb7c1):

    • decision.propose (sdk/src/cloudfall/decision.py) saves every record as proposed, read operations included. For a risk: read operation the gate is runs-freely and operations_propose already returns the check-mode output as the run's result, so the record says proposed for something that has finished
    • Nothing links a new proposal to an older open one of the same operation and targets, so repeats pile up as proposed
    • decisions list has no filter; it returns every record

    Design. This changes stored state and a published schema, so it goes through the design gate. status is an enum in config/schemas/v1/operation-decision.schema.json (proposed, approved, rejected, executed, verified, failed). No decisions log exists yet, so nothing settled is departed from. The plan:

    • Read operations: propose records them with a terminal status and the check run's exit code. Records already on disk stay proposed
    • Supersede: when propose records an operation, any open (proposed) record with the same operation id, targets and inputs gets superseded, with superseded_by naming the new decision id. Approving a superseded record is refused
    • decisions list --status <s> and cloudfall why --status <s> (repeatable) filter on status
    • Compatibility: the new enum values are additive in the v1 schema, but a record written by the new version fails to load in an older cloudfall (DecisionStatus(...) is strict). That only matters on a downgrade

    Postponed: stale needs a rule for when the evidence basis is "older" (the observation timestamp, the repository commit, or both) and a trigger (on read, or a sweep). That is a feature for a spec, not this fix; the issue stays open for it after the first PR.

  3. shipmill-romamo commented on Oct 8, 2026

    @shipmill-romamo
    Contributor

    @romamo Decision needed: how should a decision record show a read run and a replaced proposal?

    1. New status values in the v1 schema (recommended): a read run is recorded as ran with its exit code, and an older open proposal of the same operation, targets and inputs becomes superseded with superseded_by; decisions list and why get --status. Additive, but a newer record fails to load in an older cloudfall
    2. Same as 1, but reuse the existing values: a read run is executed (or failed on a non-zero exit) and a replaced proposal is rejected with a rejected_reason: superseded. No schema enum change, but rejected then means two things in the record
    3. Don't record read runs as decisions at all: they go to the observation log instead, and only supersede is added. The fewest records, but cloudfall why loses the reads an agent ran before proposing a fix

    Either way stale is postponed to a spec. Reply here with a number or your own answer; shipmill takes this up on the tick after your reply.

  4. romamo commented on Oct 9, 2026

    @romamo
    CollaboratorAuthor

    1 — new status values in the v1 schema: ran with its exit code for a read run, superseded with superseded_by for a replaced proposal, and --status on decisions list and why

    (decision by @romamo, given in chat; posted by Claude on his behalf)

  5. shipmill-romamo commented on Oct 9, 2026

    @shipmill-romamo
    Contributor

    Triage: implement, taking up the answer above (option 1), recorded as D-1 in #51.

    Plan, unchanged from the triage comment: propose records a risk: read operation as ran with the check run's exit code; a new proposal of the same operation, targets and inputs marks the older proposed record superseded with superseded_by, and approving a superseded record is refused; decisions list --status and cloudfall why --status filter on status. Records already on disk stay as they are. stale stays postponed, so this issue stays open after the PR (it says "Part of #26").

    #36 also changes operation-decision.schema.json; the same implementer builds both on separate branches, and whichever merges second rebases on the other.

  6. removed
    needs-decisionA shipmill session asked a question here; waits for a reply
    on Oct 9, 2026
  7. shipmill-romamo commented on Oct 9, 2026

    @shipmill-romamo
    Contributor

    PR: #53 (Part of #26). A read run is recorded as ran with ranExitCode; a repeat of the same operation, targets and inputs marks the older open record superseded with supersededBy (camelCase like every other key in the record; D-1's superseded_by is the Python attribute); decisions approve refuses both; decisions list --status and why --status filter. Trade-offs: operations propose now reads the existing records first, so a broken record stops it with RECORD_INVALID; an old read record still saying proposed can still be approved. stale stays open here.

  8. romamo commented on Oct 9, 2026

    @romamo
    CollaboratorAuthor

    Decision by @romamo, given in chat; posted by Claude on his behalf.

    1. New status values in the v1 schema: a read run is recorded as ran with its exit code, and an older open proposal of the same operation, targets and inputs becomes superseded with superseded_by; decisions list and why get --status. stale is postponed to a spec.

  9. shipmill-romamo commented on Oct 9, 2026

    @shipmill-romamo
    Contributor

    Triage: #53 merged parts 1 and 2 and the --status filter. The rest, stale, is postponed: it needs a rule for when the evidence basis is older (observation timestamp, repository commit, or both) and a trigger (on read or a sweep), which is a spec, not a fix. Labelled postponed; it comes back for a decision after the next stable release.

  10. added
    postponedTriage decided not now; revisited after the next stable release
    on Oct 9, 2026
  11. shipmill-romamo commented on Oct 10, 2026

    @shipmill-romamo
    Contributor

    Triage: feature, back after the 0.6.0 stable release (REVISIT). You filed this, so the stale part goes through the spec gate rather than staying postponed.

    Spec: #71 (docs/specs/001-stale-proposals.md). In short: a proposed decision is stale when a host its basis names has a newer observation; it's worked out on read and never stored (no schema change); decisions list, show and why report stale and staleHosts; --stale filters; decisions approve names the stale hosts in its prompt but doesn't refuse. A commit-based rule and a stored status are out of scope, with reasons in the spec.

    On hold: waits on #71. Implementers start once the spec merges; merging it is the approval.

  12. removed
    postponedTriage decided not now; revisited after the next stable release
    on Oct 10, 2026
  13. added a commit that references this issue on Oct 19, 2026
  14. shipmill-romamo commented on Oct 20, 2026

    @shipmill-romamo
    Contributor

    Update: the spec merged in #71 with status: approved, which is the approval. Its one build issue is #97 (all of S-001-1 to S-001-9), linked as a sub-issue; its PR sets the spec to built and closes this issue too.

  15. shipmill-romamo commented on Oct 20, 2026

    @shipmill-romamo
    Contributor

    Fixed by #105, merged with every CI job green: the stale part, built as spec S-001 (now status: built). Parts 1 and 2 and --status landed earlier in #53. Every sub-issue is closed:

    Closing this as done.

  16. shipmill-romamo commented on Oct 20, 2026

    @shipmill-romamo
    Contributor

    Released in v0.8.0rc1 (pre-release). uv tool install --prerelease allow cloudfall==0.8.0rc1
    A plain install gets it with the next stable release.
    If you filed this: react 👍 on this comment once it works for you, or reply if it doesn't.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions