@bubblebrain-ai/bubble 0.0.49 → 0.0.50

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 (38) hide show
  1. package/dist/agent/abort-errors.d.ts +4 -0
  2. package/dist/agent/abort-errors.js +30 -0
  3. package/dist/agent/provider-fingerprint.d.ts +6 -0
  4. package/dist/agent/provider-fingerprint.js +89 -0
  5. package/dist/agent/resident-history.d.ts +7 -0
  6. package/dist/agent/resident-history.js +61 -0
  7. package/dist/agent/transcript-helpers.d.ts +20 -0
  8. package/dist/agent/transcript-helpers.js +62 -0
  9. package/dist/agent/update-queue.d.ts +13 -0
  10. package/dist/agent/update-queue.js +69 -0
  11. package/dist/agent.d.ts +32 -15
  12. package/dist/agent.js +79 -297
  13. package/dist/external-runtime/grok-runtime.js +1 -1
  14. package/dist/feishu/agent-host/run-driver.js +1 -1
  15. package/dist/lsp/service.js +2 -2
  16. package/dist/model-catalog.js +8 -6
  17. package/dist/permissions/settings.d.ts +0 -1
  18. package/dist/permissions/settings.js +0 -2
  19. package/dist/provider-registry.js +13 -1
  20. package/dist/provider-transform.js +7 -8
  21. package/dist/slash-commands/commands.js +13 -9
  22. package/dist/tools/grep.js +1 -1
  23. package/dist/tui-ink/app-helpers.d.ts +43 -0
  24. package/dist/tui-ink/app-helpers.js +150 -0
  25. package/dist/tui-ink/app.d.ts +4 -11
  26. package/dist/tui-ink/app.js +56 -645
  27. package/dist/tui-ink/command-palette.d.ts +32 -0
  28. package/dist/tui-ink/command-palette.js +158 -0
  29. package/dist/tui-ink/display-reconstruct.d.ts +3 -0
  30. package/dist/tui-ink/display-reconstruct.js +82 -0
  31. package/dist/tui-ink/rewind-picker.d.ts +8 -0
  32. package/dist/tui-ink/rewind-picker.js +102 -0
  33. package/dist/tui-ink/session-picker.js +1 -1
  34. package/dist/tui-ink/stats-panel.d.ts +11 -0
  35. package/dist/tui-ink/stats-panel.js +65 -0
  36. package/dist/tui-ink/waiting-indicator.d.ts +12 -0
  37. package/dist/tui-ink/waiting-indicator.js +129 -0
  38. package/package.json +1 -1
package/dist/agent.js CHANGED
@@ -2,10 +2,10 @@
2
2
  * Agent - The core decision loop.
3
3
  * It maintains message state, calls the LLM, executes tools, and auto-continues.
4
4
  */
5
- import { compactCurrentTurnToolGroups, compactMessages, isRealUserMessage } from "./context/compact.js";
5
+ import { compactCurrentTurnToolGroups, compactMessages } from "./context/compact.js";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { compactMessagesWithLLM } from "./context/compact-llm.js";
8
- import { estimateContextTokens, getContextBudget } from "./context/budget.js";
8
+ import { getContextBudget } from "./context/budget.js";
9
9
  import { buildContextUsageSnapshot } from "./context/usage.js";
10
10
  import { isContextOverflowError } from "./context/overflow.js";
11
11
  import { computeRetryDelayMs, isProviderStreamInterruption, MAX_STREAM_INTERRUPTION_RETRIES, sleepBeforeRetry, } from "./network/retry.js";
@@ -21,7 +21,12 @@ import { DEFAULT_AGENT_ROUTING, sanitizeAgentRouting } from "./agent/routing-cat
21
21
  import { SubagentRouter } from "./agent/subagent/router.js";
22
22
  import { normalizeWaitTimeout, SubagentRuntime } from "./agent/subagent/runtime.js";
23
23
  import { WorkflowLedger } from "./agent/workflow/runs.js";
