mixdog 1.1.0 → 1.1.2

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 (174) hide show
  1. package/README.md +13 -6
  2. package/package.json +1 -2
  3. package/scripts/latest-guard.mjs +63 -0
  4. package/scripts/lib/run-node-tests.mjs +103 -27
  5. package/scripts/lib/test-failure-summary.mjs +7 -2
  6. package/scripts/merge-mac-update-feed.mjs +107 -0
  7. package/scripts/mobile-release.mjs +210 -0
  8. package/scripts/post-release-check.mjs +353 -0
  9. package/scripts/publish-asset-release.mjs +43 -0
  10. package/scripts/release-guard.mjs +63 -0
  11. package/scripts/release-paths.mjs +39 -2
  12. package/scripts/test.mjs +29 -7
  13. package/src/defaults/shipped-history.json +2 -0
  14. package/src/defaults/skills/computer-use/SKILL.md +1 -1
  15. package/src/defaults/skills/setup/references/actions.md +1 -0
  16. package/src/headless-role.mjs +2 -2
  17. package/src/lib/keychain-cjs.cjs +582 -17
  18. package/src/lib/rules-builder.cjs +0 -23
  19. package/src/rules/lead/LEAD.md +2 -2
  20. package/src/runtime/agent/orchestrator/config-storage.mjs +32 -4
  21. package/src/runtime/agent/orchestrator/config.mjs +18 -0
  22. package/src/runtime/agent/orchestrator/context/instruction-files.mjs +130 -0
  23. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +4 -0
  24. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +11 -0
  25. package/src/runtime/agent/orchestrator/providers/lib/open-login-url.mjs +9 -0
  26. package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +254 -0
  27. package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +218 -0
  28. package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +239 -0
  29. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +233 -0
  30. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-login.mjs +247 -0
  31. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +225 -0
  32. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +542 -0
  33. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +25 -0
  34. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +4 -1
  35. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +13 -0
  36. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +14 -6
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +99 -0
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +4 -8
  39. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -7
  40. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +8 -0
  41. package/src/runtime/agent/orchestrator/providers/registry.mjs +4 -0
  42. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +2 -0
  43. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +15 -1
  44. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +1 -5
  45. package/src/runtime/agent/orchestrator/session/context-utils.mjs +12 -14
  46. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +11 -1
  47. package/src/runtime/agent/orchestrator/session/loop/tool-call-ids.mjs +90 -0
  48. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +19 -11
  49. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +1 -0
  50. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +28 -0
  51. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +8 -0
  52. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +3 -0
  53. package/src/runtime/agent/orchestrator/session/manager.mjs +1 -5
  54. package/src/runtime/agent/orchestrator/session/read-dedup.mjs +0 -1
  55. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +3 -0
  56. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +2 -0
  57. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +2 -0
  58. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +23 -0
  59. package/src/runtime/agent/orchestrator/session/turn-effort.mjs +18 -0
  60. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +11 -8
  61. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +9 -4
  62. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +245 -0
  63. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/command-preflight.mjs +13 -1
  64. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +3 -1
  65. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
  66. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +49 -0
  67. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +3 -3
  68. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -7
  69. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +61 -8
  70. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +28 -3
  71. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +73 -1
  72. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +2 -2
  73. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +3 -1
  74. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +17 -6
  75. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +23 -5
  76. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +93 -43
  77. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +32 -6
  78. package/src/runtime/agent/orchestrator/tools/builtin.mjs +9 -2
  79. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +3 -1
  80. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +12 -15
  81. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +2 -2
  82. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +4 -2
  83. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -91
  84. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +19 -4
  85. package/src/runtime/channels/index.mjs +1 -5
  86. package/src/runtime/channels/lib/runtime-paths.mjs +8 -23
  87. package/src/runtime/channels/lib/tool-dispatch.mjs +1 -13
  88. package/src/runtime/channels/lib/worker-ipc.mjs +2 -2
  89. package/src/runtime/channels/lib/worker-main.mjs +4 -34
  90. package/src/runtime/computer-bridge/client.mjs +25 -1
  91. package/src/runtime/computer-bridge/linux-action-capabilities.mjs +84 -0
  92. package/src/runtime/computer-bridge/linux-readiness.d.mts +52 -0
  93. package/src/runtime/computer-bridge/linux-readiness.mjs +411 -0
  94. package/src/runtime/computer-bridge/test-linux-display.mjs +5 -0
  95. package/src/runtime/effort-judge/compact-tokenizer.mjs +13 -8
  96. package/src/runtime/effort-judge/judge-input.mjs +5 -2
  97. package/src/runtime/effort-judge/model-install.mjs +9 -3
  98. package/src/runtime/effort-judge/model-manifest.json +7 -7
  99. package/src/runtime/memory/lib/embedding-worker.mjs +9 -3
  100. package/src/runtime/memory/lib/pg/supervisor.mjs +3 -0
  101. package/src/runtime/shared/config.mjs +50 -2
  102. package/src/runtime/shared/developer-options.mjs +7 -0
  103. package/src/runtime/shared/instruction-files-mode.mjs +9 -0
  104. package/src/runtime/shared/llm/model-catalog.mjs +2 -1
  105. package/src/runtime/shared/llm/openai-daybreak.mjs +79 -0
  106. package/src/runtime/shared/pristine-execution-contract.json +9 -0
  107. package/src/runtime/shared/pristine-execution.mjs +1 -1
  108. package/src/runtime/shared/provider-accounts.mjs +2 -0
  109. package/src/runtime/shared/resource-admission.mjs +10 -3
  110. package/src/runtime/shared/schedule-model-ref.mjs +11 -0
  111. package/src/runtime/shared/semver.d.mts +1 -0
  112. package/src/runtime/shared/semver.mjs +57 -0
  113. package/src/runtime/shared/session-approval-wait.mjs +35 -0
  114. package/src/runtime/shared/shell-display.mjs +4 -2
  115. package/src/runtime/shared/tool-surface.mjs +2 -2
  116. package/src/runtime/shared/turn-worktree-snapshot.mjs +11 -10
  117. package/src/runtime/shared/update-checker.mjs +2 -31
  118. package/src/session-runtime/boot/apis.mjs +5 -0
  119. package/src/session-runtime/boot/begin.mjs +0 -2
  120. package/src/session-runtime/boot/tools.mjs +5 -4
  121. package/src/session-runtime/model-recency.mjs +3 -0
  122. package/src/session-runtime/provider-auth/login.mjs +10 -10
  123. package/src/session-runtime/provider-auth/setup.mjs +22 -2
  124. package/src/session-runtime/provider-models/row-hydration.mjs +12 -4
  125. package/src/session-runtime/provider-readiness.mjs +4 -0
  126. package/src/session-runtime/resource-mcp-api.mjs +7 -3
  127. package/src/session-runtime/runtime-bootstrap.mjs +0 -4
  128. package/src/session-runtime/runtime-facade.mjs +5 -0
  129. package/src/session-runtime/runtime-paths.mjs +0 -1
  130. package/src/session-runtime/schedule-session-run.mjs +3 -1
  131. package/src/session-runtime/services/agent-tool/index-write-queue.mjs +55 -14
  132. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +15 -3
  133. package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +2 -0
  134. package/src/session-runtime/services/agent-tool/worker-index/approval-wait.mjs +97 -0
  135. package/src/session-runtime/services/agent-tool/worker-index/mutation-batch.mjs +4 -2
  136. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -2
  137. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +3 -0
  138. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +58 -2
  139. package/src/session-runtime/services/agent-tool/worker-index.mjs +69 -15
  140. package/src/session-runtime/services/agent-tool/worker-rows.mjs +2 -0
  141. package/src/session-runtime/services/channel-admin.mjs +4 -2
  142. package/src/session-runtime/services/channel-worker.mjs +1 -1
  143. package/src/session-runtime/services/provider-admin.mjs +80 -17
  144. package/src/session-runtime/services/seeds.mjs +7 -10
  145. package/src/session-runtime/services/usage-dashboard-model.mjs +6 -0
  146. package/src/session-runtime/settings-builtin-tools-api.mjs +15 -0
  147. package/src/session-runtime/settings-profile-api.mjs +27 -2
  148. package/src/session-runtime/setup-tool/executor.mjs +5 -0
  149. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  150. package/src/session-runtime/starter-guide-state.mjs +62 -0
  151. package/src/session-runtime/tool-policy-refresh.mjs +3 -0
  152. package/src/session-runtime/webhook-session-run.mjs +3 -1
  153. package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -2
  154. package/src/standalone/channel-binding.mjs +1 -2
  155. package/src/standalone/channel-transport/call-dispatch.mjs +4 -10
  156. package/src/standalone/daemon-channels-loader.mjs +2 -3
  157. package/src/standalone/local-session-runtime.mjs +2 -78
  158. package/src/standalone/session-protocol.mjs +2 -0
  159. package/src/tui/app/model-options.mjs +13 -1
  160. package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +10 -4
  161. package/src/tui/app/provider-setup/oauth-actions.mjs +5 -0
  162. package/src/tui/dist/index.mjs +84 -34
  163. package/src/tui/session/context-state.mjs +2 -0
  164. package/src/tui/session/live-share/owner.mjs +17 -0
  165. package/src/tui/session/oauth-flows.mjs +2 -0
  166. package/src/tui/session/session-api/extensions.mjs +12 -8
  167. package/src/tui/session/session-api/integrations.mjs +9 -8
  168. package/src/tui/session/session-api/routes.mjs +2 -2
  169. package/src/tui/session/session-api/settings.mjs +1 -0
  170. package/src/tui/session/session-api/shared.mjs +10 -6
  171. package/src/tui/session/tool-card-results/grouped-result.mjs +11 -2
  172. package/src/tui/session/transcript-spill/spill-dir.mjs +22 -6
  173. package/src/tui/spinner-meta.mjs +26 -10
  174. package/src/runtime/channels/tool-defs.mjs +0 -17
