Kin / Agents & automation

Local clients

Host and attach to local Kin conversations over stdio or a per-user Unix socket.

Read as Markdown

Local programs and personal browser homes can host Kin conversations through the same harness, permissions, and journal as the terminal.

Form Use
kin --stdio Spawn one conversation and exchange typed JSON lines over its standard input/output.
kin --serve Host conversations across windows on a per-user Unix socket, with one writer and multiple observers.
kin --attach Join a service conversation from the terminal.
kin -p --attach Run unattended work in the service and leave human decisions live for another client.

The protocol is local: stdio opens no socket, and the service checks peer uid on its Unix socket. Neither opens a TCP port. See headless runs for unattended output and exit codes.

kin --stdio --workdir ~/code/project              # a fresh conversation, spawned by the client
kin --stdio --workdir ~/code/project --resume ID  # exactly one saved conversation
kin --serve                                       # the per-user service on $XDG_RUNTIME_DIR/kin.sock

Rename conversation metadata

Gate editing on features.session_names: 1, advertised in welcome and live conversations discovery. Protocol remains 1. A service management connection can query it without opening a conversation or constructing a model:

{"type":"hello","protocol":1,"client":"example","manage":{"action":"capabilities"}}

It returns {"type":"management","action":"capabilities","features":{"session_names":1},"name_max_chars":256}. An older companion lacks this signal; do not emulate saving with client preferences or journal writes.

For a live or saved root, send a separate management hello:

{"type":"hello","protocol":1,"client":"example","manage":{"action":"session.rename","id":"s_example","workdir":"/workspace/project","title":"Investigate invoice totals","expected_revision":1}}

After checked persistence, Kin returns:

{"type":"management","action":"session.rename","session":{"id":"s_example","workdir":"/workspace/project","title":"Investigate invoice totals","title_source":"manual","title_revision":2}}

An attached writer or observer, on the service or a spawned stdio stream, can instead send the same payload with "type":"rename" and no action. Its reply is {"type":"renamed","session":…}. Rename makes zero model calls, adds no turn, does not resume an ended conversation and does not transfer the keyboard. Live edits serialize through the owning Session; saved edits acquire Kin’s native lease. An owner elsewhere is busy, never an excuse to write around it.

Identity, bounds and refusal

The object is closed: exactly action or type, id, workdir, title and expected_revision. id is the root conversation id, not a turn id: 1–128 ASCII letters, digits, underscore or hyphen. workdir is an absolute 1–4,096-character string without NUL and must match the native workspace. Use the canonical path Kin returned, including a copy’s own workspace.

A title is a nonempty printable single line, at most 256 Unicode code points before trimming (therefore at most 1,024 UTF-8 bytes). Newlines, CR, tabs, escapes, surrogate code points and nonprinting format controls are refused, not silently stripped. Kin returns the trimmed title without truncating it. expected_revision is an integer, not a boolean: 0 <= value < 2^53 - 1.

A refusal is {"type":"refused","of":"manage","reason":"session_name","code":…,"detail":…}; attached refusals use of:"rename". Codes:

Code Meaning
malformed Invalid shape, field type, id/path or revision bound
invalid_name Empty, multiline, nonprinting or overbound title
not_found No saved conversation at that workspace/id
invalid_identity Not the requested native root, or an ambiguous identity
wrong_workspace The requested workspace does not match
not_saved The live Session has no durable journal
closing The owning Session/host is closing
busy Another native owner holds its lease
unsafe_owner Journal, directory, lease or write-lock identity is unsafe
unavailable Native locking or workspace-use admission is unavailable
incompatible Required journal state needs a newer Kin
save_failed Checked staging/sync/replacement failed; no success event is published
conflict The expected revision changed; current title/source/revision accompany the refusal

Keep the edit draft on refusal. A lost response is not proof that saving failed: re-read metadata before retrying. A conflict requires a deliberate retry against what is now current, not automatically sending an old draft with a newer revision.

Canonical presentation and ordering

Read one metadata object: id, canonical workdir, title, title_source and title_revision. Source is fallback, automatic, manual or legacy; legacy scalar titles are protected without guessing who chose their words. An unreadable newer journal uses title_source:"unavailable" and title_revision:null, and is not editable from that snapshot.

An empty new root begins at revision 0. First-input fallback capture publishes revision 1. Each rename, even unchanged text, and the single automatic upgrade increments revision; compaction, clear/rewind, conversion, resume and discovery do not. An acknowledged live change broadcasts session_titled with this metadata before the direct reply. Both roles receive it; late attachment projects current metadata, not a stale cached title. Saved sessions and live conversations rows carry the same fields. Offline rename has no attached owner to broadcast to; its reply and subsequent discovery are authoritative without reopening it.

Merge by (workdir, id, title_revision), never saved-over-live preference, event seq, modification time or socket arrival order. Ignore smaller revisions; equal revision has equal title/source. Concurrent or late renames with an old expected revision cannot undo an acknowledged newer name. Automatic titling checks ownership before starting, before committing and before publishing an in-flight result. Hosted naming uses the assigned Utility route or makes no call.

Names live in required version-1 journal metadata, not a second client name store. Older supported writers refuse updated journals rather than dropping ownership; there is no supported downgrade by stripping the record. The checked logical append stages and fsyncs journal bytes before an atomic authority switch. It retains native journal replacement’s best-effort parent-directory sync compatibility, not a new power-loss guarantee on filesystems that cannot sync directory entries.

The contract in one exchange

An idle conversation supervises settled background agents while at least one client is attached. An internal turn_started has internal:true and empty text: mark the conversation active, but add no human message. Pending human input and decisions run first. Reattachment can deliver a settlement that waited while no client was present.

A service advertising settle accepts a writer’s {"type":"close","settle":true}. It runs background work to quiescence before closing; failure returns closed with reason:"unsettled". A pending human block is still a human gate. Release, take and detach cancel the requested settle.

Writer guidance belongs to the current writer. The request head stays stable through a handover; the next human turn receives a separate update removing or restoring client guidance and naming unavailable advertised tools. Generated wakes, reports and context updates have user_text:null, are excluded from human turn counts and do not replay as the person’s messages. MCP connects only in the in-process terminal, not in this service or an attached terminal.

The session service also supports personal clients that need the complete conversation without reading journal files. After attachment, send {"type":"history"}. Kin returns history_begin, bounded history_event lines, and history_end, including unfinished root prose. next and revision from the begin line identify the older page: {"type":"history","before":80,"revision":"…","limit":80}. A changed revision requires a fresh snapshot. Append text parts marked continuation to the preceding row; do not treat them as separate messages. A client that includes "reasoning":true in its hello capabilities also receives cached reasoning from the current root turn on this latest snapshot. The cache keeps the first 64 Ki characters per message and replays it in 4000-character pieces, with content_truncated when shortened and a final done only for a completed round. Older pages and journals contain no reasoning. Ending or interrupting the turn clears this cache; restarting the service cannot replay it.

