You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by zh...@apache.org on 2026/10/19 10:43:46 UTC

(maka) branch refactor/cli-run-interface created (now 5c6c1a1987)

This is an automated email from the ASF dual-hosted git repository.

me2seeks pushed a change to branch refactor/cli-run-interface
in repository https://gitbox.apache.org/repos/asf/maka.git


      at 5c6c1a1987 feat(cli): simplify Session controls and persist model selection

This branch includes the following new commits:

     new 5c6c1a1987 feat(cli): simplify Session controls and persist model selection

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


(maka) 01/01: feat(cli): simplify Session controls and persist model selection

Posted by zh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

me2seeks pushed a commit to branch refactor/cli-run-interface
in repository https://gitbox.apache.org/repos/asf/maka.git

commit 5c6c1a1987c560b3e4817751133172c58923a5f8
Author: me2seeks <me...@gmail.com>
AuthorDate: Mon Oct 19 18:43:01 2026 +0800

    feat(cli): simplify Session controls and persist model selection
    
    Use the existing Host configuration operation to save model settings independently of an explicit resume or new prompt. Accept compatible option combinations, shorten usage errors and parent-agent examples, and streamline the Maka CLI skill.
    
    Generated-by: Codex
---
 packages/cli/README.md                             |  75 ++++++---
 packages/cli/README.zh-CN.md                       |  68 +++++++--
 packages/cli/src/__tests__/run-command.test.ts     | 108 ++++++++++++-
 .../cli/src/__tests__/run-session-recap.test.ts    | 168 +++++++++++++++++----
 packages/cli/src/run-command-core.ts               | 124 +++++++++------
 packages/cli/src/run-session-command.ts            |  98 ++++++++++--
 packages/cli/src/run-session-result.ts             |   2 +
 skills/maka-cli/SKILL.md                           | 144 ++++--------------
 skills/maka-cli/references/session-control.md      |  87 +++++++++++
 9 files changed, 627 insertions(+), 247 deletions(-)

diff --git a/packages/cli/README.md b/packages/cli/README.md
index 17f055dfe9..4225aaa0a2 100644
--- a/packages/cli/README.md
+++ b/packages/cli/README.md
@@ -117,30 +117,68 @@ and network access and should only be used in an environment you are prepared to
 modify.
 
 
-For another agent to drive a Session, request structured output or explicitly submit without waiting:
+For a parent agent coordinating work across calls, prefer `--no-wait`, save the returned
+`sessionId` as `$session`, then wait when ready to handle an interaction or result:
 
 ```sh
-maka run "Inspect this project" --json
 maka run "Inspect this project" --no-wait
+maka run --session "$session" --wait --json
 ```
 
-Both forms return a `sessionId` in their JSON result. Ordinary runs still wait and print the final
-answer by default. Built-in AskUser is an exception: it returns question JSON and exits 0
-while the Turn waits. Scripts that need a final answer should use `--json` and inspect `status`,
-not treat exit code 0 alone as task completion. `--no-wait` returns after admission; use the
-Session ID to observe the eventual result. Given a returned Session ID in `$session`:
+`--no-wait` already returns JSON; an extra `--json` is unnecessary. Ordinary `maka run` keeps its
+blocking default and prints the final answer, making it suitable for a single shell call.
+Use `maka run "Inspect this project" --json` for a structured blocking result.
+
+Choose the next Session call by purpose:
 
 ```sh
 maka run --session "$session" --status
 maka run --session "$session" --recap --json
-maka run --session "$session" --wait --json
-maka run --session "$session" --answer '["Yes",null]' --wait --json
-maka run --session "$session" "Inspect the tests next" --queue --wait --json
+maka run --session "$session" --answer '["Yes",null]' --json
+maka run --session "$session" "Inspect the tests next" --no-wait
 maka run --session "$session" "Focus on the parser" --steer --no-wait
 maka run --session "$session" --result --json
-maka run --session "$session" --resume --wait --json
+maka run --session "$session" --resume --json
+```
+
+An added prompt queues by default, so `--queue` is optional. Repeating the same flag or value is
+accepted; conflicting values and incompatible actions still produce a usage error. Execution commands wait unless given
+`--no-wait`; `--session <id> --wait` explicitly observes the current Turn without submitting work.
+
+| Call | Default stdout | Meaning of exit 0 |
+| --- | --- | --- |
+| Ordinary run or Session wait/result | Final answer text; pending interactions are JSON | Inspect the outcome; a pending interaction still needs a decision |
+| `--no-wait` | JSON admission receipt | Submission accepted, not task completion |
+| `--status` / `--stop` | JSON | Observation or stop request succeeded |
+| Model settings without an action | JSON with `configuration` | Settings saved; execution is unchanged |
+| `--recap` | Summary text | Independent summary generated |
+
+Use `--json` on waits, results, and recaps for uniform structured results. `status` is the lifecycle;
+`finalOutput` is the task answer; `recap` is progress; `submission` is an admission receipt. CLI errors
+go to stderr and can leave stdout empty. For a remote Host, repeat the same `--host <profile-id>`
+on every call. `--session` inherits saved settings and accepts persistent model/connection/thinking updates.
+Workspace, timeout, and Graph overrides still require legacy `--resume <id>` and are rejected
+before connecting when combined with `--session`.
+
+Change a Session's model persistently, then explicitly choose continuation or a new message:
+
+```sh
+maka run --session "$session" --model "other-model"
+maka run --session "$session" --model "other-model" --resume --json
+maka run --session "$session" --model "other-model" "Continue the review" --no-wait
 ```
 
+These are three alternatives. The first only saves settings and returns a JSON `configuration`
+receipt with `connection`, `model`, and `thinking`; it does not start or resume execution.
+Add `--connection <slug>` to select a different saved account. Model/connection changes reset
+thinking to the model default unless `--thinking` is supplied. The new settings persist for future
+calls, like Desktop model selection; global defaults and permissions are unchanged.
+
+Settings are saved before an explicit action, so they remain saved if that later action fails.
+CLI does not classify why execution stopped or choose a model automatically. Host errors such
+as busy-session rejection are returned without implicitly stopping the Turn or resubmitting the
+old prompt. Resume uses the Host's existing safe-continuation operation; a new prompt is new input.
+
 `--recap` generates a one-sentence summary of recorded Session progress using the same
 independent model call as TUI `/recap`. It works during an active Turn without steering,
 queueing, stopping, or waiting for that Turn to finish. Without `--json` it prints the summary;
@@ -154,7 +192,8 @@ The recap is a snapshot of the history captured when generation starts. It canno
 work or results from a tool that has not returned, and execution can advance while it is generated.
 Each invocation requests a fresh summary and consumes model usage; `--status` remains the cheap
 status query. A recap is not the final answer (`--result`) or a reply from the running agent.
-Do not combine `--recap` with a prompt, another Session action, `--wait`, or `--no-wait`.
+Combine `--status --recap` to return JSON containing both status/boundary and progress. Other
+Session actions, prompts, and wait flags cannot be combined with recap.
 Successful recap generation exits 0 even if the Session's latest Turn failed; generation errors
 exit 1. Ctrl+C cancels the CLI observer and leaves Host execution running.
 
@@ -177,8 +216,8 @@ For Host-owned Session execution, `--wait` and `--status` return `needs_approval
 `expansion`, and `justification`; internal request IDs stay inside the CLI. Review the scope, then:
 
 ```sh
-maka run --session "$session" --approve --wait --json
-maka run --session "$session" --deny --wait --json
+maka run --session "$session" --approve --json
+maka run --session "$session" --deny --json
 ```
 
 These are alternative decisions, not a sequence to execute. Both continue the original Turn and
@@ -193,7 +232,7 @@ Ordinary blocking runs retain their existing sandbox-boundary denial behavior.
 For coordination, submit follow-ups without blocking, then service the Session's interactions:
 
 ```sh
-maka run --session "$session" "Inspect the tests next" --queue --no-wait --json
+maka run --session "$session" "Inspect the tests next" --no-wait
 maka run --session "$session" --wait --json
 ```
 
@@ -209,7 +248,7 @@ Assistant Inbox conversations instead return `submission: "inbox"`, without a wo
 
 Queue is the default: each explicit queued message opens its own subsequent Turn. Steer uses the
 Host's existing current-Turn delivery and may carry over if it misses that Turn's last boundary.
-Submitting a queued prompt with `--wait` follows that message's execution. It does not return
+Submitting a queued prompt without `--no-wait` follows that message's execution (`--wait` is optional). It does not return
 questions from an earlier Turn blocking the queue; service those through a separate Session
 wait/answer call, or use the coordination workflow above. Steer with `--wait`
 observes the Turn selected on attachment; it does not guarantee consumption before that Turn ends. A standalone `--wait` fixes its
@@ -227,9 +266,7 @@ remain waiting in the Host. A wait reports the actual interaction kind and a rec
 interactive client on the same Host, or stop the Turn explicitly. Detached admission does not
 promise that subsequent execution will need no interaction.
 
-Session commands use the saved configuration. Keep using the legacy `--resume <session-id>`
-selector for blocking runs that supply configuration, cwd, timeout or Graph options; that selector
-continues accepting a new prompt. `--session <id> --resume` instead requests safe continuation of
+`--session <id> --resume` instead requests safe continuation of
 stopped execution. An idle Session prompt can use `--max-steps`; a queued or steering message
 cannot attach a step cap to an existing Turn.
 
diff --git a/packages/cli/README.zh-CN.md b/packages/cli/README.zh-CN.md
index 46ef71b002..be4715d5f7 100644
--- a/packages/cli/README.zh-CN.md
+++ b/packages/cli/README.zh-CN.md
@@ -107,28 +107,64 @@ Maka 默认会在执行高权限工具操作前询问。`maka run --yolo` 会授
 权限,只应在你允许任务修改的环境中使用。
 
 
-主代理可以请求结构化结果,或显式提交后返回:
+主代理跨多次调用协调任务时,推荐使用 `--no-wait`,把返回的 `sessionId` 保存为 `$session`,
+准备处理交互或接收结果时再等待:
 
 ```sh
-maka run "检查这个项目" --json
 maka run "检查这个项目" --no-wait
+maka run --session "$session" --wait --json
 ```
 
-这两种调用的 JSON 结果都包含 `sessionId`。普通调用仍默认等待并输出最终答案。
-`--no-wait` 在提交被接收后返回;使用会话 ID 查询后续结果。假设返回的会话 ID 保存在
-`$session` 中:
+`--no-wait` 本身就返回 JSON,无需再加 `--json`。普通 `maka run` 仍默认等待并输出最终答案,
+适合单次 shell 调用;需要结构化的阻塞结果时使用 `maka run "检查这个项目" --json`。
+
+根据目的选择下一次会话调用:
 
 ```sh
 maka run --session "$session" --status
 maka run --session "$session" --recap --json
-maka run --session "$session" --wait --json
-maka run --session "$session" --answer '["是",null]' --wait --json
-maka run --session "$session" "接下来检查测试" --queue --wait --json
+maka run --session "$session" --answer '["是",null]' --json
+maka run --session "$session" "接下来检查测试" --no-wait
 maka run --session "$session" "重点检查参数解析" --steer --no-wait
 maka run --session "$session" --result --json
-maka run --session "$session" --resume --wait --json
+maka run --session "$session" --resume --json
+```
+
+追加 prompt 默认排队,无需重复 `--queue`。重复相同参数或相同值可以接受;值冲突或无法
+同时执行的动作组合仍返回用法错误。执行命令默认等待,指定 `--no-wait` 才提交后返回;
+`--session <id> --wait` 则是不提交新工作,只观察当前 Turn。
+
+| 调用 | 默认 stdout | 退出码 0 的含义 |
+| --- | --- | --- |
+| 普通 run 或会话 wait/result | 最终答案文本;待处理交互返回 JSON | 仍须检查结果,待处理交互需要调用方决定 |
+| `--no-wait` | JSON 接收回执 | 提交被接收,不代表任务完成 |
+| `--status` / `--stop` | JSON | 状态查询或停止请求成功 |
+| 模型设置且无动作 | 带 `configuration` 的 JSON | 设置已保存,执行状态不变 |
+| `--recap` | 摘要文本 | 独立摘要生成成功 |
+
+在 wait、result、recap 上加 `--json` 可统一为结构化结果。`status` 表示生命周期,
+`finalOutput` 是任务答案,`recap` 是进展,`submission` 是接收回执。CLI 错误写入 stderr,
+此时 stdout 可能为空。远程 Host 的每次调用都应携带相同的 `--host <profile-id>`。
+`--session` 继承已保存的设置,并允许持久更新模型、连接和 thinking。工作目录、timeout 和
+Graph 覆盖仍需使用旧选择器 `--resume <id>`;与 `--session` 组合时会在连接前拒绝。
+
+会话模型切换会持久保存,再由显式参数决定续跑或发送新消息:
+
+```sh
+maka run --session "$session" --model "other-model"
+maka run --session "$session" --model "other-model" --resume --json
+maka run --session "$session" --model "other-model" "继续审查" --no-wait
 ```
 
+这三条是不同用法。第一条只保存设置,返回带有 `connection`、`model`、`thinking` 的 JSON
+`configuration` 回执,不会启动或恢复执行。加 `--connection <slug>` 可以选择另一个已保存的
+账号。切换模型或连接会将 thinking 重置为模型默认值,除非显式指定 `--thinking`。
+与 Desktop 切换模型一样,新设置对后续调用持续生效;全局默认值和权限不变。
+
+设置先保存,再执行显式动作;即使后续动作失败,已保存的设置仍然保留。CLI 不判断会话因何
+停止,也不自动选择模型。Host 返回忙碌等错误时会直接返回,不隐式停止 Turn 或重发旧 prompt。
+resume 使用 Host 已有的安全续跑操作;提供新 prompt 则是新输入。
+
 `--recap` 复用 TUI `/recap` 的独立模型调用,用一句话总结会话已记录的进展。
 它可以在 Turn 运行中使用,不会 steering、排队、停止当前 Turn,也不等 Turn 完成。
 不加 `--json` 时直接输出摘要;加上后返回 `sessionId`、当前 `status` 和独立的 `recap` 字段,例如:
@@ -140,7 +176,8 @@ maka run --session "$session" --resume --wait --json
 摘要基于生成开始时读取的历史快照,无法看到尚未记录的工作或尚未返回的工具结果;
 生成期间任务可能继续推进。每次调用都会重新请求模型并产生模型用量;只查状态继续用
 `--status`。摘要不是最终答案(`--result`),也不是正在运行的代理直接回复的消息。
-`--recap` 不能与 prompt、其他会话动作、`--wait` 或 `--no-wait` 组合使用。
+`--status --recap` 可组合返回包含状态、执行边界和进展的 JSON;recap 不能与其他会话动作、
+prompt 或等待参数组合使用。
 摘要生成成功返回退出码 0,即使会话最近的 Turn 失败;生成失败返回退出码 1。
 Ctrl+C 取消 CLI 观察,不会停止 Host 中的执行。
 
@@ -162,8 +199,8 @@ steer 和回答问题不需要重复 `--yolo`,也不会改变执行边界;
 ID 不暴露。查看范围后,选择批准或拒绝:
 
 ```sh
-maka run --session "$session" --approve --wait --json
-maka run --session "$session" --deny --wait --json
+maka run --session "$session" --approve --json
+maka run --session "$session" --deny --json
 ```
 
 上面两条命令是二选一,不能顺序执行。它们都继续原 Turn,要求会话中恰好有一个待处理沙箱
@@ -176,7 +213,7 @@ Host 扩展受管沙箱边界,并保存供该会话后续执行使用,不会
 协调线程推荐先后台入队,再处理会话交互:
 
 ```sh
-maka run --session "$session" "接下来检查测试" --queue --no-wait --json
+maka run --session "$session" "接下来检查测试" --no-wait
 maka run --session "$session" --wait --json
 ```
 
@@ -190,7 +227,7 @@ Turn 的问题,或 `needs_approval` 和沙箱请求;先回答交互,后续
 助手 Inbox 会话返回 `submission: "inbox"`,不包含工作 Turn 的结果或队列计数,即使指定 `--wait` 也立即返回接收回执。这仅确认消息已进入 Inbox;委派工作的结果需通过对应工作会话查询。
 
 默认使用 queue,每条显式排队消息对应一个独立后续 Turn。steer 沿用 Host 当前 Turn 的追加
-机制,错过最后处理边界时可能进入后续 Turn。提交排队消息时的 `--wait` 跟踪该消息对应的执行,不会返回前一 Turn 阻塞队列的问题;
+机制,错过最后处理边界时可能进入后续 Turn。提交排队消息且未指定 `--no-wait` 时,调用跟踪该消息对应的执行(可显式加 `--wait`),不会返回前一 Turn 阻塞队列的问题;
 这类问题要通过独立的会话 wait/answer 处理,或采用上面的协调流程。steer 配合 `--wait` 观察连接时选定的
 Turn,不保证追加消息一定在该 Turn 结束前消费。
 单独 `--wait` 在连接时固定目标,空闲时读取最近结果。`--result` 不等待仍在执行的 Turn。
@@ -204,8 +241,7 @@ run 的取消行为保持原样。显式停止 Host 执行可用 `maka run --ses
 命令:使用同一 Host 的交互式客户端处理,或显式停止该 Turn。后台提交成功不保证后续执行
 不会请求交互。
 
-会话控制使用已保存的配置。需要指定配置、cwd、timeout 或 Graph 选项的阻塞运行,继续使用
-兼容入口 `--resume <session-id>` 选择会话并提供新 prompt。`--session <id> --resume` 则表示
+`--session <id> --resume` 则表示
 安全恢复已停止的执行。空闲会话的新 prompt 可以使用 `--max-steps`,排队或 steer 消息不能
 给已有 Turn 附加步数上限。
 
diff --git a/packages/cli/src/__tests__/run-command.test.ts b/packages/cli/src/__tests__/run-command.test.ts
index a0ac238756..7264b92299 100644
--- a/packages/cli/src/__tests__/run-command.test.ts
+++ b/packages/cli/src/__tests__/run-command.test.ts
@@ -109,6 +109,80 @@ describe('maka run argument parsing', () => {
     assert.deepEqual(JSON.parse(stdout.join('')), { sessionId: 's', status: 'running' });
   });
 
+  test('rejects unsupported Session overrides before connecting while preserving legacy selectors', () => {
+    for (const setting of [['--cwd', '/tmp'], ['--timeout', '10'], ['--graph']]) {
+      assert.equal(parseMakaRunArgs(['--session', 's', 'check', ...setting]).kind, 'error');
+      assert.equal(parseMakaRunArgs(['check', '--resume', 's', ...setting]).kind, 'run');
+    }
+    assert.equal(parseMakaRunArgs(['--session', 's', 'check', '--max-steps', '2']).kind, 'run');
+    for (const extra of [
+      [],
+      ['--status'],
+      ['--resume'],
+      ['--queue', 'check'],
+      ['--steer', 'check'],
+    ])
+      assert.equal(
+        parseMakaRunArgs(['--session', 's', '--max-steps', '2', ...extra]).kind,
+        'error',
+        extra.join(' '),
+      );
+  });
+
+  test('Session model settings persist independently of explicit resume or a new prompt', () => {
+    for (const settings of [
+      ['--model', 'other-model'],
+      ['--connection', 'other-account', '--model', 'other-model'],
+      ['--thinking', 'high'],
+    ]) {
+      const change = parseMakaRunArgs(['--session', 's', ...settings]);
+      assert.equal(change.kind, 'run');
+      if (change.kind !== 'run') return;
+      assert.equal(change.options.resumeExecution, undefined);
+      assert.equal(change.options.prompt, undefined);
+      const resume = parseMakaRunArgs(['--session', 's', ...settings, '--resume']);
+      assert.equal(resume.kind, 'run');
+      if (resume.kind === 'run') assert.equal(resume.options.resumeExecution, true);
+      const prompt = parseMakaRunArgs(['--session', 's', ...settings, 'continue']);
+      assert.equal(prompt.kind, 'run');
+      if (prompt.kind === 'run') {
+        assert.equal(prompt.options.prompt, 'continue');
+        assert.equal(prompt.options.resumeExecution, undefined);
+      }
+    }
+  });
+
+  test('invalid Session options return a short usage error without opening the Host or reading stdin', async () => {
+    for (const args of [
+      ['--session', 's', '--status', '--cwd', '/tmp'],
+      ['--session', 's', '--wait', '--max-steps', '2'],
+      ['--unknown'],
+    ]) {
+      const stdout: string[] = [];
+      const stderr: string[] = [];
+      const exit = await runMakaTextCliCore(
+        args,
+        {
+          createContext: async () => assert.fail('must not prepare execution'),
+          listSessions: async () => assert.fail('must not select a Session'),
+          runSession: async () => assert.fail('must not connect to Host'),
+        },
+        {
+          cliCommand: () => 'npm run cli:dev --',
+          stdinIsTTY: () => false,
+          readStdin: async () => assert.fail('must not read stdin'),
+          writeStdout: (text) => stdout.push(text),
+          writeStderr: (text) => stderr.push(text),
+        },
+      );
+      assert.equal(exit, 2);
+      assert.deepEqual(stdout, []);
+      assert.equal(stderr.join('').trim().split('\n').length, 2);
+      assert.match(stderr.join(''), /npm run cli:dev -- run --help/);
+      assert.doesNotMatch(stderr.join(''), /Usage:/);
+    }
+  });
+
   test('recap selects one Session action and rejects execution and wait combinations', () => {
     assert.deepEqual(parseMakaRunArgs(['--session', 's', '--recap', '--json']), {
       kind: 'run',
@@ -118,7 +192,6 @@ describe('maka run argument parsing', () => {
     for (const extra of [
       ['prompt'],
       ['-'],
-      ['--status'],
       ['--result'],
       ['--stop'],
       ['--resume'],
@@ -137,6 +210,39 @@ describe('maka run argument parsing', () => {
       );
   });
 
+  test('accepts equivalent repeated flags and combines status with recap without discarding either', () => {
+    assert.deepEqual(
+      parseMakaRunArgs([
+        '--session',
+        's',
+        '--session',
+        's',
+        '--status',
+        '--recap',
+        '--json',
+        '--json',
+      ]),
+      {
+        kind: 'run',
+        options: { sessionId: 's', status: true, recap: true, json: true, stdinPrompt: false },
+      },
+    );
+    assert.deepEqual(
+      parseMakaRunArgs(['check', '--no-wait', '--no-wait', '--model', 'm', '--model', 'm']),
+      {
+        kind: 'run',
+        options: { prompt: 'check', noWait: true, model: 'm', stdinPrompt: false },
+      },
+    );
+    for (const extra of [['--answer', '["Yes"]'], ['--result'], ['--stop']])
+      assert.equal(
+        parseMakaRunArgs(['--session', 's', '--status', '--recap', ...extra]).kind,
+        'error',
+      );
+    assert.equal(parseMakaRunArgs(['--session', 's', '--session', 'other']).kind, 'error');
+    assert.equal(parseMakaRunArgs(['check', '--model', 'm', '--model', 'other']).kind, 'error');
+  });
+
   test('recap prints text or JSON without reading stdin or returning the Turn failure code', async () => {
     for (const json of [false, true]) {
       const stdout: string[] = [];
diff --git a/packages/cli/src/__tests__/run-session-recap.test.ts b/packages/cli/src/__tests__/run-session-recap.test.ts
index e9a302ca90..57544f757e 100644
--- a/packages/cli/src/__tests__/run-session-recap.test.ts
+++ b/packages/cli/src/__tests__/run-session-recap.test.ts
@@ -29,14 +29,13 @@ import { deferred, waitFor } from '@maka/core/test-only/async-primitives';
 import { openInteractiveExecutionStoresForWrite } from '@maka/storage/execution-stores';
 import { openInteractiveRuntimePolicyStoresForWrite } from '@maka/storage/runtime-policy-stores';
 import { resolveStorageRoot, tryAcquireInteractiveRootOwner } from '@maka/storage/root-authority';
-import { FakeBackend, FAKE_HOLD_OPEN_PROMPT } from '@maka/runtime/test-only/fake-backend';
 import { connectRuntimeHost, type RuntimeHostConnection } from '@maka/runtime-host/client';
 import { RUNTIME_HOST_PROTOCOL_VERSION } from '@maka/runtime-host/protocol';
 import { startExecutionRuntimeHostService } from '@maka/runtime-host/server';
 
 const fixturePath = fileURLToPath(new URL('./run-session-process-fixture.js', import.meta.url));
 
-test('external CLI recap reads active progress without steering or waiting for the Turn', {
+test('external CLI combines queries and persists model changes before explicit continuation', {
   timeout: 90_000,
 }, async (t) => {
   const root = await mkdtemp(join(tmpdir(), 'maka-cli-recap-'));
@@ -45,6 +44,8 @@ test('external CLI recap reads active progress without steering or waiting for t
   let connection: RuntimeHostConnection | undefined;
   const recapText = 'You asked for a review; the agent is still working on it.';
   const requests: string[] = [];
+  const modelRequests: Array<{ model: string; messages: unknown[] }> = [];
+  let holdFirstRun = true;
   let requestArrived = deferred<void>();
   let releaseResponse = deferred<void>();
   let emptyResponse = false;
@@ -52,6 +53,37 @@ test('external CLI recap reads active progress without steering or waiting for t
     void (async () => {
       let body = '';
       for await (const chunk of request) body += chunk;
+      const input = JSON.parse(body);
+      if (input.stream) {
+        modelRequests.push(input);
+        response.setHeader('content-type', 'text/event-stream');
+        const chunk = (delta: object, finishReason: string | null) =>
+          `data: ${JSON.stringify({
+            id: 'model-response',
+            object: 'chat.completion.chunk',
+            created: 1,
+            model: input.model,
+            choices: [{ index: 0, delta, finish_reason: finishReason }],
+          })}\n\n`;
+        response.write(
+          chunk(
+            {
+              role: 'assistant',
+              content: holdFirstRun
+                ? 'Inspected the parser; verification is pending.'
+                : `Ran using ${input.model}.`,
+            },
+            null,
+          ),
+        );
+        if (holdFirstRun) {
+          holdFirstRun = false;
+          return; // The original provider request stays active until the caller explicitly stops it.
+        }
+        response.write(chunk({}, 'stop'));
+        response.end('data: [DONE]\n\n');
+        return;
+      }
       requests.push(body);
       requestArrived.resolve();
       await releaseResponse.promise;
@@ -97,33 +129,40 @@ test('external CLI recap reads active progress without steering or waiting for t
   let sessionId: string;
   try {
     const policy = await openInteractiveRuntimePolicyStoresForWrite(owner.lease);
-    const created = await policy.connectionCatalog.create({
-      expectedCatalogRevision: 0,
-      connection: {
-        slug: 'fake',
-        name: 'Fake',
-        providerType: 'ollama',
-        baseUrl: `http://127.0.0.1:${address.port}/v1`,
-        enabled: true,
-        enabledModelIds: ['fake-model'],
-      },
-    });
-    assert.equal(created.kind, 'committed');
-    if (created.kind !== 'committed') throw new Error('Connection setup failed');
-    const connectionId = created.snapshot.connections[0]!.connectionId;
-    const fetch = await policy.operations.beginModelFetch(connectionId);
-    assert.equal(fetch.kind, 'ready');
-    if (fetch.kind !== 'ready') throw new Error('Model setup failed');
-    assert.equal(
-      (
-        await policy.operations.completeModelFetch(fetch.ticket, {
-          models: [{ id: 'fake-model' }],
-          source: 'fetched',
-          fetchedAt: Date.now(),
-        })
-      ).kind,
-      'committed',
-    );
+    let connectionId = '';
+    for (const slug of ['fake', 'alternate']) {
+      const catalog = await policy.connectionCatalog.getSnapshot();
+      const created = await policy.connectionCatalog.create({
+        expectedCatalogRevision: catalog.revision,
+        connection: {
+          slug,
+          name: slug,
+          providerType: 'ollama',
+          baseUrl: `http://127.0.0.1:${address.port}/v1`,
+          enabled: true,
+          enabledModelIds: ['fake-model', 'other-model'],
+        },
+      });
+      assert.equal(created.kind, 'committed');
+      if (created.kind !== 'committed') throw new Error('Connection setup failed');
+      const selectedId = created.snapshot.connections.find(
+        (item) => item.slug === slug,
+      )!.connectionId;
+      if (slug === 'fake') connectionId = selectedId;
+      const fetch = await policy.operations.beginModelFetch(selectedId);
+      assert.equal(fetch.kind, 'ready');
+      if (fetch.kind !== 'ready') throw new Error('Model setup failed');
+      assert.equal(
+        (
+          await policy.operations.completeModelFetch(fetch.ticket, {
+            models: [{ id: 'fake-model' }, { id: 'other-model' }],
+            source: 'fetched',
+            fetchedAt: Date.now(),
+          })
+        ).kind,
+        'committed',
+      );
+    }
     const stores = await openInteractiveExecutionStoresForWrite(owner.lease);
     try {
       sessionId = (
@@ -152,8 +191,6 @@ test('external CLI recap reads active progress without steering or waiting for t
           context,
           { ...options, bootstrapRuntimePolicy: false },
           {
-            primaryBackendFactory: (backendContext: ConstructorParameters<typeof FakeBackend>[0]) =>
-              new FakeBackend(backendContext),
             generateSessionTitle: async () => undefined,
           },
         ),
@@ -206,7 +243,7 @@ test('external CLI recap reads active progress without steering or waiting for t
     );
     return { child, result };
   };
-  const started = await cli(FAKE_HOLD_OPEN_PROMPT, '--no-wait', '--json').result;
+  const started = await cli('Inspect the parser and verify it.', '--no-wait').result;
   assert.equal(started.code, 0, started.stderr);
   await waitFor(
     async () => {
@@ -217,6 +254,10 @@ test('external CLI recap reads active progress without steering or waiting for t
   );
   const turn = (await readSnapshot()).snapshot.rootTurn!;
   const boundary = await probe.request('session.execution_boundary.query', { sessionId });
+  const busyChange = await cli('--model', 'other-model').result;
+  assert.equal(busyChange.code, 1);
+  assert.match(busyChange.stderr, /configuration cannot change while a linked Turn is active/);
+  assert.equal((await readSnapshot()).snapshot.rootTurn?.runId, turn.runId);
   const pendingRecap = cli('--recap', '--json').result;
   await requestArrived.promise;
   const during = await cli('--status', '--json').result;
@@ -231,7 +272,7 @@ test('external CLI recap reads active progress without steering or waiting for t
     queued: 0,
     recap: recapText,
   });
-  assert.match(requests[0]!, /Fake backend waiting/);
+  assert.match(requests[0]!, /Inspected the parser/);
   assert.match(requests[0]!, /ONE sentence/);
   const afterRecap = (await readSnapshot()).snapshot;
   assert.equal(afterRecap.rootTurn?.turnId, turn.turnId);
@@ -247,6 +288,15 @@ test('external CLI recap reads active progress without steering or waiting for t
   assert.equal(plain.code, 0, plain.stderr);
   assert.equal(plain.stdout, `${recapText}\n`);
   assert.equal(requests.length, 2, 'each invocation requests a fresh recap');
+  const combined = await cli('--status', '--recap').result;
+  assert.equal(combined.code, 0, combined.stderr);
+  assert.deepEqual(JSON.parse(combined.stdout), {
+    sessionId,
+    status: 'running',
+    queued: 0,
+    recap: recapText,
+    executionBoundary: boundary,
+  });
   emptyResponse = true;
   const failed = await cli('--recap', '--json').result;
   assert.equal(failed.code, 1);
@@ -272,4 +322,56 @@ test('external CLI recap reads active progress without steering or waiting for t
   assert.equal(stopped.code, 0, stopped.stderr);
   assert.equal(JSON.parse(stopped.stdout).status, 'failed');
   assert.equal(JSON.parse(stopped.stdout).recap, recapText);
+  const invalidModel = await cli('--model', 'missing-model').result;
+  assert.equal(invalidModel.code, 1);
+  const changed = await cli('--model', 'other-model').result;
+  assert.equal(changed.code, 0, changed.stderr);
+  assert.deepEqual(JSON.parse(changed.stdout).configuration, {
+    connection: 'fake',
+    model: 'other-model',
+    thinking: null,
+  });
+  assert.equal(
+    (await readSnapshot()).snapshot.rootTurn?.runId,
+    turn.runId,
+    'model change alone does not resume',
+  );
+  assert.equal(
+    modelRequests.length,
+    1,
+    'no model request is dispatched by a configuration-only command',
+  );
+  const saved = await probe.request('session.catalog.query', { kind: 'get', sessionId });
+  assert.equal(saved.kind, 'session');
+  assert.equal(
+    saved.kind === 'session' && saved.session && 'model' in saved.session && saved.session.model,
+    'other-model',
+  );
+  const resumed = await cli('--model', 'other-model', '--resume', '--json').result;
+  assert.equal(resumed.code, 0, resumed.stderr);
+  assert.equal(JSON.parse(resumed.stdout).sessionId, sessionId);
+  assert.match(JSON.parse(resumed.stdout).finalOutput, /Ran using other-model/);
+  assert.equal(modelRequests.at(-1)?.model, 'other-model');
+  const newInput = await cli('--model', 'fake-model', 'Continue the review.', '--json').result;
+  assert.equal(newInput.code, 0, newInput.stderr);
+  assert.match(JSON.parse(newInput.stdout).finalOutput, /Ran using fake-model/);
+  assert.equal(modelRequests.at(-1)?.model, 'fake-model');
+  const moved = await cli('--connection', 'alternate', '--model', 'other-model').result;
+  assert.equal(moved.code, 0, moved.stderr);
+  assert.deepEqual(JSON.parse(moved.stdout).configuration, {
+    connection: 'alternate',
+    model: 'other-model',
+    thinking: null,
+  });
+  const afterMove = await cli('Use the saved settings.', '--json').result;
+  assert.equal(afterMove.code, 0, afterMove.stderr);
+  assert.match(JSON.parse(afterMove.stdout).finalOutput, /Ran using other-model/);
+  const persisted = await probe.request('session.catalog.query', { kind: 'get', sessionId });
+  assert.equal(
+    persisted.kind === 'session' &&
+      persisted.session &&
+      'llmConnectionSlug' in persisted.session &&
+      persisted.session.llmConnectionSlug,
+    'alternate',
+  );
 });
diff --git a/packages/cli/src/run-command-core.ts b/packages/cli/src/run-command-core.ts
index 8749e49b19..c3fb22c161 100644
--- a/packages/cli/src/run-command-core.ts
+++ b/packages/cli/src/run-command-core.ts
@@ -151,7 +151,6 @@ const VALUE_FLAGS = new Set([
   'project',
 ]);
 
-const REPEATABLE_VALUE_FLAGS = new Set<string>();
 const BOOLEAN_FLAGS = new Set([
   'continue',
   'yolo',
@@ -186,18 +185,16 @@ export function parseMakaRunArgs(argv: readonly string[]): ParseMakaRunArgsResul
     if (!literal && arg.startsWith('--')) {
       const name = arg.slice(2);
       if (BOOLEAN_FLAGS.has(name) || (name === 'resume' && selectsSession)) {
-        if (booleanFlags.has(name)) return { kind: 'error', message: `option repeated: ${arg}` };
         booleanFlags.add(name);
         continue;
       }
       if (!VALUE_FLAGS.has(name)) return { kind: 'error', message: `unknown option: ${arg}` };
-      if (!REPEATABLE_VALUE_FLAGS.has(name) && flags.has(name)) {
-        return { kind: 'error', message: `option repeated: ${arg}` };
-      }
       const value = argv[index + 1];
       if (value === undefined || value.startsWith('--')) {
         return { kind: 'error', message: `option ${arg} needs a value` };
       }
+      if (flags.has(name) && flags.get(name) !== value)
+        return { kind: 'error', message: `conflicting values for ${arg}` };
       flags.set(name, value);
       index += 1;
       continue;
@@ -222,7 +219,7 @@ export function parseMakaRunArgs(argv: readonly string[]): ParseMakaRunArgsResul
     return { kind: 'error', message: '--queue and --steer cannot be used together' };
   }
   const actions =
-    Number(booleanFlags.has('status')) +
+    Number(booleanFlags.has('status') && !booleanFlags.has('recap')) +
     Number(booleanFlags.has('result')) +
     Number(booleanFlags.has('recap')) +
     Number(flags.has('answer')) +
@@ -233,8 +230,7 @@ export function parseMakaRunArgs(argv: readonly string[]): ParseMakaRunArgsResul
   if (actions > 1)
     return {
       kind: 'error',
-      message:
-        'choose one of --status, --result, --recap, --answer, --approve, --deny, --resume or --stop',
+      message: 'choose one Session action, or combine --status with --recap',
     };
   if ((booleanFlags.has('approve') || booleanFlags.has('deny')) && booleanFlags.has('yolo'))
     return {
@@ -251,6 +247,24 @@ export function parseMakaRunArgs(argv: readonly string[]): ParseMakaRunArgsResul
       message: '--session cannot be combined with --continue or --project',
     };
   }
+  if (sessionId) {
+    const override = ['cwd', 'timeout', 'graph'].find(
+      (name) => flags.has(name) || booleanFlags.has(name),
+    );
+    if (override)
+      return {
+        kind: 'error',
+        message: `--${override} cannot be used with --session; use saved settings or legacy --resume <id> for run configuration`,
+      };
+    if (
+      flags.has('max-steps') &&
+      (!positional.length || actions || booleanFlags.has('queue') || booleanFlags.has('steer'))
+    )
+      return {
+        kind: 'error',
+        message: '--max-steps requires a Session prompt without --queue/--steer',
+      };
+  }
   if (actions && positional.length)
     return { kind: 'error', message: 'session actions do not accept a prompt' };
   if ((booleanFlags.has('queue') || booleanFlags.has('steer')) && (!positional.length || actions)) {
@@ -372,7 +386,9 @@ export async function runMakaTextCliCore(
     return 0;
   }
   if (parsed.kind === 'error') {
-    deps.writeStderr(`maka run: ${parsed.message}\n\n${makaRunHelpText(deps.cliCommand())}\n`);
+    deps.writeStderr(
+      `maka run: ${parsed.message}\nRun \`${deps.cliCommand()} run --help\` for usage.\n`,
+    );
     return 2;
   }
 
@@ -385,7 +401,7 @@ export async function runMakaTextCliCore(
       if (options.prompt !== undefined || options.stdinPrompt)
         options.prompt = await resolveRunPrompt(options, deps);
       const result = await deps.runSession(options, deps);
-      writeSessionResult(result, deps, options.json === true);
+      writeSessionResult(result, deps, options.json === true || options.status === true);
       const returnsTaskOutcome =
         options.prompt !== undefined ||
         options.answers !== undefined ||
@@ -635,10 +651,10 @@ export function writeSessionResult(
   deps: Pick<MakaRunDeps, 'writeStdout' | 'writeStderr'>,
   json: boolean,
 ): void {
-  if (!json && result.recap !== undefined) deps.writeStdout(withTrailingNewline(result.recap));
-  else if (json || result.status !== 'completed' || result.finalOutput === undefined)
-    deps.writeStdout(`${JSON.stringify(result)}\n`);
-  else deps.writeStdout(withTrailingNewline(result.finalOutput));
+  const text = result.recap ?? (result.status === 'completed' ? result.finalOutput : undefined);
+  deps.writeStdout(
+    json || text === undefined ? `${JSON.stringify(result)}\n` : withTrailingNewline(text),
+  );
   if (result.status === 'failed' && result.error) deps.writeStderr(`maka run: ${result.error}\n`);
 }
 
@@ -662,46 +678,58 @@ async function canonicalDirectory(input: string): Promise<string> {
 function makaRunHelpText(cliCommand: string): string {
   return [
     `Usage: ${cliCommand} run [PROMPT] [options]`,
-    `       ${cliCommand} -p [PROMPT] [options]`,
+    `       ${cliCommand} run --session <id> [PROMPT | ACTION] [options]`,
     '',
-    'Input:',
-    '  -                         Read the complete prompt from stdin',
-    '  PROMPT with piped stdin   Use PROMPT as instruction and stdin as context',
+    'Ordinary runs wait and print the final answer. For parent-agent coordination:',
+    `  ${cliCommand} run "Inspect this project" --no-wait`,
+    `  ${cliCommand} run --session <id> --wait --json`,
     '',
-    'Options:',
-    '  --cwd <path>              Working directory (default: current directory)',
-    '  --connection <slug>       Model connection to use',
-    '  --host <profile-id>       Connect through a saved Runtime Host profile',
-    '  --project <project-id>    Select an existing Project on a remote Host',
-    '  --model <id>              Model to use',
-    '  --thinking <level>        off|on|minimal|low|medium|high|xhigh|max|default',
-    '  --timeout <seconds>       Invocation timeout',
-    '  --max-steps <count>       Tool-step cap',
-    '  --yolo                    Grant full access; Session controls otherwise inherit saved access',
-    '  --session <session-id>    Select a Session (without a prompt: show its status)',
-    '  --wait                    Wait for this message/current Turn; return input/approval early',
-    '  --no-wait                 Submit and return (Host-owned execution only)',
-    '  --status                  Read current Session status without executing',
+    'Execution and output:',
+    '  --no-wait                 Submit and return a JSON receipt (persistent Host required)',
+    '  --wait                    Wait for this message/current Turn (default for prompts)',
+    '  --json                    Return JSON, including final answers and recaps',
+    '  --host <profile-id>       Use the same saved Host profile for every Session call',
+    '',
+    'Session actions (--session <id>; model settings below persist before the action):',
+    '  [no action]               Show status, or save supplied model settings without running',
+    '  --status                  Read status, pending questions/approvals, and execution boundary',
+    '  --recap                   Summarize recorded progress with a separate model call',
     '  --result                  Read the latest Turn result without waiting',
-    '  --recap                   Generate a one-sentence progress recap, even during execution',
-    '  --stop                    Request stop for the current Turn (no --yolo needed)',
     '  --answer <json-array>     Answer the current AskUser, e.g. ["Yes",null]',
-    '  --approve                 Approve the single pending sandbox expansion',
-    '  --deny                    Deny the single pending sandbox expansion',
-    '  --queue                   Queue this prompt as its own next Turn (default)',
-    '  --steer                   Add this prompt to the current execution',
-    '  --json                    Return a structured result, including the Session ID',
-    '  --session <id> --resume   Safely resume stopped execution',
-    '  --resume <session-id>     Legacy Session selector (prefer --session)',
-    '  --continue                Continue the latest compatible task for cwd',
-    '  --graph                   Run this turn in Graph Mode and wait for graph completion',
+    '  --approve / --deny        Decide the single pending sandbox expansion',
+    '  PROMPT                    Queue a new Turn; --steer targets ongoing execution',
+    '  --resume                  Safely continue stopped execution',
+    '  --stop                    Request stop for the current Turn',
+    '',
+    'Model settings (creation or persistent Session updates):',
+    '  --connection <slug>       Select a model connection; otherwise keep the saved account',
+    '  --model <id>              Select a model; otherwise keep the saved model',
+    '  --thinking <level>        off|on|minimal|low|medium|high|xhigh|max|default',
+    '',
+    'Run settings (creation or legacy --resume <id>):',
+    '  --cwd <path>              Working directory (default: current directory)',
+    '  --timeout <seconds>       Invocation timeout',
+    '  --max-steps <count>       Tool-step cap; also supported for an idle Session prompt',
+    '  --project <project-id>    Select an existing Project on a remote Host',
+    '  --graph                   Run in Graph Mode and wait for graph completion',
+    '  --yolo                    Explicitly grant full access, including on Session execution',
+    '',
+    'Input and compatibility:',
+    '  -                         Read the prompt from stdin; piped stdin otherwise adds context',
+    '  --queue                   Explicit spelling of the default Session prompt placement',
+    '  --steer                   Deliver this prompt to ongoing execution',
+    '  --resume <session-id>     Legacy blocking selector; permits run-setting overrides',
+    '  --continue                Legacy selector for the latest compatible task in cwd',
+    `  ${cliCommand} -p [PROMPT]  Alias for run`,
     '  -h, --help                Show help',
     '',
-    'Session controls inherit saved permissions; --yolo explicitly grants full access.',
-    '--status reports the execution boundary; a read-only prompt does not restrict permissions.',
-    '--recap makes a separate model call over recorded progress; it does not steer or wait for the Turn.',
-    'Legacy --resume <id> still requires --yolo for a full-access Session.',
-    '--approve/--deny decide a sandbox request without changing the Session to full access.',
+    'Receipts, status, and pending interactions are JSON without an extra --json.',
+    'Check status, not just exit 0: needs_input/needs_approval require a caller decision.',
+    'finalOutput is a task result; recap is progress; submission is admission; configuration is saved settings.',
+    'Combine --status --recap for a JSON snapshot with both state and progress.',
+    '--status/--result/--recap/--stop do not accept wait flags. Recap consumes model usage.',
+    'Session controls inherit bypass access too; a read-only prompt does not restrict it.',
+    'Legacy --resume <id> requires --yolo for bypass. --approve/--deny do not switch to bypass.',
   ].join('\n');
 }
 
diff --git a/packages/cli/src/run-session-command.ts b/packages/cli/src/run-session-command.ts
index 6481f4e6fb..749ffd9021 100644
--- a/packages/cli/src/run-session-command.ts
+++ b/packages/cli/src/run-session-command.ts
@@ -19,10 +19,16 @@
 
 import { isInteractionAnswerValidForRequest } from '@maka/core/interaction';
 import type { StoredMessage } from '@maka/core/session';
-import { abortable, type RuntimeHostConnection } from '@maka/runtime-host/client';
+import {
+  abortable,
+  readRuntimeHostConnectionCatalog,
+  type RuntimeHostConnection,
+} from '@maka/runtime-host/client';
 import { updateRuntimeHostSession } from './runtime-host-session-update.js';
 import type {
   InteractionPendingSnapshot,
+  SessionConfigurationPatch,
+  SessionCatalogProjection,
   SessionContinuitySnapshot,
 } from '@maka/runtime-host/protocol';
 import { RuntimeHostSessionChannel } from './runtime-host-session-channel.js';
@@ -46,17 +52,9 @@ export async function runMakaSessionCommand(
       'Session control execution requires a persistent installation; use the blocking legacy run path for temporary npx invocations',
     );
   // Never silently discard options that the old turn.start path supports.
-  if (
-    options.graph ||
-    options.connection ||
-    options.model ||
-    options.thinking !== undefined ||
-    options.cwd ||
-    options.projectId ||
-    options.timeoutMs
-  ) {
+  if (options.graph || options.cwd || options.projectId || options.timeoutMs) {
     throw new Error(
-      'Session commands use the saved configuration; use the blocking legacy --resume <id> path for configuration, cwd, timeout or Graph options',
+      'Session workspace, timeout and Graph overrides require the blocking legacy --resume <id> path',
     );
   }
   if (
@@ -79,6 +77,19 @@ export async function runMakaSessionCommand(
   };
   try {
     assertActive();
+    const configured =
+      options.model !== undefined ||
+      options.connection !== undefined ||
+      options.thinking !== undefined
+        ? await configureSessionModel(connection, options, assertActive)
+        : undefined;
+    const configuration = configured
+      ? {
+          connection: configured.llmConnectionSlug,
+          model: configured.model,
+          thinking: configured.thinkingLevel ?? null,
+        }
+      : undefined;
     let target = observer.snapshot.rootTurn?.turnId;
     if (options.recap) {
       const result = await abortable(
@@ -92,7 +103,16 @@ export async function runMakaSessionCommand(
       );
       assertActive();
       if (result.kind === 'failed') throw new Error(`Session recap failed: ${result.errorClass}`);
-      return { ...observer.status(), recap: result.text };
+      const executionBoundary = options.status
+        ? await connection.request('session.execution_boundary.query', { sessionId })
+        : undefined;
+      assertActive();
+      return {
+        ...observer.status(),
+        recap: result.text,
+        ...(configuration ? { configuration } : {}),
+        ...(executionBoundary ? { executionBoundary } : {}),
+      };
     }
     let messageId: string | undefined;
     let submission: MakaRunSessionResult['submission'];
@@ -259,7 +279,11 @@ export async function runMakaSessionCommand(
         sessionId,
       });
       assertActive();
-      return { ...observer.status(), executionBoundary };
+      return {
+        ...observer.status(),
+        executionBoundary,
+        ...(configuration ? { configuration } : {}),
+      };
     }
     if (options.noWait) {
       if (submission !== 'queued' && submission !== 'steering')
@@ -328,6 +352,54 @@ function terminal(status: string): boolean {
   return status === 'completed' || status === 'failed' || status === 'cancelled';
 }
 
+async function configureSessionModel(
+  connection: RuntimeHostConnection,
+  options: MakaRunOptions,
+  assertActive: () => void,
+): Promise<SessionCatalogProjection> {
+  const selected =
+    options.connection !== undefined
+      ? (await readRuntimeHostConnectionCatalog(connection)).connections.find(
+          (candidate) => candidate.slug === options.connection,
+        )
+      : undefined;
+  if (options.connection !== undefined && !selected)
+    throw new Error(`Runtime Host model connection is unavailable: ${options.connection}`);
+  return updateRuntimeHostSession(
+    connection,
+    options.sessionId!,
+    (current) => {
+      const changesModel = options.model !== undefined || options.connection !== undefined;
+      let patch: SessionConfigurationPatch = {
+        thinkingLevel: options.thinking ?? null,
+      };
+      if (changesModel) {
+        if (current.executorId && !selected) {
+          patch = { ...patch, executorConfig: { model: options.model! } };
+        } else {
+          const connectionId = selected?.connectionId ?? current.llmConnectionId;
+          if (!connectionId) throw new Error('Model selection requires an explicit --connection');
+          patch = {
+            ...patch,
+            modelTarget: {
+              kind: 'explicit',
+              connectionId,
+              connectionSlug: selected?.slug ?? current.llmConnectionSlug,
+              model: options.model ?? current.model,
+            },
+          };
+        }
+      }
+      return connection.request('session.configuration.update', {
+        sessionId: options.sessionId!,
+        expectedRevision: current.revision,
+        patch,
+      });
+    },
+    { operation: 'session.configuration.update', assertRequestAllowed: assertActive },
+  );
+}
+
 class SessionRunObserver {
   readonly #connection: RuntimeHostConnection;
   readonly #sessionId: string;
diff --git a/packages/cli/src/run-session-result.ts b/packages/cli/src/run-session-result.ts
index cf6bc79ea1..084514904a 100644
--- a/packages/cli/src/run-session-result.ts
+++ b/packages/cli/src/run-session-result.ts
@@ -23,6 +23,7 @@ import type {
 } from '@maka/core/interaction';
 import type { InteractionPendingSnapshot, TurnProviderRetry } from '@maka/runtime-host/protocol';
 import type { ExecutionBoundarySummary } from '@maka/core/sandbox-boundary';
+import type { ThinkingLevel } from '@maka/core/model-thinking';
 
 export interface MakaRunSessionResult {
   sessionId: string;
@@ -30,6 +31,7 @@ export interface MakaRunSessionResult {
   finalOutput?: string;
   /** Model-generated snapshot of recorded progress, independent of the Turn's final output. */
   recap?: string;
+  configuration?: { connection: string; model: string; thinking: ThinkingLevel | null };
   error?: string;
   questions?: readonly InteractionQuestion[];
   pendingQuestionCount?: number;
diff --git a/skills/maka-cli/SKILL.md b/skills/maka-cli/SKILL.md
index d988edc546..94ac41a9d5 100644
--- a/skills/maka-cli/SKILL.md
+++ b/skills/maka-cli/SKILL.md
@@ -1,6 +1,6 @@
 ---
 name: maka-cli
-description: Delegate work to Maka through its CLI and manage sessions, waits, AskUser answers, queued prompts, steering, and safe continuation. Use when an agent needs to invoke maka run or coordinate Maka as a child agent.
+description: Coordinate Maka CLI as a worker to submit tasks, inspect progress, update saved models, and handle Session questions or approvals.
 ---
 <!--
   Licensed to the Apache Software Foundation (ASF) under one
@@ -23,140 +23,50 @@ description: Delegate work to Maka through its CLI and manage sessions, waits, A
 
 # Maka CLI
 
-Use Maka as a child agent while keeping the parent responsible for the task and final judgment.
+Delegate work to Maka; the parent owns the assignment and final judgment.
 
-## Check the installed interface
+## Submit, then wait
 
-Read `maka run --help` before choosing a workflow. The installed CLI is the authority for available flags; if session controls are missing, use its supported blocking run or report the version gap. Do not assume installing this skill installs or upgrades Maka.
+Check `maka run --help` for the installed interface. This skill does not upgrade Maka. Preserve the user's Host, model, cwd, and permissions; otherwise use existing defaults. Repeat the same `--host <profile-id>` on subsequent calls. Model, connection, and thinking options can also persistently update an existing Session.
 
-Use the user's chosen connection, model, Host, and working directory. Otherwise use existing defaults. Supply configuration such as `--connection`, `--model`, and `--cwd` when creating the Session; subsequent `--session` commands use its saved configuration. Do not change login, defaults, or provider to work around an error without authorization.
-
-## Submit and observe
-
-For a simple task, ordinary run waits by default:
-
-```sh
-maka run "Inspect the project and report your findings" --json
-```
-
-For work the parent will coordinate across CLI calls:
+Prefer this flow when coordinating work across calls:
 
 ```sh
-maka run "Inspect the project and report your findings" --no-wait --json
-# Save the returned sessionId as $session.
-maka run --session "$session" --status --json
+maka run "Inspect the project and report findings" --no-wait
+# Save the returned sessionId with its Host and assignment as $session.
 maka run --session "$session" --wait --json
 ```
 
-Keep the returned Session ID with its assignment. Admission with `status: running` is not task completion. Use blocking `--wait` rather than repeatedly polling status when the parent only needs an answer or final result.
-
-A standalone wait selects the current Turn when it attaches; if idle, it reads the latest result. It does not wait for every queued Turn. Read the latest Turn result without waiting with:
-
-```sh
-maka run --session "$session" --result --json
-```
+`--no-wait` already returns JSON. It is the recommended parent-agent workflow, not the CLI's global default: ordinary `maka run "…" --json` still waits and suits a single call. Detached work requires a persistent installation and cannot own CLI MCP servers or use Graph Mode.
 
-Interpret the structured result:
+Wait when ready for the next interaction or result; avoid repeated status polling. A Session-only wait selects its current Turn, or the latest result if idle. It does not drain the whole queue.
 
-| Status | Parent action |
+| `status` | Parent action |
 | --- | --- |
-| `running` | Execution is not complete; wait when ready. |
-| `needs_input` | Read the questions and submit answers. This is a normal handoff, not failure. |
-| `needs_approval` | Review the sandbox expansion and justification, then approve or deny. |
-| `completed` | Read `finalOutput` and assess it against the assignment. |
-| `failed` | Read the error and report or address its cause before retrying. |
-| `idle` | No execution result is selected; do not infer that the assignment succeeded. |
-
-## Answer AskUser
-
-An ordinary built-in AskUser returns questions and options with `needs_input` and exit code 0. The Host keeps the original Turn waiting. Supply one string or `null` per question, in order:
-
-```sh
-maka run --session "$session" --answer '["Blue"]' --wait --json
-```
-
-Use the returned options and the parent's task context. If the decision belongs to the user and the parent lacks an answer, ask the user. An answer continues the original tool call and Turn; do not use `--resume` for it. If another question arrives, repeat the handoff. When answer submission fails or its outcome is uncertain, read current status/questions before deciding whether to submit again. If multiple pending batches prevent unique selection, report the ambiguity.
-
-## Decide a sandbox request
-
-For Host-owned Session execution, wait/status return `needs_approval` and `approvalRequests`
-containing the requested expansion and justification. Decide from that scope and the parent's task
-authorization. Choose one of these alternatives:
-
-```sh
-maka run --session "$session" --approve --wait --json
-maka run --session "$session" --deny --wait --json
-```
-
-These answer only sandbox-boundary requests and continue the original Turn. They require exactly
-one pending sandbox request in the Session; ambiguity is rejected. Neither needs `--yolo`, and
-combining them with it is rejected. Approval expands the Host's saved managed boundary for subsequent Session execution rather than
-switching the Session to bypass. Ordinary blocking run still denies sandbox expansions.
-
-The command targets the single request pending when it attaches, which may differ from a request
-seen by an earlier invocation. Never automatically retry an uncertain decision; read the current
-request and choose again. Request IDs are tracked internally by the CLI.
-
-## Add work or change direction
-
-Queue is the default for an added prompt; each queued message gets its own later Turn. For
-coordination, submit first, then service the Session's current interactions:
-
-```sh
-maka run --session "$session" "Inspect the tests next" --queue --no-wait --json
-maka run --session "$session" --wait --json
-```
-
-Read `submission` as an admission receipt: `started`, `queued`, or `steering`. It is not completion.
-The accompanying `status` refers to the currently observed Session; a queued receipt can include
-questions from the Turn ahead of it. The queue count can be omitted until its projection catches
-up; status reads a fresh view. Handle `needs_input`/`needs_approval`, then keep observing the
-Session. `queued` counts follow-up plus steering messages, not an exact queue position. Status
-reports the execution boundary and any Host-published `providerRetry`; absence of activity details
-is not proof of a stalled task.
+| `running` | Keep the Session ID; execution is incomplete. |
+| `needs_input` | Answer `questions` using the task context or the user's decision. |
+| `needs_approval` | Assess `approvalRequests` and approve or deny. |
+| `completed` | Read and assess `finalOutput` from wait/result. |
+| `failed` | Read the wait/result error before retrying. |
+| `idle` | No result is selected; do not infer success. |
 
-Use `--queue --wait` when you need that message's exact result and can separately service earlier
-Turn interactions. It follows the submitted message and does not return a predecessor's AskUser;
-waiting on it alone can leave a queue blocked on an unanswered question. A Session-only wait
-selects the current Turn, and result reads the latest Turn, not every queued assignment.
-
-Use steer to change ongoing work:
-
-```sh
-maka run --session "$session" "Focus on the parser" --steer --no-wait --json
-```
-
-Steer is delivered through the Host's current execution boundaries. It can miss the current Turn and carry over; admission does not prove consumption. Steer with `--wait` observes the Turn selected on attachment. A later `--result` means the latest Turn, not an arbitrary earlier assignment.
-
-## Permissions, interruption, and recovery
-
-Session controls (`--session`) inherit saved permissions. They do not need repeated `--yolo`
-for prompts, queue/steer, answers, or safe resume. Use `--yolo` only to explicitly grant full access.
-Inspect `executionBoundary` in status to distinguish managed read-only/writable from bypass.
-Inheriting bypass is not an enforced read-only task, even if the prompt asks only for review.
-The legacy blocking `--resume <id>` selector still requires `--yolo` for a bypass Session.
-
-To stop Host execution explicitly, use the same Host selection and Session ID:
+## Inspect progress or add work
 
 ```sh
-maka run --session "$session" --stop --json
+maka run --session "$session" --status
+maka run --session "$session" --recap --json
+maka run --session "$session" "Inspect the tests next" --no-wait
+maka run --session "$session" "Focus on the parser" --steer --no-wait
 ```
 
-This targets the current Turn observed on attachment and needs no `--yolo`. A successful stop
-request exits 0; it does not mean the assignment succeeded. A successful status query also exits 0
-when it reports task failure. Wait/result continue to reflect task failure through their exit code.
-For permission requests, MCP forms, or other unsupported interactions, wait reports the actual kind
-and leaves the Host waiting; use an interactive client or explicitly stop the Turn. Do not
-repeatedly wait expecting the pending interaction to resolve itself.
+Status returns lifecycle and pending interactions; `--status --recap` returns both state/boundary and progress as JSON. If installed help advertises `--recap`, use it to summarize recorded progress during execution. Each recap calls the model independently and can lag execution; it is neither final output nor a direct worker reply. Portable `session-export` is not a progress query.
 
-Ctrl+C on Session observation ends the observer while Host execution continues. Legacy ordinary blocking run cancellation can stop execution. After interruption of the parent or CLI, query status; do not assume the task stopped or automatically restarted with the parent. For stopped execution, request safe continuation:
+An added prompt queues by default; `--queue` is optional. `submission` confirms admission, while `status` describes the observed Session, possibly an earlier Turn. Service current questions/approvals before expecting queued work to execute. Status and admission receipts already return JSON; use `--json` on waits/results/recaps to keep their output structured.
 
-```sh
-maka run --session "$session" --resume --wait --json
-```
+## Handle decisions and interruption
 
-Add `--yolo` if the Session has full access. The Host can refuse unsafe continuation; report the reason instead of blindly submitting the original task again. Legacy `--resume <session-id>` selects a Session for a blocking run; it differs from `--session <id> --resume`.
+Read [Session control details](references/session-control.md) when answering a question, deciding a sandbox request, changing models or resuming, selecting one queued result, or handling `submission: "inbox"`. It covers request selection and uncertain outcomes.
 
-Preserve provider/auth/quota errors and exit codes. A failed CLI call does not prove a submitted message was rejected; inspect the Session before retrying a potentially duplicate action. Do not interpret a nonzero observer exit alone as child-task failure.
+`--session` controls inherit saved access, including bypass, without repeated `--yolo`. That flag explicitly grants full access; a read-only prompt does not restrict it. Ctrl+C on a Session observer leaves Host execution running; `maka run --session "$session" --stop` explicitly requests stop.
 
-Use a persistent Maka installation for detached work and Session execution controls. Temporary npx invocations can own the Host lifetime. CLI-owned MCP servers cannot detach with `--no-wait`, and MCP form continuation across CLI processes and Graph interaction handoff are unsupported. Keep those workloads on their supported blocking or interactive path rather than promising a cross-process answer/resume.
+Exit 0 can mean admission or a pending interaction, not task completion. After interruption or an uncertain submission, inspect the Session before replaying work. Preserve provider/auth/quota errors and exit codes.
diff --git a/skills/maka-cli/references/session-control.md b/skills/maka-cli/references/session-control.md
new file mode 100644
index 0000000000..b45c0c9e58
--- /dev/null
+++ b/skills/maka-cli/references/session-control.md
@@ -0,0 +1,87 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+# Session control details
+
+Read the section relevant to the current handoff or control action.
+
+## Output and exit codes
+
+Without `--json`, completed answers and recaps print text; receipts, status (including `--status --recap`), and pending interactions print JSON. `--json` also structures final answers and recaps. CLI errors go to stderr and can leave stdout empty.
+
+`finalOutput`/`error` describe a task outcome; status-only queries can omit them. `recap` summarizes recorded evidence and cannot see unreturned tool results or unrecorded work. `submission` (`started`, `queued`, `steering`, `inbox`) is admission only. `queued` counts follow-up plus steering messages, not position, and admission can omit it until the projection catches up. Status also reports `executionBoundary` and any Host-published `providerRetry`; silence alone does not prove a stall.
+
+Successful status, stop, and recap calls exit 0 even when the observed task failed. Wait/result reflect task failure through their exit code. A failed observer alone does not establish worker failure; inspect status/result before retrying.
+
+## Answer AskUser
+
+Built-in AskUser returns `needs_input`, questions/options, and exit 0 while the original Turn waits. Supply one string or `null` per question, in order:
+
+```sh
+maka run --session "$session" --answer '["Blue",null]' --json
+```
+
+Answering continues the original tool call and Turn; do not use `--resume`. If another question arrives, repeat the handoff. Multiple pending batches can prevent unique selection; report that ambiguity. After a failed or uncertain answer submission, reread current questions before sending again.
+
+## Decide a sandbox request
+
+Inspect the expansion and justification in `approvalRequests`, then choose one alternative within the task's authorization:
+
+```sh
+maka run --session "$session" --approve --json
+maka run --session "$session" --deny --json
+```
+
+Exactly one sandbox request must be pending. The command decides the request present on attachment, which can differ from an earlier observation. Never automatically retry an uncertain decision. Neither flag needs `--yolo`; combining them with it is rejected. Approval saves a managed-boundary expansion for subsequent Session execution; it does not switch to bypass. Ordinary blocking runs deny sandbox expansions, so use detached submission and Session controls when the parent must decide.
+
+Permission requests, MCP forms, and other unsupported kinds remain waiting when reported. Use a supported interactive client or explicitly stop; repeated waits will not resolve them.
+
+## Queue, steering, and Inbox
+
+For coordination, submit a follow-up with `--no-wait`, then use Session-only wait/answer calls. Each queued message gets its own later Turn. Omit `--no-wait` and use `--json` only when following that message's exact result while separately servicing earlier Turns: that wait does not return an earlier Turn's AskUser and can leave the queue blocked.
+
+Steering is delivered at Host execution boundaries and can carry over if it misses the current Turn. Admission does not prove consumption. Steer with `--wait` observes the Turn selected on attachment. `--result` reads the latest Turn, not an arbitrary previous assignment.
+
+`submission: "inbox"` returns immediately even with `--wait`, without a worker result or queue count. Inspect delegated work through its own worker Session.
+
+## Persist model settings, then choose an action
+
+```sh
+maka run --session "$session" --model "other-model"
+maka run --session "$session" --model "other-model" --resume --json
+maka run --session "$session" --model "other-model" "Continue the review" --no-wait
+```
+
+These are alternatives: only save settings; save then safely resume; or save then submit new user input. `--connection <slug>` changes the saved account; otherwise the existing account remains. `--thinking` can be updated too; model/connection changes reset thinking to the model default unless explicitly supplied. Changes persist like Desktop model selection without changing global defaults or permissions. A settings-only call returns a JSON `configuration` receipt and leaves execution u [...]
+
+Settings are saved before an explicit action and remain saved if that action fails. Choose a model from the user's instruction or existing authorization; CLI does not classify failures or select replacements. Busy or unsafe-continuation errors come from the Host. The CLI does not implicitly stop execution or replay the old prompt. A new prompt and safe resume are distinct operations.
+
+## Recovery and compatibility
+
+`--stop` targets the current Turn observed on attachment and needs no `--yolo`. Ctrl+C on a Session observer detaches; ordinary legacy blocking-run cancellation can stop execution. Query status after interruption. For stopped work:
+
+```sh
+maka run --session "$session" --resume --json
+```
+
+Saved permissions, including bypass, are inherited. If the Host refuses safe continuation, report the reason instead of resending the assignment.
+
+Legacy `--resume <session-id>` is a blocking selector, distinct from `--session <id> --resume`. It permits workspace/timeout/Graph overrides and still requires `--yolo` for bypass. Prefer `--session` for ordinary controls and model changes.
+
+Temporary npx invocations may own Host lifetime and cannot provide detached work or Session execution controls. CLI-owned MCP servers cannot detach; MCP form continuation across CLI processes and Graph interaction handoff are unsupported. Use their supported blocking or interactive paths.


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org