@@ -0,0 +1,99 @@
1
+ /**
2
+ * openai-oauth-catalog-store.mjs — where the openai-oauth model catalog is
3
+ * cached, and the catalog's own route successors.
4
+ *
5
+ * Kept free of the provider runtime so config load can read the last fetched
6
+ * catalog synchronously: a saved route on a model the catalog has superseded
7
+ * moves to the model the catalog names for it.
8
+ */
9
+ import { makeModelCache } from './model-cache.mjs';
10
+ import { accessProgramModelParameters, accessProgramOnly } from '../../../shared/llm/openai-daybreak.mjs';
11
+ import { formatScheduleModelRef, parseScheduleModelRef } from '../../../shared/schedule-model-ref.mjs';
12
+
13
+ export const CODEX_MODEL_CACHE_OPTIONS = Object.freeze({
14
+ fileName: 'openai-oauth-models.json',
15
+ ttlMs: 24 * 60 * 60_000,
16
+ version: 6,
17
+ });
18
+
19
+ export const CODEX_PROVIDER_ID = 'openai-oauth';
20
+
21
+ let snapshotCache = null;
22
+
23
+ /** The last saved catalog, stale or not; null when none was ever fetched. */
24
+ function savedCatalog() {
25
+ snapshotCache ||= makeModelCache(CODEX_MODEL_CACHE_OPTIONS);
26
+ return snapshotCache.loadSync({ allowStale: true });
27
+ }
28
+
29
+ /**
30
+ * The route a model moves to, read off the catalog alone; null when it stays.
31
+ * - A standalone access-program variant (its access programs omit the
32
+ * standard one) moves to the first listed regular model, by catalog
33
+ * priority, that accepts the same program, with that program's toggle on.
34
+ * - A model whose `upgrade.retirement_at` has passed moves to `upgrade.model`.
35
+ * Returns { model, modelParameters? }.
36
+ */
37
+ export function codexModelSuccessor(models, modelId, now = Date.now()) {
38
+ if (!Array.isArray(models) || !modelId) return null;
39
+ const entry = models.find((model) => model?.id === modelId);
40
+ if (!entry) return null;
41
+ const program = accessProgramOnly(entry);
42
+ if (program) {
43
+ const modelParameters = accessProgramModelParameters(program);
44
+ if (!modelParameters) return null;
45
+ const target = models
46
+ .filter(
47
+ (model) =>
48
+ model?.id &&
49
+ model.id !== modelId &&
50
+ model.hidden !== true &&
51
+ !accessProgramOnly(model) &&
52
+ Array.isArray(model.accessPrograms) &&
53
+ model.accessPrograms.includes(program)
54
+ )
55
+ .sort((a, b) => (a.priority ?? Infinity) - (b.priority ?? Infinity))[0];
56
+ return target ? { model: target.id, modelParameters } : null;
57
+ }
58
+ const upgrade = entry.upgrade;
59
+ const retiresAt = Date.parse(upgrade?.retirementAt || '');
60
+ if (upgrade?.model && Number.isFinite(retiresAt) && retiresAt <= now) return { model: upgrade.model };
61
+ return null;
62
+ }
63
+
64
+ /** A saved openai-oauth route moved to its catalog successor; any other route unchanged. */
65
+ export function migrateCodexRoute(route, models = savedCatalog()) {
66
+ if (String(route?.provider || '').trim() !== CODEX_PROVIDER_ID) return route;
67
+ const successor = codexModelSuccessor(models, String(route.model || '').trim());
68
+ if (!successor) return route;
69
+ const current =
70
+ route.modelParameters && typeof route.modelParameters === 'object' && !Array.isArray(route.modelParameters)
71
+ ? route.modelParameters
72
+ : {};
73
+ return {
74
+ ...route,
75
+ model: successor.model,
76
+ ...(successor.modelParameters || Object.hasOwn(route, 'modelParameters')
77
+ ? { modelParameters: { ...current, ...(successor.modelParameters || {}) } }
78
+ : {}),
79
+ };
80
+ }
81
+
82
+ /** A stored schedule/webhook model ref ("provider/model@effort+fast?k=v") moved
83
+ * to its catalog successor; preset names and current models unchanged. */
84
+ export function migrateCodexModelRef(ref, models = savedCatalog()) {
85
+ const route = parseScheduleModelRef(ref);
86
+ if (!route || typeof route !== 'object') return ref;
87
+ const migrated = migrateCodexRoute(route, models);
88
+ return migrated === route ? ref : formatScheduleModelRef(migrated);
89
+ }
90
+
91
+ /** `modelSettings` keys whose model the catalog has superseded (they apply to nothing any more). */
92
+ export function supersededCodexSettingsKey(key, models = savedCatalog()) {
93
+ const prefix = `${CODEX_PROVIDER_ID}/`;
94
+ return key.startsWith(prefix) && Boolean(codexModelSuccessor(models, key.slice(prefix.length)));
95
+ }
96
+
97
+ export function loadSavedCodexCatalog() {
98
+ return savedCatalog();
99
+ }
@@ -8,10 +8,10 @@
8
8
  * levels, visibility), so the catalog is normalized and enriched once here and
