Policy & sessions
This is an explanation document — the reasoning behind how the interactive client models a conversation. Its companion, Understanding rooms and policies, explains the two server concepts; this one explains how the client should present them so a user gets a Claude-Code-style chat instead of a room-picker.
The one-line version:
Policy is the model. The session is the thread. A user picks a policy the way they pick a model, types a question, and gets an answer; continuing the conversation, resuming it later, and swapping the policy mid-chat are all properties of a client-owned session, not of a room they must choose first.
Two axes, three vocabularies
Every surface already encodes the same two orthogonal questions under different names. The confusion is purely lexical:
| Axis | REST | Completions API | Interactive client |
|---|---|---|---|
| How it runs (recipe) | policy_id |
model (nsed:<tag>) |
policy picker → the "model" |
| Where it lives / resume / who sees | room |
room_id = session_id (x-nsed-session-id) |
the session/thread |
The Chat Completions layer already made the call: a client sends a model
(= policy) and a message array; the server auto-mints the room_id as a
session/thread and returns it as x-nsed-session-id so the client can resume.
The code names it session_id / thread_prefix /
find_active_job_for_session. So room and session are the same thing —
"session" is the client-facing name for it.
Why the client owns the transcript (Completions, not Responses)
Two continuation models exist. They are not equivalent for a chat client:
- Chat Completions — the client holds the full message array and resends it.
Server-side, a new turn folds the messages into the single task string the
deliberation core consumes (see why the
[role]flatten exists). The client is the source of truth for the conversation. - Responses API — the server holds the thread; the client sends
previous_response_id. Parameters are locked for the thread's life — a follow-up cannot change the effort or the policy.
For a chat that must store, restore, and swap policy mid-conversation, the client must own the transcript:
- Restore = reload the local session file and replay — independent of the
server's history retention (NATS
nsed_hist_*has a TTL). - Swap policy mid-chat = the next turn carries a different policy. On
the Completions path a changed
policy_iddeliberately forces a fresh deliberation, which the client stitches into the same session. The Responses path forbids this — its thread is parameter-locked.
So the interactive client speaks Chat Completions and keeps its own session store; the Responses API remains the cleaner fit for external stateful clients that want server-held threads.
What a session is, client-side
A session is a small client-owned record (not the existing sessions.json,
which is unrelated Claude-CLI-UUID plumbing):
ChatSession {
id, title, created, updated,
active_policy, // the "model" in force; recorded per turn too
orchestrator,
server_thread, // x-nsed-session-id, for cheap same-policy continuation
turns: [ { role, content, policy_id, job_id, ts } ],
}
The transcript is the durable artefact; server_thread is an optimisation for
same-policy follow-ups, and falls back to replaying the transcript when the
server thread has expired.
Tool calls work over this — for the record
A deliberation is not text-only. When a request carries tools, an agent
that calls one surfaces a standard tool_calls response
(finish_reason: "tool_calls" on Chat Completions; requires_action +
function_call on Responses), the client executes it, and the result is fed
back to the blocked agent — a real closed loop on both compat surfaces. So the
chat session model does not preclude tool use; a client's tools ride down as
user_tools.
Why our own TUI, not a wrapped coding agent
A tool-capable OpenAI-compatible client (e.g. OpenCode) can drive quorum today —
the compat surface was built for exactly that. Wrapping Claude Code
specifically has two frictions: it speaks the Anthropic Messages API
(/v1/messages), which the orchestrator does not yet route (a proxy or that
surface would be needed); and a deliberation's per-turn latency (rounds ×
agents) fits interactive single-model expectations poorly. More fundamentally,
the deliberation UX — rounds, competing proposals, convergence, and the coming
channel model — will diverge from any generic coding-agent client over time.
That divergence is why the interactive client is built in-house rather than
borrowed. The design principle it commits to is the one at the top: policy is
the model, the session is the thread.
A note on the [role] flatten
The deliberation core consumes one task string, not a message array (agents
are N proposers/evaluators, provider-agnostic, each building their own prompt).
The compat layer therefore flattens messages[] into one string with [role]
prefixes so agents can still tell a user's question from a prior answer. This is
server-internal — clients speak vanilla Chat Completions. A structured
turns-with-deliberation-native-roles input is the clean future, and shares the
same design space as the channel work.
See also
- Understanding rooms and policies — the two server concepts and the public/private visibility rules.
- Glossary — room, policy, session, job, effort.
- Run an agent fleet —
quorum.ymlin practice.