The writer can request {"type":"control"} to discover model, mode and prepared effort choices. While idle it can send setting and value for mode (auto or strict), effort, or model (also naming provider). The response separates configured provider and model from active_model, the last resolved name when known, and label, the service’s short name for the configured model (such as the Kinra Gateway’s kinra-35b-a3b) or empty. Show label when present and keep active_model as the model that answered. When efforts is only a reasoning switch, effort_switch names its levels as {"on": …, "off": …} (otherwise it is null); present them as On and Off and send the named level. Reconnect sends the current provider in session and replays model_discovered, with its label, when a resolved name is available. Observers cannot apply controls. Failures return a typed refusal.

Management needs no conversation or configured model. A separate connection can send {"type":"hello","protocol":1,"manage":{"action":"chatgpt.status"}}. Other actions are chatgpt.connect, chatgpt.cancel, chatgpt.disconnect, github.status, github.connect, github.cancel, github.repos, github.disconnect, models, unattended, unattended.set, unattended.clear, capabilities, session.rename, and sessions (with absolute workdir). Session rows include created and updated (journal modification time) so a client can distinguish start from last activity. The device connection outlives this short connection. Status distinguishes saved sign-in from an account model listing that is ready to use; provider tokens remain in Kin. The models response includes per-connection model labels and a default choice for the official Kinra Gateway when that connection is available. Its model stays the default alias; its label is the Gateway’s short name for the model, else the resolved name when metadata discovery succeeds, otherwise “Kinra default.” The service refreshes a stale or changed Kinra profile on its own, at startup and when a conversation sees a new profile revision. Discovery is bounded and cached for one minute. This suggestion does not change global settings or an existing conversation, including when ChatGPT is selected globally. chatgpt.connect only signs in: it selects no model and moves no job.

unattended reports what a kin -p naming no model would run on as {chosen, ready, route, provider, model}, never a credential. unattended.set with a connection provider and a model assigns that job through Kin’s own unattended route; a ChatGPT model must be one the signed-in account offers. unattended.clear returns the job to the Kinra Gateway default. ready is false when the chosen route no longer validates or its ChatGPT sign-in is gone, or when nothing is chosen and Kin is not connected to Kinra. An older Kin rejects an unattended assignment it does not know, so set it only after every Kin sharing the home runs a release that has it.

workspace.move renames a workspace folder and carries its saved conversations and workspace trust, as kin workspace move does. Send {"action":"workspace.move","old":"/abs/old","new":"/abs/new"}, with "check":true to report without moving. It replies with {"type":"management","action":"workspace.move","state":…}, where state is moved, already_moved, or for a check ready or resume, plus the counts of sessions, children, rewritten journals and trust grants that travelled. A refusal is {"type":"refused","of":"manage","reason":"workspace_move","code":…,"detail":…} with code one of invalid, busy, conflict, cross_device, not_found or interrupted. It refuses while a conversation uses either path. An interrupted move keeps both paths closed until the same request finishes it. Once completion has released the paths, a retry only settles recovery and keeps new conversations intact. already_moved verifies that the moved directory is still at the destination, even when it had no saved conversations or trust. An older Kin answers unknown. The CLI delegates to a running service; a disconnect or timeout after connecting reports an interruption for a retry.

The worktree.* actions work with a repository’s copies. Each takes an absolute workdir in the repository and needs it trusted (untrusted otherwise):

Action Keys Reply
worktree.list workdir main {path, branch, in_use}, copies[], ended[]
worktree.open workdir, name? copy, created, source_dirty
worktree.land workdir, name, expected_head, expected_tree, message?, keep? landed, branch, head, removed
worktree.update workdir, name, expected_head, expected_tree replayed, head
worktree.remove workdir, name, discard?, expected_head?, expected_tree? removed, kept_as?

A copy row names the copy (name, path, branch, kind, base_branch, created), what it holds (head, tree, changed, files, total, unique, ahead, behind, subjects, ignored, readable), who uses it (in_use, base_of) and its root conversations[]. ended[] lists removed copies whose conversations remain. Pass back the head and tree you showed: bringing work back and discarding act on exactly that state or refuse changed. To open a conversation in a copy, send worktree.open, then an ordinary hello whose conversation.workdir is the copy’s path; an older Kin answers worktree.open with unknown rather than opening the main checkout. Or, in one step, declare the worktree capability and add "worktree": {} (a new copy Kin names) or {"name": NAME} (made or reopened) to hello.conversation, whose workdir is the main checkout or a folder in it. Kin settles the repository’s trust first, carrying its question as for any new conversation, then opens or resumes the conversation in the copy. The welcome echoes the capability and adds worktree {name, branch, main, path}, with created and source_dirty when this hello made or reopened it; a welcome without them opened the folder itself. An observer names a copy that exists. A refused copy ends the connection with {"type":"fatal","reason":"worktree","code":…} and the same codes and facts, and Kin removes a copy it made for a conversation that did not open. A refusal is {"type":"refused","of":"manage","reason":"worktree","code":…,"detail":…} with the facts to explain it (files, total, branch, base_branch, behind, copies); code is one of not_repo, bare, unborn, untracked, invalid, not_found, exists, link, tracked, identity, uncommitted, changed, nothing, behind, main_changed, main_busy, branch_changed, detached, busy, diverged, conflict, in_use, has_work or base_of.

github.repos lists repository metadata from the connected GitHub CLI account, including owned, collaborator and organization repositories. Start at page:0 with optional refresh:true, then follow next_page until it is null. Pages carry at most 100 rows and share an account-bound snapshot; later pages never start another network listing. Snapshots refresh after one minute on page zero, retain at most 300 repositories, and mark incomplete listings as truncated. total counts retained rows. A missing connection returns ready:false and an empty list; fixed errors never contain raw GitHub output. github.disconnect cancels device login and signs out the inspected account on this machine, including an unhealthy stored account. Existing clones stay in place. Credentials stay with GitHub CLI throughout.

GitHub status reports installed, ready and the active login. Its device flow reports starting, then waiting with code and the fixed GitHub verification url, then connected, cancelled or a bounded error. GitHub CLI owns OAuth and token storage, including its private file fallback when a system keyring is unavailable. A missing or older CLI needs installation or an update; no token entry is accepted by this management seam. Closing the client leaves either device flow running; cancellation targets only its named provider, and stopping the service joins both.

A writer declaring present can receive a presentation handoff for a file within its workspace or an explicit loopback HTTP application. Render it in an isolated content origin and return the exact handoff acknowledgement. Generated content cannot approve tools or inherit account credentials.

Every line is one JSON object. The client speaks first.

