Release v0.0.202: governed /goal + /loop, clock_sleep, context-overflow follow-ups - #228
Merged
Conversation
…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
…output 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cuts v0.0.202 to
main. Two bodies of work:Governed
/goal+/loop(new)/goal pause | resume | status(only active goals steer; paused goals go quiet; persists with legacy-string upgrade).clock_sleepagent tool: interruptible, capped, in-turn wall-clock pause for autonomous /loop runs #225 —clock_sleepagent tool: interruptible, 12h-capped, feature-flagged (--clock-sleep/GRAFF_CLOCK_SLEEP=1), root-only./loopcontinuation replacing the old[harness note]string: runs autonomously and stops with a named outcome (accepted/blocked/cancelled/exhausted) on real completion (attempt_completionor finished checklist), with a 25-turn safety cap; Esc/steer cancels./goallightweight).zig build testgreen; live-verified end-to-end (goal lifecycle +/loopstopping "accepted" and marking the goal complete).Context-overflow follow-ups (release/v0.0.202)
Tagging
v0.0.202after merge triggers the release build (all 6 desktop targets); the macOS CLI binaries are then notarized locally.