@bubblebrain-ai/bubble 0.0.58 → 0.0.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +56 -6
  2. package/dist/agent/child-runner.js +5 -1
  3. package/dist/agent/internal-reminder-sanitizer.d.ts +1 -1
  4. package/dist/agent/internal-reminder-sanitizer.js +7 -1
  5. package/dist/agent/profiles.js +4 -4
  6. package/dist/agent/routing-catalog.d.ts +19 -1
  7. package/dist/agent/routing-catalog.js +32 -13
  8. package/dist/agent/subagent/runtime.d.ts +28 -0
  9. package/dist/agent/subagent/runtime.js +34 -30
  10. package/dist/agent/subagent-control.d.ts +7 -0
  11. package/dist/agent/subagent-control.js +2 -0
  12. package/dist/agent/subagent-lifecycle-reminder.js +2 -1
  13. package/dist/agent/subagent-wait-guidance.d.ts +2 -0
  14. package/dist/agent/subagent-wait-guidance.js +8 -0
  15. package/dist/agent/subtask-policy.js +4 -4
  16. package/dist/agent.d.ts +20 -11
  17. package/dist/agent.js +200 -117
  18. package/dist/anthropic-thinking.d.ts +4 -0
  19. package/dist/anthropic-thinking.js +11 -0
  20. package/dist/approval/controller.d.ts +20 -6
  21. package/dist/approval/controller.js +67 -8
  22. package/dist/approval/protected-paths.d.ts +1 -0
  23. package/dist/approval/protected-paths.js +35 -0
  24. package/dist/approval/session-cache.d.ts +12 -26
  25. package/dist/approval/session-cache.js +20 -50
  26. package/dist/approval/types.d.ts +27 -1
  27. package/dist/cli.d.ts +5 -0
  28. package/dist/cli.js +25 -2
  29. package/dist/config.d.ts +3 -0
  30. package/dist/config.js +7 -0
  31. package/dist/context/checkpoint.d.ts +18 -0
  32. package/dist/context/checkpoint.js +117 -0
  33. package/dist/context/compact-llm.js +7 -0
  34. package/dist/context/compact.d.ts +8 -0
  35. package/dist/context/compact.js +40 -7
  36. package/dist/context/llm-compactor.d.ts +36 -2
  37. package/dist/context/llm-compactor.js +227 -44
  38. package/dist/context/session-write-lock.d.ts +20 -0
  39. package/dist/context/session-write-lock.js +162 -0
  40. package/dist/context/tool-images.d.ts +3 -0
  41. package/dist/context/tool-images.js +35 -0
  42. package/dist/context/usage.js +22 -5
  43. package/dist/feishu/agent-host/run-driver.js +56 -9
  44. package/dist/lsp/service.d.ts +13 -1
  45. package/dist/lsp/service.js +163 -43
  46. package/dist/main.js +82 -16
  47. package/dist/mcp/config.d.ts +4 -0
  48. package/dist/mcp/config.js +17 -0
  49. package/dist/mcp/manager.d.ts +13 -1
  50. package/dist/mcp/manager.js +32 -1
  51. package/dist/mcp/transports.js +11 -0
  52. package/dist/memory/db.d.ts +1 -0
  53. package/dist/memory/db.js +20 -4
  54. package/dist/memory/phase1.js +122 -16
  55. package/dist/memory/prompts.js +3 -0
  56. package/dist/model-catalog.js +19 -3
  57. package/dist/model-config.d.ts +1 -0
  58. package/dist/model-config.js +5 -5
  59. package/dist/model-pricing.js +26 -9
  60. package/dist/network/provider-transport-log.d.ts +12 -0
  61. package/dist/network/provider-transport-log.js +32 -0
  62. package/dist/network/provider-transport.js +6 -1
  63. package/dist/network/retry.d.ts +1 -1
  64. package/dist/network/retry.js +1 -1
  65. package/dist/oauth/grok.d.ts +1 -0
  66. package/dist/oauth/grok.js +20 -16
  67. package/dist/oauth/openai-codex.d.ts +1 -0
  68. package/dist/oauth/openai-codex.js +29 -25
  69. package/dist/oauth/refresh-control.d.ts +12 -0
  70. package/dist/oauth/refresh-control.js +42 -0
  71. package/dist/oauth/storage.d.ts +55 -1
  72. package/dist/oauth/storage.js +281 -16
  73. package/dist/permissions/rule.d.ts +6 -3
  74. package/dist/permissions/rule.js +71 -12
  75. package/dist/permissions/settings.d.ts +28 -1
  76. package/dist/permissions/settings.js +93 -3
  77. package/dist/permissions/shell-command.d.ts +36 -0
  78. package/dist/permissions/shell-command.js +226 -0
  79. package/dist/permissions/trust-prompt.d.ts +10 -0
  80. package/dist/permissions/trust-prompt.js +34 -0
  81. package/dist/permissions/trust.d.ts +26 -0
  82. package/dist/permissions/trust.js +113 -0
  83. package/dist/prompt/compose.d.ts +3 -0
  84. package/dist/prompt/compose.js +24 -7
  85. package/dist/prompt/environment.js +2 -2
  86. package/dist/prompt/reminders.js +1 -1
  87. package/dist/prompt/runtime.js +1 -1
  88. package/dist/provider-anthropic.d.ts +6 -2
  89. package/dist/provider-anthropic.js +98 -18
  90. package/dist/provider-error-record.d.ts +9 -0
  91. package/dist/provider-error-record.js +42 -14
  92. package/dist/provider-grok.js +5 -1
  93. package/dist/provider-openai-codex.d.ts +8 -0
  94. package/dist/provider-openai-codex.js +107 -24
  95. package/dist/provider-registry.d.ts +45 -2
  96. package/dist/provider-registry.js +300 -88
  97. package/dist/provider-transform.js +8 -0
  98. package/dist/provider-zen-errors.d.ts +9 -0
  99. package/dist/provider-zen-errors.js +36 -0
  100. package/dist/provider.js +36 -5
  101. package/dist/sdk/index.d.ts +69 -8
  102. package/dist/sdk/index.js +252 -39
  103. package/dist/sdk/replay-event-log.d.ts +7 -0
  104. package/dist/sdk/replay-event-log.js +66 -16
  105. package/dist/session-context-fence.d.ts +16 -0
  106. package/dist/session-context-fence.js +26 -0
  107. package/dist/session-log.d.ts +11 -2
  108. package/dist/session-log.js +187 -39
  109. package/dist/session-types.d.ts +5 -1
  110. package/dist/session.d.ts +39 -7
  111. package/dist/session.js +341 -93
  112. package/dist/skills/format.d.ts +31 -1
  113. package/dist/skills/format.js +45 -24
  114. package/dist/slash-commands/commands.d.ts +3 -0
  115. package/dist/slash-commands/commands.js +29 -14
  116. package/dist/system-prompt.d.ts +3 -1
  117. package/dist/tasks/manager.d.ts +7 -0
  118. package/dist/tasks/manager.js +97 -18
  119. package/dist/tools/agent-lifecycle.js +11 -7
  120. package/dist/tools/child-tools.d.ts +1 -1
  121. package/dist/tools/child-tools.js +4 -4
  122. package/dist/tools/edit-apply.d.ts +0 -3
  123. package/dist/tools/edit-apply.js +45 -249
  124. package/dist/tools/edit.js +3 -3
  125. package/dist/tools/exit-plan-mode.d.ts +5 -0
  126. package/dist/tools/exit-plan-mode.js +3 -2
  127. package/dist/tools/file-state.d.ts +4 -0
  128. package/dist/tools/file-state.js +12 -3
  129. package/dist/tools/grep.js +20 -1
  130. package/dist/tools/index.d.ts +1 -1
  131. package/dist/tools/index.js +3 -3
  132. package/dist/tools/ls.d.ts +3 -0
  133. package/dist/tools/ls.js +85 -0
  134. package/dist/tools/path-utils.d.ts +6 -0
  135. package/dist/tools/path-utils.js +38 -1
  136. package/dist/tools/sensitive-paths.d.ts +8 -0
  137. package/dist/tools/sensitive-paths.js +85 -3
  138. package/dist/tools/skill.d.ts +2 -0
  139. package/dist/tools/skill.js +34 -2
  140. package/dist/tools/task-tools.js +11 -7
  141. package/dist/tui/app.js +22 -12
  142. package/dist/tui/components/approval-dialog.d.ts +5 -5
  143. package/dist/tui/components/approval-dialog.js +16 -51
  144. package/dist/tui/components/skills-panel.d.ts +4 -0
  145. package/dist/tui/components/skills-panel.js +15 -1
  146. package/dist/tui/components/streaming-message.js +7 -2
  147. package/dist/tui/components/tasks-pane.d.ts +38 -0
  148. package/dist/tui/components/tasks-pane.js +182 -32
  149. package/dist/tui/components/transcript.d.ts +11 -4
  150. package/dist/tui/components/transcript.js +158 -71
  151. package/dist/tui/composer-autocomplete.d.ts +17 -2
  152. package/dist/tui/composer-autocomplete.js +79 -2
  153. package/dist/tui/controller/controller.d.ts +9 -4
  154. package/dist/tui/controller/controller.js +87 -95
  155. package/dist/tui/controller/goal-runtime-controller.js +1 -1
  156. package/dist/tui/controller/session-transition.js +1 -1
  157. package/dist/tui/model/subagent-view.d.ts +11 -19
  158. package/dist/tui/model/subagent-view.js +56 -85
  159. package/dist/tui/model/task-lifecycle.d.ts +57 -0
  160. package/dist/tui/model/task-lifecycle.js +124 -0
  161. package/dist/tui/model/trace-groups.js +10 -9
  162. package/dist/tui/trace-groups.js +10 -9
  163. package/dist/types.d.ts +20 -0
  164. package/dist/variant/variant-resolver.js +2 -0
  165. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts +1 -0
  166. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts.map +1 -1
  167. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js +6 -0
  168. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js.map +1 -1
  169. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts +2 -0
  170. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts.map +1 -1
  171. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js +8 -4
  172. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js.map +1 -1
  173. package/package.json +11 -3
  174. package/dist/prompt/skills.d.ts +0 -2
  175. package/dist/prompt/skills.js +0 -4
  176. package/dist/tools/glob.d.ts +0 -5
  177. package/dist/tools/glob.js +0 -199