→ {"type":"hello","protocol":1,"client":"front-door","capabilities":{"attention_context":true,"open_url":true,"open_workspace":true}}
← {"type":"welcome","protocol":1,"kin":"…","capabilities":["attention_context","open_url","open_workspace"]}
← {"type":"session","session_id":"s_…","model":"…","workspace":"/home/…/project","posture":"…"}
← {"type":"ready"}
← {"type":"mode_changed","mode":"auto"}
→ {"type":"turn","id":"t1","text":"What does this project's release gate check?"}
← {"type":"accepted","of":"turn","id":"t1"}
← {"type":"thinking","seq":41,"active":true,"status":"Working","phase":"requesting"}
← {"type":"thinking","seq":42,"active":true,"status":"running tools…","phase":"tools"}
← {"type":"tool_call","id":"call_1","name":"read_file","arguments":"{\"path\":\"Taskfile.yml\"}"}
← {"type":"tool_result","id":"call_1","name":"read_file","result":"…","result_truncated":true}
← {"type":"thinking","seq":45,"active":true,"status":"Working","phase":"requesting"}
← {"type":"thinking","seq":46,"active":true,"status":"","phase":"answering"}
← {"type":"stream_chunk","message_id":"m1","content":"The gate runs …"}
← {"type":"stream_done","message_id":"m1"}
← {"type":"thinking","seq":49,"active":false,"status":"","phase":""}
← {"type":"done","reason":"stop"}
← {"type":"turn_ended","id":"t1"}
→ {"type":"close"}
← {"type":"accepted","of":"close"}
← {"type":"closed"}

The events between accepted and turn_ended are the ordinary event vocabulary — the same shapes the terminal renders — with a few fields bounded for the pipe.

Folding delegated work

A local client does not need its own agent registry. subagent_started opens one presentation entry for every registered foreground or background physical run; reduce it by (agent_id, run_seq) and place it beneath parent_tool_call_id. Child tool events carry that same parent id, so they can become a folded current-step log. Correlated progress and the exactly-once subagent_completed settle the entry.

← {"type":"subagent_started","agent_id":"agent2","parent_tool_call_id":"call_17",
   "run_seq":1,"execution":"foreground","profile":"planner","launch_kind":"task",
   "context_scope":"isolated","inherited_turns":0,…}
← {"type":"tool_call","id":"call_child_1","name":"read_file",…,
   "parent_tool_call_id":"call_17"}
← {"type":"subagent_completed","agent_id":"agent2","parent_tool_call_id":"call_17",
   "run_seq":1,"execution":"foreground","profile":"planner","launch_kind":"task",
   "lifecycle":"idle","outcome":"stop","duration_s":12.4,…}

A retained continuation uses the same agent_id, increments run_seq, and belongs to the current agent_message call; its child tools are rebound to that current parent too. A background run may complete after the root turn_ended, so ending one turn must not discard live agent entries while the host remains open. subagent_dispatched (background) and tool_stream(spawns_agent=true) (foreground) remain compatibility hints for older clients, not a second lifecycle.

Presenting root work

The additive thinking.phase field is the one root-work contract shared with the Textual client. A local client does not need to infer reasoning from the presence or absence of private content:

Phase Front Door presentation
requesting “Working…”
reasoning “Thinking…” in the reasoning tone
answering No busy text; let the answer stream
tools Use the following tool_call and tool progress/result words
anything else Present status verbatim
inactive End the line

The phase transition always precedes the event that proves it. A reasoning-then-text answer projects as requesting → reasoning → answering → stream_chunk → thinking(active=false) → done → turn_ended. A tool round projects as requesting → tools → tool_call → tool_result → requesting → …. Each phase transition is an ordinary event with the normal monotonic seq; the closer is exactly one inactive event per root turn.

Reasoning text is optional. A client that declares "reasoning":true in its hello capabilities receives root reasoning deltas; the welcome echoes the accepted capability. Without it, the client receives only the reasoning phase. Each connection has its own gate, including history envelopes: a writer cannot opt an observer in. Content is capped at 4000 characters per wire event, with content_truncated when shortened. Child reasoning and child phases remain private; their correlated tool and lifecycle events carry delegated work. A client that shows reasoning should also declare reasoning_retry.

Client messages

Message Fields Meaning
hello protocol (must be 1), client (≤64 chars, optional; the terminal says terminal), capabilities (object of booleans, optional), conversation (service only) Required first message. Declares the typed handoffs the client can perform. Current capabilities are workspace_context, attention_context, open_url, open_workspace, workspace_trust, present, and reasoning (with workspace_read described below), plus the display and control opt-ins todos, steering, commands, shell_passthrough, agents, reasoning_retry, and turn_started described under Opt-in additions. Under kin --serve, conversation names the workspace, the conversation, and the role — see The session service.
turn id (≤64 chars), text (≤32,000 chars), context (optional protocol-1 compatibility, ≤8 items) Start one root turn. The text is literal turn data — no ! shell passthrough and no / skill dispatch, exactly like an interactive launch prompt. New clients keep context absent by default.
rename id, workdir, title, expected_revision Checked conversation metadata for either role, without a turn or keyboard transfer; see Rename conversation metadata.
cancel — Interrupt the running turn, as ++escape++ does in the terminal. Refused as idle when nothing runs.
resolve kind, id, and the answer Answer the exact pending blocking event: question (index, optional text), approval (outcome once/always/deny, optional note, and the display_signature the event carried), plan (index), plan_mode (enter boolean, answering a plan_mode_request), proposal (decision apply/reject), trust (decision persistent/session/decline, only for a trust_request).
handoff_result id, ok, detail (≤500 chars), capability-specific context, workspace or catalog Report what the environment did with a handoff. Only an attention_context handoff accepts a bounded context list; only a workspace_context handoff accepts a workspace object. The workspace_catalog handoff accepts the catalog described below.
close settle (service only, optional boolean) End the session: the turn is cancelled, the journal closes, the lease is released, and the last line is closed. Under the service an observer’s close detaches only that observer; the writer’s close ends the conversation for everyone. A writer that declared settle can request settle:true to finish background work before closing; failure closes with reason unsettled.
release — Service only. The writer steps down to observer; every attached client sees writer_changed.
claim take (boolean, optional) Service only. An observer takes the writer role. With take: true it takes the role from a writer that holds it, and that writer stays attached as an observer; without it, a held role is refused as busy, naming the holder. A take that is not a boolean is refused malformed. The claimer receives every pending block replayed before its writer_changed.

Ordinary turn.text and steer.text are saved separately as the journal’s user_text before trimming and context composition. A client that already rendered a skill may send optional turn.user_text (a string of at most 32,000 characters, or null for generated input). It changes only journal provenance, not the model’s text or any permission. Omit it for ordinary turns. Existing 64 KiB line bounds still apply to the whole message. A service command can likewise carry optional user_text with the submitted slash line. When absent, the journal uses null rather than inventing the person’s wording; known generated input cannot appoint the conversation’s fallback name.

End of input, SIGTERM, SIGINT, and SIGHUP close the spawned form the same way. Anything else is refused by name — never guessed at.

Explicit context attached to a turn follows the person’s text in the canonical journal and keeps its untrusted framing. A default saved-session name uses the person’s first line. Context remains available when inspecting the full turn.

A path fact names a file the person selected, and an image among them is inspected for this turn through the operator’s vision route — the same isolated call an @image mention makes, under the same gates: a known credential store is refused before the file is probed, and a missing, oversize or invalid image path simply adds nothing. Each image is limited to 4.5 MiB encoded (about 3.4 MiB raw) and 8,000 pixels per dimension. The client sends a path, never bytes, and never learns why one was declined. The framed context block is unchanged; the observation and its reference marker arrive with the turn as harness reminders.

Current workspace observations

A writer declaring workspace_context gives the root session get_workspace_context. Kin may use it to continue project work, relate a selected saved version to current work, or inspect local, saved, review and interrupted state. Ordinary file questions use ordinary file tools.

The existing environment reminder includes a short capability pointer when this tool is available. It identifies client-owned pending work as a separate source from Git and forge state. It contains no observations and makes no client request. The environment opt-out also disables this pointer.

A handoff carries kind:"workspace_context" and payload.workdir for the attached absolute directory. Answer the exact ID with ok:true and a workspace JSON object containing the same workdir, at most 16,384 UTF-8 bytes. Include observation times, unavailable services, partial lists and exact revisions where relevant. Return ok:false when observations are unavailable. The tool times out after 20 seconds. A reply from a writer that no longer supports the capability is refused.

Kin treats the result as untrusted observations. It is not instructions, permission, or proof of file contents. An interrupted operation may already have taken effect even when its receipt is incomplete. Native operations must recheck current authority and exact state. No observations are injected into ordinary turns; clients without the capability and child agents have no tool. The harness has no dependency on a particular browser or custody service.

Trusted-project discovery

A writer declaring workspace_read can offer the root-only list_workspaces tool. Require the capability in Kin’s accepted welcome; an older Kin keeps ordinary conversation and handoffs without gaining external discovery. This is a fixed Kin-owned tool, not client-defined tool registration or a new trust prompt.

The tool requests a workspace_catalog handoff with payload.workdir naming the conversation’s exact directory and payload.cursor starting at 0. Answer the pending handoff ID with ok:true and a catalog object containing exactly:

  • workdir: the same absolute conversation directory;
  • entries: at most 64 objects, each with only name and absolute path;
  • observed_at: an ISO timestamp with a timezone;
  • next_cursor: a greater integer when more candidates remain, or null.

The catalog object is bounded to 16,384 UTF-8 bytes. Names are at most 200 characters and paths at most 4,096. The tool waits at most 20 seconds; answer ok:false if the catalog is unavailable. A fresh cursor 0 clears the prior candidate set; follow next_cursor rather than treating one page as complete.

Send candidate metadata only, not file bodies, trust records, credentials or instructions. Kin filters candidates against current exact persistent workspace trust and rechecks eligibility on each external discovery operation. The accepted roots extend ls, glob, grep and search_workspace only when they name an explicit absolute path. They do not move the session, authorize writes or commands, or load destination configuration. read_file retains its existing exact-path contract.

The candidate set belongs to the attached writer, not the journal. Releasing, disconnecting or replacing that writer invalidates it; a returning capable writer requests a fresh catalog. Stale replies cannot restore it. Observers, headless runs and children do not acquire it. The native checks are not an OS sandbox; see permissions.

Space offers this only in attended conversations in Kin’s own workspace. Its catalog excludes the broad home, apps and copies. Concrete project changes use its existing reviewed session brief rather than a new automatic start mechanism.

Attention stays empty until requested

New clients do not attach ambient context to a turn. If the person explicitly asks Kin to use what is currently selected or focused, a client that declared attention_context gives the root session one conditional get_attention_context tool. Kin names the exact subset it needs from window, workspace, output, and selection:

← {"type":"handoff","id":"ho_4","kind":"attention_context","payload":{"kinds":["selection","window"]}}
→ {"type":"handoff_result","id":"ho_4","ok":true,"detail":"current focus",
   "context":[{"kind":"selection","label":"selected text","value":"one line"},
              {"kind":"window","label":"Brave","value":"Kinra — Docs"}]}

The client may return only kinds Kin requested. Labels are capped at 80 characters, values at 500, and the complete list at eight items; every usable value is framed as untrusted before it reaches the model. A missing, declined, widened, malformed, or unanswered handoff adds no context.

Protocol 1 still accepts the original push-side turn.context field for compatible clients. Its kinds also include path and note, but new clients should omit the field: merely being able to observe a window or selection is not a reason to inject it.

Host messages

Beside the ordinary events, the host writes a small set of its own:

Message Fields Meaning
trust_request id, path, replaced, persistent, reason, detail The workspace is not yet trusted and the client declared workspace_trust: Kin’s own trust question, before any workspace material loads. Answer with resolve of kind trust; welcome follows an accepted answer.
welcome protocol, kin, capabilities; under the service also conversation, role, workdir, turn, idle The hello was accepted; the accepted capabilities follow. The service adds the conversation id, the role granted, the workspace, the id of a turn already running ("" when idle), and the idle policy in seconds.
accepted of, id A turn, cancel, resolve, handoff_result, close, release, or claim was taken, or one of the opt-in messages.
refused of, reason, detail A message was not taken: malformed, unknown, bounds, busy, idle, stale, closing, untrusted (a turn while a trust request is open), protocol, or — service only — role (an observer sent a writer’s message, or a role message that does not fit the sender’s role). The session is unaffected.
writer_changed writer (boolean), client, role Service only, to every attached client: whether a writer now holds the conversation, that client’s declared name, and the recipient’s own role after the move, writer or observer. Two windows of one client share a name, so read your own role from role. An observer’s attach replay ends with one naming the current holder.
conversations conversations (rows), truncated Service only, answering a discovery hello (conversations: true): the bounded, content-free snapshot of live conversations — see Discovering conversations. closed follows and the connection ends.
turn_ended id The turn settled, whichever way, after its one inactive thinking closer. Normal and loop-managed errors also carry done; cancellation carries interrupted; a host-level error may precede settlement.
handoff id, kind, payload Kin asks the environment to perform one typed effect; answer with handoff_result.
session_titled id, workdir, title, title_source, title_revision Canonical acknowledged name metadata; merge by revision as described in Rename conversation metadata.
renamed session Attached rename succeeded after checked persistence; the complete canonical metadata is returned.
fatal reason, detail The host cannot serve — protocol, untrusted, lease, resume, backend, or host; under the service also peer (the connection’s uid is not the service’s), busy (the conversation already has a writer; carries conversation and client), and absent (no live conversation to observe) — and writes closed last. The spawned form then exits.
closed reason (service only, when the conversation ended under this client), optional detail The last line. The service says why a conversation ended under an attached client: writer (its writer closed it), idle (no writer within the idle policy), stopped (the service stopped), or unsettled (the requested settlement failed, with detail). A client’s own successful close gets a plain closed.