9
9
  * every consumer reads the same records.
10
10
  *
11
- * openai-oauth.mjs re-exports the lookups it used to own so existing importers
12
- * resolve unchanged.
11
+ * openai-oauth.mjs re-exports these lookups as its facade.
13
12
  */
14
13
  import { makeModelCache } from './model-cache.mjs';
14
+ import { CODEX_MODEL_CACHE_OPTIONS } from './openai-oauth-catalog-store.mjs';
15
15
  import { enrichModels } from './model-catalog.mjs';
16
16
  import { sanitizeModelList } from './model-list-sanitize.mjs';
17
17
  import { modelSupportsServiceTier } from './model-service-tiers.mjs';
@@ -21,8 +21,6 @@ import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
21
21
  import { CODEX_OAUTH_ORIGINATOR, codexModelsUrl } from './openai-codex-endpoints.mjs';
22
22
  import { _normalizeCodexModel, _markLatestCodex } from './openai-codex-model.mjs';
23
23
 
24
- const CODEX_MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
25
- const CODEX_MODEL_CACHE_SCHEMA_VERSION = 6;
26
24
  const CATALOG_FETCH_TIMEOUT_MS = 10_000;
27
25
 
28
26
  // In-memory mirror of the on-disk catalog, same pattern as anthropic-oauth.
@@ -31,9 +29,7 @@ let _mirror = null;
31
29
  let _refreshInFlight = null;
32
30
 