package/README.md CHANGED
@@ -1,11 +1,39 @@
1
1
  # Bubble
2
2
 
3
- Bubble is a terminal coding agent. It works inside a local project folder: it reads and edits files, runs commands behind configurable approval controls, searches and navigates code with language-server intelligence, browses the web, loads reusable skills, connects MCP tools, fans work out to subagents, and keeps persistent memory across sessions.
3
+ Bubble is a local coding agent with a desktop app and a terminal interface. It works inside a local project folder: it reads and edits files, runs commands behind configurable approval controls, searches and navigates code with language-server intelligence, browses the web, loads reusable skills, connects MCP tools, fans work out to subagents, and keeps persistent memory across sessions.
4
4
 
5
5
  It is provider-agnostic. Bring an API key for OpenAI, Anthropic, Google, DeepSeek, Moonshot/Kimi, Zhipu, Z.AI, MiniMax, Groq, Together, Fireworks, a local OpenAI-compatible endpoint, and more; sign in to ChatGPT with OAuth; or sign in with OAuth to use a ChatGPT or Grok subscription directly.
6
6
 
7
7
  ---
8
8
 
9
+ ## Desktop app
10
+
11
+ The desktop app is the primary direction for interactive development. It uses the existing Bubble SDK, providers and session history, with the Aegis desktop layout, working trace, KanBan, automations, pull requests and settings adapted for a standalone Bubble agent. Skills and model selection use Bubble directly.
12
+
13
+ ```bash
14
+ npm ci
15
+ npm run desktop:install
16
+ npm run dev
17
+ ```
18
+
19
+ `npm run dev` (or `npm run desktop:dev`) starts Electron with Vite hot updates. UI edits refresh in place; Electron/preload/shared code and Agent SDK edits compile and restart the desktop automatically. The first launch compiles the SDK and host, without a renderer production build. Stop everything with Ctrl+C. Use `npm run dev:tui` for terminal development.
20
+
21
+ `npm run desktop` still builds and starts the desktop without hot updates; `npm --prefix desktop start` opens the last compiled build. For a local macOS dogfood app and DMG, run `npm run desktop:package`; it includes the Agent runtime and never publishes. Use `npm run desktop:build` to check a build and `npm run desktop:test` to run desktop checks. See [desktop architecture and migration notes](docs/desktop-architecture.md) for runtime boundaries, data migration and development commands. The CLI remains available.
22
+
23
+ In **Settings → Providers → Bubble**, OpenAI offers **ChatGPT subscription** browser sign-in and a separate **API key** mode. **Grok Subscription** uses browser sign-in only. You can cancel a pending login or sign out from the same panel; OAuth tokens stay in the selected profile's Agent home and are never shown in the API key editor.
24
+
25
+ Desktop data is separated into three profiles (macOS paths shown):
26
+
27
+ | Command | Purpose | Desktop data | Agent data |
28
+ | --- | --- | --- | --- |
29
+ | `npm run dev` | Your persistent development/test sessions | `~/Library/Application Support/Bubble Dev` | `~/.bubble-dev` |
30
+ | `npm run desktop:qa` | Agent verification; a fresh profile each launch | System temporary directory `bubble-desktop-qa-*/desktop` | Same temporary root, `agent/` |
31
+ | `npm run desktop` / `npm --prefix desktop start` | Normal use with production data | `~/Library/Application Support/Bubble` | `~/.bubble` |
32
+
33
+ Sessions, board state, automations, UI preferences, model/account configuration, OAuth data, memory and MCP settings follow the selected profile. History import reads only that profile's Agent home. Dev sessions survive restarts; QA uses a different temporary root for every launch (and keeps that root across hot restarts). Configure development models separately: production history and credentials are not automatically copied. Existing production data is left in place. These profiles separate app data, not the project files you ask the Agent to edit.
34
+
35
+ Run automated verification with `npm run desktop:test`; manual/agent desktop verification must use `npm run desktop:qa`. If your development server is already running, use `PORT=4337 npm run desktop:qa`. QA paths are logged at startup and retained in the system temporary directory for diagnostics. The normal launch commands select their own profile and ignore inherited data-directory overrides.
36
+
9
37
  ## Requirements