One turn, exact answers, bounded lines

Only one turn runs at a time; a second turn is refused as busy until turn_ended. Cancel first if the new request replaces the old one.

A resolve is accepted only for the exact pending block of the stated kind. A wrong id, a wrong kind, a duplicate answer, or an answer after the turn settled is refused as stale and resolves nothing. An approval must echo the display_signature its event carried, so a stale review can never approve a changed call. Git credential and GitHub connection requests also require the original host-computed display_signature.

For git_credential, render the complete operation, repository, remote, identity, account, helper, hook and warnings before answering. Send action accept with scope once, session or host; or decline/cancel with empty scope. An optional note is at most 500 characters. Native Kin owns persistence and invalidates trust when the resolved connection changes.

For github_connection, connect the person’s account through GitHub CLI or the service management flow. Return action="connected", scope="review" only after the person chooses to continue, or cancel/decline with empty scope. Kin re-inspects the account and asks its exact Git credential question. A connection answer cannot grant route trust. Both answers name the original block id and display_signature; only the current writer can send them.

Client lines are limited to 64 KiB; an oversize or malformed line, including one carrying an unpaired surrogate escape, is refused under its own kind and id and drained, and the stream continues. Host lines can be longer: read them up to 16 MiB, since a review body such as an approval’s full diff arrives on one line. In the other direction, tool arguments and results are capped and flagged (arguments_truncated, result_truncated), image blocks are dropped, and tool_call_draft is not forwarded. todos and tasks_changed reach only connections that declared todos, and root reasoning only those that declared reasoning; every opt-in event is decided per connection, so a client that declares nothing receives the stream it always did. The complete thinking event, including phase, and correlated subagent lifecycle fields pass through unchanged. Review bodies — approvals, plans, proposals — arrive complete.

Opt-in additions

These messages and events exist for clients that carry a whole conversation surface, such as a browser home. Each new event is sent only to connections that named its capability in hello; each new client message is the writer’s alone, and an observer’s copy is refused as role. The protocol number stays 1.

Plan mode

Plan mode belongs to the person. They turn it on, or Kin asks and they answer; nothing enters the read-only freeze by side effect.

  • When Kin thinks a task needs design agreement first it calls enter_plan_mode, which parks the turn on {"type":"plan_mode_request","id","reason"} (reason at most 500 characters). The writer answers {"type":"resolve","id","kind":"plan_mode","enter":true|false}; the nested form {"type":"resolve","id","answer":{"kind":"plan_mode","enter":…}} is accepted too. true begins the freeze before the turn continues; false tells Kin to carry on normally. Like every block it is replayed, id intact, to a writer that attaches or claims later.
  • The writer’s control message takes setting: "planning" with a boolean value, idle-only like the other controls. It is the person’s door in both directions, as /plan is in the terminal. The controls reply reports planning.
  • planning_changed reaches every attached client, and a client that attaches into a freeze receives it in its attach replay.

Checklists: todos

With capability todos a client receives the root checklist and task list:

← {"type":"todos","todos":[{"content","status"}],"truncated":false}
← {"type":"tasks_changed","snapshot":{"tasks":[{"id","content","status","blockedBy","blocks"}]},"truncated":false}

Each list holds at most 64 rows, each text field at most 200 characters, and truncated says when either limit applied. blockedBy and blocks are always lists. A client that attaches or reconnects gets one of each, read live, after the handshake echoes and before the phase line and pending blocks, even when empty; treat that pair as the whole current state. They are display state, so history pages never carry them.

Queueing a message: steering

A second turn is still refused as busy. A writer that declared steering can instead queue a message for the running turn’s next safe boundary:

Message Reply
{"type":"steer","id","text","context"?} accepted of steer, or refused: idle (nothing is running; send a turn), unavailable (a block is pending), full (16 messages or 64 KiB), bounds, malformed (also an id already queued), closing, capability
{"type":"unsteer"} {"type":"steering_withdrawn","items":[{"id","text"}]} with the complete texts, to the asker

Every declaring client sees the same queue as {"type":"steering","pending":[{"id","text","truncated"}]} (text clipped to 500 characters) whenever it changes and once at attach. When the loop joins a message to the turn, steering_delivered carries its id.

If the turn ends with messages still queued, because its last round had already begun or it was interrupted, the service starts the oldest as a fresh turn itself: the text is sent literally, steering_delivered carries fresh_turn: true, and that turn’s turn_ended uses the steer’s id. It does this with no client attached, so a browser that went away still gets its message run. Closing the conversation discards the queue.

The words that begin a turn: turn_started

A client that shows a conversation it is not writing needs each message as well as Kin’s answer. With capability turn_started, a connection receives the words that began each turn once the host has accepted it:

← {"type":"turn_started","id":"t7","text":"Summarise this file","truncated":false,
   "context":[{"kind":"path","label":"File selected by the person","value":"notes/plan.md"}]}
  • Every declaring client gets it, writer and observers alike. The writer’s copy follows its own accepted; match it by id so your own message is drawn once.
  • For a turn, text is the turn’s text and context its accepted attention context, omitted when empty. For a command that begins a turn (a skill, retry, plan <task>), text is its slash line.
  • text is clipped to 4000 characters, with truncated saying so.
  • A queued message joining a turn, or a fresh turn the service starts from the queue, arrives as steering_delivered instead, never as both.
  • The line is presentation only. A later history read replays the journaled message, so replace what you drew from these lines rather than adding to it.

Commands

Turn text is never parsed, so slash commands have their own two doors.

The catalog is one management request:

→ {"type":"hello","protocol":1,"client":"home","manage":{"action":"commands","workdir":"/home/…/proj"}}
← {"type":"management","action":"commands","truncated":false,"commands":[
     {"name":"compact","description":"…","argument_hint":"[focus]","kind":"compact","source":"builtin","scope":"service"}, …]}

The workdir must already be trusted (refused, reason untrusted, otherwise). source is builtin, project, user, or bundled. scope says how to run a row: turn (a skill) and service (a built-in) go through command; client is presentation-local, such as a theme, a picker, an editor, or an account flow, so the client implements it or hides it.

