Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 12 additions & 13 deletions client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,22 @@ enumerating server threads. `/list` shows only threads created by this local
client profile; `/open THREAD_ID` opens an explicitly known thread without adding
it to that local list. A list number may be used after `/list`. Use
`/new REPOSITORY [TITLE]` to create a thread and `/help` to see the complete
interactive command set. The server
assigns both thread IDs and execution-session IDs. After a message starts an
execution session, the client streams the orchestrator terminal without locking
the prompt. Additional ordinary input is durably appended and delivered as a
follow-up to that open thread's active orchestrator. Use `/wait` when you want
to stop entering commands until the current execution replies. While a thread
interactive command set. The server assigns both Thread and Session IDs. After a
message starts a Session, the client streams the orchestrator terminal without
locking the prompt. Additional ordinary input is durably appended and delivered
as a follow-up to that open thread's active orchestrator. Use `/wait` when you
want to stop entering commands until the current Session replies. While a thread
is open, the client maintains an authenticated WebSocket to receive conversation
events, thread state, heartbeats, and bounded subagent status. A separate
session WebSocket carries live orchestrator terminal output only while an
execution is active. The interactive TTY reserves a small bottom pane for each
events, thread state, heartbeats, and bounded subagent status. A separate Session
WebSocket carries live orchestrator terminal output only while a Session is
active. The interactive TTY reserves a small bottom pane for each
subagent's state, role, and current progress as a compact graph rooted at the
orchestrator. Before the first delegation, the graph labels the orchestrator
`planning` and says that no agents have been delegated yet; it does not imply
that a separate discovery operation is running. The stable `› ` input area
remains available while agents work;
asynchronous output redraws it without discarding partially typed follow-up
text. When an execution finishes, the pane keeps a concise result summary,
text. When a Session finishes, the pane keeps a concise result summary,
wrapped to at most three terminal lines, showing the latest public outcome, and
labels the orchestrator `complete` instead of reducing the result to `idle`.
Bounded clarification responses are shown as questions and wait for ordinary
Expand All @@ -43,7 +42,7 @@ reconstructs that summary when a thread is reopened.

Pending Slack repair proposals are shown before the normal prompt in a clearly
labelled review window. Enter `yes` to bind the exact review question to a fresh
human-authorized execution session, or `no` to reject it and permanently close
human-authorized user Session, or `no` to reject it and permanently close
that thread to further messages. Use `/reviews` to refresh the pending queue.

The first command securely prompts for the password. For a non-interactive
Expand Down Expand Up @@ -84,8 +83,8 @@ logout

Non-interactive commands emit formatted JSON. `threads watch` emits newline-delimited
JSON events so scripts and agents can consume the stream incrementally. A
thread ID is never reused as an execution-session ID; the server creates and
returns execution sessions when a message needs a fresh runtime.
Thread ID is never reused as a Session ID; the server creates and returns a
Session when a message needs a fresh runtime.

## Development

Expand Down
6 changes: 3 additions & 3 deletions client/src/client.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const interactiveHelp = `Commands:
/list List threads created by this local client
/open THREAD_ID Open a thread
/new REPO [TITLE] Create and open a server-assigned thread
/sessions List execution sessions for the open thread
/sessions List Sessions for the open thread
/refresh Replay new events
/wait Wait for the current execution to reply
/help Show this help
Expand Down Expand Up @@ -410,7 +410,7 @@ export async function runInteractive({
if (line === "/sessions") {
if (!current) { stdout.write("Open a thread first.\n"); continue; }
const sessions = (await client.request(`/api/threads/${encodeURIComponent(current.id)}/sessions`)).value.sessions || [];
if (!sessions.length) stdout.write("No execution sessions yet.\n");
if (!sessions.length) stdout.write("No Sessions yet.\n");
else sessions.forEach((session) => stdout.write(` ${session.ordinal}. ${session.id} [${session.status}]\n`));
continue;
}
Expand Down Expand Up @@ -457,7 +457,7 @@ export async function runInteractive({
threads = [...threads.filter((thread) => thread.id !== current.id), current];
agentPane.setOutcome("", "");
agentPane.setThread(current, routed.session?.status || "starting");
stdout.write(`\nApproved ${review.id}. Started fresh execution ${routed.session.id} for ${current.id}.\n`);
stdout.write(`\nApproved ${review.id}. Started fresh Session ${routed.session.id} for ${current.id}.\n`);
await replay({ all: true });
startThreadConnection(current.id);
await startMonitor(routed.session.id);
Expand Down
4 changes: 2 additions & 2 deletions client/test/client.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ test("users list only locally created threads through individually authorized lo
assert.deepEqual(JSON.parse(output.output), [{ id: "thread-1", state: "idle", repository: "multiagent" }]);
});

test("thread creation lets the server generate both the thread and execution session IDs", async () => {
test("thread creation lets the server generate both the Thread and Session IDs", async () => {
const sessionFile = await sessionFixture({ threadIds: [] });
const output = writer();
const requests = [];
Expand Down Expand Up @@ -792,6 +792,6 @@ test("TTY startup shows a pending repair and yes starts its fresh session", asyn
});
assert.match(output.output, /REPAIR REVIEW REQUIRED/);
assert.match(output.output, /Approve restarting api in testnet\?/);
assert.match(output.output, /Approved review-session-diagnose\. Started fresh execution session-repair/);
assert.match(output.output, /Approved review-session-diagnose\. Started fresh Session session-repair/);
assert.match(output.output, /slack> Slack alert/);
});
7 changes: 4 additions & 3 deletions control-server/src/kubernetes-session.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function renderSessionTemplate(value, replacements) {
return rendered;
}

export function sessionSecret(id, namespace, task, actor, threadId = id, leaseGeneration = 1, authorizingEventId = id, gatewayToken = "", authorityScope = "human") {
export function sessionSecret(id, namespace, task, actor, threadId = id, leaseGeneration = 1, authorizingEventId = id, gatewayToken = "", authorityScope = "user", mutationGrant = null) {
return {
apiVersion: "v1",
kind: "Secret",
Expand All @@ -39,6 +39,7 @@ export function sessionSecret(id, namespace, task, actor, threadId = id, leaseGe
"lease-generation": Buffer.from(String(leaseGeneration), "utf8").toString("base64"),
"authorizing-event-id": Buffer.from(authorizingEventId, "utf8").toString("base64"),
"authority-scope": Buffer.from(authorityScope, "utf8").toString("base64"),
"mutation-grant.json": Buffer.from(JSON.stringify(mutationGrant), "utf8").toString("base64"),
...(gatewayToken ? { "gateway-token": Buffer.from(gatewayToken, "utf8").toString("base64") } : {}),
},
};
Expand Down Expand Up @@ -109,8 +110,8 @@ export class KubernetesSessionClient {
return `/api/v1/namespaces/${encodeURIComponent(this.namespace)}/${resource}${name ? `/${encodeURIComponent(name)}` : ""}${query}`;
}

async createSession({ id, threadId = id, leaseGeneration = 1, authorizingEventId = id, gatewayToken = "", task, actor, authorityScope = "human", repositoryName, repositoryUrl, repositoryAuthentication = "anonymous", resume, template }) {
const secret = sessionSecret(id, this.namespace, task, actor, threadId, leaseGeneration, authorizingEventId, gatewayToken, authorityScope);
async createSession({ id, threadId = id, leaseGeneration = 1, authorizingEventId = id, gatewayToken = "", task, actor, authorityScope = "user", mutationGrant = null, repositoryName, repositoryUrl, repositoryAuthentication = "anonymous", resume, template }) {
const secret = sessionSecret(id, this.namespace, task, actor, threadId, leaseGeneration, authorizingEventId, gatewayToken, authorityScope, mutationGrant);
const job = renderSessionTemplate(template, {
SESSION_ID: id,
THREAD_ID: threadId,
Expand Down
Loading
Loading