24
- import { AgentAbortError, EMPTY_ASSISTANT_FALLBACK } from "./agent/abort-errors.js";
24
+ import { EMPTY_ASSISTANT_FALLBACK, throwIfAborted, isAbortLikeError, isAbortError } from "./agent/abort-errors.js";
25
+ import { appendProviderContentBlock, buildProviderRequestFingerprint } from "./agent/provider-fingerprint.js";
26
+ import { countUserTurns, estimateResidentChars, estimateToolPayloadChars, getCurrentHeapUsed, RESIDENT_HISTORY_HEAP_HARD_LIMIT, RESIDENT_HISTORY_KEEP_RECENT_TURNS } from "./agent/resident-history.js";
27
+ import { cancelledToolResult, createModelInterruptedMessage, findMissingRequiredArgs, isSubagentLifecycleTool, lastProviderMessage, shouldAppendModelInterruptedBoundary } from "./agent/transcript-helpers.js";
28
+ import { createUpdateQueue } from "./agent/update-queue.js";
29
+ export { createUpdateQueue } from "./agent/update-queue.js";
25
30
  import { isHiddenToolResult } from "./agent/tool-visibility.js";
26
31
  import { createStreamingInternalReminderSanitizer, sanitizeAssistantProviderMetadata, sanitizeInternalReasoningText, sanitizeInternalReminderBlocks, } from "./agent/internal-reminder-sanitizer.js";
27
32
  import { debugReasoningStream, summarizeDebugText } from "./reasoning-debug.js";
@@ -29,8 +34,6 @@ import { buildToolPromptOptions } from "./tools/prompt-metadata.js";
29
34
  import { stopAutoServersForSession } from "./tools/server-manager.js";
30
35
  import { summarizeAgentEventForTrace, summarizeTraceError, summarizeTraceMessage, summarizeTraceToolResult, summarizeTraceValue, traceEvent, } from "./debug-trace.js";
31
36
  const MAX_CONSECUTIVE_OVERFLOW_RECOVERIES = 3;
32
- const RESIDENT_HISTORY_KEEP_RECENT_TURNS = 3;
33
- const RESIDENT_HISTORY_HEAP_HARD_LIMIT = 768 * 1024 * 1024;
34
37
  const MAX_EMPTY_ASSISTANT_RECOVERIES = 1;
35
38
  const EMPTY_ASSISTANT_RECOVERY_REMINDER = "The previous model response contained no user-visible assistant content and no tool calls. " +
36
39
  "Respond now with a concise, user-visible answer, or call an available tool if more work is required. " +
@@ -74,7 +77,23 @@ function agentEventFromHookProgress(event) {
74
77
  };
75
78
  }