Invocation is the writer’s {"type":"command","id","name","arguments"?,"confirm"?,"user_text"?}, answered accepted of command or refused: unknown, busy, unavailable (also any client row), malformed, unconfirmed, idle, closing. Optional user_text is a string of at most 32,000 characters or null; it records the person’s submitted command without changing the rendered skill or granting authority. Existing clients can omit it. A queued skill keeps the same submitted line when it starts its turn; plan <task> and retry accept it too.

  • A skill is rendered by the service and starts a turn whose id is the command id; accepted carries turn and the literal display (/name args). While a turn runs, a writer that declared steering has it queued instead (queued: true).
  • The service runs clear, compact, plan, retry, tokens, cost, export, fork, rewind, reload, grants, nudges, mode, effort, settings, skills, and stop. Pickers become arguments: nudges [key…|none], effort <level>, grants [revoke <n|key>], rewind [n] [conversation|files|both]. rewind refuses a word it does not know rather than rewinding one turn. cost and settings take on/true/1/yes/ enable or off/false/0/no/disable, as the terminal does. retry resends the person’s last message, never a reminder or budget notice. compact, retry, and plan <task> return turn and end with that id’s turn_ended.
  • clear, and any rewind beyond one conversation turn, need confirm: true: the client asks the person, as the terminal’s dialog does. If saving a clear fails, the service replies refused with reason unavailable and keeps the conversation attached and its history intact. It sends no history_changed; correct the journal error before retrying.
  • fork names the copy in accepted.conversation; open it with an ordinary hello. reload re-reads settings, skills, guidance, and agent profiles, not the model connection or MCP servers.
  • Information comes back to the asker only, as {"type":"command_result","id","name","format":"markdown"|"text","body","truncated"} (body at most 16,000 characters).
  • After clear, a conversation rewind, or compact, clients that declared commands receive {"type":"history_changed","reason"} and should page history again.

The person’s shell command: shell_passthrough

The terminal composer’s ! is available to a writer that declared shell_passthrough. It is the person’s own command, run as the service’s user in the conversation workdir with that user’s environment. Kin cannot reach it: it is not a tool, a command, or a skill.

→ {"type":"shell","id":"s1","command":"git status -sb","context":true}
← {"type":"accepted","of":"shell","id":"s1"}
← {"type":"shell_started","id":"s1","command":"git status -sb","context":true}
← {"type":"shell_output","id":"s1","chunk":"## main\n"}
← {"type":"shell_done","id":"s1","exit":0,"truncated":false,"duration_s":0.04,"timed_out":false,"cancelled":false}

context: true (the default) is idle-only, refused busy during a turn, and its command, exit code, and output join the next turn as untrusted data. context: false may run during a turn and is never sent to Kin. Both are recorded in the journal. Output streams in chunks of at most 4000 characters and stops at 16 KiB, when truncated is true. {"type":"cancel_shell","id"} ends the command and everything it started; shell_done still follows. Other refusals: malformed, bounds (over 8000 characters), full (four already running), capability, closing. Render output as literal text.

A retried round: reasoning_retry

When a root model round fails transiently after showing reasoning but before any answer text, Kin retries it rather than ending the turn in an error. A client that declared both reasoning and reasoning_retry first receives:

← {"type":"reasoning_withdrawn","message_id":"rsn_…"}

Drop what that message_id delivered since its round began. The retry’s reasoning then arrives under the same id as a fresh round, after the ordinary retry notice. Nothing from the withdrawn attempt entered the conversation, and a late attacher’s reasoning replay no longer includes it. Without reasoning_retry, the retry’s reasoning simply follows the partial in the same round. A round that had already shown answer text still ends with the error.

Agents

With capability agents a client gets one picture of the conversation’s registered agents at attach, and either role may ask for it again with {"type":"agents"}:

← {"type":"agents","truncated":false,"agents":[
     {"agent_id","run_seq","profile","execution","lifecycle","outcome","health",
      "last_activity_at","activity","tool_calls","prompt_tokens","completion_tokens",
      "parent_tool_call_id","age_s"}]}

At most 64 rows, newest first; activity is the active tool names or the prompt preview, at most 200 characters. It is metadata only: apply the ordinary subagent lifecycle events on top of it. The writer may send {"type":"agent_control","agent_id","action"} with kill, pause, resume, or interrupt; refusals are malformed, unknown, or unavailable with the reason in detail.

Handoffs: how the environment joins a turn

A client that declares open_url gives the root session one extra tool. When Kin decides a page should open, it calls open_url with one exact http/https URL; the host parks a handoff on the client:

← {"type":"handoff","id":"ho_3","kind":"open_url","payload":{"url":"https://kinra.ai/kin"}}
→ {"type":"handoff_result","id":"ho_3","ok":true,"detail":"Brave"}
← {"type":"tool_result","id":"call_4","name":"open_url","result":"opened https://kinra.ai/kin in the person's browser (Brave)"}

open_workspace is a second effect in the same closed family. Kin may call it only when the person’s current words explicitly name work in another repository or directory, with one bounded printable absolute path that already names a directory different from the calling conversation’s workspace:

← {"type":"handoff","id":"ho_5","kind":"open_workspace","payload":{"path":"/home/…/scope"}}
→ {"type":"handoff_result","id":"ho_5","ok":true,"detail":"confirmed"}
← {"type":"tool_result","id":"call_8","name":"open_workspace","result":"opened a new conversation in /home/…/scope (confirmed)"}

The environment confirms with the person, may decline, and on acceptance opens its own new conversation bound to that directory through the ordinary workspace-trust flow. The calling conversation, its workspace, journal, grants, and writer remain unchanged. Kin never chooses the target from attention facts, window titles, file contents, or ambient context; an ambiguous name remains a question instead of a guessed effect. The tool is unavailable while the caller is in the read-only planning freeze, so a fresh conversation cannot shed that human-controlled capability reduction. The tool waits up to five minutes for review; an expired request cannot be revived by a late reply. Report success only after the destination conversation actually opens. Clients may stage a person-reviewed draft there, but opening it does not send a turn.

The environment owns every handoff effect and reports a fact, never authority over Kin. No page content comes back from open_url — reading stays with web_fetch. attention_context uses the same exact correlation but returns only the requested current facts, as shown above. A handoff the environment never answers settles on the tool’s own timeout. Without the matching capability the tool does not exist, so a plain terminal Kin cannot open a browser, open a workspace conversation, or inspect compositor attention. A client must never infer an action from a response; the typed handoff is the only door.

Trust, leases, and handing off to the terminal

A host never creates workspace trust on its own. A fresh launch whose workspace is not yet trusted, from a client that declared workspace_trust, receives Kin’s own question first — the same facts the terminal prompt prints, with the resolved path — and the client returns the person’s answer in the prompt’s own words:

→ {"type":"hello","protocol":1,"client":"front-door","capabilities":{"workspace_trust":true}}
← {"type":"trust_request","id":"trust_1","path":"/home/…/Kinra","replaced":false,"persistent":true,"reason":"",
   "detail":"Kin and the code it runs can read, write, execute, and use credentials as your OS user."}