10
38
 
11
39
  - Node.js 22.19 or newer — Bubble's only runtime
@@ -50,7 +78,7 @@ bubble
50
78
 
51
79
  On first launch, connect a provider:
52
80
 
53
- - Run `/login` to sign in to ChatGPT (OAuth) and use the Codex models, or
81
+ - Run `/login` and pick an account: ChatGPT (OAuth, Codex models) or Grok Subscription, or
54
82
  - Run `/provider` to select Grok Subscription or add a provider with an API key.
55
83
 
56
84
  Then just type what you want done. Bubble plans, edits files, and runs commands, asking for approval where the current permission mode requires it.
@@ -139,7 +167,13 @@ Turns are driven by the SDK itself, not by iterator consumption: a returned
139
167
  iterator is a replay subscription, so dropping it never stalls or cancels the
140
168
  turn. `openSession(sessionId)` returns a durable handle whose `events` iterable
141
169
  replays the whole session log from sequence 1 and `eventsFrom(n)` resumes after
142
- the last sequence the host durably processed. `stop(sessionId)` is
170
+ the last sequence the host durably processed. Snapshot `handle.latestSequence`
171
+ before sending to subscribe only to new work, including automatic fallback
172
+ turns. Each turn ends with a `terminal` record (`completed`, `failed`, or
173
+ `cancelled`) after its execution slot is released. A completed turn may have
174
+ already started a queued successor; hosts should drain buffered records and
175
+ check `getSessionRunState(sessionId).active` before showing the session as idle.
176
+ `stop(sessionId)` is
143
177
  Claude-style: it interrupts the active turn and keeps the queue; pass
144
178
  `{ cancelQueued: true }` to clear both. `clearQueue(sessionId)` cancels only
145
179
  queued turns, and `deleteSession(sessionId)` awaits teardown before the JSONL