76
79
  export class Agent {
77
- messages = [];
80
+ /**
81
+ * Backing store for the public `messages` accessor. All whole-array
82
+ * rewrites (clear/compact/rewind/session switch/subagent fork) go through
83
+ * the setter, which notifies `onContextChanged` subscribers — that push
84
+ * channel is what keeps derived UI (e.g. the footer usage readout) in sync
85
+ * without per-path refresh calls that are easy to forget.
86
+ */
87
+ _messages = [];
88
+ contextChangedListeners = new Set();
89
+ notifyingContextChange = false;
90
+ get messages() {
91
+ return this._messages;
92
+ }
93
+ set messages(value) {
94
+ this._messages = value;
95
+ this.notifyContextChanged();
96
+ }
78
97
  provider;
79
98
  sessionID;
80
99
  /**
@@ -216,7 +235,7 @@ export class Agent {
216
235
  persistDir,
217
236
  });
218
237
  if (options.systemPrompt) {
219
- this.messages.push({ role: "system", content: options.systemPrompt });
238
+ this._messages.push({ role: "system", content: options.systemPrompt });
220
239
  }
221
240
  for (const tool of options.tools) {
222
241
  this.tools.set(tool.name, tool);
@@ -362,6 +381,46 @@ export class Agent {
362
381
  renderModelRoutingPromptFor(parent) {
363
382
  return this.router.promptSectionFor(parent);
364
383
  }
384
+ /**
385
+ * Push subscription for derived-context consumers (TUI footer, hosts).
386
+ * Fires whenever resident context mutates in a way that changes a
387
+ * `getContextUsageSnapshot()` reading: whole-array message rewrites,
388
+ * appended messages, system-prompt replacement, or model/provider switches
389
+ * (window size / estimation basis). Returns an unsubscribe function.
390
+ */
391
+ onContextChanged(listener) {
392
+ this.contextChangedListeners.add(listener);
393
+ return () => {
394
+ this.contextChangedListeners.delete(listener);
395
+ };
396
+ }
397
+ notifyContextChanged() {
398
+ if (this.notifyingContextChange)
399
+ return;
400
+ this.notifyingContextChange = true;
401
+ try {
402
+ for (const listener of [...this.contextChangedListeners]) {
403
+ try {
404
+ listener();
405
+ }
406
+ catch (error) {
407
+ // A broken subscriber must never corrupt agent state mid-mutation,
408
+ // but leave a trace — otherwise a throwing consumer (e.g. the footer
409
+ // readout hitting a pathological message) fails silently forever.
410
+ traceEvent("context_listener_error", {
411
+ error: error instanceof Error ? error.message : String(error),
412
+ }, {
413
+ sessionFile: this.sessionID,
414
+ provider: this._providerId || "none",
415
+ model: this.apiModel || "none",
416
+ });
417
+ }
418
+ }
419
+ }
420
+ finally {
421
+ this.notifyingContextChange = false;
422
+ }
423
+ }
365
424
  getContextUsageSnapshot() {
366
425
  return buildContextUsageSnapshot({
367
426
  providerId: this.providerId,
@@ -434,13 +493,21 @@ export class Agent {
434
493
  return this._model;
435
494
  }
436
495
  set model(value) {
496
+ if (this._model === value)
497
+ return;
437
498
  this._model = value;
499
+ // Context window (and thus usage readings) is model-derived.
500
+ this.notifyContextChanged();
438
501
  }
439
502
  get providerId() {
440
503
  return this._providerId;
441
504
  }
442
505
  set providerId(value) {
506
+ if (this._providerId === value)
507
+ return;
443
508
  this._providerId = value;
509
+ // Token estimation basis is provider-specific.
510
+ this.notifyContextChanged();
444
511
  }
445
512
  get apiModel() {
446
513
  if (this._model.includes(":")) {
@@ -504,10 +571,14 @@ export class Agent {
504
571
  setSystemPrompt(prompt) {
505
572
  const systemMessage = { role: "system", content: prompt };
506
573
  if (this.messages[0]?.role === "system") {
574
+ if (this.messages[0].content === prompt)
575
+ return;
507
576
  this.messages[0] = systemMessage;
577
+ this.notifyContextChanged();
508
578
  return;
509
579
  }
510
580
  this.messages.unshift(systemMessage);
581
+ this.notifyContextChanged();
511
582
  }
512
583
  async *run(userInput, cwd, options = {}) {
513
584
  const abortSignal = options.abortSignal;
@@ -1836,6 +1907,7 @@ export class Agent {
1836
1907
  model: this.apiModel || "none",
1837
1908
  });
1838
1909
  this.onMessageAppend?.(message);
1910
+ this.notifyContextChanged();
1839
1911
  }
1840
1912
  appendInterruptedAssistantBoundary(currentAssistant, currentAssistantAppended) {
1841
1913
  const last = lastProviderMessage(this.messages);
@@ -1946,293 +2018,3 @@ export class Agent {
1946
2018
  }
1947
2019
  }
1948
2020
  }
1949
- function findMissingRequiredArgs(schema, args) {
1950
- const required = schema?.required;
1951
- if (!required || required.length === 0)
1952
- return [];
1953
- const missing = [];
1954
- for (const name of required) {
1955
- const value = args ? args[name] : undefined;
1956
- // Empty strings/arrays are intentionally allowed — writing an empty file
1957
- // or passing an empty list can be legitimate. Only undefined/null counts
1958
- // as "missing", because the observed failure mode is `finalArgs: "{}"`
1959
- // where the field is entirely absent.
1960
- if (value === undefined || value === null) {
1961
- missing.push(name);
1962
- }
1963
- }
1964
- return missing;
1965
- }
1966
- function estimateResidentChars(messages) {
1967
- let total = 0;
1968
- for (const message of messages) {
1969
- switch (message.role) {
1970
- case "system":
1971
- case "meta":
1972
- total += message.content.length;
1973
- break;
1974
- case "tool":
1975
- total += message.content.length + message.toolCallId.length;
1976
- break;
1977
- case "assistant":
1978
- total += message.content.length + (message.reasoning?.length ?? 0);
1979
- total += message.toolCalls?.reduce((sum, toolCall) => sum + toolCall.id.length + toolCall.name.length + toolCall.arguments.length, 0) ?? 0;
1980
- break;
1981
- case "user":
1982
- if (typeof message.content === "string") {
1983
- total += message.content.length;
1984
- }
1985
- else {
1986
- total += message.content.reduce((sum, part) => {
1987
- if (part.type === "text") {
1988
- return sum + part.text.length;
1989
- }
1990
- return sum + part.image_url.url.length;
1991
- }, 0);
1992
- }
1993
- break;
1994
- }
1995
- }
1996
- return total;
1997
- }
1998
- function appendProviderContentBlock(message, provider, block) {
1999
- const current = message.providerMetadata?.[provider]?.contentBlocks ?? [];
2000
- message.providerMetadata = {
2001
- ...message.providerMetadata,
2002
- [provider]: {
2003
- ...message.providerMetadata?.[provider],
2004
- contentBlocks: [...current, cloneProviderRawContentBlock(block)],
2005
- },
2006
- };
2007
- }
2008
- function buildProviderRequestFingerprint(messages, tools, providerId, toolChoice) {
2009
- const roleCounts = {};
2010
- let contentChars = 0;
2011
- let reasoningChars = 0;
2012
- let toolResultChars = 0;
2013
- let maxToolResultChars = 0;
2014
- let assistantToolCalls = 0;
2015
- let rawAnthropicBlocks = 0;
2016
- let rawAnthropicThinkingBlocks = 0;
2017
- let rawAnthropicSignatureChars = 0;
2018
- for (const message of messages) {
2019
- roleCounts[message.role] = (roleCounts[message.role] ?? 0) + 1;
2020
- if (message.role === "assistant") {
2021
- contentChars += message.content.length;
2022
- reasoningChars += message.reasoning?.length ?? 0;
2023
- assistantToolCalls += message.toolCalls?.length ?? 0;
2024
- const blocks = message.providerMetadata?.anthropic?.contentBlocks ?? [];
2025
- rawAnthropicBlocks += blocks.length;
2026
- for (const block of blocks) {
2027
- if (block.type === "thinking" || block.type === "redacted_thinking") {
2028
- rawAnthropicThinkingBlocks += 1;
2029
- }
2030
- if (typeof block.signature === "string") {
2031
- rawAnthropicSignatureChars += block.signature.length;
2032
- }
2033
- }
2034
- }
2035
- else if (message.role === "tool") {
2036
- toolResultChars += message.content.length;
2037
- maxToolResultChars = Math.max(maxToolResultChars, message.content.length);
2038
- }
2039
- else if (message.role === "user") {
2040
- contentChars += typeof message.content === "string"
2041
- ? message.content.length
2042
- : message.content.reduce((sum, part) => sum + (part.type === "text" ? part.text.length : part.image_url.url.length), 0);
2043
- }
2044
- else {
2045
- contentChars += message.content.length;
2046
- }
2047
- }
2048
- const systemMessages = messages.filter((message) => message.role === "system");
2049
- const bodyMessages = messages.filter((message) => message.role !== "system");
2050
- const systemJsonBytes = Buffer.byteLength(JSON.stringify(systemMessages), "utf8");
2051
- const bodyJsonBytes = Buffer.byteLength(JSON.stringify(bodyMessages), "utf8");
2052
- const toolSchemaJsonBytes = Buffer.byteLength(JSON.stringify(tools), "utf8");
2053
- return {
2054
- roleCounts,
2055
- estimatedTokens: estimateContextTokens(messages, providerId),
2056
- projectedJsonBytes: Buffer.byteLength(JSON.stringify(messages), "utf8"),
2057
- systemJsonBytes,
2058
- bodyJsonBytes,
2059
- toolSchemaJsonBytes,
2060
- staticPrefixJsonBytes: Buffer.byteLength(JSON.stringify({
2061
- system: systemMessages,
2062
- tools,
2063
- tool_choice: toolChoice,
2064
- }), "utf8"),
2065
- toolChoice,
2066
- contentChars,
2067
- reasoningChars,
2068
- toolResultChars,
2069
- maxToolResultChars,
2070
- assistantToolCalls,
2071
- rawAnthropicBlocks,
2072
- rawAnthropicThinkingBlocks,
2073
- rawAnthropicSignatureChars,
2074
- };
2075
- }
2076
- function cloneProviderRawContentBlock(block) {
2077
- return JSON.parse(JSON.stringify(block));
2078
- }
2079
- function throwIfAborted(signal) {
2080
- if (!signal?.aborted)
2081
- return;
2082
- const reason = signal.reason;
2083
- if (reason instanceof Error)
2084
- throw reason;
2085
- throw new AgentAbortError(typeof reason === "string" ? reason : undefined);
2086
- }
2087
- function isAbortLikeError(error, signal) {
2088
- if (signal?.aborted)
2089
- return true;
2090
- if (error instanceof AgentAbortError)
2091
- return true;
2092
- if (error instanceof DOMException && error.name === "AbortError")
2093
- return true;
2094
- if (typeof error === "object" && error !== null && error.name === "AbortError")
2095
- return true;
2096
- return false;
2097
- }
2098
- function isAbortError(error, signal) {
2099
- return isAbortLikeError(error, signal);
2100
- }
2101
- function shouldAppendModelInterruptedBoundary(messages) {
2102
- return messages.at(-1)?.role === "tool";
2103
- }
2104
- function createModelInterruptedMessage(error, metadata) {
2105
- return {
2106
- role: "assistant",
2107
- content: `[model request interrupted before a final answer was produced: ${summarizeInterruptError(error)}]`,
2108
- model: metadata.model,
2109
- providerId: metadata.providerId,
2110
- modelId: metadata.modelId,
2111
- };
2112
- }
2113
- function summarizeInterruptError(error) {
2114
- const message = error instanceof Error
2115
- ? error.message
2116
- : typeof error === "string"
2117
- ? error
2118
- : String(error);
2119
- return message.replace(/\s+/g, " ").trim().slice(0, 240) || "unknown error";
2120
- }
2121
- function lastProviderMessage(messages) {
2122
- for (let i = messages.length - 1; i >= 0; i--) {
2123
- const message = messages[i];
2124
- if (message.role === "system" || message.role === "meta")
2125
- continue;
2126
- return message;
2127
- }
2128
- return undefined;
2129
- }
2130
- function cancelledToolResult(toolName) {
2131
- return {
2132
- content: `Tool "${toolName}" was cancelled.`,
2133
- isError: true,
2134
- status: "cancelled",
2135
- metadata: { reason: "cancelled" },
2136
- };
2137
- }
2138
- /** Exported for tests: the wake latch below is easiest to pin directly. */
2139
- export function createUpdateQueue() {
2140
- const items = [];
2141
- let waiter;
2142
- let abortCleanup;
2143
- // Latch for wakes that arrive with no parked waiter — a wake() fired while
2144
- // the consuming generator is suspended at a yield would otherwise be lost.
2145
- // Callers today each pair their wake with a synchronously-checked flag
2146
- // (items / settled / pendingSubagentUpdates), so no wake is currently
2147
- // dropped; the latch keeps that correctness inside the queue instead of
2148
- // depending on every future caller repeating the pairing.
2149
- let signaled = false;
2150
- return {
2151
- push(item) {
2152
- items.push(item);
2153
- this.wake();
2154
- },
2155
- drain() {
2156
- return items.splice(0, items.length);
2157
- },
2158
- hasItems() {
2159
- return items.length > 0;
2160
- },
2161
- wait(signal) {
2162
- if (items.length > 0)
2163
- return Promise.resolve("woken");
2164
- if (signaled) {
2165
- signaled = false;
2166
- return Promise.resolve("woken");
2167
- }
2168
- if (signal?.aborted)
2169
- return Promise.resolve("aborted");
2170
- return new Promise((resolve) => {
2171
- abortCleanup?.();
2172
- abortCleanup = undefined;
2173
- const finish = (status) => {
2174
- if (waiter !== resolve)
2175
- return;
2176
- waiter = undefined;
2177
- abortCleanup?.();
2178
- abortCleanup = undefined;
2179
- resolve(status);
2180
- };
2181
- if (signal) {
2182
- const onAbort = () => finish("aborted");
2183
- signal.addEventListener("abort", onAbort, { once: true });
2184
- abortCleanup = () => signal.removeEventListener("abort", onAbort);
2185
- }
2186
- waiter = resolve;
2187
- });
2188
- },
2189
- wake() {
2190
- const resolve = waiter;
2191
- if (!resolve) {
2192
- signaled = true;
2193
- return;
2194
- }
2195
- waiter = undefined;
2196
- abortCleanup?.();
2197
- abortCleanup = undefined;
2198
- resolve("woken");
2199
- },
2200
- };
2201
- }
2202
- function estimateToolPayloadChars(messages) {
2203
- return messages.reduce((sum, message) => {
2204
- if (message.role !== "tool") {
2205
- return sum;
2206
- }
2207
- return sum + message.content.length;
2208
- }, 0);
2209
- }
2210
- function countUserTurns(messages) {
2211
- // Real user turns only: projected reminders and summary envelopes are
2212
- // user-ROLE but not user TURNS — counting them used to shrink the keep
2213
- // window and fire compaction almost every turn.
2214
- return messages.reduce((count, message) => count + (isRealUserMessage(message) ? 1 : 0), 0);
2215
- }
2216
- function getCurrentHeapUsed() {
2217
- try {
2218
- return process.memoryUsage().heapUsed;
2219
- }
2220
- catch {
2221
- return 0;
2222
- }
2223
- }
2224
- function isSubagentLifecycleTool(name) {
2225
- return name === "subagent"
2226
- || name === "spawn_agent"
2227
- || name === "wait_agent"
2228
- || name === "send_input"
2229
- || name === "close_agent"
2230
- || name === "list_agents"
2231
- || name === "run_workflow"
2232
- || name === "wait_workflow"
2233
- // Legacy names: still present in transcripts recorded before the tools
2234
- // were removed (2026-07-06); forked children must not inherit their
2235
- // dangling tool_calls either.
2236
- || name === "agent_team"
2237
- || name === "agent_batch";
2238
- }
@@ -530,7 +530,7 @@ export class GrokRuntimeManager {
530
530
  try {
531
531
  if (!this.lock)
532
532
  temporaryLock = await this.acquireProfileLock();
533
- const { profile, env, binary } = await this.prepareAndVerify();
533
+ const { env, binary } = await this.prepareAndVerify();
534
534
  const result = await runGrokCommand(this.spawn, binary.path, ["--no-auto-update", "--cwd", this.workspace, "models"], { cwd: this.workspace, env, maxStdoutBytes: 64 * 1024 });
535
535
  if (result.code !== 0) {
536
536
  throw new GrokRuntimeError("protocol_error", "Grok could not list subscription models.", result.stderr);
@@ -301,7 +301,7 @@ export class RunDriver {
301
301
  this.opts.approvalUI.cancelForChat(req.chatId, "Run ended");
302
302
  }
303
303
  }
304
- async resolveProvider(session, promptCacheKey) {
304
+ async resolveProvider(_session, promptCacheKey) {
305
305
  const registry = this.opts.deps.providerRegistry;
306
306
  const userConfig = this.opts.deps.userConfig;
307
307
  // Read session metadata for an explicit model preference, fall back to
@@ -436,7 +436,7 @@ const TypeScriptServer = {
436
436
  extensions: TS_EXTENSIONS,
437
437
  root: nearestRoot(NODE_ROOT_MARKERS, DENO_ROOT_MARKERS),
438
438
  languageId: languageIdFor,
439
- async spawn(root, ctx) {
439
+ async spawn(root, _ctx) {
440
440
  const requireFromRoot = createRequire(join(root, "package.json"));
441
441
  const requireFromSelf = createRequire(import.meta.url);
442
442
  const tsserverPath = resolveModule(requireFromRoot, "typescript/lib/tsserver.js")
@@ -495,7 +495,7 @@ const ESLintServer = {
495
495
  extensions: [...TS_EXTENSIONS, ".vue"],
496
496
  root: eslintRoot,
497
497
  languageId: languageIdFor,
498
- async spawn(root, ctx) {
498
+ async spawn(root, _ctx) {
499
499
  const requireFromRoot = createRequire(join(root, "package.json"));
500
500
  const requireFromSelf = createRequire(import.meta.url);
501
501
  const eslint = resolveModule(requireFromRoot, "eslint");
@@ -41,13 +41,12 @@ const OPENAI_CHAT_LEVELS = ["off"];
41
41
  // Internal representation for APIs that expose thinking as enabled/disabled.
42
42
  // UI code must render supported toggle models as on/off, not as "medium" effort.
43
43
  const TOGGLE_THINKING_LEVELS = ["off", "medium"];
44
- // GLM-5.2 is the first GLM to accept OpenAI-style `reasoning_effort`. The API
45
- // enum is none/minimal/low/medium/high/xhigh/max; we expose high and max (the
46
- // two effort tiers worth offering a coding agent) plus "off", which disables
47
- // thinking outright via `thinking: {type: "disabled"}`. Order matters: "high"
48
- // is first so it is the default (getDefaultThinkingLevel falls back to levels[0]
49
- // when "medium" is absent), since GLM-5.2 is a thinking-on-by-default model.
44
+ // GLM-5.2 accepts OpenAI-style `reasoning_effort`. We expose high and max plus
45
+ // "off", which disables thinking outright via `thinking: {type: "disabled"}`.
50
46
  const GLM_5_2_LEVELS = ["high", "max", "off"];
47
+ // GLM-5.3 Coding Plan normalizes efforts into exactly three real tiers. Its
48
+ // default is max, and disabling thinking maps to low rather than turning it off.
49
+ const GLM_5_3_LEVELS = ["low", "high", "max"];
51
50
  // Kimi K2.7 Code variants only support thinking mode (disabling it errors), so
52
51
  // "off" is not offered — the model is always in its thinking variant.
53
52
  const KIMI_THINKING_ONLY_LEVELS = ["medium"];
@@ -124,6 +123,9 @@ export const BUILTIN_MODELS = [
124
123
  { id: "glm-5.1", name: "GLM-5.1", providerId: "zhipuai", tier: "balanced", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 200000 },
125
124
  { id: "glm-4.7", name: "GLM-4.7", providerId: "zhipuai", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 204800 },
126
125
  { id: "glm-4.6", name: "GLM-4.6", providerId: "zhipuai", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 204800 },
126
+ // Coding Plan exposes GLM-5.3 before GET /models lists it (verified live
127
+ // 2026-08-14). The standard API is still marked "coming soon" in the docs.
128
+ { id: "glm-5.3", name: "GLM-5.3", providerId: "zhipuai-coding-plan", tier: "strong", reasoningLevels: GLM_5_3_LEVELS, defaultReasoningLevel: "max", contextWindow: 1000000 },
127
129
  { id: "glm-5.2", name: "GLM-5.2", providerId: "zhipuai-coding-plan", tier: "strong", reasoningLevels: GLM_5_2_LEVELS, contextWindow: 1000000 },
128
130
  { id: "glm-5.1", name: "GLM-5.1", providerId: "zhipuai-coding-plan", tier: "balanced", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 200000 },
129
131
  { id: "glm-4.7", name: "GLM-4.7", providerId: "zhipuai-coding-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 204800 },
@@ -43,7 +43,6 @@ export interface SettingsManagerOptions {
43
43
  bubbleHome?: string;
44
44
  }
45
45
  export declare class SettingsManager {
46
- private readonly cwd;
47
46
  private readonly paths;
48
47
  private raw;
49
48
  private fileDiagnostics;
@@ -25,7 +25,6 @@ const KNOWN_MODES = new Set([
25
25
  "bypassPermissions",
26
26
  ]);
27
27
  export class SettingsManager {
28
- cwd;
29
28
  paths;
30
29
  raw = {
31
30
  user: null,
@@ -34,7 +33,6 @@ export class SettingsManager {
34
33
  };
35
34
  fileDiagnostics = [];
36
35
  constructor(cwd, options = {}) {
37
- this.cwd = cwd;
38
36
  const bubbleHome = options.bubbleHome ?? getBubbleHome();
39
37
  this.paths = {
40
38
  user: join(bubbleHome, "settings.json"),
@@ -458,7 +458,19 @@ export class ProviderRegistry {
458
458
  if (!options.forceRefresh) {
459
459
  const cached = this.modelDiscoveryCache.get(key);
460
460
  if (cached && cached.expiresAt > now) {
461
- const result = { ...cached.result, source: "cache" };
461
+ // A disk cache can outlive the application version that wrote it. Keep
462
+ // the current curated catalog authoritative over stale cached metadata,
463
+ // then retain cached remote-only ids for non-authoritative union results.
464
+ // This lets newly-added builtins surface immediately after an upgrade
465
+ // without waiting up to 24 hours or requiring a manual Ctrl+R refresh.
466
+ const local = this.localModelsForProvider(provider);
467
+ const localIds = new Set(local.map((model) => model.id));
468
+ const models = cached.result.source === "static"
469
+ ? local
470
+ : cached.result.authoritative
471
+ ? cached.result.models
472
+ : [...local, ...cached.result.models.filter((model) => !localIds.has(model.id))];
473
+ const result = { ...cached.result, models, source: "cache" };
462
474
  const current = this.getConfigured().find((item) => item.id === provider.id);
463
475
  if (!current || this.modelDiscoveryKey(current) === key) {
464
476
  this.applyDynamicDiscoveryMetadata(provider, result);
@@ -114,16 +114,15 @@ export function resolveProviderRequestConfig(providerId, modelId, requestedLevel
114
114
  // Zhipu/Z.AI OpenAI-compatible endpoints expose reasoning via a provider-specific
115
115
  // `thinking` block rather than OpenAI's `reasoning_effort` shape.
116
116
  if (["zhipuai", "zhipuai-coding-plan", "zai", "zai-coding-plan"].includes(providerId)) {
117
- // GLM-5.2 is the only GLM that also accepts `reasoning_effort` (we expose
118
- // high/max, which map 1:1 onto the API enum). "off" disables thinking via
119
- // `thinking: {type: "disabled"}` — otherwise the server default (thinking
120
- // on, effort max) would make "off" a no-op. The effort field rides inside
121
- // the body alongside `thinking`, so it goes in extraBody, not the
122
- // OpenRouter-style `reasoningEffort` config field.
123
- if (modelId === "glm-5.2") {
117
+ // GLM-5.2+ also accept `reasoning_effort`. GLM-5.2 can disable thinking;
118
+ // GLM-5.3 requires thinking and Coding Plan normalizes its effort into
119
+ // low/high/max. The catalog clamps 5.3 choices before this serialization
120
+ // boundary. The effort field rides inside the body alongside `thinking`,
121
+ // so it goes in extraBody, not the OpenRouter-style config field.
122
+ if (modelId === "glm-5.2" || modelId === "glm-5.3") {
124
123
  return {
125
124
  effectiveThinkingLevel,
126
- extraBody: effectiveThinkingLevel === "off"
125
+ extraBody: modelId === "glm-5.2" && effectiveThinkingLevel === "off"
127
126
  ? { thinking: { type: "disabled" } }
128
127
  : {
129
128
  thinking: { type: "enabled", clear_thinking: false },