→ {"type":"resolve","kind":"trust","id":"trust_1","decision":"persistent"}
← {"type":"accepted","of":"resolve","id":"trust_1","kind":"trust","decision":"persistent"}
← {"type":"welcome",…}

persistent records the grant exactly as the terminal prompt would, session trusts this host only, and decline ends with the untrusted fatal. When persistent is false the reason says why only session is available (the home directory or an ancestor of it is too broad). Nothing from the workspace — its instructions, its .kin/settings.toml overlay, its project skills — is read before the answer, a turn sent meanwhile is refused as untrusted, and the client cannot infer, remember, or widen the decision. A resume cannot ask, like the terminal’s own resume: it needs trust that already exists. Without the capability the host behaves as before — the first line is {"type":"fatal","reason":"untrusted",…}, the last is closed, and the process exits 2; trust the workspace once from an interactive kin there.

One conversation has one live client. --stdio --resume ID is refused (fatal with reason lease) while a terminal or another host owns that conversation, and the terminal’s --resume is refused while a host lives. To move a conversation to the terminal — for an operation the client does not render, or simply deeper work — the client sends close, waits for closed, and then launches kin --resume ID. The journal is the transcript; the client keeps at most the opaque session id.

Continuity in a client’s workspace

A client uses visible workspace guidance and files. It may seed AGENTS.md and .kin/skills/ without overwriting existing bundles; Kin reads them only after trust. The host writes no hidden memory or end-of-session notes.

The remember and recall skills maintain visible notes with human approval where no owning document exists. Client turns have no slash dispatch, so client-facing skills must be model-invocable. Authored bundles use the ordinary reviewed propose_skill transaction. Prompt instructions do not travel in a separate protocol field.

The session service

kin --serve runs one Kin per user that hosts conversations for local clients. It listens on $XDG_RUNTIME_DIR/kin.sock (mode 0600; --socket PATH for another path) or, under systemd socket activation, on the socket the user manager passes in as descriptor 3 with LISTEN_FDS/LISTEN_PID. Every connection’s peer credentials must carry the service’s own uid — the first line to any other process is {"type":"fatal","reason":"peer",…} — and there is never a TCP port. The environment owns the process: a kin.socket user unit (ListenStream=%t/kin.sock, SocketMode=0600) with a kin.service unit (ExecStart=kin --serve) starts it on first connection and stops it at logout. The activated pathname remains systemd-owned across service stops and restarts, so a replacement Kin can accept new clients through the same name. ExecStart must run the kin entry point directly or through a wrapper that execs it: activation is addressed to the pid systemd started, so a wrapper that merely spawns kin makes the service ignore the handed-over socket (it says so on stderr) and try to create its own. Without units, kin --serve in a login session creates the socket itself and removes it on exit.

Each connection speaks the protocol above. The one addition is hello.conversation:

→ {"type":"hello","protocol":1,"client":"front-door","capabilities":{"open_url":true},
   "conversation":{"workdir":"/home/…/Kinra","role":"writer","idle":1800}}
← {"type":"welcome","protocol":1,"kin":"…","capabilities":["open_url"],
   "conversation":"s_…","role":"writer","workdir":"/home/…/Kinra","turn":"","idle":1800}
← {"type":"session",…}
← {"type":"ready"}
← {"type":"mode_changed","mode":"auto"}
← {"type":"writer_changed","writer":true,"client":"front-door"}
Field Values Meaning
workdir absolute path, required The workspace. Trust is the workspace’s own, exactly as for --stdio.
id a conversation id, optional Attach to that live conversation, or — as a writer — resume it from its journal.
continue boolean, optional The newest live conversation in the workspace; else, as a writer, the newest saved one; else a fresh one. Not combinable with id.
role writer (default) or observer What this connection may do.
idle seconds, 0–86400, default 1800 How long the conversation may live with no writer attached. The current writer’s hello sets it.

One writer, any observers. The writer holds the conversation lease through the session, sends turns, and resolves blocks. Observers receive the stream, tool calls, questions and turn_ended, with reasoning gated by each observer’s own declared capability, and may close/claim, read history or agent metadata, and send checked name metadata edits without claiming the keyboard. Writer-only messages are refused as role. A writer hello for a conversation that already has one is a busy fatal naming the holder. The role moves only explicitly: the writer sends release, an observer sends claim (or a new connection sends a writer hello), and a claim with take: true moves it from a writer that holds it. Contention and timeouts never move it. Every attached client sees each move as writer_changed, carrying its own role. A writer whose role is taken stays attached as an observer. What it sent before the move was answered as the writer’s, and anything after is refused as role with its id, so a message is never lost or applied twice. The running turn, the queue, pending blocks and background agents stay with the conversation; the new writer may cancel the turn, answer the blocks, or withdraw the queue. The conditional tools — open_url, open_workspace, get_workspace_context, and get_attention_context — can execute only when the current writer declares the matching capability. A transfer can remove that dispatch availability immediately while the running request head keeps its declarations. The next human turn names unavailable tools in a context update; gains or redefinitions refresh the head at that boundary.

A previously declared client tool can wait within five seconds of writer release or disconnection for a capable writer to return. Kin checks its schema and permissions after waiting. An incapable successor or an older departure still refuses; observer capabilities never grant tools. Saved journals record content-free writer transitions and fixed tool names to help diagnose these refusals.

Conversations outlive windows. A writer that disconnects without close leaves the conversation open: a running turn keeps running and a pending question keeps waiting. The next writer — by id or continue — receives the handshake, the current thinking line if a turn is running (welcome.turn names it), and every pending block replayed with its original id, so it can answer with the same resolve it would have sent the first time. A conversation ends when its writer closes it, when no writer has been attached for the idle policy, when the service stops, or when a requested settlement fails. Attached clients receive the corresponding closed reason — writer, idle, stopped, or unsettled — except that the closing writer receives a plain closed after a successful close. The journal keeps everything for a later resume in the service or the terminal. An observer’s close detaches only that observer.

An observer’s attach ends with one writer_changed naming the current holder, so every client tracks the role from that one line. An observer that claims receives every pending block replayed with its original id before its writer_changed, exactly as a writer hello after a disconnect does, so a question or approval survives a transfer and is answered by the new writer alone.

The service never creates trust. A fresh open from a writer that declared workspace_trust receives the same trust_request a spawned host would send; a resume or an undeclared client gets the untrusted fatal; observers never trigger the question because a live conversation is already trusted. kin --serve takes the model, preset, --mode, and --no-save flags for the conversations it opens; it refuses -p, --stdio, --agent, a prompt, --workdir, --resume, --continue, and --new, because each hello names its own conversation. It exits 0 when stopped by SIGTERM/SIGINT/SIGHUP and 1 when it cannot start (no XDG_RUNTIME_DIR, a socket another service is already accepting on, or a malformed activation environment).