33
31
  const _modelCache = makeModelCache({
34
- fileName: 'openai-oauth-models.json',
35
- ttlMs: CODEX_MODEL_CACHE_TTL_MS,
36
- version: CODEX_MODEL_CACHE_SCHEMA_VERSION,
32
+ ...CODEX_MODEL_CACHE_OPTIONS,
37
33
  onSave: (models) => {
38
34
  _mirror = Array.isArray(models) ? models.slice() : null;
39
35
  },
@@ -96,7 +92,7 @@ export function resolveLatestCodexModel() {
96
92
  if (!Array.isArray(_mirror)) return null;
97
93
  let best = null;
98
94
  for (const m of _mirror) {
99
- if (!m?.id || m.visibility === 'hide' || !Number.isFinite(m.priority)) continue;
95
+ if (!m?.id || m.visibility === 'hide' || m.hidden === true || !Number.isFinite(m.priority)) continue;
100
96
  if (!best || m.priority < best.priority) best = m;
101
97
  }
102
98
  return best?.id || null;
@@ -12,6 +12,7 @@ import { createServer } from 'node:http';
12
12
  import { randomBytes } from 'node:crypto';
13
13
  import { createOAuthPkce, parseOAuthCodeInput } from './lib/oauth-pkce.mjs';
14
14
  import { OAUTH_PAGE_CONTENT_TYPE, oauthSuccessHtml } from './lib/oauth-page.mjs';
15
+ import { openLoginUrl } from './lib/open-login-url.mjs';
15
16
 
16
17
  const TOKEN_URL = 'https://auth.openai.com/oauth/token';
17
18
  const AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize';
@@ -141,13 +142,7 @@ export function createOpenAIOAuthLogin(deps) {
141
142
  process.stderr.write(
142
143
  `\n[openai-oauth] Open this URL to log in to ChatGPT (OpenAI OAuth):\n${url.toString()}\n\n`
143
144
  );
144
- if (!openBrowser) return;
145
- try {
146
- const { openInBrowser } = await import('../../../shared/open-url.mjs');
147
- openInBrowser(url.toString());
148
- } catch (err) {
149
- process.stderr.write(`[openai-oauth] browser open failed: ${String(err?.message || err).slice(0, 200)}\n`);
150
- }
145
+ if (openBrowser) await openLoginUrl('openai-oauth', url.toString());
151
146
  });
152
147
  server.on('error', (err) =>
153
148
  finish(
@@ -7,6 +7,7 @@ import { isResponsesFreeformTool, toResponsesCustomTool } from './custom-tool-wi
7
7
  import { _envFlag } from './openai-oauth-http-sse.mjs';
8
8
  import { findCachedCodexModel } from './openai-oauth-catalog.mjs';
9
9
  import { resolveOpenAiServiceTier } from './openai-service-tier.mjs';
10
+ import { daybreakAccessPrograms } from '../../../shared/llm/openai-daybreak.mjs';
10
11
 
11
12
  export function toOpenAIResponsesTool(t) {
12
13
  if (t?.name === 'load_tool' || t?.name === 'tool_search') {
@@ -219,6 +220,12 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
219
220
  toolChoice,
220
221
  });
221
222
  const verbosity = requestedVerbosity(opts);
223
+ // Daybreak Blue rides on the ChatGPT route only, and only for a model that
224
+ // accepts it; the server still decides entitlement.
225
+ const accessPrograms =
226
+ promptCacheProvider === 'openai-oauth'
227
+ ? daybreakAccessPrograms(findCachedCodexModel(model) || model, opts.modelParameters)
228
+ : null;
222
229
  // Match the request body shape the OAuth backend expects so the
223
230
  // server-side auto-cache routes correctly: text.verbosity / include /
224
231
  // tool_choice / parallel_tool_calls are inert for most callers but their
@@ -246,6 +253,7 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
246
253
  ...(serviceTier ? { service_tier: serviceTier } : {}),
247
254
  prompt_cache_key: promptCacheKey,
248
255
  text: { verbosity },
256
+ ...(accessPrograms ? { access_programs: accessPrograms } : {}),
249
257
  ...(maxOutputTokens ? { max_output_tokens: maxOutputTokens } : {}),
250
258
  };
251
259
  }
@@ -26,6 +26,8 @@ const OAUTH_PROVIDER_NAMES = new Set([
26
26
  'anthropic-oauth',
27
27
  'openai-oauth',
28
28
  'grok-oauth',
29
+ 'muse-code-oauth',
30
+ 'mimo-oauth',
29
31
  'cursor-oauth',
30
32
  'antigravity-oauth',
31
33
  ]);
@@ -143,6 +145,8 @@ const PROVIDER_MODULES = {
143
145
  'openai-oauth': ['./openai-oauth.mjs', 'OpenAIOAuthProvider'],
144
146
  'anthropic-oauth': ['./anthropic-oauth.mjs', 'AnthropicOAuthProvider'],
145
147
  'grok-oauth': ['./grok-oauth.mjs', 'GrokOAuthProvider'],
148
+ 'muse-code-oauth': ['./muse-code-oauth.mjs', 'MuseCodeOAuthProvider'],
149
+ 'mimo-oauth': ['./mimo-oauth.mjs', 'MimoOAuthProvider'],
146
150
  'cursor-oauth': ['./cursor.mjs', 'CursorOAuthProvider'],
147
151
  'antigravity-oauth': ['./antigravity-oauth.mjs', 'AntigravityOAuthProvider'],
148
152
  'cursor-api': ['./cursor.mjs', 'CursorApiProvider'],
@@ -25,6 +25,8 @@ const EFFORT_OPTIONS_BY_PROVIDER = {
25
25
  // xAI Grok 4.6+: low/medium/high/xhigh. Reasoning cannot be disabled.
26
26
  xai: ['low', 'medium', 'high', 'xhigh'],
27
27
  'grok-oauth': ['low', 'medium', 'high', 'xhigh'],
28
+ 'muse-code-oauth': ['minimal', 'low', 'medium', 'high', 'xhigh'],
29
+ 'mimo-oauth': ['minimal', 'low', 'medium', 'high'],
28
30
  // Antigravity carries the effort tier inside the wire model id
29
31
  // (gemini-3-pro-high / -low, claude-*-thinking), so the route exposes none.
30
32
  'antigravity-oauth': [],
@@ -129,7 +129,21 @@ export function validateGoalToolCall(value) {
129
129
  if (!action) throw new Error('goal action is required');
130
130
  if (!ACTION_FIELDS[action]) throw new Error(`goal action must be one of: ${Object.keys(ACTION_FIELDS).join(', ')}`);
131
131
  const extras = Object.keys(value).filter((key) => !TOOL_FIELDS.has(key));
132
- if (extras.length) throw new Error(`goal arguments contain unknown fields: ${extras.join(', ')}`);
132
+ if (extras.length) {
133
+ throw new Error(
134
+ `goal arguments contain unknown fields: ${extras.join(', ')}. Allowed for ${action}: ${ACTION_FIELDS[action].join(', ')}`
135
+ );
136
+ }
137
+ // A JSON-stringified tasks/updates array is the same list; parse it in place.
138
+ for (const key of ['tasks', 'updates']) {
139
+ if (typeof value[key] !== 'string' || !value[key].trim().startsWith('[')) continue;
140
+ try {
141
+ const parsed = JSON.parse(value[key]);
142
+ if (Array.isArray(parsed)) value[key] = parsed;
143
+ } catch {
144
+ /* left as-is for the handler's own rejection */
145
+ }
146
+ }
133
147
  // Restored providers can fill every optional field. Consume only the active
134
148
  // action's payload; an absent/empty revision keeps old frozen schemas usable.
135
149
  if (ACTION_FIELDS[action].includes('revision') && value.revision != null && value.revision !== '') {
@@ -98,8 +98,7 @@ const admitted = (tools, names) => (tools || []).filter((tool) => names.includes
98
98
 
99
99
  /**
100
100
  * Assemble this runtime's tool definitions from the loaded tool-def modules.
101
- * Pure: callers resolve their own availability flags (skills env gate, channel
102
- * ownership) and pass the already-built goal/agent tool arrays.
101
+ * Pure: callers resolve their own availability flags (skills env gate) and pass the already-built goal/agent tool arrays.
103
102
  *
104
103
  * - `standaloneTools` is the model-facing set (also the deferred catalog base).
105
104
  * - `internalToolDefs` adds the `public: false` web-search tools: reachable
@@ -108,7 +107,6 @@ const admitted = (tools, names) => (tools || []).filter((tool) => names.includes
108
107
  export function collectStandaloneToolDefs({
109
108
  webSearchToolDefs,
110
109
  memoryToolDefs,
111
- channelToolDefs,
112
110
  codeGraphToolDefs,
113
111
  browserToolDefs = [],
114
112
  computerToolDefs = [],
@@ -119,7 +117,6 @@ export function collectStandaloneToolDefs({
119
117
  setupToolDefs = [],
120
118
  goalTools = [],
121
119
  agentTools = [],
122
- isChannelTool = () => false,
123
120
  skillToolEnabled = true,
124
121
  }) {
125
122
  const webSearchRuntimeTools = admitted(webSearchToolDefs?.TOOL_DEFS, WEB_SEARCH_RUNTIME_TOOLS);
@@ -129,7 +126,6 @@ export function collectStandaloneToolDefs({
129
126
  CWD_TOOL,
130
127
  ...webSearchRuntimeTools.filter((tool) => tool?.public !== false),
131
128
  ...admitted(memoryToolDefs?.TOOL_DEFS, MEMORY_RUNTIME_TOOLS),
132
- ...(channelToolDefs?.TOOL_DEFS || []).filter((tool) => isChannelTool(tool?.name)),
133
129
  ...admitted(codeGraphToolDefs?.CODE_GRAPH_TOOL_DEFS, CODE_GRAPH_RUNTIME_TOOLS),
134
130
  ...admitted(browserToolDefs, BROWSER_RUNTIME_TOOLS),
135
131
  ...admitted(computerToolDefs, COMPUTER_RUNTIME_TOOLS),
@@ -1,6 +1,13 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { estimateTokens } from './token-estimate.mjs';
3
- import { beginIdentity, endIdentity, putJsonElement, putJsonString, putRaw, putShapeText } from './context-identity-writer.mjs';
3
+ import {
4
+ beginIdentity,
5
+ endIdentity,
6
+ putJsonElement,
7
+ putJsonString,
8
+ putRaw,
9
+ putShapeText,
10
+ } from './context-identity-writer.mjs';
4
11
  import { positiveInt } from '../../../shared/numbers.mjs';
5
12
  import { hasPlainPrototype } from '../../../shared/object.mjs';
6
13
  import { createContextFingerprinter } from './context-fingerprint.mjs';
@@ -39,19 +46,10 @@ export {
39
46
  resolveSessionCompactPolicy,
40
47
  } from './context-compaction-policy.mjs';
41
48
 
42
- // ---------------------------------------------------------------------------
43
- // Token estimation lives in ./token-estimate.mjs — a conservative
44
- // Unicode-aware heuristic. It is accurate ENOUGH because context pressure is
45
- // anchored on the provider usage baseline (the exact prompt-token count the
46
- // provider reports for the previous request) and the heuristic only prices the
47
- // delta appended since then. Measured across 5,552 real deltas the worst
48
- // underestimate was 2,985 tokens (1.5% of a 200K window) with a +6.5%
49
- // aggregate bias toward overcounting.
50
- //
51
- // providerTokenCalibration() below reconciles that estimate with actual
52
- // billing at the provider-aware aggregation boundary (compaction pressure /
53
- // context gauge), never inside the provider-agnostic per-message memo.
54
- // ---------------------------------------------------------------------------
49
+ // providerTokenCalibration() below reconciles the ./token-estimate.mjs
50
+ // heuristic with actual billing at the provider-aware aggregation boundary
51
+ // (compaction pressure / context gauge), never inside the provider-agnostic
52
+ // per-message memo.
55
53
 
56
54
  // Billed-prompt / estimate ratio per provider family. Env overrides let a
57
55
  // deployment recalibrate without a code change; values are clamped to a
@@ -15,6 +15,7 @@ import { sendWithRecovery } from '../send-with-recovery.mjs';
15
15
  import { stripInlineImages } from '../image-strip-recovery.mjs';
16
16
  import { runWithProviderRequestToolsScope } from '../../runtime-core/provider-request-tools.mjs';
17
17
  import { REPEAT_FAIL_LIMIT } from './loop-state.mjs';
18
+ import { createToolCallIdResolver } from './tool-call-ids.mjs';
18
19
 
19
20
  /**
20
21
  * Open an iteration: refresh the live cwd, drain queued steering after a
@@ -104,13 +105,22 @@ export async function beginIteration(state) {
104
105
  getNextIteration: () => nextIteration,
105
106
  repeatFailLimit: REPEAT_FAIL_LIMIT,
106
107
  });
108
+ const resolveToolCallId = createToolCallIdResolver(messages);
107
109
  opts.onToolCall = (call) => {
110
+ resolveToolCallId(call);
108
111
  try {
109
112
  opts.onAssistantToolCallObserved?.(call, { eagerStarted: false });
110
113
  } catch {}
111
114
  return eager.onToolCall(call);
112
115
  };
113
- return { iterT0, nextIteration, sendTools: boundary.sendTools, requestToolScope: boundary.requestToolScope, eager };
116
+ return {
117
+ iterT0,
118
+ nextIteration,
119
+ sendTools: boundary.sendTools,
120
+ requestToolScope: boundary.requestToolScope,
121
+ eager,
122
+ resolveToolCallId,
123
+ };
114
124
  }
115
125
 
116
126
  /** Project the transcript for the provider and send it with recovery. */
@@ -0,0 +1,90 @@
1
+ /**
2
+ * tool-call-ids.mjs — a tool call id is unique within a transcript.
3
+ *
4
+ * Tool results are paired to their assistant turn by id, and the pairing
5
+ * repairs treat a second assistant turn that reuses an id as a duplicate
6
+ * with no result: they drop it, so its (already executed) result never
7
+ * reaches the model and the model asks again, forever. Local and
8
+ * OpenAI-compatible servers reuse ids across turns (`call_0`, `call_1`), so
9
+ * a call whose id the history already carries is renamed before anything —
10
+ * eager dispatch, the transcript, the UI — records it.
11
+ */
12
+ import { collectAssistantToolCallIds } from '../context-tool-pairs.mjs';
13
+
14
+ function usedToolCallIds(messages) {
15
+ const used = new Set();
16
+ for (const message of messages) {
17
+ if (message?.role === 'tool' && message.toolCallId) used.add(message.toolCallId);
18
+ for (const id of collectAssistantToolCallIds(message)) used.add(id);
19
+ }
20
+ return used;
21
+ }
22
+
23
+ /**
24
+ * One resolver per provider round. A call keeps its id unless the history, an
25
+ * id this round already handed out, or (for the final batch) another call of
26
+ * the batch holds it; then it gets the smallest free `<id>-<n>`.
27
+ *
28
+ * A call is seen twice per round: streamed (`resolve`, one at a time, often a
29
+ * clone) and in the final batch (`resolve.all`). The n-th call carrying an
30
+ * original id in the final batch is the n-th streamed one, so both resolve to
31
+ * the same new id while two distinct calls that share an id stay distinct.
32
+ * Mutates and returns the calls.
33
+ */
34
+ export function createToolCallIdResolver(messages) {
35
+ const handedOut = new Set();
36
+ const byCall = new WeakMap();
37
+ const byOriginal = new Map();
38
+
39
+ const isTaken = (id, reserved) => handedOut.has(id) || reserved.has(id) || usedToolCallIds(messages).has(id);
40
+
41
+ const allocate = (call, reserved) => {
42
+ const original = call.id;
43
+ let id = original;
44
+ if (isTaken(id, new Set())) {
45
+ let n = 2;
46
+ while (isTaken(`${original}-${n}`, reserved)) n += 1;
47
+ id = `${original}-${n}`;
48
+ }
49
+ handedOut.add(id);
50
+ if (!byOriginal.has(original)) byOriginal.set(original, []);
51
+ byOriginal.get(original).push(id);
52
+ byCall.set(call, { original, id });
53
+ call.id = id;
54
+ return call;
55
+ };
56
+
57
+ const resolve = (call) => {
58
+ if (!call?.id) return call;
59
+ const known = byCall.get(call);
60
+ if (known) {
61
+ call.id = known.id;
62
+ return call;
63
+ }
64
+ return allocate(call, new Set());
65
+ };
66
+
67
+ resolve.all = (calls) => {
68
+ const seen = new Map();
69
+ const originals = calls.map((call) => byCall.get(call)?.original ?? call?.id);
70
+ calls.forEach((call, index) => {
71
+ if (!call?.id) return;
72
+ const original = originals[index];
73
+ const nth = seen.get(original) ?? 0;
74
+ seen.set(original, nth + 1);
75
+ const known = byCall.get(call) ?? null;
76
+ const mapped = known?.id ?? byOriginal.get(original)?.[nth];
77
+ if (mapped) {
78
+ call.id = mapped;
79
+ if (!known) byCall.set(call, { original, id: mapped });
80
+ return;
81
+ }
82
+ // Ids later in the batch that no earlier pass has placed yet stay theirs.
83
+ const reserved = new Set(originals.slice(index + 1).filter(Boolean));
84
+ allocate(call, reserved);
85
+ });
86
+ return calls;
87
+ };
88
+
89
+ return resolve;
90
+ }
@@ -4,22 +4,30 @@
4
4
  */
5
5
  import { resolvePreToolAskApproval } from '../tool-helpers.mjs';
6
6
  import { runAbortable, throwIfAborted } from '../../../../../shared/abort-race.mjs';
7
+ import { beginSessionApprovalWait } from '../../manager/runtime-liveness.mjs';
7
8
 
8
9
  const isArgsObject = (value) => value && typeof value === 'object' && !Array.isArray(value);
9
10
 
10
11
  async function askApproval(call, { callerSessionId, executeOpts, toolApprovalHook, decision }) {
11
12
  const askReason = String(decision?.reason || 'approval requested by hook').trim();
12
- const askOutcome = await runAbortable(executeOpts.signal, () =>
13
- resolvePreToolAskApproval({
14
- toolName: call.name,
15
- args: call.args,
16
- cwd: call.cwd,
17
- sessionId: callerSessionId,
18
- toolCallId: executeOpts.toolCallId || null,
19
- askReason,
20
- toolApprovalHook,
21
- })
22
- );
13
+ // While the user decides, the session is waiting on them, not working.
14
+ const releaseApprovalWait = beginSessionApprovalWait(callerSessionId);
15
+ let askOutcome;
16
+ try {
17
+ askOutcome = await runAbortable(executeOpts.signal, () =>
18
+ resolvePreToolAskApproval({
19
+ toolName: call.name,
20
+ args: call.args,
21
+ cwd: call.cwd,
22
+ sessionId: callerSessionId,
23
+ toolCallId: executeOpts.toolCallId || null,
24
+ askReason,
25
+ toolApprovalHook,
26
+ })
27
+ );
28
+ } finally {
29
+ releaseApprovalWait();
30
+ }
23
31
  if (askOutcome.denial) return { denial: askOutcome.denial };
24
32
  const approval = askOutcome.approval;
25
33
  if (approval && typeof approval === 'object' && isArgsObject(approval.args)) call.args = approval.args;
@@ -18,6 +18,7 @@ export async function runToolPhase(state, round, sent, onToolCall) {
18
18
  const { opts, messages, tools, sessionId, sessionRef, signal, response, suppressMidTurnText } = state;
19
19
  state.noToolTurn.noteToolCallTurn();
20
20
  const calls = response.toolCalls;
21
+ round.resolveToolCallId.all(calls);
21
22
  state.toolCallsTotal += calls.length;
22
23
  // Surface any mid-turn assistant text (preamble before a tool call) to the
23
24
  // UI. Providers that stream via onTextDelta already rendered it; providers
@@ -19,6 +19,7 @@
19
19
  // closed?: boolean, // flipped by closeSession()
20
20
  // }
21
21
  import { createAbortController } from '../../../../shared/abort-controller.mjs';
22
+ import { emitSessionApprovalWaitChange } from '../../../../shared/session-approval-wait.mjs';
22
23
  import { publishHeartbeat, deleteHeartbeat } from '../store.mjs';
23
24
  import { DEFAULT_ACTIVITY_HEARTBEAT_MS } from '../../stall-policy.mjs';
24
25
  import { finishBrowserTurn } from '../../../../browser-bridge/client.mjs';
@@ -286,6 +287,32 @@ export function markSessionToolOutputTail(id, tail) {
286
287
  entry.toolOutputTail = text ? text.slice(-4000) : null;
287
288
  entry.toolOutputTailAt = Date.now();
288
289
  }
290
+ // A turn blocked on the user: a PreToolUse `ask` waiting for the tool
291
+ // approval only the user can give. Published as `awaitingUser: 'approval'` so
292
+ // status surfaces stop presenting the session as working. Counted, because one
293
+ // tool batch can hold several approvals at once; the field clears when the
294
+ // last one settles. Never creates an entry — a real turn always has one.
295
+ // Returns the release, which is idempotent.
296
+ export function beginSessionApprovalWait(id) {
297
+ const entry = id ? _runtimeState.get(id) : null;
298
+ if (!entry) return () => {};
299
+ entry.approvalWaits = (entry.approvalWaits || 0) + 1;
300
+ entry.awaitingUser = 'approval';
301
+ // The waiting turn: the end transition carries the begin's turn even when
302
+ // the turn has settled (askStartedAt cleared) by the time it releases.
303
+ const meta = { turnAt: entry.askStartedAt || 0 };
304
+ // Only the begin/end transitions are announced (the worker index persists them).
305
+ if (entry.approvalWaits === 1) emitSessionApprovalWaitChange(id, 'approval', meta);
306
+ let released = false;
307
+ return () => {
308
+ if (released) return;
309
+ released = true;
310
+ entry.approvalWaits = Math.max(0, (entry.approvalWaits || 0) - 1);
311
+ if (entry.approvalWaits > 0) return;
312
+ entry.awaitingUser = null;
313
+ emitSessionApprovalWaitChange(id, null, meta);
314
+ };
315
+ }
289
316
  // Parent AbortSignal listeners are dropped on askSession unwind (finally /
290
317
  // terminal return) and on error/cancel/close — not in markSessionDone, which
291
318
  // also runs between queued follow-up turns within one ask.
@@ -411,6 +438,7 @@ export function getSessionProgressSnapshot(sessionId) {
411
438
  ),
412
439
  waitingForTransport,
413
440
  waitingForFirstSemantic,
441
+ ...(entry.awaitingUser ? { awaitingUser: entry.awaitingUser } : {}),
414
442
  };
415
443
  }
416
444
 
@@ -3,6 +3,7 @@
3
3
  // the volatile environment block that closes them, and the seed messages that
4
4
  // carry all four to the provider with their cache tiers.
5
5
  import { buildSkillManifest, composeSystemPrompt } from '../../context/collect.mjs';
6
+ import { configuredInstructionFilesMode, loadInstructionFilesContext } from '../../context/instruction-files.mjs';
6
7
  import {
7
8
  _buildBaseRules,
8
9
  _buildAgentRules,
@@ -97,6 +98,12 @@ export function composeSessionSystem(opts, { profile, providerName, modelName, s
97
98
  // as [session, bp3EnvironmentContext], so the language block must
98
99
  // already sit at the end of this string to stay last after a refresh.
99
100
  const environmentTailContext = [shellEnvironmentContext, languageContext].filter(Boolean).join('\n\n---\n\n');
101
+ // User instruction files (AGENTS.md / CLAUDE.md, global then the project
102
+ // root's); a tool-free role and a profile skipping `claudemd` get none.
103
+ const instructionFilesContext =
104
+ toolFreeRole || profile?.skip?.claudemd
105
+ ? ''
106
+ : loadInstructionFilesContext({ mode: configuredInstructionFilesMode(), cwd: opts.cwd });
100
107
  const composed = composeSystemPrompt({
101
108
  userPrompt: opts.systemPrompt,
102
109
  agentRules: injectedRules || undefined,
@@ -108,6 +115,7 @@ export function composeSessionSystem(opts, { profile, providerName, modelName, s
108
115
  agent: resolvedAgent,
109
116
  workflowContext: opts.workflowContext || null,
110
117
  coreMemoryContext: opts.coreMemoryContext || null,
118
+ instructionFilesContext: instructionFilesContext || null,
111
119
  skillManifest: buildSkillManifest(skills),
112
120
  environmentContext: shellEnvironmentContext,
113
121
  provider: providerName || null,
@@ -78,6 +78,9 @@ export function sessionOriginFields(opts, { profile, presetObj, providerCacheOpt
78
78
  // Agent Runtime metadata — optional. Applied on every ask() to merge
79
79
  // profile-driven cache settings into provider sendOpts.
80
80
  profileId: profile?.id || null,
81
+ // A profile skipping `claudemd` keeps instruction files out of later
82
+ // empty-session prompt rebuilds too (tool-policy-refresh.mjs).
83
+ skipInstructionFiles: profile?.skip?.claudemd === true,
81
84
  permissionMode: opts.permissionMode ?? null,
82
85
  providerCacheOpts: providerCacheOpts || null,
83
86
  parentSessionId: opts.parentSessionId || null,
@@ -1,9 +1,5 @@
1
1
  // src/runtime/agent/orchestrator/session/manager.mjs
2
- // Thin facade over the split session-manager modules in ./manager/. This file
3
- // re-exports the EXACT public surface the previous monolith exposed so every
4
- // importer (loop.mjs, loop/tool-exec.mjs, agent-dispatch.mjs, session-builder,
5
- // abort-lookup, statusline-agents, headless-role, session-runtime, and the
6
- // smoke scripts) keeps resolving its symbols through manager.mjs unchanged.
2
+ // Thin facade over the split session-manager modules in ./manager/.
7
3
  //
8
4
  // Module map:
9
5
  // manager/runtime-liveness.mjs — in-memory stage/heartbeat + accessors
@@ -1,5 +1,4 @@
1
1
  // Thin re-export facade — re-exports all cache symbols from their split modules.
2
- // Existing import paths keep working unchanged.
3
2
  export * from './cache/read-cache.mjs';
4
3
  export * from './cache/scoped-cache.mjs';
5
4
  export * from './cache/prefetch-cache.mjs';
@@ -323,6 +323,9 @@ function projectChildWorkerRow(row, { now, heartbeatMtimes }) {
323
323
  fast: row.fast === true || session?.fast === true,
324
324
  status: declaredWorking && !working ? 'unknown' : declaredStatus,
325
325
  stage: declaredWorking && !working ? 'unknown' : declaredStage,
326
+ // A live turn blocked on the user's approval (persisted by the agent
327
+ // tool's worker index); never carried by a row that is no longer working.
328
+ ...(working && row.awaitingUser === 'approval' ? { awaitingUser: 'approval' } : {}),
326
329
  startedAt: row.startedAt || row.createdAt || session?.createdAt || null,
327
330
  turnStartedAt: working ? row.turnStartedAt || null : null,
328
331
  createdAt: row.createdAt || session?.createdAt || null,
@@ -15,6 +15,7 @@ import { crossTurnSignature, isEditProgressTool } from '../loop/completion-guard
15
15
  import { isEagerDispatchable, isToolCallDedupEligible, parseNativeToolSearchPayload } from '../loop/tool-helpers.mjs';
16
16
  import { restoreToolCallBodyForId } from '../loop/stored-tool-args.mjs';
17
17
  import { stageToolResult } from './state.mjs';
18
+ import { toolMetaPatchFromMessage } from '../transcript-restore/tool-result-status.mjs';
18
19
  import {
19
20
  compactToolResultImage,
20
21
  resizeImageBuffer,
@@ -281,6 +282,7 @@ function completedToolResult(completed, result, postprocessStartedAt) {
281
282
  toolTiming: toolTiming(completed, postprocessStartedAt),
282
283
  ...(completed.nativeToolSearch ? { nativeToolSearch: completed.nativeToolSearch } : {}),
283
284
  ...(uiDiff !== null ? { uiDiff } : {}),
285
+ ...toolMetaPatchFromMessage(completed.result),
284
286
  };
285
287
  }
286
288
 
@@ -23,6 +23,7 @@ import {
23
23
  aggregateLoadingTargets,
24
24
  aggregateRawResult,
25
25
  aggregateResultPatch,
26
+ assignToolMetaFromMessage,
26
27
  assignUiDiffFromMessage,
27
28
  mergeAggregateCategoryEntries,
28
29
  stringUiDiffPatch,
@@ -131,6 +132,7 @@ function attachRestoredToolResult(message, pendingByCallId) {
131
132
  const text = (typeof message?.content === 'string' ? message.content : toolResultText(message?.content)) || '';
132
133
  target.result = text;
133
134
  assignUiDiffFromMessage(target, message);
135
+ assignToolMetaFromMessage(target, message);
134
136
  // Cancel/crash control bodies are not red failures — show Cancelled tone.
135
137
  if (toolResultTerminalStatus(text) === 'cancelled') {
136
138
  target.isError = false;