@@ -179,6 +213,18 @@ Built-in providers include OpenAI, Anthropic, Google, DeepSeek, Moonshot (CN and
179
213
 
180
214
  For Doubao Seed models on Volcengine Ark, run `/provider --add doubao` and paste your Ark API key. The built-in endpoint is `https://ark.cn-beijing.volces.com/api/v3` and uses Ark's Responses API. The model picker exposes `minimal`, `low`, `medium`, and `high`, defaulting to `high`; `minimal` disables Ark thinking, while the other levels enable it.
181
215
 
216
+ For **Xiaomi MiMo Token Plan**, choose **Xiaomi MiMo Token Plan** in desktop
217
+ Settings → Providers, or run `/provider --add mimo-token-plan` in the CLI.
218
+ Enter the Token Plan key (`tp-…` for personal plans, `ttp-…` for team plans),
219
+ then select `/model mimo-token-plan:mimo-v2.6-pro` or `mimo-v2.6-flash`.
220
+ Bubble uses Anthropic Messages at
221
+ `https://token-plan-cn.xiaomimimo.com/anthropic/v1/messages`, with streaming,
222
+ tools, and a thinking on/off switch (on by default). Both models have a 1M
223
+ context window and a 128K output cap. V2.5 Pro and V2.5 are also available;
224
+ their announced retirement date is October 21, 2026. Use the plan-specific key,
225
+ not a pay-as-you-go API key. See the
226
+ [official integration guide](https://mimo.mi.com/docs/zh-CN/tokenplan/integration/tools-overview).
227
+
182
228
  ### Custom providers and models
183
229
 
184
230
  For StepFun's standard API, run `/provider --add stepfun-api`, enter your API
@@ -216,7 +262,7 @@ Bubble gates risky actions behind a permission mode. Press `Tab` to cycle modes
216
262
 
217
263
  | Mode | Behavior |
218
264
  | --- | --- |
219
- | Default (Build) | File edits and writes auto-approve; bash and other tools prompt unless covered by an allow rule. |
265
+ | Default (Build) | Workspace file edits and writes auto-approve, except protected files (`.git/`, `.bubble/settings*.json`, `.claude/settings*.json`). Bash, MCP tools and edits outside the workspace prompt unless covered by an allow rule or an "allow for this session" approval. |
220
266
  | Plan | Read-only investigation. The agent proposes a plan and waits for your approval before making changes. |
221
267
  | Bypass | Auto-approves every tool and disables all safety prompts. Enable deliberately with `--dangerously-skip-permissions`. |
222
268
 
@@ -226,6 +272,10 @@ Allow/deny rules are configured per scope and persisted across sessions. Manage
226
272
  - `<project>/.bubble/settings.json` — project scope (commit to share with your team)
227
273
  - `<project>/.bubble/settings.local.json` — local overrides (gitignore)
228
274
 
275
+ The two project files arrive with the repository, so what they grant — allow rules, `mcpServers` (which Bubble would start) and LSP server definitions — only loads once you trust the folder. Like Kimi Code, Bubble asks when it opens such a folder (at startup in the terminal, on the first message in the desktop app), listing what would be enabled; any later change to those settings asks again. Deny rules, and switches that only turn LSP servers off, always apply; deny rules apply even in Bypass mode.
276
+
277
+ Bash allow rules understand compound commands: every command in `a && b | c` must be covered by some allow rule, and commands with `$(...)`, backticks or file redirections always prompt. Deny rules match any part of the line. "Don't ask again this session" remembers the exact command only. MCP tools are named `mcp__<server>__<tool>`; `mcp__<server>` allows every tool of that server.
278
+
229
279
  Rules use a simple pattern syntax, for example:
230
280
 
231
281
  ```json
@@ -245,7 +295,7 @@ Rules use a simple pattern syntax, for example:
245
295
 
246
296
  ## What Bubble can do
247
297
 
248
- **Files and code.** Read, write, and make targeted edits; find files by glob; search contents with ripgrep; and navigate with language-server operations (go-to-definition, find references, hover, document/workspace symbols, call hierarchy).
298
+ **Files and code.** Read, write, and make targeted edits; list directories with ls; find files through Bash with find or rg --files; search contents with ripgrep; and navigate with language-server operations (go-to-definition, find references, hover, document/workspace symbols, call hierarchy).
249
299
 
250
300
  **Shell and dev servers.** Run bounded bash commands with streaming output, and start/stop/inspect long-running dev servers (`npm run dev`, Vite, Next, etc.) with readiness checks and captured logs.
251
301
 
@@ -363,7 +413,7 @@ npm test # run the test suite (vitest)
363
413
  npm start # run the built agent
364
414
  ```
365
415
 
366
- `npm run dev` compiles and launches in one step. The interactive TUI is built on React Ink.
416
+ `npm run dev:tui` compiles and launches the terminal interface in one step.
367
417
 
368
418
  ## Feishu host (optional)
369
419
 
@@ -101,7 +101,7 @@ export class ChildRunner {
101
101
  options.abortSignal,
102
102
  record.abortController.signal,
103
103
  ]);
104
- for await (const event of subAgent.run(input, runCwd, { abortSignal: childAbortSignal, resumeWithoutInput })) {
104
+ for await (const event of subAgent.run(input, runCwd, { abortSignal: childAbortSignal, resumeWithoutInput, inputController: record.inputQueue, continueOnPendingInput: true })) {
105
105
  if (event.type === "turn_start") {
106
106
  // Leftovers here belong to a half-built attempt the agent discarded
107
107
  // (stream-interruption retry re-issues the whole request); keeping
@@ -135,6 +135,10 @@ export class ChildRunner {
135
135
  turnSummaryBuffer = "";
136
136
  turnHadToolCall = false;
137
137
  }
138
+ if (event.type === "input_applied")
139
+ record.inputDelivery = "applied";
140
+ if (event.type === "input_rejected")
141
+ record.inputDelivery = "rejected";
138
142
  record.updatedAt = Date.now();
139
143
  emit("running", event);
140
144
  }
@@ -18,7 +18,7 @@ export declare function formatInternalReminderBlock(kind: string, content: strin
18
18
  export declare function formatInternalContextBlock(kind: string, content: string): string;
19
19
  export declare function sanitizeInternalReminderBlocks(text: string): string;
20
20
  export declare function sanitizeInternalReasoningText(text: string): string;
21
- export declare function sanitizeAssistantProviderMetadata(metadata: AssistantProviderMetadata | undefined): AssistantProviderMetadata | undefined;
21
+ export declare function sanitizeAssistantProviderMetadata(metadata: AssistantProviderMetadata | undefined, model?: string): AssistantProviderMetadata | undefined;
22
22
  export declare function createStreamingInternalReminderSanitizer(): {
23
23
  push(delta: string): string;
24
24
  flush(): string;
@@ -1,3 +1,4 @@
1
+ import { isClaudeWithBoundThinking } from "../anthropic-thinking.js";
1
2
  const INTERNAL_TAG_PREFIX = "<bubble_internal_";
2
3
  const MEMORY_CITATION_TAG = "<oai-mem-citation";
3
4
  const INTERNAL_TAG_NAMES = ["reminder", "context"];
@@ -71,7 +72,12 @@ export function sanitizeInternalReasoningText(text) {
71
72
  .filter((paragraph) => !containsInternalReminderReference(paragraph))
72
73
  .join("\n\n");
73
74
  }
74
- export function sanitizeAssistantProviderMetadata(metadata) {
75
+ export function sanitizeAssistantProviderMetadata(metadata, model) {
76
+ // These blocks are wire data, not display text. Removing thinking or editing
77
+ // earlier text invalidates later signatures. The separate content/reasoning
78
+ // fields remain sanitized for display; context checkpoints reset wire thinking.
79
+ if (isClaudeWithBoundThinking(model))
80
+ return metadata;
75
81
  const anthropic = metadata?.anthropic;
76
82
  const blocks = anthropic?.contentBlocks;
77
83
  if (!metadata || !anthropic || !blocks?.length)
@@ -5,7 +5,7 @@ import { randomInt } from "node:crypto";
5
5
  import { getSubtaskPolicy } from "./subtask-policy.js";
6
6
  const READONLY_PRESET = [
7
7
  "read",
8
- "glob",
8
+ "ls",
9
9
  "grep",
10
10
  "lsp",
11
11
  "web_search",
@@ -123,12 +123,12 @@ export function builtinAgentProfiles() {
123
123
  "You are an explorer subagent for codebase reconnaissance.",
124
124
  "Answer the specific question by inspecting the repository directly. Prefer precise file paths and line-level evidence.",
125
125
  "Keep the answer compact and avoid broad refactors or implementation plans unless asked.",
126
- ].join("\n"), ["read", "glob", "grep", "lsp", "memory", "skill"]),
126
+ ].join("\n"), ["read", "ls", "grep", "lsp", "memory", "skill"]),
127
127
  roleProfile("worker", "Implementation-planning worker subagent", [
128
128
  "You are a worker subagent. In this Phase 1 runtime you are read-only, so you must not modify files.",
129
129
  "Analyze the assigned implementation slice, identify exact files to change, and return a concrete patch plan or findings.",
130
130
  "If write-capable worker mode is needed, say so explicitly.",
131
- ].join("\n"), ["read", "glob", "grep", "lsp", "memory", "skill"]),
131
+ ].join("\n"), ["read", "ls", "grep", "lsp", "memory", "skill"]),
132
132
  toProfile("search"),
133
133
  toProfile("security_investigation"),
134
134
  toProfile("evidence_correlation"),
@@ -145,7 +145,7 @@ export function builtinAgentProfiles() {
145
145
  model: "inherit",
146
146
  tools: {
147
147
  preset: "explicit",
148
- include: ["read", "glob", "grep", "edit", "write", "bash", "lsp"],
148
+ include: ["read", "ls", "grep", "edit", "write", "bash", "lsp"],
149
149
  exclude: [],
150
150
  },
151
151
  approval: "fail",
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { type ModelTier } from "../model-catalog.js";
12
12
  import type { ProviderRegistry } from "../provider-registry.js";
13
- import { selectTierCandidates, type AgentCategoriesConfig, type TierRoutingContext } from "./categories.js";
13
+ import { selectTierCandidates, type AgentCategoriesConfig, type TierCatalogEntry, type TierRoutingContext } from "./categories.js";
14
14
  import type { ThinkingLevel } from "../types.js";
15
15
  export interface AgentRoutingConfig {
16
16
  /** Automatic builtin-tier downgrade routing (§3.2). */
@@ -36,6 +36,15 @@ export interface RoutingSnapshot {
36
36
  model: string;
37
37
  tier?: ModelTier;
38
38
  };
39
+ /**
40
+ * Subscription (OAuth) catalogs are account-scoped: the server decides which
41
+ * models this account may use, so only models the CURRENT identity's
42
+ * discovery snapshot (or the user's models.json) confirmed may be chosen by
43
+ * automatic tier routing. API-key providers keep their builtin tiers routable.
44
+ */
45
+ accountScopedCatalog: boolean;
46
+ /** Models automatic tier routing may pick from (see accountScopedCatalog). */
47
+ tierCatalog: TierCatalogEntry[];
39
48
  /** Catalog-effective provider id (openai OAuth -> "openai-codex" alias). */
40
49
  effectiveProviderId: string;
41
50
  membershipSource: RoutingMembershipSource;
@@ -74,6 +83,15 @@ export declare function buildRoutingSnapshot(registry: ProviderRegistry, parent:
74
83
  providerId: string;
75
84
  model: string;
76
85
  }, agentCategories: AgentCategoriesConfig, agentRouting: AgentRoutingConfig): RoutingSnapshot;
86
+ /**
87
+ * Catalog visible to automatic tier routing. On an account-scoped catalog only
88
+ * models confirmed by the current identity's discovery snapshot, or listed by
89
+ * the user in models.json, are candidates. A builtin-only entry (e.g. a static
90
+ * fast-tier model the ChatGPT plan does not include) or an overlay entry left
91
+ * by another account's cache would otherwise be routed to and rejected
92
+ * server-side. Without a snapshot nothing but user-listed models qualifies.
93
+ */
94
+ export declare function tierCatalogEntries(models: readonly RoutingModelEntry[], accountScopedCatalog: boolean, confirmedIds?: ReadonlySet<string>): TierCatalogEntry[];
77
95
  /** Tier-resolution context derived from a snapshot (consumed by categories §3.2). */
78
96
  export declare function tierContextFromSnapshot(snapshot: RoutingSnapshot, agentRouting: AgentRoutingConfig): TierRoutingContext;
79
97
  /**
@@ -37,6 +37,7 @@ export function buildRoutingSnapshot(registry, parent, agentCategories, agentRou
37
37
  const effectiveProviderId = parent.providerId === "openai" && configured?.authType === "oauth"
38
38
  ? "openai-codex"
39
39
  : parent.providerId;
40
+ const accountScopedCatalog = configured?.authType === "oauth";
40
41
  const builtins = listBuiltinModels(effectiveProviderId);
41
42
  const builtinIndex = new Map(builtins.map((model, index) => [model.id, index]));
42
43
  const dynamic = listDynamicModelMetadata(effectiveProviderId);
@@ -101,8 +102,16 @@ export function buildRoutingSnapshot(registry, parent, agentCategories, agentRou
101
102
  const parentTier = models.find((model) => model.id === parent.model)?.tier
102
103
  ?? getBuiltinModel(effectiveProviderId, parent.model)?.tier;
103
104
  const runnableProviderIds = registry.getEnabled().map((provider) => provider.id);
105
+ // The dynamic overlay is provider-wide and is rebuilt from EVERY unexpired
106
+ // disk-cache entry at startup (other accounts, older client pins included),
107
+ // so "dynamic" membership alone does not prove this account can use a model.
108
+ // Only the current identity's own COMPLETE discovery does: a failed fetch is
109
+ // cached briefly as a builtin fallback and must not confirm anything.
110
+ const tierCatalog = tierCatalogEntries(models, accountScopedCatalog, discovery?.complete ? new Set(discovery.models.map((model) => model.id)) : undefined);
104
111
  return {
105
112
  parent: { ...parent, tier: parentTier },
113
+ accountScopedCatalog,
114
+ tierCatalog,
106
115
  effectiveProviderId,
107
116
  membershipSource,
108
117
  models,
@@ -110,19 +119,34 @@ export function buildRoutingSnapshot(registry, parent, agentCategories, agentRou
110
119
  registryRevision: registry.getRoutingRevision(),
111
120
  discoveryExpiresAt: discovery?.expiresAt,
112
121
  runnableProviderIds,
113
- resolvedCategories: resolveCategoriesForMenu(parent, parentTier, models, agentCategories, agentRouting),
122
+ resolvedCategories: resolveCategoriesForMenu(parent, parentTier, tierCatalog, agentCategories, agentRouting),
114
123
  };
115
124
  }
125
+ /**
126
+ * Catalog visible to automatic tier routing. On an account-scoped catalog only
127
+ * models confirmed by the current identity's discovery snapshot, or listed by
128
+ * the user in models.json, are candidates. A builtin-only entry (e.g. a static
129
+ * fast-tier model the ChatGPT plan does not include) or an overlay entry left
130
+ * by another account's cache would otherwise be routed to and rejected
131
+ * server-side. Without a snapshot nothing but user-listed models qualifies.
132
+ */
133
+ export function tierCatalogEntries(models, accountScopedCatalog, confirmedIds) {
134
+ return models
135
+ .filter((model) => (!accountScopedCatalog
136
+ || model.source === "custom"
137
+ || (confirmedIds?.has(model.id) ?? false)))
138
+ .map((model) => ({
139
+ id: model.id,
140
+ tier: model.tier,
141
+ routingPriority: model.routingPriority,
142
+ builtinIndex: model.builtinIndex,
143
+ }));
144
+ }
116
145
  /** Tier-resolution context derived from a snapshot (consumed by categories §3.2). */
117
146
  export function tierContextFromSnapshot(snapshot, agentRouting) {
118
147
  return {
119
148
  parentTier: snapshot.parent.tier,
120
- models: snapshot.models.map((model) => ({
121
- id: model.id,
122
- tier: model.tier,
123
- routingPriority: model.routingPriority,
124
- builtinIndex: model.builtinIndex,
125
- })),
149
+ models: snapshot.tierCatalog,
126
150
  autoTier: agentRouting.autoTier,
127
151
  };
128
152
  }
@@ -150,12 +174,7 @@ function resolveCategoriesForMenu(parent, parentTier, models, agentCategories, a
150
174
  const merged = mergeAgentCategoriesWithProvenance(agentCategories);
151
175
  const tierContext = {
152
176
  parentTier,
153
- models: models.map((model) => ({
154
- id: model.id,
155
- tier: model.tier,
156
- routingPriority: model.routingPriority,
157
- builtinIndex: model.builtinIndex,
158
- })),
177
+ models,
159
178
  autoTier: agentRouting.autoTier,
160
179
  };
161
180
  return Object.entries(merged).map(([name, entry]) => {
@@ -1,3 +1,28 @@
1
+ /**
2
+ * Subagent thread lifecycle + dynamic-workflow execution.
3
+ *
4
+ * Extracted from Agent: spawn/wait/send/close, the scheduler dispatch and its
5
+ * terminal callbacks, admission, ingestion, worktree-backed child construction
6
+ * and the QuickJS workflow driver all lived on the Agent class alongside the
7
+ * conversation loop, sharing its private fields.
8
+ *
9
+ * Two rules keep the boundary honest:
10
+ * - Everything the runtime needs from the parent arrives through
11
+ * `SubagentRuntimeParent` as a LIVE accessor, never a construction-time
12
+ * snapshot: `/model` reassigns the parent's provider/model/thinking
13
+ * mid-session and children must inherit the current values.
14
+ * - Conversation state is never pushed back into the parent. Tool updates
15
+ * and ingestion notices are PULLED (`drainToolUpdates` /
16
+ * `drainIngestionNotices`). The only direct sink is a terminal,
17
+ * fail-closed provider diagnostic that contains no child transcript.
18
+ *
19
+ * `new Agent(...)` stays on Agent, reached through `parent.createChild`. That
20
+ * is the only genuine call back into the parent, and keeping it there is what
21
+ * guarantees the child-construction argument set — including the fields that
22
+ * must stay ABSENT (onMessageAppend, sessionID, routing catalog) — has exactly
23
+ * one definition site.
24
+ */
25
+ import type { SkillSummary } from "../../skills/types.js";
1
26
  import { type AgentProfile, type SubagentRunResult } from "../profiles.js";
2
27
  import { type SubagentThreadRecord, type SubagentThreadSnapshot } from "../subagent-control.js";
3
28
  import { SubagentStore } from "../subagent-store.js";
@@ -36,6 +61,9 @@ export interface SubagentRuntimeParent {
36
61
  readonly apiModel: string;
37
62
  readonly thinkingLevel: ThinkingLevel;
38
63
  readonly memoryPrompt: string | undefined;
64
+ /** Optional so lightweight parents (tests, embedders) need not provide a catalog. */
65
+ getSkillSummaries?(): SkillSummary[];
66
+ getSkillCatalogChars?(): number | undefined;
39
67
  readonly providerFactory: ((route: ResolvedSubagentRoute) => Provider | Promise<Provider>) | undefined;
40
68
  /**
41
69
  * The UNFILTERED tool map — deferred-but-unlocked entries included. Profile
@@ -1,27 +1,4 @@
1
- /**
2
- * Subagent thread lifecycle + dynamic-workflow execution.
3
- *
4
- * Extracted from Agent: spawn/wait/send/close, the scheduler dispatch and its
5
- * terminal callbacks, admission, ingestion, worktree-backed child construction
6
- * and the QuickJS workflow driver all lived on the Agent class alongside the
7
- * conversation loop, sharing its private fields.
8
- *
9
- * Two rules keep the boundary honest:
10
- * - Everything the runtime needs from the parent arrives through
11
- * `SubagentRuntimeParent` as a LIVE accessor, never a construction-time
12
- * snapshot: `/model` reassigns the parent's provider/model/thinking
13
- * mid-session and children must inherit the current values.
14
- * - Conversation state is never pushed back into the parent. Tool updates
15
- * and ingestion notices are PULLED (`drainToolUpdates` /
16
- * `drainIngestionNotices`). The only direct sink is a terminal,
17
- * fail-closed provider diagnostic that contains no child transcript.
18
- *
19
- * `new Agent(...)` stays on Agent, reached through `parent.createChild`. That
20
- * is the only genuine call back into the parent, and keeping it there is what
21
- * guarantees the child-construction argument set — including the fields that
22
- * must stay ABSENT (onMessageAppend, sessionID, routing catalog) — has exactly
23
- * one definition site.
24
- */
1
+ import { AgentRunInputQueue } from "../input-controller.js";
25
2
  import { randomUUID } from "node:crypto";
26
3
  import { buildSystemPrompt } from "../../system-prompt.js";
27
4
  import { buildToolPromptOptions } from "../../tools/index.js";
@@ -78,6 +55,12 @@ export class SubagentRuntime {
78
55
  recordProviderError: (record, error, request) => this.recordSubagentProviderError(record, error, request),
79
56
  notifyWaiters: (record) => this.store.notifyWaiters(record),
80
57
  onFinal: (record, options) => {
58
+ if ((record.inputQueue?.pendingInputCount() ?? 0) > 0) {
59
+ record.inputQueue?.closePendingInputs();
60
+ record.inputDelivery = "rejected";
61
+ record.toolNotes.push("Pending supplementary input was not delivered because the run ended.");
62
+ this.emitSubagentLifecycle(record, options, record.status === "closed" ? "cancelled" : record.status);
63
+ }
81
64
  this.reclaimWorktree(record);
82
65
  // Workflow-internal agents are not persisted (they never re-import into
83
66
  // the store on restart) and never ingest into parent context (option C).
@@ -240,17 +223,31 @@ export class SubagentRuntime {
240
223
  }
241
224
  if (record.status === "running" || record.status === "queued") {
242
225
  if (!options.interrupt) {
243
- throw new Error(`Subagent ${agentId} is still running. Call wait_agent first or pass interrupt:true.`);
226
+ if (typeof input !== "string")
227
+ throw new Error("Running subagents currently accept text follow-ups only.");
228
+ record.inputQueue ??= new AgentRunInputQueue(`child-${agentId}`);
229
+ if (record.inputQueue.tryEnqueue(input)) {
230
+ record.inputDelivery = "queued";
231
+ record.updatedAt = Date.now();
232
+ this.queueSubagentUpdate(record, record.status, undefined, "Supplementary input queued for the next safe boundary.");
233
+ return this.snapshotSubagent(record);
234
+ }
235
+ // Admission closed at the final boundary. Resume only after the old
236
+ // run has fully settled; never run the same child concurrently.
237
+ await record.promise?.catch(() => undefined);
238
+ }
239
+ if (options.interrupt) {
240
+ record.abortController.abort(new SubagentAbortError(`Subagent ${agentId} interrupted.`, "interrupt"));
241
+ await record.promise?.catch(() => undefined);
244
242
  }
245
- record.abortController.abort(new SubagentAbortError(`Subagent ${agentId} interrupted.`, "interrupt"));
246
- await record.promise?.catch(() => undefined);
247
- record.abortController = new AbortController();
248
243
  }
249
244
  if (record.status === "closed") {
250
245
  throw new Error(`Subagent ${agentId} is closed.`);
251
246
  }
252
- record.parentToolCallId = options.parentToolCallId ?? record.parentToolCallId;
253
- record.parentToolName = "send_input";
247
+ // Identity and trace anchor belong to the child, not a follow-up tool call.
248
+ record.abortController = new AbortController();
249
+ record.inputQueue = new AgentRunInputQueue(`child-${agentId}-${Date.now()}`);
250
+ record.inputDelivery = undefined;
254
251
  record.task = typeof input === "string" ? input : "(multimodal task)";
255
252
  record.summary = "";
256
253
  record.toolNotes = [];
@@ -638,6 +635,7 @@ export class SubagentRuntime {
638
635
  toolNotes: [],
639
636
  createdAt: now,
640
637
  updatedAt: now,
638
+ inputQueue: new AgentRunInputQueue(`child-input-${randomUUID()}`),
641
639
  abortController: new AbortController(),
642
640
  waiters: new Set(),
643
641
  };
@@ -692,6 +690,10 @@ export class SubagentRuntime {
692
690
  mode: childMode,
693
691
  workingDir: childCwd,
694
692
  ...buildToolPromptOptions(tools),
693
+ // A child that can load skills sees the same catalog as its parent.
694
+ ...(childToolNames.includes("skill")
695
+ ? { skills: this.parent.getSkillSummaries?.(), skillCatalogChars: this.parent.getSkillCatalogChars?.() }
696
+ : {}),
695
697
  memoryPrompt: childToolNames.some((name) => name === "memory")
696
698
  ? this.parent.memoryPrompt
697
699
  : undefined,
@@ -779,6 +781,8 @@ export class SubagentRuntime {
779
781
  error: record.error,
780
782
  createdAt: record.createdAt,
781
783
  updatedAt: record.updatedAt,
784
+ pendingInputCount: record.inputQueue?.pendingInputCount() ?? 0,
785
+ inputDelivery: record.inputDelivery,
782
786
  }],
783
787
  },
784
788
  };
@@ -1,3 +1,4 @@
1
+ import type { AgentRunInputQueue } from "./input-controller.js";
1
2
  import type { AgentProfile, AgentProfileSource, SubagentRunResult } from "./profiles.js";
2
3
  import type { ResolvedSubagentRoute } from "./categories.js";
3
4
  import type { SubagentWorktree } from "./worktree.js";
@@ -31,6 +32,8 @@ export interface SubagentThreadSnapshot {
31
32
  createdAt: number;
32
33
  updatedAt: number;
33
34
  deliveredAt?: number;
35
+ pendingInputCount?: number;
36
+ inputDelivery?: "queued" | "applied" | "rejected";
34
37
  /** 1-based position in the scheduler queue while status is "queued". */
35
38
  queuePosition?: number;
36
39
  /** Present for write_worktree children: where the isolated checkout lives. */
@@ -72,12 +75,16 @@ export interface SubagentThreadRecord {
72
75
  hookStopPending?: boolean;
73
76
  abortController: AbortController;
74
77
  waiters: Set<() => void>;
78
+ inputQueue?: AgentRunInputQueue;
79
+ inputDelivery?: "queued" | "applied" | "rejected";
75
80
  agent?: {
76
81
  messages: Message[];
77
82
  injectSystemReminder(content: string): void;
78
83
  run(input: string | ContentPart[], cwd: string, options?: {
79
84
  abortSignal?: AbortSignal;
80
85
  resumeWithoutInput?: boolean;
86
+ inputController?: import("../types.js").AgentInputController;
87
+ continueOnPendingInput?: boolean;
81
88
  }): AsyncIterable<AgentEvent>;
82
89
  };
83
90
  messages?: Message[];
@@ -33,6 +33,8 @@ export function snapshotSubagentThread(record) {
33
33
  createdAt: record.createdAt,
34
34
  updatedAt: record.updatedAt,
35
35
  deliveredAt: record.deliveredAt,
36
+ pendingInputCount: record.inputQueue?.pendingInputCount() ?? 0,
37
+ inputDelivery: record.inputDelivery,
36
38
  worktree: record.worktree ? { ...record.worktree } : undefined,
37
39
  };
38
40
  }
@@ -1,3 +1,4 @@
1
+ import { SUBAGENT_WAIT_GUIDANCE } from "./subagent-wait-guidance.js";
1
2
  const FINAL_STATUSES = new Set(["completed", "failed", "blocked", "cancelled", "closed"]);
2
3
  const STATUS_ORDER = ["queued", "running", "completed", "blocked", "failed", "cancelled", "closed"];
3
4
  export function buildSubagentLifecycleReminder(snapshots, toolResults) {
@@ -17,7 +18,7 @@ export function buildSubagentLifecycleReminder(snapshots, toolResults) {
17
18
  ...subagents.map(formatSubagentLine),
18
19
  "- Count unique agent_id values only; do not count repeated spawn_agent/wait_agent tool calls or repeated UI Subagents blocks as additional subagents.",
19
20
  "- Do not describe a subagent as running or still working if its status above is completed, failed, blocked, cancelled, or closed.",
20
- "- After spawn_agent, call wait_agent before user-facing progress narration unless you are doing concrete non-overlapping local work.",
21
+ ...SUBAGENT_WAIT_GUIDANCE.map((guidance) => `- ${guidance}`),
21
22
  "- When writing a synthesis, use the exact unique subagent count and statuses above.",
22
23
  ];
23
24
  return lines.join("\n");
@@ -0,0 +1,2 @@
1
+ /** Scheduling advice, not a timeout policy: the model keeps control of each wait. */
2
+ export declare const SUBAGENT_WAIT_GUIDANCE: readonly ["Coordinate around dependencies: after delegation, continue useful non-overlapping work that does not require the child's result; wait_agent when that result is needed for your next meaningful step or final synthesis.", "Choose a wait timeout according to the task and your next decision. It is an upper bound, not a mandatory delay: wait_agent returns early when a target finishes, so a generous timeout does not slow down a short task.", "A short wait is appropriate when its outcome informs a concrete scheduling decision. Avoid repeated short checks solely to see whether time has passed; use list_agents for a status snapshot when that information is useful.", "The same decision-value principle applies to list_agents: do not replace short waits with repeated status polling. With multiple children, wait_agent may return as soon as any target finishes; use explicit IDs for the remaining children whose results you still need, rather than repeatedly collecting an already-finished child.", "A timeout ends only that wait, not the child's work. Reassess available independent work and whether the result still blocks progress before waiting again; do not duplicate the delegated task."];
@@ -0,0 +1,8 @@
1
+ /** Scheduling advice, not a timeout policy: the model keeps control of each wait. */
2
+ export const SUBAGENT_WAIT_GUIDANCE = [
3
+ "Coordinate around dependencies: after delegation, continue useful non-overlapping work that does not require the child's result; wait_agent when that result is needed for your next meaningful step or final synthesis.",
4
+ "Choose a wait timeout according to the task and your next decision. It is an upper bound, not a mandatory delay: wait_agent returns early when a target finishes, so a generous timeout does not slow down a short task.",
5
+ "A short wait is appropriate when its outcome informs a concrete scheduling decision. Avoid repeated short checks solely to see whether time has passed; use list_agents for a status snapshot when that information is useful.",
6
+ "The same decision-value principle applies to list_agents: do not replace short waits with repeated status polling. With multiple children, wait_agent may return as soon as any target finishes; use explicit IDs for the remaining children whose results you still need, rather than repeatedly collecting an already-finished child.",
7
+ "A timeout ends only that wait, not the child's work. Reassess available independent work and whether the result still blocks progress before waiting again; do not duplicate the delegated task.",
8
+ ];
@@ -1,11 +1,11 @@
1
1
  const POLICY_MAP = {
2
2
  search: {
3
3
  type: "search",
4
- allowedTools: ["read", "glob", "grep", "web_search", "web_fetch", "skill"],
4
+ allowedTools: ["read", "ls", "grep", "web_search", "web_fetch", "skill"],
5
5
  reminder: [
6
6
  "Subtask policy: search",
7
7
  "- Focus on locating relevant files, symbols, and evidence quickly.",
8
- "- Use glob for file discovery and grep for content search.",
8
+ "- Use ls for directory contents and grep for content search.",
9
9
  "- Return a concise summary of what you found and where.",
10
10
  ].join("\n"),
11
11
  resultStatus: "success",
@@ -13,7 +13,7 @@ const POLICY_MAP = {
13
13
  },
14
14
  security_investigation: {
15
15
  type: "security_investigation",
16
- allowedTools: ["read", "glob", "grep", "web_search", "web_fetch", "skill"],
16
+ allowedTools: ["read", "ls", "grep", "web_search", "web_fetch", "skill"],
17
17
  reminder: [
18
18
  "Subtask policy: security_investigation",
19
19
  "- Investigate only in read-only mode.",
@@ -37,7 +37,7 @@ const POLICY_MAP = {
37
37
  },
38
38
  general_readonly: {
39
39
  type: "general_readonly",
40
- allowedTools: ["read", "glob", "grep", "web_search", "web_fetch", "skill"],
40
+ allowedTools: ["read", "ls", "grep", "web_search", "web_fetch", "skill"],
41
41
  reminder: [
42
42
  "Subtask policy: general_readonly",
43
43
  "- Stay in read-only mode.",