Discovering conversations

A local client can ask the service what is live without attaching to anything: a hello that carries conversations: true instead of conversation is answered with one typed snapshot, then closed, and the connection ends.

→ {"type":"hello","protocol":1,"client":"front-door","conversations":true}
← {"type":"conversations","truncated":false,"conversations":[
     {"id":"s_…","workdir":"/home/…/proj","title":"Reading the manual",
      "writer":false,"client":"","turn":"h1","phase":"tools","agents_running":1,"idle":14400,"age_s":812.4,
      "pending":[{"id":"q_1","kind":"question"}],"turns":3}]}
← {"type":"closed"}

Rows are newest-first, at most 64 (truncated says whether more exist), and each carries at most 16 pending entries. title is the canonical name, including a neutral fallback before naming material exists; title_source and title_revision travel with it. The response advertises features.session_names: 1. turns counts the person’s turns since the conversation opened, and a conversation working in a copy carries worktree {name, branch, main, path}. phase is the running turn’s root thinking.phase ("" when idle) and agents_running counts background agents still working. The listing is deliberately content-free — block ids and kinds only, never the question text or approval body; a client that wants the blocks attaches to the conversation and receives them replayed exactly. There is no subscription or push channel: the environment polls when its own signal says to look — typically a kin -p --attach exit 2, whose sidecar already names the waiting conversation. See Headless runs for that flow: a scheduled run’s human gate becomes a live pending block here, discovered with this listing, claimed with the ordinary claim, and answered with the ordinary resolve — the original turn then continues.

The terminal attaches

kin --attach runs the ordinary terminal as one more client of the service instead of hosting the harness in its own process. It is how “Open in Kin” becomes one conversation for the Front Door and the terminal rather than a close-and-resume.

kin --attach                       # a fresh service conversation, this terminal as its writer
kin --attach --resume s_…          # that conversation: live in the service, else resumed from its journal
kin --attach --continue            # the newest live conversation here, else the newest saved, else a fresh one
kin --attach --continue --observe  # watch the live conversation; send nothing until /role claim

The conversation flags keep their in-process meaning; --observe needs a live target (--resume or --continue) and attaches as an observer; --socket PATH names another socket. --attach refuses --stdio, --serve, --agent, --no-save, --mode, and the model, provider, and preset flags, because the service’s own launch decided them; combined with -p it is not the terminal at all but the service-hosted headless run. The terminal runs its own workspace-trust prompt first, before it reads anything under the workspace, with the same rule as an in-process launch (a resume cannot ask); it declares no handoff or trust capability, so it never carries the service’s trust question, and a fresh conversation in a workspace the service does not already trust is refused as untrusted with the route to trust it. Every refusal — no service (unavailable), peer, busy naming the writer, absent, untrusted, lease, resume, backend, protocol — is one line on stderr. untrusted exits 2; other refusals exit 1, and a busy refusal suggests --observe.

What you see is the conversation as it is. The terminal paints the existing transcript from the journal — read once, read-only; the service keeps writing the same file and holds no transcript of its own — then the live stream: the same events, questions, approvals, tool activity, and subagent lifecycle the in-process terminal renders. A turn already running when you attach shows as busy, and the journal’s unfinished tool rows take the results that follow. The attached terminal does not declare the reasoning capability, so it receives phase-only reasoning; long tool results are cut at the wire’s cap and say so.

The role is explicit. The status bar shows writer, observing · writer: front-door, or observing · no writer; /role says the same in the transcript. /role release steps down to observer; /role claim takes the role, from its holder if another client has it, and that client goes on observing. When another client takes the role from the terminal, the window says so and keeps what you typed. Every move is the service’s own release and claim, and every attached client sees it as writer_changed with the terminal’s name, terminal. While observing, another client’s turn opens with the words that began it, the composer refuses to send, esc does not cancel, and a pending question or approval shows as a note naming the writer; the moment you claim, the service replays the pending block and the ordinary dialog opens, answering with the block’s original id and signature. /plan, /tokens, /cost, /grants, /reload, /mode and shift+tab (which cycles the mode), /effort (bare opens the picker from the service’s efforts), /settings, /nudges, /export, and /fork run in the service; /fork prints the kin --attach --resume … command that opens the copy. A contextual ! and a local !! both run through the service’s writer-only shell door and are journaled; !! output still never reaches Kin. The pinned checklist follows the service’s list. Commands that act on this conversation’s own session — /compact, /clear, /model, /resume, /history, /rewind, /mcp, the agent panel, and queueing a message while a turn runs — refuse with the route (/close, then kin --resume ID); account and configuration screens refuse with “run it from kin without --attach”, leaving this conversation open. The slash menu offers only what works over the wire. This terminal cannot answer the protocol’s Git credential and GitHub connection prompts yet: esc cancels the turn, or /role release lets a client that can answer claim the writer role. Prompt skills, the file tree, search, themes, and copy all work as usual.

Leaving is not ending. Quitting the window (ctrl+c, ctrl+q, /quit) releases the writer role if you held it and detaches; the conversation stays in the service — a running turn keeps running — under its writer’s idle policy, and the shell shows the exact kin --attach --resume … to come back. Only the writer’s /close ends the conversation for every client; the journal keeps it for a later attach or an in-process kin --resume. When the conversation ends under you — its writer closed it, it idled out, or the service stopped — the transcript says which, the window leaves, and the same hint follows; a socket that dies without a closed line is reported as lost.

For Kinra OS, “Open in Kin” is the Front Door sending release, waiting for its accepted, and launching kin --attach --resume <id> --workdir <workspace>; it then renders as an observer, opens no dialog of its own for a pending block, and takes the role back with claim once it sees writer_changed with writer: false — the terminal released or detached. A claim while the terminal writes is refused busy naming terminal; a closed with reason writer means the terminal closed the conversation. The terminal’s /role claim takes the role even while the Front Door holds it, and the Front Door then receives writer_changed with role observer.

Exit codes

Code Meaning
0 The client closed the session or its end of the pipe.
1 Startup or runtime failure: bad hello, held lease, unknown resume target, invalid model configuration, or a lost/refusing service (see the fatal line’s detail).
2 Workspace trust is needed, including a declined trust request.
64 Invalid command-line usage (EX_USAGE), including unsupported flag combinations.

--stdio is mutually exclusive with -p, --serve, --attach, --agent, and a positional prompt; it composes with --workdir, --resume, --continue, --new, --no-save, --mode, and the model and preset flags. --attach exits 0 after a detach or clean conversation end, 1 for a socket/host failure or non-trust refusal, 2 for missing workspace trust, and 64 for invalid command-line usage. --serve startup failures use 1.