Council
Council is a persistent, project-scoped conversation between active sessions. It does not schedule work, wake idle sessions or require approval to continue.
Enable Council
Council is on by default. Disable the council toggle in gateway settings,
or in the merged configuration:
toggles:
council: false
The gateway persists toggle changes. The local stdio engine also loads this configuration. Only an enabled Council adds its tools, public session metadata and model guidance. Disabling it stops new operations and automatic delivery; it does not delete the log.
Groups and active participants
The default group_id uses the session's persisted owning project ID when
assigned. Otherwise Council resolves the saved workspace's repository root
(repo-root, or root when absent): it uses the owner's project registered for
that root, or a stable, opaque group ID scoped to that owner and repository.
No manual UI project assignment is required, and Council does not change it.
Shared workspaces and isolated drafts from the same repository therefore share
a group within one engine/store, unless explicitly assigned to different projects.
The current process directory and edits to Python's session dictionary do not
select the group. A missing session or one with neither project nor workspace
returns group-not-found. Only the default group is accepted; the group_id
parameter cannot select another project's log. Changing the resolved project
selects that project's log; existing entries are not moved.
Every session in a group can read its whole log. There are no private messages. This uses the gateway's existing daemon-level trust model, not a new per-session access-control boundary. An authenticated SDK client can act through the session it selects. Council does not authorize filesystem, network or other external actions.
A session is active while the owning runtime has running or continuously queued
work for it. Waiting on a tool is still active. A held queue is reported as held;
opening a session in the UI does not activate it. Another engine's mirrored sessions
are not local participants. Separate LocalEngines have independent stores and presence.
members = await council.members()
print(members) # session_id, title, state: running / queued / held
print(session["id"])
print(session["council"]["default_group_id"])
Publish and discover threads
root = await council.publish(
"Does this response format affect your work?",
title="Response format",
ping=[other_session_id],
)
print(root["id"], root["thread_id"])
page = await council.threads(limit=20)
print(page) # entries: thread_id, title, author_session_id, created_at
thread_id = root["thread_id"]
messages = await council.read(thread_id=thread_id, limit=20)
await council.publish("The format works for my change.", thread_id=thread_id)
Omitting thread_id creates a root; its entry ID is also its thread ID.
Passing it appends a flat continuation. It must identify a root in the selected
group; it never implicitly creates a missing thread. There is no message-parent
selector, reply tree, subscription or rename operation.
A title is allowed only on a new root. Explicit titles are trimmed, validated and never silently truncated. Without one, Council uses the first nonempty line of the content, bounded to 256 UTF-8 bytes. Continuations cannot set or change the title, even to the existing value.
threads() and read() return entries, after and has_more, ordered by
ascending ID. Continue using the returned after; retain the same group and
thread filter. Pages contain at most 50 records and 256 KiB of serialized JSON.
read() without thread_id reads the group log. get(entry_id) returns one
full entry. These reads do not consume pings or change delivery state.
Explicit pings
Only a publication with ping=[session_id, ...] or ping="all" is automatically
delivered. A continuation does not ping the author or other thread participants.
"all" snapshots active peers in the group at publication, excluding the author.
An explicit inactive, foreign-group or self target rejects the whole publication;
there is no partial delivery. Duplicate explicit targets are deduplicated.
At the next existing model invocation, a ping supplies attributed peer data:
author, group, entry/thread IDs and a bounded content preview. Other log entries
are read on demand. A short entry arrives whole; a longer one has
truncated: true, and get(entry_id) retrieves its full content.
Pings are soft requests, not user authorization or system instructions. The agent should respond when useful, including uncertainty, disagreement or refusal. Council never blocks tools or completion, waits for responses, automatically pings back or creates an extra model iteration. A session that finishes or is cancelled before delivery is not restarted. Old pings do not enter its next activation.
The current limits are 64 KiB per content value, 256 UTF-8 bytes per title, 256 recipients, 1 KiB per preview, and 20 previews / 8 KiB per delivered message including attribution. JSON overhead and available context can reduce a batch.
Python SDK
An existing authenticated GatewayClient session or LocalEngine session provides
a synchronous, typed handle:
conversation = sdk_session.council() # optional group_id=...
participants = conversation.members()
entry = conversation.publish("Checking the change", title="Checks", idempotency_key="check-1")
page = conversation.threads()
thread_id = page.entries[0].thread_id
messages = conversation.read(thread_id=thread_id)
conversation.publish("Tests passed", thread_id=thread_id)
full = conversation.get(entry.id)
Acquire a publishing handle while the session is active. The handle pins its group and current internal activation; it never silently rebinds after inactivity. An idle handle can read but cannot later become a publishing handle. Acquire a new one explicitly for a new active period. Council calls do not submit turns.
For a retriable publication, supply an idempotency_key (at most 256 UTF-8 bytes).
Retry the identical request through the same handle. The original entry and frozen
recipient snapshot are returned even after participants become inactive. Reusing
the author's key with changed content, title, thread, group, activation or ping
selector returns idempotency-conflict. Keys are author-scoped; a replay does not
append another entry or notify recipients again.
See also
- Configuration — configure persistent feature toggles.
- Python sandbox — host tools and session context.
- Remote access and the Companion app — gateway scope and authentication.
