Repository navigation
Conversation
Kimi runs two sign-in servers: auth.kimi.ai for international accounts and auth.kimi.com for mainland China. graff only used kimi.com, so international users were sent to a page their account does not use. `graff login kimi` now picks the site from the env override, the server a saved login recorded, or a mainland clock/locale (else kimi.ai); `graff login kimi.ai` / `kimi.com` choose it outright. The login records `oauth_host` and refresh returns there; logins without it keep refreshing on kimi.com. The device poll now honours expires_in, adds 5s per slow_down, reports an unreachable server and a declined sign-in, and lives in oauth_kimi.zig. Every `graff login` copies its sign-in link when stdout is a terminal (pbcopy/clip/wl-copy/xclip; OSC 52 over SSH) and skips opening a browser over SSH or with GRAFF_NO_BROWSER. Piped output is unchanged apart from the Kimi wording, and no new line starts with a check mark. Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Connection and listener receipts bypassed the diagnostic opt-ins and could interleave with typed input. Honor those switches without changing server connection, catalog updates, or ownership safeguards. Keep boot receipts in the trace and expose them on stderr only when requested. Label Ctrl+T in both live thinking states and preserve buffered replay and row accounting. Cover the real keyboard interaction with an offline PTY fixture that keeps title requests separate from the scripted turn. Fixes #1750 Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Keep the terminal UX changes on top of the latest release fixes rather than releasing from an older baseline. Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Keep normal terminal startup quiet without changing listener ownership. Document live thinking controls and gate their offline terminal probe before publication. Check that background catalogs still join in both quiet and debug modes, and ratchet the suite floor for the added regression. Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Document the integrated resource-sharing and report-boundary changes, refresh the command-line release summary, and ratchet the unit-test floor to the verified suite total. Preserve the existing release history. Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Resolve linked prefixes against the canonical tool root instead of rejecting every symlink. Retain lexical traversal and escape denial, reject unresolved links, and permit missing write targets beneath safe directories. Cover dependency reads and listings, writes, edits, root aliases, and escape cases with regression tests and an offline trajectory. Document the shared policy and its preflight-only race limitation. Fixes #1748 Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Include the current stable fixes before cutting the next terminal UX release. Co-Authored-By: Codegraff <blackfloofie@codegraff.com> # Conflicts: # CHANGELOG.md # scripts/eval/tier1-manifest.json # src/cli.zig
Combine the quiet terminal controls with on-demand compressed help, release history, and playbooks while preserving the stable baseline and all release notes. Keep the existing mixed optimization settings unchanged. The combined regression run also exposed a fixed buffer that silently skipped the macOS background-launch shim for long paths; size it for the supported paths so background launch protections remain active. Ratchet the suite floor to the combined test total. Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Publish the combined runtime-text and terminal fixes under a fresh version because the preceding tag already exists. Keep each release summary aligned with the changes actually present in its tag and preserve all prior release history. Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Allow native file tools to follow dependency links only when every resolved prefix stays within the selected canonical root. Preserve escape, dangling-link, cycle, and extra-root boundaries; this remains a preflight guard rather than an atomic filesystem sandbox. Carry the verified change onto the stable baseline without merging into main, resolve the ADR index numbering collision, refresh release notes, and ratchet the test floor to the combined suite. Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
The integration renumbered the record to avoid an existing ADR number; keep its title consistent with the filename and index. Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
The identical-batch guard in tool_repeat.zig skips batches that are still waiting on work: pending or cancelled results, and shell job polls that report `[job N: running...]`. An agent_output snapshot of a background subagent that has not finished returns `[agent N: running]` (subagent.agentStatusText) and was counted, so a parent waiting on a child with wait_ms=0 on a non-interactive surface (ACP, headless, a workflow child) could hit the no-progress notice and have its turn ended while the child was still working. - fingerprint() now treats any `[agent N: running]` line as waiting, covering agent_output with `ids`, where per-agent reports are joined by blank lines. A finished report still counts, so a loop re-reading a completed agent still stops on the 8th identical batch. - clock_sleep stays counted: it has no rail bypass (#225), and a sleep between re-checks already puts a different batch between them. Fixes #1706 Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
read_image took the media type from the path suffix (vision.imageMediaType, image/png for anything unknown), and so did its downscale branch and vision.stageImagePath (/image, paste, GUI @[path] attachments). A WebP saved as .png went out as image/png; the provider rejected the block, and because the block stays in the transcript every later turn failed the same way. Add image_sniff.zig: sniff() recognizes PNG, JPEG, GIF and WebP by their signatures, sniffBase64() does the same off the first 12 decoded bytes, and resolveDeclared() lets recognized bytes override a declared type. - read_image sniffs the file head before staging and the encoded bytes before queueing (so a downscale temp is typed by what was written). Bytes that are none of the four formats are refused with a tool error naming the supported formats; nothing is queued. The extension only labels the note a text-only model gets. - stageImagePath types the bytes it encodes and returns a new StageResult.unsupported (with a stageMessage line) for non-raster bytes. - mcpImageBlock (MCP tool results and ACP image prompt blocks) and the JSON/serve image_base64 path keep the declared type only when the bytes are unrecognized; recognized bytes win. - readline_data_uri's magic check now uses the shared sniffer. imageMediaType stays as a label-only fallback. Fixes #1762 Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
A turn that ended in an error before any reply (retries exhausted, ApiError, any other non-cancel failure) left its user prompt as the last row of history. turn_dedup.enqueue skips a prompt only when history already ends on that exact user row (#714), so resending the identical prompt after the endpoint recovered was treated as a host replay and the turn finished without a request. turn_dedup.settleFailedTurn now closes such a turn with an assistant row: whatever streamed, then "[request failed: <last_api_error or error name>; no reply was received]". It is a no-op when the turn already left a row after the prompt (cancel and stream-end markers, tool results), so those paths keep their own text. The REPL/JSON main loop (ApiError and other failures), the ACP live turn, and the one-shot failure path call it before saving the session, so the boundary is persisted and survives a checkpoint reload. A pending prompt with no recorded failure is still deduped, and the request retry loop, cancellation, and provider fallback are unchanged. Tests: unit coverage in turn_dedup_tests.zig (retry after failure starts, stuck latch never trips on real retries, partial/error-name text, no double settle, #714 replay still skipped, boundary survives save plus history_wire.prepare on every wire), and tier-2 case retry-after-failed-request-1729 (fails without the fix: one request). Fixes #1729 Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
An SSE body holding only an error event could still fail reassembly and
end as an unparseable response (or, on Interactions, a silent empty turn)
instead of reaching the api-error path:
- Anthropic Messages: assembleAnthropic recognized only typed
{"type":"error"} frames; an untyped {"error":{...}} frame from a
compatible proxy, or a bare frame under `event: error`, reassembled to
null and fell through to the plain-JSON parse.
- OpenAI Responses: parseResponses skipped any frame without a string
`type`, so the same shapes left the body Unparseable ("unparseable
codex response"), and ResponsesFailure dropped error.type, so the retry
classifiers saw an empty class.
- Interactions: the assembler had no error handling; an error event
reassembled as an interaction with no steps.
- Chat completions was already handled by the first #748 pass; it now
also keeps a string `error` as the message and honors `event: error`.
New sse_error_event.zig owns the recognition: namesError tracks the SSE
`event:` name, and envelope() turns a typed, untyped, string, or named
error frame into the {"type":"error","error":{...}} root request()
reports, filling message/code from top-level fields and using Google's
`status` as the type. Every assembler checks it before reading normal
events. Responses failures now carry etype, and request() passes it to
afterServerErrorOrParseReject, so an invalid_request_error stays off the
short-flake ladder while overloaded/server errors keep the bounded
transient retry.
Fixes #748
Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
After the workspace tool moved the root session, graff told the ACP client only at turn end (acp_workspace.emitChange, compared against a snapshot taken before the turn). A client that serves its own path-resolving tools, such as an image attachment tool rooted at the session folder, kept resolving later calls in the same turn against the old tree and refused images in the new one. The payload also carried only graff/worktree, which is null for a main checkout, so a switch there named no folder at all. acp_workspace now remembers the cwd the client was last told about. The ACP event stream calls emitMoved after every root tool result, so the switch is reported as session_info_update right after the workspace tool's own update and before the model's next call. Turn-end emitChange reports only what was not already sent (a /workspace use slash switch). The update's _meta adds graff/cwd, the session's absolute working directory, next to graff/worktree. graff's own image attachment (read_file on an image) already resolves through the native tools' confinement against the current cwd; a regression test pins that after enterPath an image in the switched tree attaches and one outside both trees is refused. ADR 0202 and the ACP compatibility table describe the new timing and field. Refs #1737 (the client must also re-root its image tool on session_info_update) Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
subagent_retained.allocate rebuilt the checkpoint path from the process cwd on every call: <cwd>/.graff/sessions/<family hash>/workers/<task_id>.json. The worker's checkpoint was written under the folder that was current at spawn, but subagent_resume and agent_message looked it up under the folder current at follow-up time. Any chdir in between (the workspace tool or /workspace use, ACP cwd adoption on session/new or session/load, re-entering a saved tree) sent the lookup to a tree with no checkpoint, and the tool forwarded the raw readFileAlloc error as "Worker follow-up refused: FileNotFound". create now records the root each task_id was saved under in a small process-wide table, and allocate prefers that root over the cwd, so a follow-up in the same process finds the worker wherever the parent has moved. Ids the table does not know (another process, evicted) still use the cwd. subagent_resume maps load failures to actionable text through retained.loadFailureText: a malformed task_id, a missing checkpoint (with the likely reasons and the instruction to brief a new worker), another parent's worker, a forgotten worker, and a worker that is still running or locked are now distinguishable instead of a bare error name. Fixes #1724 Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
…t as scope changes A session whose user withdrew or delegated verification steps could not finish. Omitted verification items are restored (#844), and the "cancelled" status added in #1545 accepted a verification item only after a newer user message, so a scope change given as an ask_user answer was refused. attempt_completion kept deferring with "completion deferred: N unresolved verification item(s) remain". todo_write items take a "reason" string, and it is required when the status is "cancelled". A write that cancels any item without one is rejected whole and leaves the list unchanged. A re-listed item that was already cancelled keeps its recorded reason. The reason is one trimmed line, capped at 160 bytes, and is saved with the session. The cancel guard's request fingerprint (todo_cancel.askFingerprint) now includes the count of recorded ask_user answers. An answer that arrives after a verification item was written unlocks its cancellation, as a newer user message already did. A cancelled item stays on the list. It no longer counts as open work and does not block completion, but taskVerified still treats it as unverified. renderTodos and the todo_write reply show it as "[-] content (cancelled: reason)". The line REPL's WORKING block draws it as "⊘ content (reason)", leaves it out of the done/total count, and treats a list whose other items are all done as finished. ACP has no plan update; clients see the reason in the todo_write call's raw input, and a rejected write closes its row as failed. The cancel rules moved from goal_todo.zig into the new todo_cancel.zig. ADR 0305 records the decision. The SDKs regenerate with no diff because they list only tool names. Fixes #1520 Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
…ed (#1520) missingReason rejected a write that re-listed an item cancelled in an older session, where no reason was recorded, so a model re-sending its list unchanged after an upgrade had the whole write refused. An item whose prior status is already cancelled is not a new cancel; skip it. Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Changelog, release notes, and --version what's-new for v0.302.57. Re-pin the codex .responses arm source test to the #748 call that passes the failure's error type to the retry policy, and ratchet the tier-1 test count baseline to 3542. Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Boot awaited every live catalog GET (Kimi always, router specs with a stale cache), and the graff/models gateway refresh joined its HTTP worker on timeout. A worker parked in DNS or TLS cannot be cancelled, so a slow host held initialize for seconds and ACP clients reported "graff model discovery timed out". Catalog GETs now run on detached workers (catalog_bounded.zig) with their own Io and allocator; their only output is the disk cache. Cached catalogs activate immediately and refresh behind; only uncached ones wait, together, up to 800ms. Kimi's catalog gains a 6h disk cache. The fleet elite pull starts with io.concurrent so a busy pool cannot run it inline. ADR 0306. Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
noteConstraint rejects text over playbook.max_text (240 bytes), but the tool schema never said so, so a model learned the limit only from the rejection. The tool description and the `text` field now state it, and the rejection message, all from the same constant. Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Changelog, release notes, and --version what's-new for v0.302.58, and ratchet the tier-1 test count baseline to 3545. Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Io.Threaded.init installs a process-wide no-op SIGIO/SIGPIPE handler and deinit restores the action it replaced. A catalog worker outlives the Io around it, so its deinit could reinstate the default SIGIO action another Io had replaced; that Io's next cancellation signal then terminated the process (seen as a test runner killed by SIGIO on Linux). Workers now keep the no-op handler installed. Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Changelog, release notes, and --version what's-new for v0.302.62, and ratchet the tier-1 test count baseline to 3576.
Release v0.302.62
A provider can hold a sign-in and a metered API key at once. Every
terminal authentication rejection reported "provider authentication
failed: sign in again or check the API key", so a rejected API key
(including the key a spent plan hands over to) read as signed out to
clients and prompted a sign-in that cannot fix it.
auth_recovery.failureText now words the failure after Provider.source
and credential_failover.promoted: a sign-in seat keeps "sign in again"
and names its login command; a key seat and a promoted key say "API key
was rejected" with the source label and none of the signed-out phrases.
quota.handle and the async-tools-started path both use it.
Key seats also carry {"kind":"api_key_rejected","provider":<id>}: ACP v1
error.data on -32603, ACP v2 _meta["graff/recovery"]. reauth_required is
unchanged. Provider failover and subagent failure classes accept the new
texts. ADR 0310.
Name the rejected credential in auth failure text
Changelog, release notes, and --version what's-new for v0.302.63, and ratchet the tier-1 test count baseline to 3579.
Release v0.302.63
An Anthropic Console token past its expiry with no refresh token stored cannot be renewed: `ant auth print-credentials --access-token` warns, exits 0 and prints the same expired token. graff took that as a successful refresh, so the dead token was seated (or parked behind a Claude plan as the fallback) and every request it carried came back 401. Treat a renewal that returns the old token as failed. At startup an expired, unrenewable profile is neither seated nor parked; mid-session the refresh offers no token rather than the expired one. Errors name the cause: "Anthropic Console sign-in has expired: run `graff login console` again", the promoted fallback variant, and a spent plan whose fallback was skipped for this reason.
Skip an expired Console sign-in that ant cannot renew
Changelog, release notes, and --version what's-new for v0.302.64, and ratchet the tier-1 test count baseline to 3580.
Release v0.302.64
Claude Code runs Opus/Fable/Sonnet/Haiku 5.x at 1M natively and its result reports contextWindow 1000000, but graff's catalog listed 200k for every claude-agent model. graff compacted at a fifth of the window and clients showed 200k. The rows now say 1M, and a `<id>[1m]` name resolves to 1M instead of the unknown-model default.
Changelog, release notes, and --version what's-new for v0.302.65, and ratchet the tier-1 test count baseline to 3581.
Release v0.302.65
…nge (#1786) A lean -p first completion with no local tool call was bounced with the file-edit note. Hosted search runs inside the model call and never counts as a local tool call, so a search-backed answer looked like a described change; the follow-up reply then replaced it. Skip the bounce when the completion's output items include a hosted search call.
Keep a hosted-search answer instead of bouncing it as a described change
Changelog, release notes, and --version what's-new for v0.302.66, and ratchet the tier-1 test count baseline to 3583.
Release v0.302.66
… task
When graff's tool list changed (usually load_tool_schemas), the Claude
Agent transport removed its SDK MCP server from Claude Code and added it
back so Claude Code would re-read tools/list. The load_tool_schemas call
was still held at that moment, so Claude Code failed it with "Connection
closed" and the model often ended its turn there, or later got "No such
tool available". graff now sends MCP notifications/tools/list_changed for
its server and serves the tools/list Claude Code sends back before
anything else; the server stays connected and held calls stay open. If
the list is not re-read within 15 s, a fresh session starts. Results
sent after a refresh tell the model that graff's tools are named
mcp__graff__<name>, since load_tool_schemas prints bare names and the
model's first call to a just-loaded tool used one.
A turn started by a peer wake ("peer_message action=inbox") preloads
peer_message with its first request, as a request naming subagents
preloads subagent. Its first call used to be to the unloaded tool, which
Claude Code refuses itself, forcing a fresh session.
attempt_completion now settles a task like a final answer (#1137): room
mail stays parked for the next human prompt, and mail addressed to this
session can still start a turn. It used to block addressed mail too, so
whether a peer's follow-up reached a finished worker depended on which of
the two the model ended with. yield_turn keeps its strict latch.
ADR 0311.
Changelog, release notes, and --version what's-new for v0.302.67, and ratchet the tier-1 test count baseline to 3585.
Release v0.302.67
…1792) A prompt sent after a turn that ended on a tool call (attempt_completion's "completion recorded" is a user message) reached Claude Code with its images replaced by the text "[image]". freshContent took the trailing user messages as the prompt only when none held a tool_result, so the result at the head of that run sent the whole history, new prompt included, into the text checkpoint. The prompt is now what follows the last tool_result; the result itself stays in the checkpoint, and a restart mid-loop still continues from it. delta, on a live session, folded every non-result block that arrived with results into extra_text, images as "[image]". Images now ride with the last result as MCP image items (toolResult takes them), which Claude Code shows the model as images. Other non-text blocks still render as text, labelled by their kind. Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Claude Agent: keep a prompt's images when it follows a tool result (#1792)
Compaction runs on a fresh tools-off Claude Code process. Cut mid tool loop, its history ends on tool results (or a call) followed by the summary request. freshContent used to fold the request into the text checkpoint and add "Continue from where the checkpoint ends.", so the model carried on with the task or asked the user a question, and that reply was installed as the summary. #1792's change to freshContent fixed this; this pins both shapes.
Changelog, release notes, and --version what's-new for v0.302.68, and ratchet the tier-1 test count baseline to 3587.
Release v0.302.68
# Conflicts: # src/cli.zig # src/commands_model.zig # src/main.zig # src/startup.zig
justrach
added a commit
that referenced
this pull request
Nov 2, 2026
Merged
Owner
Author
|
Merged into main through #1800 (v0.302.69), on top of current main. This PR targeted an old release branch, so it is closed by hand. |
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.
Kimi runs two sign-in servers: auth.kimi.ai for international accounts and auth.kimi.com for mainland China.
graff login kimionly used kimi.com, so international accounts were sent to a page they do not use.graff login kimipicks the site fromKIMI_CODE_OAUTH_HOST/KIMI_OAUTH_HOST, the server a saved login recorded, or a mainland clock/locale (otherwise kimi.ai).graff login kimi.ai/graff login kimi.com(and/login kimi.ai) choose it outright.oauth_host; refresh goes back to that server. Logins without it keep refreshing on kimi.com.expires_in, adds 5s perslow_down, reports an unreachable server and a declined sign-in. Kimi OAuth moves tooauth_kimi.zig.graff logincopies its sign-in link when stdout is a terminal (pbcopy, clip, wl-copy, xclip; OSC 52 over SSH) and says so. Over SSH or withGRAFF_NO_BROWSERit no longer opens a browser the user can't see.graff login) is not copied; the Kimi intro still says "open this URL", the code line is unchanged, and no new line starts with a check mark.Tier 1 green; cross-compiles for x86_64/aarch64 Linux and Windows.