Skip to content

Release v0.0.202: governed /goal + /loop, clock_sleep, context-overflow follow-ups - #228

Merged
justrach merged 12 commits into
mainfrom
feat/governed-runs
Jul 15, 2026
Merged

Release v0.0.202: governed /goal + /loop, clock_sleep, context-overflow follow-ups#228
justrach merged 12 commits into
mainfrom
feat/governed-runs

Conversation

@justrach

Copy link
Copy Markdown
Owner

Cuts v0.0.202 to main. Two bodies of work:

Governed /goal + /loop (new)

Context-overflow follow-ups (release/v0.0.202)

Tagging v0.0.202 after merge triggers the release build (all 6 desktop targets); the macOS CLI binaries are then notarized locally.

justrach and others added 12 commits July 13, 2026 17:50
…193 follow-up)

#195 closed the codex/gpt-5.x context-overflow gap but left anthropic and openai
exposed: they had neither a per-tool-output cap (only .responses caps
function_call_output, via normalizeResponsesHistory) nor the in-turn recovery
that reclaims room and retries when the backend rejects an over-window request
(that lived only in the codex .err branch). A dense tool-output burst — or one
uncapped result (a runaway MCP tool, a big fetch on a small-window model) — that
slips past the byte/4 pre-send estimate still died on those providers.

Two provider-agnostic layers, mirroring the #195 split (bound + recover):

1. Per-output cap (agent_compact.capOversizedToolOutputs): at send time bound any
   single tool output to Provider.perOutputCap() = context*2 bytes (~50% of the
   window in est tokens) across all three wire shapes, reusing the existing
   truncateToolOutput. Window-proportional, so large-context models (>=128k
   window: every existing tool cap already fits) keep full results untouched;
   only a result big enough to threaten the window on its own is trimmed, with a
   marker. Guarantees no single output can alone overflow past what emergencyTrim
   can reclaim (it keeps the most-recent outputs verbatim). Runs in the same
   pre-loop slot as normalizeResponsesHistory, so the codex WS delta stays
   consistent (buildBody re-reads the trimmed history after).

2. In-turn recovery for anthropic/openai (agent_request.recoverContextOverflow):
   isContextOverflow() detects the rejection across wire formats, pins the meter
   to the window, emergency-trims and retries the request once (shared
   context_retried guard so a second overflow falls through and never loops).
   Wired into all three error branches an overflow can surface through — streamed
   error event, non-streamed {"type":"error"} envelope, and the generic
   apiErrorMessage path. These formats resend full input each rebuild, so no
   closeCodexWs re-anchor is needed. The codex branch keeps its own inline copy
   (it must closeCodexWs) and now shares isContextOverflow for detection.

Verified: zig build test 179/179 (adds isContextOverflow, recoverContextOverflow,
capOversizedToolOutputs unit tests), zig build + zig fmt clean; a 5-lens
adversarial review of the diff (control-flow, provider-routing, codex-WS
watermark, regression, tests) returned zero findings.

Co-Authored-By: blackfloofie <265516171+blackfloofie@users.noreply.github.com>
…output

After Enter, readline wrote a single CRLF so the model's first output line
butted directly against the submitted prompt, making it hard to see where the
response started. Emit a second newline so there is a clear blank-line gutter.

TUI half only; the GUI transcript renderer needs the same gutter to not
diverge (tracked in #205).

Co-Authored-By: blackfloofie <265516171+blackfloofie@users.noreply.github.com>
…202)

