textmachine/backend/internal/pipeline/cutcall.go
2026-09-15 14:18:58 +03:00

447 lines
28 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package pipeline
import (
"context"
"errors"
"fmt"
"time"
"textmachine/backend/internal/chunk"
"textmachine/backend/internal/config"
"textmachine/backend/internal/llm"
"textmachine/backend/internal/store"
)
// cutcall.go: the money and the disposition of a call THE ENGINE ITSELF cut short (backlog row 360;
// the owner's word on the amount is D39.230 п.1). The transport half — how a cut is detected and told
// apart from an undelivered request — lives in internal/llm/attemptcut.go; this file decides what it
// COSTS and what happens to the chunk.
//
// A delivered request is generated whether or not we are still on the line. Three things end our side
// of it, they share the money, and they differ in everything else:
//
// attempt_timeout our deadline fired mid-generation. 1 call, flagged, NOT retried. Resume: 0 calls.
// cancelled a person stopped the run. 1 call, marked `cancelled`. Resume: 1 call, SAME budget.
// connection_lost the socket broke after delivery. 2 calls, then an infra pause. Resume: 1 call, SAME budget.
//
// Until this file existed all three arrived at a branch commented «No 2xx ever arrived: nothing was
// billed», which released the reservation and booked ZERO. On the provider that ships today that
// comment is false by construction: DeepSeek answers 200 with empty lines while a request waits to be
// scheduled, so a status line says nothing about whether a generation was bought.
// cutCall is the identity of the call being settled — the same tuple runAttempt already holds, passed
// as one value because thirteen positional arguments is how a chapter index and a chunk index end up
// swapped without the compiler noticing.
type cutCall struct {
stage config.Stage
chunk chunk.Chunk
job *store.Job
model string
reqHash string
resv store.Reservation
estimate float64
attempt int
escalation bool
}
// stoppedPosition is the chunk×stage a stopped run was working on. It is a struct for the reason
// cutCall is: `snapID` and `contentHash` are adjacent strings, and a swap between them compiles,
// writes a row addressed to nothing, and is found by nobody.
type stoppedPosition struct {
stage config.Stage
chunk chunk.Chunk
snapshotID string
contentHash string
cumCostUSD float64
attempts int
// paidAttempts is how many attempts of this position produced a CLASSIFICATION — a reply that was
// paid for (freshly, or on an earlier run and replayed from its checkpoint) and judged.
//
// ⛔ IT IS NOT `attempts`, AND THE DIFFERENCE IS A UNIT. `attempts` follows the attempt INDEX, which
// walks over burned keys and includes the index a reservation was refused at — so after a stopped run
// the first fresh purchase of a position happens at index ≥ 1 with nothing classified at all. A mark
// keyed on the index would write a verdict about a unit nobody has translated once.
paidAttempts int
// inHand is the last attempt that WAS classified: its index and what it answered. The ceiling mark
// below needs both, because its own reason says why the re-attack never happened and nothing else on
// the row would then say what the paid attempt came back as.
inHand stageAttempt
// firstFlagReason is what the FIRST attempt of this position failed with — the telemetry column
// (chunk_status.first_flag_reason) every row whose own verdict is no longer that failure carries.
//
// ⛔ A MARK THAT DROPPED IT WOULD BLIND THE ECHO METRIC ON A RUN THAT PAID FOR AN ECHO. The metric
// counts a draft whose first attempt echoed through `flag_reason OR first_flag_reason` (quality.go):
// a marked row's flag_reason says why the PURCHASE did not happen, so without this column the unit
// stays in the denominator and leaves the numerator, and the echo rate FALLS on the run that bought
// the echo — the 25.07 defect («echo_draft=0.0% of 20») one column further along.
firstFlagReason FlagReason
}
// settleUSDForCutCall is the ONE place that answers «what does a call we cut short cost».
//
// The answer is the reservation's own estimate (D39.230 п.1). Nobody knows what the provider actually
// billed, and whether it bills a cut call at all is deliberately OPEN — the paid balance probe that
// would answer it was declined — so the estimate is an upper bound under the assumption that the
// vendor charges for what it generated. Booking zero understates the book against its own ceiling and
// hands the platform a margin it cannot measure.
//
// It is a function and not a YAML field because a money policy behind a config knob can be changed by
// an operator who is not deciding a money policy, silently, between two runs of one book — leaving the
// ledger with two answers to one question and nothing on the row saying which.
func settleUSDForCutCall(estimate float64) float64 { return estimate }
// settleCutCall books the money for a delivered call the engine cut short, records the row, and
// returns the disposition that cause deserves.
//
// The money is written FIRST, before anything decides what happens to the chunk: the provider's side
// of the transaction already happened, and a process that dies between here and the verdict must leave
// the spend recorded. The checkpoint is what makes booking it possible at all — the store cannot write
// spend without one — and for two of the three causes it is also what stops the next run from buying
// the same call again.
func (r *Runner) settleCutCall(ctx context.Context, c cutCall, cut *llm.AttemptCutError, err error, att stageAttempt) (stageAttempt, error) {
// ⛔ THE MONEY IS DRAWN ON THE PROVIDER'S OWN ACKNOWLEDGEMENT, not on our write. A request whose
// bytes entered the peer's TCP window may never reach the application behind it — a load balancer
// accepts, the backend never sees it — and settling for that charges a reader for a call nobody
// ran, which is the one direction the canon forbids (D39.196 п.2а). A 2xx object in our hands is
// the acknowledgement; without it the same checkpoint is written for ZERO.
//
// The row is identical either way: same key, same class, same flag, same resume. Only the number
// and the `estimated` mark move, so an operator sees the class and the reader pays only for what a
// provider confirmed taking.
cost := 0.0
if cut.Billable {
cost = settleUSDForCutCall(c.estimate)
}
finish := string(cut.Cause)
if serr := r.Store.SettleWithCheckpoint(c.resv, cost, store.Checkpoint{
RequestHash: c.reqHash, JobID: c.job.ID, ChunkIdx: c.chunk.ChunkIdx, Attempt: c.attempt,
Stage: c.stage.Name, Role: c.stage.Role, ModelRequested: c.model, ModelActual: c.model,
ResponseText: "", UsageJSON: "{}", CostUSD: cost, FinishReason: finish,
Escalation: c.escalation,
}, r.events.spendLine()); serr != nil {
// The rule of every settle on this path: money state falling behind reality is a loud infra
// fault, never something to continue on top of.
return att, fmt.Errorf("pipeline: settle after a %s cut a delivered call: %w", finish, serr)
}
r.events.flush()
att.finish = finish
// `att` arrives carrying the money of any burned keys runAttempt walked over, and that money is
// this chunk's just as much as this call's — dropping it would under-state a position that was cut
// twice. `runCost` takes only what THIS run bought.
att.cumCost, att.runCost = att.cumCost+cost, cost
rl := r.baseRequestLog(c.stage, c.chunk, c.model, c.reqHash)
rl.CostUSD, rl.LatencyMS, rl.FinishReason = cost, att.latency, finish
rl.Degraded, rl.Err, rl.OK = finish, err.Error(), false
if cut.Deliveries > 1 {
// ⚠ THE LEDGER IS SHORT BY DESIGN HERE, AND THE ROW SAYS SO. The provider was asked to generate
// more than once inside one retry chain, and the store books spend only through a checkpoint —
// they share one key, so one settle covers all of them. Under-counting is the ratified
// direction (D39.196 п.2а: the deploy absorbs it, the user's balance is never overstated), so
// this pack does NOT quietly multiply the charge; it makes the gap readable instead, because a
// silent under-count is what row 360 was opened about.
// ⛔ THE NOTE GOES FIRST, and that is not cosmetics. The column an operator reads is bounded —
// `errTail` keeps 120 bytes — and the transport error in front of it is routinely longer than
// that, so a note appended to the end reached nobody, ever. It also says what was ACTUALLY
// booked: on a cut the provider never acknowledged, the answer is nothing, and a line claiming
// «one estimate is booked» beside a $0 row is a sentence that has to be disbelieved to be used.
rl.Err = cutErrLine(err, cut.Deliveries, cost)
}
// «Never write a silent $0 usage; estimate and FLAG it» is the one discipline every surveyed
// harness converges on (research/21 §5.3: crush `EstimatedUsage`, goose `CostSource`). The flag is
// what the operator's legend, the machine-readable pair beside committed_usd and the platform's
// «≥ X, up to Y» all read.
rl.Estimated, rl.EstTokens = cost > 0, r.estOutTokens(c.chunk.Text)
r.Store.LogRequest(ctx, r.Log, rl)
r.Log.WarnContext(ctx, "we cut a delivered call; the reservation estimate is charged as an ESTIMATE only when the provider had acknowledged it with a reply",
"stage", c.stage.Name, "chapter", c.chunk.Chapter, "chunk", c.chunk.ChunkIdx, "attempt", c.attempt,
"cause", finish, "after_headers", cut.AfterHeaders, "bytes_read", cut.BytesRead,
"whitespace_only", cut.WhitespaceOnly, "elapsed", cut.Elapsed.Round(time.Millisecond).String(),
"deliveries", cut.Deliveries, "estimate_usd", fmt.Sprintf("%.6f", cost))
if cut.Cause == llm.CutBySelfDeadline {
// Our deadline, the run is healthy: a disposition, not an infra failure. Nothing retries it —
// a retry asks the provider to generate, and bill, the very thing it is generating right now.
att.cls = classification{FlagAttemptTimeout, cut.Error()}
r.setJobStatus(ctx, c.job.ID, "done")
return att, nil
}
// A stopped run and a dead socket both end this stage. The money above is recorded either way; the
// error travels so the run pauses instead of walking the rest of the book against a dead provider.
//
// ⛔ BUT THE TWO DO NOT GET THE SAME WORD (backlog row 389, and this site is not one of the two the row
// names — it is the one a stop actually goes through). A call a person cut was healthy when they cut
// it; a dead socket IS a failure and keeps the word. The question is asked once, for the whole family
// — see noteJobFailed.
r.noteJobFailed(ctx, c.job.ID, err)
return att, fmt.Errorf("pipeline: stage %s call (ch%d/chunk%d, model %s): %w",
c.stage.Name, c.chunk.Chapter, c.chunk.ChunkIdx, c.model, err)
}
// cutErrLine is the sentence an operator reads when one retry chain asked the provider to generate more
// than once. It is a named function rather than a Sprintf at the call site because both of its
// properties are load-bearing and neither is visible from there.
//
// ⛔ THE NOTE COMES FIRST. The column that prints this is bounded (cmd/tmctl's errTail keeps 120 bytes)
// and the transport error is routinely longer, so a note appended at the end reached nobody — while
// being the only place the gap between what the provider generated and what the ledger booked is
// visible at all.
//
// ⛔ AND IT SAYS WHAT WAS ACTUALLY BOOKED. On a cut the provider never acknowledged, the answer is
// nothing; «one estimate is booked» printed beside a $0 row is a sentence a reader has to disbelieve
// before they can use it.
func cutErrLine(err error, deliveries int, cost float64) string {
booked := "ONE estimate is booked for all of them"
if cost <= 0 {
booked = "NOTHING is booked: the provider acknowledged none of them"
}
return fmt.Sprintf("[the provider was asked %d times; %s] %s", deliveries, booked, err.Error())
}
// stopMark is the row a stopped run leaves on the position it was working on: its reason, the sentence a
// person reads afterwards, and how many attempts that position actually made.
type stopMark struct {
reason FlagReason
detail string
attempts int
// firstFlag is the superseded first failure, written exactly as the ok path writes it
// (recoveredFirstFlag): empty when the row's own verdict IS that failure, so nothing is counted twice.
firstFlag string
}
// stopMarkFor decides WHETHER a stop leaves a mark on this position and WHICH one. It is a pure function
// of the position and the error so the decision can be asked directly in a test, rather than only through
// a run that has to be arranged to stop in the right way.
//
// TWO stops leave a mark, and they are different facts about the same shape — money was spent on this
// position and the work behind it was not finished:
//
// - a HUMAN stopped the run over a call that was already on the wire (`cancelled`). The call was
// healthy, so the mark reads «stopped; the resume re-does it on the same budget»;
// - a USD CEILING refused to reserve the RE-ATTACK of an attempt this position had already paid for
// (`retry_unaffordable`, backlog row 291). The run stops like any other ceiling halt — that part is
// ratified and unchanged — but the position used to leave NO row at all, so the unit read `pending`,
// indistinguishable from one nobody had started, while its first attempt was paid for and on disk.
// Measured on the fixture of retrystopmark_test.go before this existed: `committed=$0.003640` with
// ONE chunk_status row for the two units the run had touched.
//
// ⛔ EVERY OTHER STOP LEAVES NOTHING, and that is a statement rather than an omission. A ceiling that
// refuses attempt 0 has bought nothing this position can DELIVER: `pending` is then the truth about the
// TEXT, and a row would invent a half-done unit. That is why the ceiling branch asks `paidAttempts`, not
// the attempt index.
//
// ⚠ «NOTHING IT CAN DELIVER» IS NARROWER THAN «NOTHING», and the difference is a door this pack leaves
// open: a position whose only purchase was a BURNED key (money, no result — burnedByCut) and whose next
// index a ceiling refused has `committed > 0` with no row, which is the same invisible shape this mark
// exists to end, minus any text to account for. Closing it needs a reason of its own («paid for, nothing
// came back, nothing translated») or the owner's word that a burn is visible through the ledger alone —
// and the table of stopMarkFor pins today's answer so the next reader finds a decision, not a gap.
//
// A flag lying about its cause is forbidden in its own right (D39.93 п.2), which is why the second case
// gets a reason of its own instead of borrowing `cancelled`: nobody stopped that run by hand.
func stopMarkFor(p stoppedPosition, err error) (stopMark, bool) {
// ⛔ THE STOP IS ASKED OF THE ERROR AS A WHOLE, and the delivery of ANY cut in it — not of whichever
// cut `errors.As` happens to reach first. A retry chain hands up the cut with the strongest money
// claim (llm's chainError), which is deliberately the EARLIER one when a later free cut would mask
// it — so a run stopped over an attempt behind a paid `connection_lost` arrives here carrying that
// cause, and a guard reading only the first cut's cause decided «this is not a stop» and returned in
// silence. The money was settled, the position got no row, and the export showed an unexplained gap:
// the hole §4.2 forbids «at any moment».
// ⚠ «A cut» IS «a delivered cut», by construction and in one place: cutError returns NIL when the
// request never went out, and the single construction of the type sets Delivered true. So one
// errors.As answers both halves, and a walk over the error tree looking for a delivered one would be
// asking a question that cannot come back different.
var cut *llm.AttemptCutError
if errors.Is(err, context.Canceled) && errors.As(err, &cut) {
return stopMark{
reason: FlagCancelled,
detail: "the run was stopped while this call was in flight; it was paid for at the reservation estimate and the resume re-does it on the same budget",
attempts: p.attempts,
firstFlag: recoveredFirstFlag(p.firstFlagReason, FlagCancelled),
}, true
}
if p.paidAttempts > 0 && errors.Is(err, errReserveCeiling) {
return stopMark{
reason: FlagRetryUnaffordable,
detail: ceilingStopDetail(p, err),
// ⚠ ONE FEWER THAN `attempts`, AND THE BRANCH ABOVE DOES NOT SUBTRACT — the two stops differ
// in exactly this. `attempts` is the index the loop is ON (attemptsMade = attempt + 1, set
// before the error check), so for a cancelled call it counts the call that DID go out, while
// here the index it counts bought nothing at all. What is left is every index this position
// really consumed, burned keys included — the same thing the ok path's count includes, so a
// row cut from `paidAttempts` instead would silently drop a burn this position paid for.
//
// ⚠ IT CANNOT GO NEGATIVE, and the reason is not local: `paidAttempts > 0` above means the loop
// classified something, and the loop sets attemptsMade = attempt + 1 ≥ 1 before any error is
// read (stagerun.go). A future shape that marked a position without that guarantee would have
// to bring the floor with it.
attempts: p.attempts - 1,
// ⚠ BOTH MARKS CARRY IT, and the cancelled one did not until this pack: a position stopped over
// its SECOND attempt has a first failure too, and the same blinding applied to it. One rule for
// the two stop marks rather than a rule and an exception.
firstFlag: recoveredFirstFlag(p.firstFlagReason, FlagRetryUnaffordable),
}, true
}
return stopMark{}, false
}
// ceilingStopDetail is the sentence the money mark leaves behind. It carries three things the row cannot
// get anywhere else: WHAT the paid attempt answered (the reason column now says why the re-attack never
// happened, not what came back), WHICH ceiling refused, and HOW MUCH was missing. The log line says the
// same, and dies with the process; the row is what a person reads the next morning.
//
// ⚠ IT NAMES THE RE-ATTACK, and that is only true while every OTHER paid step of a position degrades on a
// ceiling instead of propagating it: the escalation hop, the repair sub-step and the terminology pass all
// catch errReserveCeiling (escalation.go, repair.go, terminologist.go), so the one that reaches here is
// the attempt loop's. A new paid sub-step that let a ceiling through would make this sentence misname
// which purchase was refused.
func ceilingStopDetail(p stoppedPosition, err error) string {
// WHICH ceiling is named by the typed stop, and the whole noun phrase is built here rather than left to
// a placeholder inside the sentence: a bare «%s» with a defensive default produced «the a USD ceiling»
// on the day the type was not there, and this line is what a person reads the next morning.
//
// ⚠ THE DEFAULT IS UNREACHABLE TODAY, and the condition is worth naming rather than trusting: both
// wrappers of errReserveCeiling are inside a *CeilingHalt (stagerun.go), so an `errors.Is` that holds
// cannot sit on an `errors.As` that fails. It becomes reachable the day something wraps that sentinel
// without the type — and then the sentence still reads.
ceiling, shortfall := "a USD ceiling", int64(0)
var halt *CeilingHalt
if errors.As(err, &halt) {
ceiling, shortfall = "the "+halt.Scope+" USD ceiling", halt.ShortfallMicroUSD
}
// «Short by N» is omitted rather than printed as zero when the stop did not state it (a day-scope
// refusal, or a ledger read that failed — shortfallMicroUSD): a money figure of 0 reads as «you are
// not short of anything», which is the opposite of what happened.
missing := ""
if shortfall > 0 {
missing = fmt.Sprintf(", short by %d micro-USD", shortfall)
}
return fmt.Sprintf("attempt %d was paid for and came back %s; %s refused to reserve the re-attack%s — raise the ceiling and the resume finishes this unit",
p.inHand.attempt, p.inHand.cls.Reason, ceiling, missing)
}
// recordStoppedPosition marks a position a stopped run was working on, so the stop leaves no unexplained
// gap. It never changes the outcome it is called on: the run is ending, and this says what it was
// doing when it did. WHICH stops leave a mark, and why, is stopMarkFor above.
//
// A write failure is logged, not returned: the caller is already returning the error that stopped the
// run, and replacing it with a bookkeeping failure would hide why the run stopped.
func (r *Runner) recordStoppedPosition(ctx context.Context, p stoppedPosition, err error) {
mark, marked := stopMarkFor(p, err)
if !marked {
return
}
// ⛔ A CUT MUST NOT BLIND A VERDICT IT DID NOT PRODUCE, and this row is an UPSERT: chunk_status is
// keyed (book, chapter, chunk, stage), so the write below REPLACES whatever the position already held
// — including `final_hash`, which is the only pointer to the text a previous run bought and the
// export ships.
//
// ⚠ IT IS REACHABLE, and it was measured rather than argued (backlog row 375, the ⛔ half): a prompt
// edit moves both the rendered content and the snapshot, so the resume fast-path above refuses the
// stored row and the position is re-attacked; cut the run there and the row went from
// `ok / final_hash=770e5563 / cost=$0.001820` to `flagged(cancelled) / final_hash="" / cost=$0`, the
// checkpoint holding the text stayed on disk with nothing addressing it, and the export shipped an
// empty chapter where it had shipped prose. The run made the book WORSE than it found it, which is
// what D39.240 forbids of a stop: it may cost money, it may not leave rubbish.
//
// So the mark is written only where it was meant to be written — over a position that has no verdict
// of its own — and a position that already holds one keeps it. Nothing is hidden by that: the resume
// re-attacks this position anyway (its stored content hash no longer matches, which is why it was
// re-attacked in the first place), the cut call's money is on the ledger through its checkpoint and
// its request_log row, and the line below says out loud what was kept and why.
//
// ⚠ THE REDRIVE ROUTE CANNOT REACH THIS: ResetChunkStages deletes the row and its checkpoints in one
// transaction before re-attacking, so there is nothing to protect and this guard is a no-op there.
if prev, perr := r.Store.GetChunkStatus(r.Book.BookID, p.chunk.Chapter, p.chunk.ChunkIdx, p.stage.Name); perr != nil {
// Not fatal and not silent: the caller is already returning the error that stopped the run, and a
// failed read here only means the guard cannot answer — so it does not, and the mark is written as
// it always was.
r.Log.WarnContext(ctx, "could not read the position's stored disposition before marking it stopped; the mark is written and may replace an earlier verdict",
"stage", p.stage.Name, "chapter", p.chunk.Chapter, "chunk", p.chunk.ChunkIdx, "err", perr)
} else if prev != nil && resolvedForResume(prev) {
r.Log.WarnContext(ctx, "the run was stopped over a position that ALREADY held a verdict from an earlier run; that verdict and its text are kept rather than overwritten by the stop mark (the resume re-does this position, and the stopped call's money is on the ledger)",
"stage", p.stage.Name, "chapter", p.chunk.Chapter, "chunk", p.chunk.ChunkIdx,
"kept_disposition", prev.Disposition, "kept_flag_reason", prev.FlagReason,
"stopped_call_cost_usd", fmt.Sprintf("%.6f", p.cumCostUSD))
return
}
if uerr := r.Store.UpsertChunkStatus(store.ChunkStatus{
BookID: r.Book.BookID, Chapter: p.chunk.Chapter, ChunkIdx: p.chunk.ChunkIdx, Stage: p.stage.Name,
SnapshotID: p.snapshotID, ContentHash: p.contentHash,
Disposition: string(DispFlagged), FlagReason: string(mark.reason),
Attempts: mark.attempts, FinalHash: "", CostUSD: p.cumCostUSD,
Detail: mark.detail, FirstFlagReason: mark.firstFlag,
}); uerr != nil {
r.Log.ErrorContext(ctx, "could not mark the stopped position; its money is recorded but the chunk will read as never started",
"stage", p.stage.Name, "chapter", p.chunk.Chapter, "chunk", p.chunk.ChunkIdx, "err", uerr)
return
}
if mark.reason == FlagRetryUnaffordable {
r.Log.WarnContext(ctx, "re-attack denied by a USD ceiling; the position is marked paid-but-unfinished so it does not read as never started, and a raised ceiling finishes it on resume",
"stage", p.stage.Name, "chapter", p.chunk.Chapter, "chunk", p.chunk.ChunkIdx,
"paid_attempt", p.inHand.attempt, "paid_reason", string(p.inHand.cls.Reason),
"cost_usd", fmt.Sprintf("%.6f", p.cumCostUSD), "detail", mark.detail)
return
}
r.Log.WarnContext(ctx, "the run was stopped over a call that had already gone out; the position is marked cancelled and the resume re-does it on the same budget",
"stage", p.stage.Name, "chapter", p.chunk.Chapter, "chunk", p.chunk.ChunkIdx, "cost_usd", fmt.Sprintf("%.6f", p.cumCostUSD))
}
// paidAfterBurns answers «was this call already paid for AND answered», asked THE WAY THE FUNNEL WILL
// ASK IT. It is the single definition of that question for every pre-gate that decides money before a
// call, and it exists because asking it any other way has now been wrong in both directions.
//
// ⛔ A FIXED ATTEMPT INDEX CANNOT ANSWER IT. runAttempt does not stop at a burned key: it walks to the
// next index at the SAME budget and buys there, so after a stopped run a position reads «attempt 0
// burned, attempt 1 paid and answered». A probe that looks only at the starting index sees the burn and
// says «not paid» — and the caller, finding no budget left, discards a translation that was already
// bought. A probe that ignores burns says «paid» — and the caller skips its budget check while the
// funnel goes and buys the work again. Both were measured, one after the other, on this very code.
//
// So the probe walks exactly as the funnel walks (stagerun.go, the burn loop) and answers about the key
// the funnel will actually use. Two questions asked one way cannot disagree; that is the whole point of
// this function existing rather than three call sites each getting the walk right.
func (r *Runner) paidAfterBurns(st config.Stage, model, snapID string, ch chunk.Chunk, attempt, maxTokens int, msgs []llm.Message) (bool, error) {
for {
cp, err := r.Store.GetCheckpoint(RequestHash(r.attemptRequest(st, model, snapID, ch, attempt, maxTokens, msgs)))
if err != nil {
return false, err
}
if cp == nil {
return false, nil // the walk ran out of keys: nothing here is paid for
}
if !burnedByCut(cp) {
return true, nil // an answer — this is what the funnel would serve for $0
}
attempt++
}
}
// resolvedForResume says a stored disposition is an ANSWER the resume may serve without calling
// anybody. Everything terminal is; two rows are not, and they are not for the same reason — each
// records something that was never DONE rather than a verdict about what came back:
//
// - `cancelled` — a person stopped the run over a call that was already on the wire;
// - `retry_unaffordable` — the first attempt was paid for and flagged, and a ceiling refused to
// reserve the retry. It is the one flag money CURES (D4: raise the ceiling, resume, the reader gets
// a good translation), so a resume must re-attack it rather than serve the shortfall as an answer.
// Re-attacking costs nothing while the ceiling stands: attempt 0 replays from its checkpoint for $0
// and the refusal recurs, which is why the re-attack is safe to repeat on every resume.
//
// Reading either as terminal degenerates the whole construction into its opposite — never re-doing
// anything that was interrupted — which is the failure mode this design is most at risk of, since it
// would look perfectly green.
func resolvedForResume(cs *store.ChunkStatus) bool {
return FlagReason(cs.FlagReason).AnswersForResume()
}
// ResolvedForResume is the same question for the CLI, which has to split a book's rows into «answers the
// next run serves for $0» and «positions it will do again».
//
// ⛔ IT IS EXPORTED RATHER THAN RE-WRITTEN THERE, and the reason is a defect that existed for the length
// of one edit: the stopped-run account had the split spelled as `FlagReason == FlagCancelled`, a second
// copy of this rule — and the day a SECOND non-resolved reason existed (retry_unaffordable) that copy
// started counting a position the resume re-does as one with a verdict, telling an operator the rest is
// served for $0. One predicate, asked by both surfaces.
func ResolvedForResume(cs *store.ChunkStatus) bool { return resolvedForResume(cs) }