Skip to content

seal the reply before its maintenance and bill unrecorded loop steps - #456

Merged
xandreeddev merged 1 commit into
mainfrom
fix/m4b
Nov 2, 2026
Merged

xandreeddev merged 1 commit into
mainfrom
fix/m4b

Conversation

@xandreeddev

@xandreeddev xandreeddev commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

What

  • A run's memory maintenance can no longer cost its reply. It now runs after the turn's completed or partial reply is recorded, drained and sealed (TurnMemory.afterReply, TurnWriter.seal), still inside the turn, billed by its call recorder and triggered on the run's reported input. An ordinary maintenance failure is logged and skipped (writer, fence and close failures stay hard). A cancel, a time limit or a shutdown during it stops the maintenance only, and the turn ends completed with its reply.
  • A process death during that work leaves the turn sealed in the session's head state (stored format 17, HeldTurn.sealed: the ending and the reply's closing drafts). A reap or takeover ends it completed and stores that reply with its text, and the turn is not resumable, so the model is not asked again. Only what the holder held after the seal (the summary it was writing) is lost; the next run's start maintains it. A begin that is refused while such a turn's holder is gone (a resume of it, which is finished) reaps it in a commit of its own, so the inbox drainer never retries resuming a sealed turn and moves on to queued work.
  • Each run's start (turn-start) is still the safety net. It is now calibrated by the last stored selected response (TurnMemory.acceptedBefore: the last step.usage with its step's context.built), so calibration carries across runs and reopened sessions.
  • The summary strategy keeps summarizeAt: "turn-end" as its default. Manual stepRequestOf → StepLoop.run compositions get the same lifecycle with nothing to call after the loop, so the unreleased prepareRun is removed.
  • turnBilledUsageOf and telemetry bill each loop step's selected usage when no Loop call names that step. Before, any recorded call hid every selected response. The call recorder reads only its own turn's rows, and a step's ceiling is lowered to a catalogued model window less its output cap.
  • Migration: custom TurnMemory implementations add deferAfterReply, afterReply and acceptedBefore. TurnWriter.seal is optional, and a writer that has it reruns sessionsConformance (new sealed check, with a careless self-test). The changelog names the event kinds reserved since 0.14.0 (memory.tool-presentation, tool.intercepted, inbox.taken, model.called, request.part, memory.snapshot): 0.14.0 let a host event take those names.

Checks

  • bun run typecheck: pass
  • bun packages/foundry/src/main.ts check --config packages/foundry/foundry.config.ts: pass
  • bun test: 3806 pass, 0 fail
  • bun run foundry demo, build:packages, verify:packages, docs:generate (no diff), bun run --cwd packages/website check, the models plugin proxy tests (on Bun 1.3.0, not 1.3.6): pass

A run's memory maintenance ran between its loop and the turn's end, so a
cancel, a time limit, a shutdown or a process death during a slow summary
stored the turn interrupted with no reply and the resume asked the model
again, and a failing summary failed a completed turn. Each accepted
response now hands its run's turn-end maintenance to the turn
(`TurnMemory.deferAfterReply`); once a completed or partial reply is
recorded and drained, `TurnMemory.afterReply` seals the turn
(`TurnWriter.seal`: stored format 17's `HeldTurn.sealed` keeps the ending
and the reply's closing drafts) and runs it inside the turn, billed by its
recorder and on the run's reported input. An ordinary failure is logged
and skipped while writer failures stay hard; a cancel, a time limit or a
shutdown stops the work only and the turn ends completed with its reply;
a reap or takeover after a process death ends it completed with the
sealed reply, and it is not resumable. A begin refused while such a
turn's holder is gone reaps it in a commit of its own, so a refused resume
still stores it and the inbox drainer moves on to queued work. A run's start remains the safety
net, calibrated by the last stored selected response
(`TurnMemory.acceptedBefore`), and manual `stepRequestOf` compositions get
the same lifecycle, so `prepareRun` is gone.

`turnBilledUsageOf` and telemetry bill each loop step's selected usage when
no `Loop` call names that step, instead of letting any recorded call hide
every selected response; the call recorder reads only its own turn's rows;
and a step's ceiling is lowered to a catalogued model window less its
output cap. The changelog names the event kinds reserved since 0.14.0.
@xandreeddev
xandreeddev merged commit 26f4a42 into main Nov 2, 2026
1 check passed
@xandreeddev
xandreeddev deleted the fix/m4b branch November 2, 2026 14:23
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