The #193 follow-up (b227ef8) added the per-output cap + in-turn overflow recovery
for anthropic/openai but left four holes the codex/.responses path had already
closed (#174/#175/#195). This brings the other providers to parity and makes
truncation visible everywhere.

G1 (#201) — recent-output wedge. perOutputCap() was context*2 bytes (~50% of the
window each), but trimOldestToolOutputs keeps keep_recent=4 outputs verbatim, so
four recent large results pinned ~2x the window past what recovery could reclaim
-> hard wedge. Cap is now ~1/8 of the window (context/2 bytes) with a 256 KB
absolute ceiling, so 4 recent outputs occupy ~50% of the window and stay
reclaimable; the ceiling also bounds one pathological result on huge windows
(codex uses a fixed cap with no recency exemption).

G3 (#202) — recordUsage froze the meter when the provider omitted usage (early
`orelse return`), so the between-turns compaction gate could never fire. It now
floors last_context_tokens at max(fullInputEstimateTokens, req_body_len/4), the
same fallback recordUsageResponses already applies (#174). recordUsage now takes
req_body_len; both call sites pass body.len.

G6 (#202) — capOversizedToolOutputs truncated silently (call site discarded the
count). It now emits a tracer note + an interactive status line when it fires;
the model already saw the inline marker.

G10 (#202) — every in-place trim zeroed the meter, opening a transient blind
window and clobbering an overflow recover-pin. Both trim sites now re-estimate
(fullInputEstimateTokens) instead of setting 0.

fullInputEstimateTokens is now member-aliased on Agent so agent_compact can call
it. Tests: +perOutputCap (#201), +recordUsage-fallback (#202); the trimOldest /
capOversized tests now assert the re-measure contract. zig build test 181/181,
zig fmt clean.

Closes #201
Closes #202

Co-Authored-By: blackfloofie <265516171+blackfloofie@users.noreply.github.com>
…red overflow detection + metering parity (#203, #204)

Follows ea230cc. Closes the local/unknown-model wedge cluster (#203) and the two
metering-parity items (#204). Together with #201/#202 this breaks the compound
wedge chain end to end (bad window guess -> undercount -> gate never fires ->
needle miss -> no recovery).

G8 (#203) — unknown/local models defaulted to a 200k guess, mis-sizing every cap
and threshold. GRAFF_CONTEXT / GRAFF_CONTEXT_WINDOW now declares the real window;
contextWindowFor applies it ONLY when the model isn't catalogued (new
pricing.isKnownModel), so a global override can't shrink a known model that
happens to be 200k. Root fix — cascades to compactAt, perOutputCap, and the gate.

G4 (#203) — the pre-send gate estimate omitted the system prompt + tool schemas
and under-fired. inputOverCompactThreshold now adds a baseline for that fixed
prefill, clamped to 1/8 of the window so it can't dominate a small local window.
Kept OUT of fullInputEstimateTokens, which must stay pure over self.messages for
the unit tests.

G2 (#203) — overflow was detected only by ~6 English substrings. isContextOverflow
now matches the structured error.code (context_length_exceeded /
context_window_exceeded) first, so a local or non-English provider whose message
differs still recovers; substrings remain the fallback. error.code is extracted at
the two envelope branches (streamed + non-streamed); codex/.responses and the
generic path pass null (their English messages match). HTTP-status detection was
intentionally not added — codex itself reads the code from the stream, not a 400,
and the code check already covers the local-provider case.

G5 (#204) — a same-format provider switch kept the previous model's absolute token
count against the new window. applyProviderInner now re-estimates the meter from
the (kept/translated) history after the switch.

G9 (#204) — the 80% compaction threshold was hardcoded. GRAFF_COMPACT_PCT overrides
it (1..100), and unlike codex's one-directional clamp it may lower OR raise.

Tests: +contextWindowFor (#203), +compactAt-override (#204); isContextOverflow gains
structured-code cases; recoverContextOverflow / isContextOverflow call sites updated
for the new code param. zig build test 183/183, zig fmt clean.

Closes #203
Closes #204

Co-Authored-By: blackfloofie <265516171+blackfloofie@users.noreply.github.com>
Drives the real REPL in a pseudo-terminal (reusing scripts/pty_harness.py),
submits a local /help, and asserts the rendered transcript has a blank line
immediately before the command output — the gutter added in 2bf68dd. Anchored on
the output line (not a fixed index) and rendered rather than raw, so it's robust
to echo-redraw timing and ONLCR. Deterministic across repeated runs; the existing
scripts/test-pty-repl.py still passes against the same binary.

Co-Authored-By: blackfloofie <265516171+blackfloofie@users.noreply.github.com>
… test (#203)

Extends the G2 work in 3a603d0. The structured-code detection covered the
anthropic-style {"type":"error"} envelopes, but the GENERIC apiErrorMessage path —
where openai-compatible errors ({"error":{...}}, no top-level "type") actually
land — still passed null for the code. So a local provider (LM Studio, mlx, any
openai-compat base_url) whose message text we don't match on would NOT be
recognized as an overflow. New errorCode(root) pulls root.error.code (or a
top-level root.code) and threads it into recoverContextOverflow there.

E2E test (scripts/test-pty-overflow.py): points graff at a mock lmstudio backend
(127.0.0.1:1234) that injects an error and drives a real terminal turn.
  A. code=context_length_exceeded with a Dutch message (matches no English
     substring) -> graff pins the ctx meter to the window (200k/200k, 100%),
     proving detection went through the structured code, and stays responsive
     (runs /help, exits cleanly) instead of wedging.
  B. code=rate_limit_exceeded, non-overflow message -> meter must NOT pin,
     proving detection is precise.
Deterministic across repeated runs; skips only if 127.0.0.1:1234 is already held.

Tests: zig build test 184/184 (+errorCode unit test), zig fmt clean, +E2E PTY test.

Refs #201 #202 #203
…recovery-path parity + local-model wedge) into release/v0.0.202
…sume|status (#223)

Replace the bare `Agent.goal: ?[]const u8` with a structured `Goal` (objective + a
frozen `GoalStatus` {active,paused,blocked,complete,budget_limited} + created/updated
timestamps). Only .active goals steer a turn; `/goal pause`/`resume` flip the status,
`/goal status` reports it, and pause/resume/status never start a turn. Persists as a
nested JSON object in the session file, with a backward-compatible loader that upgrades
legacy bare-string goals to active. Extracts a pure `goalFromValue()` with round-trip
tests (legacy string, object, paused-stays-paused, unknown-status fallback); the
steering-gate test now covers active-vs-paused.

Keystone for #224 (per-goal budget) and #226 (controller-authorized /loop continuation).

Co-authored-by: Codegraff <blackfloofie@codegraff.com>
…agged (#225)

Adds a model-facing clock_sleep meta tool that pauses the current turn for
an interruptible, capped wall-clock duration, ported from openai/codex's
clock.sleep (MAX_SLEEP_DURATION_MS = 43_200_000 = 12h). Reuses the existing
sleepInterruptible engine verbatim; root-only and off by default behind
--clock-sleep / GRAFF_CLOCK_SLEEP=1, so default sessions never advertise it
and it flows through the same --max-tool-calls / --dedupe-tool-calls rails
as any other tool call.

Co-authored-by: Codegraff <blackfloofie@codegraff.com>
…ate (#226)

Replace the one-shot "[harness note: /loop was used...]" string with a real
continuation controller. After each autonomous /loop turn the CONTROLLER — not
the model merely stopping — decides whether to run another turn: keep going while
the goal is active and the work isn't asserted done (attempt_completion set
root.completed, or the checklist is finished), else stop with a NAMED terminal
outcome (accepted | blocked | cancelled | exhausted). A hard per-/loop iteration
bound (25) guards a never-completing model; a user steer/interrupt cancels the
run — the continuation is armed only after a clean turn and consumed at the next
readline, so errored/interrupted turns never resume it. An accepted stop marks
the goal complete.

Pure continuationDecision() + continuationSteeringNote() live in repl_glue with
per-branch tests; wired into mainloop's turn loop. Also drops the now-unused
budget_limited GoalStatus variant (per-goal budgets descoped). Budget-free.

Verified: unit tests green; a live /loop stopped "accepted" on the model's
attempt_completion after one turn.

Co-authored-by: Codegraff <blackfloofie@codegraff.com>
Add /goal (set/show + pause|resume|status|clear) and /loop (autonomous, stops
with a named outcome) to the REPL command list — they were missing — plus a
short note on how goal steering and the /loop continuation gate behave.

Co-authored-by: Codegraff <blackfloofie@codegraff.com>
@justrach
justrach merged commit ba1c14d into main Jul 15, 2026
4 of 6 checks passed
@justrach
justrach deleted the feat/governed-runs branch July 15, 2026 10: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