Skip to content

Sign Kimi accounts in on kimi.ai or kimi.com and copy sign-in links - #1733

Closed
justrach wants to merge 79 commits into
release/v0.302.52from
feat/signin-link-copy
Closed

justrach wants to merge 79 commits into
release/v0.302.52from
feat/signin-link-copy

Conversation

@justrach

Copy link
Copy Markdown
Owner

Kimi runs two sign-in servers: auth.kimi.ai for international accounts and auth.kimi.com for mainland China. graff login kimi only used kimi.com, so international accounts were sent to a page they do not use.

  • graff login kimi picks the site from KIMI_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.
  • The login records oauth_host; refresh goes back to that server. Logins without it keep refreshing on kimi.com.
  • Device poll honours expires_in, adds 5s per slow_down, reports an unreachable server and a declined sign-in. Kimi OAuth moves to 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 says so. Over SSH or with GRAFF_NO_BROWSER it no longer opens a browser the user can't see.
  • Piped output (ACP clients driving 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.

justrach and others added 30 commits October 25, 2026 00:11
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>
justrach and others added 26 commits October 31, 2026 23:58
Changelog, release notes, and --version what's-new for v0.302.62, and ratchet the tier-1 test count baseline to 3576.
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.
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.
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.
…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.
… 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.
…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.
# Conflicts:
#	src/cli.zig
#	src/commands_model.zig
#	src/main.zig
#	src/startup.zig
@justrach

justrach commented Nov 2, 2026

Copy link
Copy Markdown
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.

@justrach justrach closed this Nov 2, 2026
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.

2 participants