Kin / Agents & automation
Local clients
Host and attach to local Kin conversations over stdio or a per-user Unix socket.
Read as MarkdownLocal 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 onlynameand absolutepath;observed_at: an ISO timestamp with a timezone;next_cursor: a greater integer when more candidates remain, ornull.
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"}(reasonat 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.truebegins the freeze before the turn continues;falsetells Kin to carry on normally. Like every block it is replayed, id intact, to a writer that attaches or claims later. - The writer’s
controlmessage takessetting: "planning"with a booleanvalue, idle-only like the other controls. It is the person’s door in both directions, as/planis in the terminal. Thecontrolsreply reportsplanning. planning_changedreaches 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 byidso your own message is drawn once. - For a
turn,textis the turn’s text andcontextits accepted attention context, omitted when empty. For acommandthat begins a turn (a skill,retry,plan <task>),textis its slash line. textis clipped to 4000 characters, withtruncatedsaying so.- A queued message joining a turn, or a fresh turn the service starts from
the queue, arrives as
steering_deliveredinstead, never as both. - The line is presentation only. A later
historyread 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;acceptedcarriesturnand the literaldisplay(/name args). While a turn runs, a writer that declaredsteeringhas it queued instead (queued: true). - The service runs
clear,compact,plan,retry,tokens,cost,export,fork,rewind,reload,grants,nudges,mode,effort,settings,skills, andstop. Pickers become arguments:nudges [key…|none],effort <level>,grants [revoke <n|key>],rewind [n] [conversation|files|both].rewindrefuses a word it does not know rather than rewinding one turn.costandsettingstakeon/true/1/yes/enableoroff/false/0/no/disable, as the terminal does.retryresends the person’s last message, never a reminder or budget notice.compact,retry, andplan <task>returnturnand end with that id’sturn_ended. clear, and any rewind beyond one conversation turn, needconfirm: true: the client asks the person, as the terminal’s dialog does. If saving a clear fails, the service repliesrefusedwith reasonunavailableand keeps the conversation attached and its history intact. It sends nohistory_changed; correct the journal error before retrying.forknames the copy inaccepted.conversation; open it with an ordinaryhello.reloadre-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, orcompact, clients that declaredcommandsreceive{"type":"history_changed","reason"}and should pagehistoryagain.
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.