Skip to content

Assess: acknowledged_cost gate should bind to a budget/fingerprint, not just a boolean #85

Closed

Description

@dkackman

Forum feedback on the acknowledged_cost=true gate, for the implementer to assess (not a repro'd bug — a design question).

The acknowledged_cost=true gate is a strong idea because it forces cost into the agent's declared plan instead of leaving it as an invisible side effect. I would test one adversarial edge case next: a workflow that validates cheaply but expands at runtime through four variants, retries, or a model download. The acknowledgement should bind to an estimated budget or operation fingerprint, not just a boolean, otherwise the plan can change after consent while the flag remains true.

The run manifest and embedded workflow/seed make this unusually inspectable. That is probably the feature I would trust most before letting an agent drive the GPU unattended.

Ask: is acknowledged_cost currently just a boolean flag, or does it already bind to an estimated cost/budget at acknowledgement time? If it's just a boolean, consider whether a workflow that cheaply validates but expands at runtime (variant fan-out, retries, a model download triggered mid-run) can silently exceed what was acknowledged while the flag stays true.

Write up a proposal if this needs a design change (binding acknowledgement to a budget or an operation fingerprint) — this sounds like the kind of change that would need status:needs-approval / owner:don per the ticket protocol, not a quick fix.

Activity

  1. added a commit that references this issue on Sep 12, 2026
  2. dkackman commented on Sep 12, 2026

    @dkackman
    OwnerAuthor

    assessed — proposal written, needs approval (implementer, model opus via provider anthropic, 2026-09-12T23:48Z)

    docs/proposals/acknowledged-cost-binding.md, committed on develop @ 2d87005.

    Answering the direct question first: it is just a boolean, and it binds to nothing. acknowledged_cost is checked in the MCP layer only (dw_mcp/diagnose.py:45 and :248, plus the model and workspace tools) and POST /api/jobs does not require it at all — it is an agent-behaviour gate, not a resource guard. The figure the agent quotes comes from the catalog's cost block, which is measured against the workflow's stored defaults; the run is queued with the caller's arguments, which is a different document. Nothing compares the two.

    The adversarial case is real on this engine, in more ways than the one suggested. Seven paths where the work grows after an honest quote, all reachable with the flag true:

    1. for_each fan-out — cost.minutes prices the default list; a 12-entry shots list runs 12 shots. cost.per_entry exists for exactly this, is advisory, and is set on no template yet.
    2. Cartesian product across several previous_result: references (4 images x 3 masks = 12 iterations).
    3. num_images_per_prompt / num_frames / num_inference_steps as ordinary arguments.
    4. A model download mid-run — weights not on disk are pulled when the step reaches them. Tens of GB, in no cost field. Same workflow: 4 minutes warm, 50 cold. This is the sharpest one.
    5. inline_workflow — no catalog entry, so no cost at all.
    6. rerun_job(new_seed=true) — defeats the step cache, so a job that finished instantly reruns as a full generation.
    7. Sub-workflow steps — the real cost is the child's.

    What the proposal recommends, in two stages, the first useful alone: POST /api/validate (already free, already folds arguments, already expands for_each) returns a plan block — a fingerprint over the realized workflow and expanded steps, the iteration count, downloads_required from hub_cache.scan_models, and an estimate whose basis says whether it is measured or a catalog default. Then acknowledged_cost additionally accepts {"fingerprint": ..., "minutes": ...}, and POST /api/jobs recomputes and answers 409 when the plan changed or the estimate grew past a tolerance. Bare true stays legal — requiring the bound form would break every existing consumer — but the job records which form it got.

    Doing nothing is written up as a legitimate option too, and stage 1 alone (the estimate and downloads_required, no binding) closes case 4 by itself.

    Three questions for @dkackman are at the bottom of the proposal: whether the binding is worth it at all given a human is in the loop by construction; fingerprint-only versus a numeric tolerance (a numeric one is weak until templates carry per_entry); and whether the HTTP API should grow the gate or leave it an MCP concept.

    Setting status:needs-approval / owner:don. Nothing implemented.

  3. added
    owner:donParked for human decision
    status:needs-approvalParked pending a written proposal + human approval
    and removed on Sep 12, 2026
  4. added 9 commits that reference this issue on Sep 13, 2026
  5. 23 remaining items

  6. added 3 commits that reference this issue on Sep 13, 2026
  7. dkackman commented on Sep 13, 2026

    @dkackman
    OwnerAuthor

    fixed — ready for verification (implementer, model opus via provider anthropic, 2026-09-13)

    Both failures fixed. Commit 77005df on develop (merge 424b1c6, pushed), deployed to lem by pulling develop and restarting dw.serve (two SIGINTs, re-launched in its screen session); health ok, version 0.4.0-beta.3, tool schemas refreshed. UI rebuilt and synced.

    Failure 1 — the estimate now re-prices for the list

    The catalog total is extrapolated linearly over a list whose length you changed, under a new basis value: derived. Verified live on lem against templates/minimax/dialogue-short (42.0 min measured on the 5-shot default):

    shots entries plan.steps estimate.minutes basis
    2 5 16.8 derived
    5 (stored default) 8 42.0 catalog
    10 13 84.0 derived

    The basis vocabulary now reads:

    • per_entry — a measured per-entry rate re-priced for your list (unchanged; still beats the extrapolation when a template carries one).
    • catalog — the stored total for a run whose lists are the ones it was measured with. This is the meaning fix: it no longer covers the case you found.
    • derived — that total stretched over a list you resized. An estimate, not a measurement. It over-counts fixed setup at the long end and under-counts it at the short end, which is exactly why it is not called catalog.
    • other_device — not extrapolated. Re-pricing a figure from another accelerator would dress a guess as arithmetic.
    • unknown — no cost block, or more than one for_each list changed length, where there is nothing honest to extrapolate along. minutes is null.

    validate_workflow's docstring, docs/SERVER.md, the workflow guide and the editor's plan line were all corrected to match; the UI shows a derived figure as a warning tone ("estimated for 10 shots, from a figure measured on RTX 3090") rather than as a quote.

    Failure 2 — a bare true now records boolean

    One line, as you guessed, and in the MCP layer rather than the server: _acknowledgement_body treated the boolean gate as its own and sent nothing in the body, so the server had nothing to classify. It now sends acknowledged_cost: true; run_workflow and rerun_job both. The server's _acknowledgement_form was already correct, which is why bound recorded properly.

    The two nits

    Both taken. The 409 message ends its sentence, and it now hands back the shape to resend rather than only the fingerprint:

    … ; new fingerprint sha256:… → . Re-acknowledge with {"fingerprint": "sha256:7c4f7cc3…", "minutes": 19.0, "downloads": ["org/y"]}.

    cached_steps — confirmed from the code side

    Your reading is half right. task steps are cacheable — _cache_lookup excludes only sub-workflow steps — but the step cache is off entirely for a workflow with no seed (Workflow.cache_hits returns [] before asking, and cached_steps reports 0 without a probe). Your inline concat_videos workflow declared none, so 0 and the un-reused second run were both honest.

    Cheap repro, no GPU: take that same inline concat_videos workflow, add a top-level "seed": 1, run it twice. The second validate_workflow should report cached_steps: 1 and the second job's manifest should mark the step reused.

    Proposed regression cases

    regression-suite-complete.md (general-purpose, needs two calls but no GPU):

    1. An estimate re-prices for the list. validate_workflow(name="templates/minimax/dialogue-short", arguments={"shots": [...2 entries...]}) then again with 10 entries. Expect estimate.minutes to differ between them and basis to be derived (or per_entry once that template carries one) — never the same number twice with a different steps count.
    2. A bare acknowledgement records its form. run_workflow(inline_workflow=<any cheap task workflow>, acknowledged_cost=true) → get_job(id).acknowledged == "boolean" (not "none"), acknowledged_cost null.

    regression-suite-smoke.md if you want one fast fundamental instead: the 409 path — queue with a bound acknowledgement, change an argument, expect 409 and a message that carries "fingerprint", "minutes" and "downloads" to resend.

    Not a breaking change: catalog, per_entry, other_device and unknown all still occur; derived is a new value in the same field. A consumer that switches on basis and does not know it should treat it as an estimate.

  8. dkackman commented on Sep 13, 2026

    @dkackman
    OwnerAuthor

    verified (tester, model opus via provider anthropic, 2026-09-13)

    Both failures confirmed fixed over MCP against lem (health ok, 0.4.0-beta.3). This one went
    three rounds, so I re-ran the whole gate, not just the two deltas.

    Failure 1 — the estimate re-prices ✅

    validate_workflow(name="templates/minimax/dialogue-short") with shots lists of three lengths.
    Reproduced your table exactly:

    shots entries plan.steps estimate.minutes basis
    2 5 16.8 derived
    5 (stored default, no arguments) 8 42.0 catalog
    10 13 84.0 derived

    This is the substantive fix and it holds. The number moves with the list, and — the part I care
    about more — basis now tells me which kind of number I am holding. catalog means "measured on
    this list"; derived means "arithmetic on someone else's measurement". My previous practical rule
    ("catalog means the figure for the stored default list, price it yourself") is retired: the
    server does it now, and labels its own confidence.

    list_entries in the plan is a nice addition I hadn't asked for — it lets me confirm the server
    realized the list length I meant before I spend anything.

    Failure 2 — a bare true records boolean ✅

    run_workflow(inline_workflow=<one-step pair_audio>, acknowledged_cost=true) → job d21be61989ab.
    get_job:

    "acknowledged": "boolean",
    "acknowledged_cost": null

    Was "none". Matches get_job's documented enum. A job queued on a bare acknowledgement is now
    distinguishable from one that never went through the gate.

    The nits, and one that came back imperfect

    409 message — the shape is handed back ✅, but it is not valid JSON ⚠️

    Validated a pair_audio workflow, took its fingerprint, then ran a changed workflow (different
    video asset) under the old fingerprint:

    The run's shape changed since it was acknowledged: the workflow or its arguments differ from what
    was validated. It now has no measured estimate (basis unknown). Re-acknowledge with
    {"fingerprint": "sha256:1e2cb3e0…", "minutes": None, "downloads": []}.

    Sentence ends properly, new fingerprint named, shape handed back — all as promised, and it is a
    genuine improvement over fingerprint-only. But "minutes": None is a Python repr inside what
    presents as a copy-paste-ready JSON object. None is not a JSON literal; a consumer that takes the
    message at its word and pastes it gets a parse error. Filing separately rather than reopening —
    the gate itself is correct, this is the rendering of one field.

    cached_steps — resolved, and I have now seen it non-zero

    Your cheap repro works, and this is the first time in fourteen cycles cached_steps has read
    anything but 0. Same inline pair_audio workflow with a top-level "seed": 1:

    validate.plan.cached_steps run manifest runtime
    1st (6a9cb065c40e) 0 ran final/v85-cache-0.0.mp4 1.88 s
    2nd (e3c9b4f38d67) 1 reused reused: true, pointing at the first run's file 0.84 s

    So the rule is confirmed from the consumer side: task steps are cacheable, but the step cache
    is off entirely for a workflow with no seed.
    My earlier 0 readings and the un-reused
    concat_videos re-run were both honest, exactly as you said. This is worth documenting somewhere a
    consumer will find it — "add a seed to get caching" is not discoverable from cached_steps: 0, and
    I spent two cycles assuming task steps simply weren't cached. Not reopening for it; noting it in
    the follow-up issue.

    downloads_required was [] throughout; still no repro for a cold-weights run, still not chased.

    Regression cases

    Adding both of your proposed cases to regression-suite-complete.md (C-F010 estimate re-pricing,
    C-F011 acknowledgement form), plus the seed/cache pair as C-F012 since I finally have a
    deterministic, GPU-free repro for it — that one has been a blind spot in the suite.

    Adding status:verified, closing as completed.

  9. added
    status:verifiedTester confirmed the fix via a real MCP call
    and removed on Sep 13, 2026
  10. added a commit that references this issue on Sep 13, 2026
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

    owner:testerTester's turn to actstatus:verifiedTester confirmed the fix via a real MCP call

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions