mixdog 1.2.0 → 1.2.1

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 (201) hide show
  1. package/README.md +7 -5
  2. package/package.json +1 -1
  3. package/src/defaults/shipped-history.json +4 -0
  4. package/src/defaults/skills/pptx/SKILL.md +6 -6
  5. package/src/defaults/skills/pptx/references/review.md +6 -4
  6. package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
  7. package/src/defaults/skills/setup/references/actions.md +13 -3
  8. package/src/defaults/skills/visualize/SKILL.md +12 -9
  9. package/src/rules/shared/60-verification.md +3 -1
  10. package/src/rules/shared/70-delivery.md +4 -2
  11. package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
  12. package/src/runtime/agent/orchestrator/config.mjs +10 -4
  13. package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
  14. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
  15. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
  16. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +86 -8
  17. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +5 -1
  18. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +65 -7
  19. package/src/runtime/agent/orchestrator/mcp/client.mjs +128 -44
  20. package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
  21. package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
  22. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +108 -17
  23. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  24. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +19 -11
  25. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
  26. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
  27. package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
  28. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +444 -0
  29. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  30. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
  31. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +9 -2
  32. package/src/runtime/agent/orchestrator/providers/gemini.mjs +41 -28
  33. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
  34. package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
  35. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +15 -0
  36. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +35 -0
  37. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -0
  38. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
  39. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
  41. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
  42. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +2 -1
  43. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +9 -3
  44. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +4 -4
  45. package/src/runtime/agent/orchestrator/providers/registry.mjs +35 -9
  46. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +3 -0
  47. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
  48. package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
  49. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +267 -84
  50. package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
  51. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
  52. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
  53. package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
  54. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +268 -0
  55. package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
  56. package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
  57. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +6 -2
  58. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
  59. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
  60. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
  61. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
  62. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
  63. package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
  64. package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
  65. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
  66. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
  67. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +3 -2
  68. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +6 -4
  69. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +4 -3
  70. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
  71. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -3
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +3 -1
  73. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +20 -12
  74. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +9 -2
  75. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +4 -0
  76. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +17 -4
  77. package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
  78. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +4 -3
  79. package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
  80. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
  81. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +817 -0
  82. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +7 -0
  83. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +19 -12
  84. package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
  85. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +183 -374
  86. package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
  87. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +21 -2
  89. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +38 -17
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
  91. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
  92. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
  93. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +173 -22
  95. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +5 -4
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +15 -2
  98. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
  99. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
  100. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
  101. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
  102. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
  104. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
  105. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  106. package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
  107. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
  108. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
  109. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
  110. package/src/runtime/attachments/store.d.mts +3 -0
  111. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
  112. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
  113. package/src/runtime/channels/lib/worker-main.mjs +17 -14
  114. package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
  115. package/src/runtime/effort-judge/judge-client.mjs +56 -18
  116. package/src/runtime/memory/lib/memory.mjs +1 -1
  117. package/src/runtime/memory/lib/session-ingest.mjs +2 -2
  118. package/src/runtime/shared/atomic-file.mjs +25 -4
  119. package/src/runtime/shared/extension-scopes.mjs +5 -3
  120. package/src/runtime/shared/file-lock.mjs +37 -11
  121. package/src/runtime/shared/llm/anthropic-effort.mjs +14 -0
  122. package/src/runtime/shared/llm/model-catalog.mjs +0 -12
  123. package/src/runtime/shared/llm/provider-catalog-registry.mjs +6 -0
  124. package/src/runtime/shared/plugin-manifest.mjs +18 -9
  125. package/src/runtime/shared/pristine-execution-contract.json +9 -2
  126. package/src/runtime/shared/pristine-execution.mjs +28 -4
  127. package/src/runtime/shared/provider-accounts.mjs +124 -20
  128. package/src/runtime/shared/provider-api-key.mjs +12 -0
  129. package/src/runtime/shared/shell-display.mjs +9 -3
  130. package/src/runtime/shared/skill-mentions.d.mts +10 -0
  131. package/src/runtime/shared/skill-mentions.mjs +133 -0
  132. package/src/runtime/shared/skill-script-runtime.mjs +264 -0
  133. package/src/runtime/shared/tool-primitives.mjs +35 -2
  134. package/src/runtime/shared/turn-worktree-snapshot.mjs +25 -5
  135. package/src/runtime/shared/worker-requests.mjs +15 -2
  136. package/src/session-runtime/automation-agents.mjs +8 -2
  137. package/src/session-runtime/boot/apis.mjs +55 -0
  138. package/src/session-runtime/boot/config.mjs +5 -2
  139. package/src/session-runtime/boot/facade.mjs +1 -0
  140. package/src/session-runtime/boot/infrastructure.mjs +29 -18
  141. package/src/session-runtime/conversation-import/discovery.mjs +698 -0
  142. package/src/session-runtime/conversation-import/service.mjs +369 -0
  143. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +10 -5
  144. package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
  145. package/src/session-runtime/mcp-glue/connect.mjs +57 -10
  146. package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
  147. package/src/session-runtime/mcp-glue/server-catalog.mjs +115 -30
  148. package/src/session-runtime/mcp-glue.mjs +22 -2
  149. package/src/session-runtime/new-session-config.mjs +3 -2
  150. package/src/session-runtime/notification-bus.mjs +9 -2
  151. package/src/session-runtime/provider-auth/accounts.mjs +33 -15
  152. package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
  153. package/src/session-runtime/provider-readiness.mjs +16 -10
  154. package/src/session-runtime/resource-extension-sync.mjs +20 -5
  155. package/src/session-runtime/resource-mcp-api.mjs +101 -77
  156. package/src/session-runtime/resource-plugins-api.mjs +29 -41
  157. package/src/session-runtime/resource-scope-decorate.mjs +6 -8
  158. package/src/session-runtime/resource-skills-api.mjs +23 -2
  159. package/src/session-runtime/schedule-session-run.mjs +1 -1
  160. package/src/session-runtime/services/channel-session-router.mjs +4 -1
  161. package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
  162. package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
  163. package/src/session-runtime/services/provider-admin.mjs +64 -18
  164. package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
  165. package/src/session-runtime/setup-tool/executor.mjs +12 -1
  166. package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
  167. package/src/session-runtime/setup-tool/settings-contract.mjs +1 -0
  168. package/src/session-runtime/setup-tool/tool-defs.mjs +7 -1
  169. package/src/session-runtime/skills-api.mjs +7 -3
  170. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  171. package/src/session-runtime/webhook-session-run.mjs +16 -4
  172. package/src/skill-script-entry.mjs +36 -0
  173. package/src/standalone/daemon-stored-session-views.mjs +11 -3
  174. package/src/standalone/daemon.mjs +41 -4
  175. package/src/standalone/session-protocol.mjs +5 -0
  176. package/src/standalone/session-service/session-calls/session-action.mjs +6 -1
  177. package/src/tui/App.jsx +9 -1
  178. package/src/tui/app/app-view/floating-panel.jsx +9 -1
  179. package/src/tui/app/app-view.jsx +6 -0
  180. package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
  181. package/src/tui/app/model-options.mjs +2 -0
  182. package/src/tui/app/skill-mention-palette.mjs +88 -0
  183. package/src/tui/app/slash-palette.mjs +23 -7
  184. package/src/tui/app/transcript-row-estimate.mjs +11 -3
  185. package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
  186. package/src/tui/app/use-prompt-state.mjs +5 -0
  187. package/src/tui/components/Message.jsx +22 -1
  188. package/src/tui/components/PromptInput.jsx +21 -3
  189. package/src/tui/components/SlashCommandPalette.jsx +12 -3
  190. package/src/tui/components/TranscriptItem.jsx +9 -1
  191. package/src/tui/components/prompt-input/key-handler.mjs +21 -8
  192. package/src/tui/components/user-message-skills.mjs +38 -0
  193. package/src/tui/dist/index.mjs +507 -171
  194. package/src/tui/session/queue-helpers.mjs +7 -2
  195. package/src/tui/session/session-api/extensions.mjs +4 -2
  196. package/src/tui/session/session-api/settings.mjs +7 -0
  197. package/src/tui/session/session-flow/drain.mjs +14 -4
  198. package/src/tui/session/tui-steering-persist.mjs +25 -9
  199. package/src/tui/session/turn.mjs +17 -0
  200. package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
  201. package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
@@ -50,6 +50,7 @@ export class ModelProxyChannel {
50
50
  attachPublisher(publish: (snapshot: ModelProxySnapshot) => void): () => void;
51
51
  disconnect(reason?: string): void;
52
52
  setClientPresent(present: boolean): void;
53
+ onStateChange(listener: () => void): () => void;
53
54
  request(
54
55
  call: Record<string, unknown>,
55
56
  options?: { signal?: AbortSignal | null; callbacks?: Record<string, (...args: unknown[]) => unknown> }
@@ -65,3 +66,17 @@ export class ModelProxyChannel {
65
66
  }
66
67
 
67
68
  export function getModelProxyChannel(): ModelProxyChannel;
69
+
70
+ /** The channel surface model-proxy providers use; the bridge child implements only this. */
71
+ export interface ModelProxyChannelLike {
72
+ readonly connected: boolean;
73
+ readonly generation: number;
74
+ whenConnected(timeoutMs: number): Promise<boolean>;
75
+ request(
76
+ call: Record<string, unknown>,
77
+ options?: { signal?: AbortSignal | null; callbacks?: Record<string, (...args: unknown[]) => unknown> }
78
+ ): Promise<unknown>;
79
+ }
80
+
81
+ /** Swap the process-wide channel; the returned function restores the previous one. */
82
+ export function installModelProxyChannel(channel: ModelProxyChannel | ModelProxyChannelLike): () => void;
@@ -243,8 +243,24 @@ export class ModelProxyChannel {
243
243
  });
244
244
  }
245
245
 
246
+ #stateListeners = new Set();
247
+
248
+ /** `listener()` runs after every presence/generation change (the model
249
+ * proxy bridge pushes it to its clients). Returns the unsubscribe. */
250
+ onStateChange(listener) {
251
+ this.#stateListeners.add(listener);
252
+ return () => this.#stateListeners.delete(listener);
253
+ }
254
+
246
255
  #connectionChanged() {
247
256
  this.#generation += 1;
257
+ for (const listener of [...this.#stateListeners]) {
258
+ try {
259
+ listener();
260
+ } catch {
261
+ /* a listener never breaks the transport that reported the change */
262
+ }
263
+ }
248
264
  if (!this.connected) return;
249
265
  for (const waiter of [...this.#connectionWaiters]) waiter(true);
250
266
  }
@@ -519,3 +535,22 @@ export function getModelProxyChannel() {
519
535
  globalThis[CHANNEL_KEY] ??= new ModelProxyChannel();
520
536
  return globalThis[CHANNEL_KEY];
521
537
  }
538
+
539
+ /**
540
+ * Make `channel` the process-wide channel (a headless child talks through the
541
+ * daemon's model proxy bridge instead). The returned restore puts the previous
542
+ * channel back once, unless another install replaced this one since.
543
+ */
544
+ export function installModelProxyChannel(channel) {
545
+ const hadPrevious = Object.hasOwn(globalThis, CHANNEL_KEY);
546
+ const previous = globalThis[CHANNEL_KEY];
547
+ globalThis[CHANNEL_KEY] = channel;
548
+ let restored = false;
549
+ return () => {
550
+ if (restored) return;
551
+ restored = true;
552
+ if (globalThis[CHANNEL_KEY] !== channel) return;
553
+ if (hadPrevious) globalThis[CHANNEL_KEY] = previous;
554
+ else delete globalThis[CHANNEL_KEY];
555
+ };
556
+ }
@@ -181,6 +181,17 @@ function mimoOAuthState() {
181
181
  );
182
182
  }
183
183
 
184
+ function commandCodeOAuthState() {
185
+ const paths = [
186
+ boundProviderAuthPath('command-code-oauth') ||
187
+ process.env.COMMAND_CODE_OAUTH_CREDENTIALS_PATH ||
188
+ join(resolvePluginData(), 'command-code-oauth.json'),
189
+ ];
190
+ return memoProbe('command-code-oauth', paths, () =>
191
+ resolveProbeState(paths, (docs) => docs.some((own) => typeof own?.apiKey === 'string' && !!own.apiKey.trim()))
192
+ );
193
+ }
194
+
184
195
  function museCodeOAuthState() {
185
196
  const paths = [
186
197
  boundProviderAuthPath('muse-code-oauth') ||
@@ -255,6 +266,7 @@ const OAUTH_PROBE_STATES = new Map([
255
266
  ['openai-oauth', openAIOAuthState],
256
267
  ['grok-oauth', grokOAuthState],
257
268
  ['muse-code-oauth', museCodeOAuthState],
269
+ ['command-code-oauth', commandCodeOAuthState],
258
270
  ['mimo-oauth', mimoOAuthState],
259
271
  ['cursor-oauth', cursorOAuthState],
260
272
  ['antigravity-oauth', antigravityOAuthState],
@@ -868,7 +868,8 @@ async function measureUsage(provider, providerObj, routeInfo) {
868
868
  (provider === 'cursor-oauth' ||
869
869
  provider === 'cursor-api' ||
870
870
  provider === 'antigravity-oauth' ||
871
- provider === 'muse-code-oauth') &&
871
+ provider === 'muse-code-oauth' ||
872
+ provider === 'command-code-oauth') &&
872
873
  typeof providerObj?.getUsageSnapshot === 'function'
873
874
  ) {
874
875
  return await providerObj.getUsageSnapshot();
@@ -893,12 +894,12 @@ async function measureUsageWithAuthRetry(provider, providerObj, routeInfo) {
893
894
  // refresh cannot vouch for the reading it last produced: keeping a 100% meter
894
895
  // earned by a dead token would hold the account out of the roster until its
895
896
  // reset time, so the recorded quota goes with the credential.
896
- function usageAfterFailure(err, provider, accountId, log) {
897
+ async function usageAfterFailure(err, provider, accountId, log) {
897
898
  const snapshot = provider === 'anthropic-oauth' && accountId === 'default' ? latestClaudeStatuslineUsage() : null;
898
899
  if (snapshot) return snapshot;
899
900
  if (isUsageAuthRejection(err) && ACCOUNT_PROVIDERS.includes(provider)) {
900
901
  try {
901
- clearProviderAccountQuotaState(provider, accountId);
902
+ await clearProviderAccountQuotaState(provider, accountId);
902
903
  } catch (error) {
903
904
  log(`Account usage could not be cleared: ${error.message}`);
904
905
  }
@@ -952,7 +953,7 @@ export async function fetchOAuthUsageSnapshot(routeInfo, providerObj, log = () =
952
953
  try {
953
954
  snapshot = await measureUsageWithAuthRetry(provider, providerObj, routeInfo);
954
955
  } catch (err) {
955
- snapshot = usageAfterFailure(err, provider, accountId, log);
956
+ snapshot = await usageAfterFailure(err, provider, accountId, log);
956
957
  }
957
958
  if (!isContentfulSnapshot(snapshot)) {
958
959
  cacheNegative(key, 'empty');
@@ -961,7 +962,7 @@ export async function fetchOAuthUsageSnapshot(routeInfo, providerObj, log = () =
961
962
  }
962
963
  if (ACCOUNT_PROVIDERS.includes(provider)) {
963
964
  try {
964
- recordProviderAccountUsage(provider, accountId, snapshot);
965
+ await recordProviderAccountUsage(provider, accountId, snapshot);
965
966
  } catch (error) {
966
967
  log(`Account usage could not be saved: ${error.message}`);
967
968
  }
@@ -38,7 +38,7 @@ export async function sendCompatChat(provider, messages, useModel, tools, opts)
38
38
  // global catalog must not shape a user-defined endpoint's request.
39
39
  let modelInfo = null;
40
40
  if (isCustom) modelInfo = provider.getCachedModelInfo(useModel) || null;
41
- else if (provider.name === 'opencode-go')
41
+ else if (provider.name === 'opencode-go' || provider.name === 'command-code-oauth')
42
42
  modelInfo = provider.getCachedModelInfo(useModel) || getModelMetadataSync(useModel, provider.name);
43
43
  else if (profile?.resolveModelInfo) modelInfo = profile.resolveModelInfo(provider, useModel) || null;
44
44
  // Resolved before lowering: replay eligibility depends on the snapped level
@@ -1,3 +1,4 @@
1
+ import { resolveProviderApiKey } from '../../../shared/provider-api-key.mjs';
1
2
  import { enrichModels } from './model-catalog.mjs';
2
3
  import { sanitizeModelList } from './model-list-sanitize.mjs';
3
4
  import { PROVIDER_GENERATE_TOTAL_TIMEOUT_MS, createTimeoutSignal, resolveTimeoutMs } from '../stall-policy.mjs';
@@ -10,12 +11,15 @@ const MODEL_LIST_TIMEOUT_MS = resolveTimeoutMs('MIXDOG_COMPAT_MODEL_LIST_TIMEOUT
10
11
  // `keyless` omits the Authorization header entirely (keyless endpoints);
11
12
  // `signal` aborts the request together with the list timeout.
12
13
  export async function fetchCompatModelItems(provider, { signal = null, keyless = false } = {}) {
13
- const timeout = createTimeoutSignal(signal, MODEL_LIST_TIMEOUT_MS, `${provider.name} model list`);
14
+ const name = provider.name;
15
+ const timeout = createTimeoutSignal(signal, MODEL_LIST_TIMEOUT_MS, `${name} model list`);
14
16
  try {
15
17
  const res = await fetch(`${String(provider.baseURL || '').replace(/\/+$/, '')}/models`, {
16
18
  method: 'GET',
17
19
  headers: {
18
- ...(keyless ? {} : { Authorization: `Bearer ${provider.apiKey || 'no-key'}` }),
20
+ ...(keyless
21
+ ? {}
22
+ : { Authorization: `Bearer ${resolveProviderApiKey(name, provider.config || provider) || 'no-key'}` }),
19
23
  ...(provider.defaultHeaders || {}),
20
24
  },
21
25
  signal: timeout.signal,
@@ -74,6 +74,17 @@ export function applyCompatProviderChatOptions(params, providerName, opts = {},
74
74
  }
75
75
  return params;
76
76
  }
77
+ if (providerName === 'command-code-oauth') {
78
+ // The API's field is undocumented; mirror the common `reasoning_effort`
79
+ // (levels the catalog row declares, else low/medium/high).
80
+ const rawEffort = opts.effort ?? config?.reasoningEffort;
81
+ const declared = modelInfo?.reasoningOptions?.some((option) => option?.type === 'effort');
82
+ const effort = declared
83
+ ? normalizeOpencodeGoReasoningEffort(rawEffort, modelInfo)
84
+ : normalizeReasoningEffort(rawEffort, ['low', 'medium', 'high']);
85
+ if (effort) params.reasoning_effort = effort;
86
+ return params;
87
+ }
77
88
  if (providerName === 'opencode-go') {
78
89
  const reasoningEffort = normalizeOpencodeGoReasoningEffort(opts.effort ?? config?.reasoningEffort, modelInfo);
79
90
  // OpenCode Go's OpenAI-compatible contract exposes reasoning_effort,
@@ -1,3 +1,4 @@
1
+ import { resolveProviderApiKey } from '../../../shared/provider-api-key.mjs';
1
2
  import { markProviderRecoveryExhausted, withRetry } from './retry-classifier.mjs';
2
3
  import { consumeCompatResponsesStream } from './openai-compat-stream.mjs';
3
4
  import { PROVIDER_FIRST_BYTE_TIMEOUT_MS, createPassthroughSignal } from '../stall-policy.mjs';
@@ -158,7 +159,7 @@ export async function sendXaiResponsesWebSocket(provider, messages, useModel, to
158
159
  const reason = signal.reason;
159
160
  throw reason instanceof Error ? reason : new Error('xAI Responses WebSocket request aborted by session close');
160
161
  }
161
- const apiKey = provider.config?.apiKey || process.env.XAI_API_KEY;
162
+ const apiKey = resolveProviderApiKey('xai', provider.config);
162
163
  if (!apiKey) throw new Error('xAI API key not configured');
163
164
  messages = await preparePdfTextForProvider(messages, provider);
164
165
  const chatMessagesForTrace = toOpenAIMessages(messages, provider.name, { nativePdf: provider.nativePdf });
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { noteErrorUsage } from './lib/note-error-usage.mjs';
3
- import { getAgentApiKey } from '../../../shared/provider-api-key.mjs';
3
+ import { reloadProviderApiKey, resolveProviderApiKey } from '../../../shared/provider-api-key.mjs';
4
4
  import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
5
5
  import { appendAgentTrace } from '../agent-trace.mjs';
6
6
  import { OPENAI_COMPAT_PRESETS } from './openai-compat-presets.mjs';
@@ -69,7 +69,7 @@ export class OpenAICompatProvider {
69
69
  constructor(name, config) {
70
70
  const preset = PRESETS[name];
71
71
  const baseURL = assertSafeBaseURL(config.baseURL || preset?.baseURL || 'http://localhost:8080/v1', name);
72
- const apiKey = config.apiKey || 'no-key';
72
+ const apiKey = resolveProviderApiKey(name, config) || 'no-key';
73
73
  this.name = name;
74
74
  this.config = config;
75
75
  this.baseURL = baseURL;
@@ -121,7 +121,7 @@ export class OpenAICompatProvider {
121
121
  reloadApiKey() {
122
122
  try {
123
123
  const preset = PRESETS[this.name];
124
- const newKey = getAgentApiKey(this.name) || this.config.apiKey;
124
+ const newKey = reloadProviderApiKey(this.name, this.config);
125
125
  const baseURL = assertSafeBaseURL(
126
126
  this.config.baseURL || preset?.baseURL || 'http://localhost:8080/v1',
127
127
  this.name
@@ -138,6 +138,12 @@ export class OpenAICompatProvider {
138
138
  }
139
139
  }
140
140
  async send(messages, model, tools, sendOpts) {
141
+ // A keyless-constructed instance picks up the key once it is available.
142
+ const resolvedKey = resolveProviderApiKey(this.name, this.config) || 'no-key';
143
+ if (resolvedKey !== this.apiKey) {
144
+ this.apiKey = resolvedKey;
145
+ this.client = this._createClient(resolvedKey);
146
+ }
141
147
  try {
142
148
  return await this._doSend(messages, model, tools, sendOpts);
143
149
  } catch (err) {
@@ -23,7 +23,7 @@ import { resolveOpenAiTransportPolicy } from './openai-transport-policy.mjs';
23
23
  import { applyOpenAIDirectCachePolicy } from './openai-direct-request.mjs';
24
24
  import { streamCallbacks } from './lib/send-callbacks.mjs';
25
25
  import { noteErrorUsage } from './lib/note-error-usage.mjs';
26
- import { getAgentApiKey } from '../../../shared/provider-api-key.mjs';
26
+ import { reloadProviderApiKey, resolveProviderApiKey } from '../../../shared/provider-api-key.mjs';
27
27
  import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
28
28
  import { noteRequestServiceTier } from '../../../shared/llm/usage-context.mjs';
29
29
  import { resolveProviderCacheKey, resolveProviderPromptCacheLane } from '../agent-runtime/cache-strategy.mjs';
@@ -86,7 +86,7 @@ export class OpenAIDirectProvider {
86
86
  this.config = config || {};
87
87
  }
88
88
  _ensureKey() {
89
- const k = this.config.apiKey;
89
+ const k = resolveProviderApiKey('openai', this.config);
90
90
  if (!k) throw new Error('OPENAI_API_KEY not configured (providers.openai.apiKey)');
91
91
  return k;
92
92
  }
@@ -97,7 +97,7 @@ export class OpenAIDirectProvider {
97
97
  // the WS/HTTP transports take the key per-call via the `auth` object.
98
98
  reloadApiKey() {
99
99
  try {
100
- const newKey = getAgentApiKey('openai') || this.config.apiKey;
100
+ const newKey = reloadProviderApiKey('openai', this.config);
101
101
  if (newKey) {
102
102
  this.config = { ...(this.config || {}), apiKey: newKey };
103
103
  return newKey;
@@ -295,6 +295,6 @@ export class OpenAIDirectProvider {
295
295
  }
296
296
  }
297
297
  async isAvailable() {
298
- return !!this.config.apiKey;
298
+ return !!resolveProviderApiKey('openai', this.config);
299
299
  }
300
300
  }
@@ -7,6 +7,7 @@ import { isInclusiveProvider } from '../../../shared/llm/cost.mjs';
7
7
  import { noEnabledProvidersError } from './registry-errors.mjs';
8
8
  import { providerTakesNativePdf } from '../../../attachments/store.mjs';
9
9
  import { modelProxyClientMode } from '../../../shared/model-proxy-mode.mjs';
10
+ import { getModelProxyChannel } from './model-proxy-channel.mjs';
10
11
  // OpenAI-compat provider names are self-declared by openai-compat-presets.mjs via
11
12
  // OPENAI_COMPAT_PRESETS. No parallel list maintained here.
12
13
  const providers = new Map();
@@ -28,6 +29,7 @@ const OAUTH_PROVIDER_NAMES = new Set([
28
29
  'openai-oauth',
29
30
  'grok-oauth',
30
31
  'muse-code-oauth',
32
+ 'command-code-oauth',
31
33
  'mimo-oauth',
32
34
  'cursor-oauth',
33
35
  'antigravity-oauth',
@@ -96,6 +98,28 @@ function configSignature(cfg) {
96
98
  }
97
99
  }
98
100
 
101
+ // Remote mode: a proxy provider sends through the channel it was built on. A
102
+ // headless boundary installs a fresh channel per run (model-proxy-bridge.mjs)
103
+ // and closes it afterwards, so the channel's identity is part of every reuse
104
+ // signature — an identical PC config behind a new channel is a rebuild, never
105
+ // a provider bound to a closed channel. Local mode: '' (signatures unchanged).
106
+ const _proxyChannelIds = new WeakMap();
107
+ let _proxyChannelSeq = 0;
108
+ function proxyChannelTag(channel) {
109
+ if (!channel) return '';
110
+ let id = _proxyChannelIds.get(channel);
111
+ if (!id) {
112
+ _proxyChannelSeq += 1;
113
+ id = _proxyChannelSeq;
114
+ _proxyChannelIds.set(channel, id);
115
+ }
116
+ return `\u0000model-proxy-channel:${id}`;
117
+ }
118
+ function reuseSignature(cfg, proxyChannel) {
119
+ const sig = configSignature(cfg);
120
+ return sig === null ? null : sig + proxyChannelTag(proxyChannel);
121
+ }
122
+
99
123
  function abortError(signal) {
100
124
  return signal?.reason instanceof Error
101
125
  ? signal.reason
@@ -147,6 +171,7 @@ const PROVIDER_MODULES = {
147
171
  'anthropic-oauth': ['./anthropic-oauth.mjs', 'AnthropicOAuthProvider'],
148
172
  'grok-oauth': ['./grok-oauth.mjs', 'GrokOAuthProvider'],
149
173
  'muse-code-oauth': ['./muse-code-oauth.mjs', 'MuseCodeOAuthProvider'],
174
+ 'command-code-oauth': ['./command-code-oauth.mjs', 'CommandCodeOAuthProvider'],
150
175
  'mimo-oauth': ['./mimo-oauth.mjs', 'MimoOAuthProvider'],
151
176
  'cursor-oauth': ['./cursor.mjs', 'CursorOAuthProvider'],
152
177
  'antigravity-oauth': ['./antigravity-oauth.mjs', 'AntigravityOAuthProvider'],
@@ -176,12 +201,12 @@ async function loadProviderCtor(name, signal = null, cfg = null) {
176
201
  throw new Error(`unknown enabled provider: ${name}`);
177
202
  }
178
203
 
179
- async function instantiateProvider(name, Ctor, cfg) {
204
+ async function instantiateProvider(name, Ctor, cfg, proxyChannel = null) {
180
205
  // Remote mode: the client PC executes the call with its own credentials.
181
206
  // Admission and usage accounting still wrap the daemon-side stand-in.
182
207
  if (modelProxyClientMode()) {
183
208
  const { createModelProxyProvider } = await import('./model-proxy-provider.mjs');
184
- return wrapProviderAdmission(createModelProxyProvider(name, cfg), name);
209
+ return wrapProviderAdmission(createModelProxyProvider(name, cfg, proxyChannel ?? getModelProxyChannel()), name);
185
210
  }
186
211
  if (isCustomProviderConfig(name, cfg)) return wrapProviderAdmission(await Ctor(name, cfg), name);
187
212
  if (Object.hasOwn(OPENAI_COMPAT_PRESETS, name) && name !== 'opencode-go') {
@@ -194,11 +219,12 @@ export async function initProviders(config, { signal = null } = {}) {
194
219
  throwIfAborted(signal);
195
220
  // Remote mode: provider availability and config are the PC's, never this
196
221
  // server's credential probes. No connected client fails here, clearly.
197
- if (modelProxyClientMode()) {
222
+ const proxyChannel = modelProxyClientMode() ? getModelProxyChannel() : null;
223
+ if (proxyChannel) {
198
224
  const { loadModelProxyProviderConfig } = await import('./model-proxy-provider.mjs');
199
- config = await awaitWithAbort(loadModelProxyProviderConfig(), signal);
225
+ config = await awaitWithAbort(loadModelProxyProviderConfig({ channel: proxyChannel }), signal);
200
226
  }
201
- const sig = configSignature(config);
227
+ const sig = reuseSignature(config, proxyChannel);
202
228
  // Coalesce: an identical config is already mid-init — attach to it.
203
229
  if (sig !== null && _inFlightPromise && _inFlightSig === sig) {
204
230
  return awaitWithAbort(_inFlightPromise, signal);
@@ -212,7 +238,7 @@ export async function initProviders(config, { signal = null } = {}) {
212
238
  // signatures can never run their clear()+rebuild concurrently, regardless
213
239
  // of caller-side gating (agent-tool gateOnPrior may release a queued init
214
240
  // before the prior one settled). Errors do not poison the chain.
215
- const run = () => _initProvidersUnsynchronized(config, signal);
241
+ const run = () => _initProvidersUnsynchronized(config, signal, proxyChannel);
216
242
  const next = _initChain.then(run, run);
217
243
  _initChain = next.then(
218
244
  () => {},
@@ -241,7 +267,7 @@ export async function initProviders(config, { signal = null } = {}) {
241
267
  return awaitWithAbort(tracked, signal);
242
268
  }
243
269
 
244
- async function _initProvidersUnsynchronized(config, signal = null) {
270
+ async function _initProvidersUnsynchronized(config, signal = null, proxyChannel = null) {
245
271
  throwIfAborted(signal);
246
272
  // Invariant: never wipe the live registry based on an empty / all-disabled
247
273
  // config. Without this guard, a stale `loadAgentConfig()` (e.g. mid-reload
@@ -262,7 +288,7 @@ async function _initProvidersUnsynchronized(config, signal = null) {
262
288
  // Idempotent reuse: an enabled provider whose config signature is
263
289
  // unchanged from the live registry is carried forward as-is. Only
264
290
  // added or changed providers are (re)constructed below.
265
- const sig = configSignature(cfg);
291
+ const sig = reuseSignature(cfg, proxyChannel);
266
292
  if (sig !== null && providers.has(name) && signatures.get(name) === sig) {
267
293
  return { name, inst: providers.get(name), sig };
268
294
  }
@@ -270,7 +296,7 @@ async function _initProvidersUnsynchronized(config, signal = null) {
270
296
  // A proxied provider never loads the real adapter module.
271
297
  const Ctor = modelProxyClientMode() ? null : await loadProviderCtor(name, signal, cfg);
272
298
  throwIfAborted(signal);
273
- const inst = await instantiateProvider(name, Ctor, cfg);
299
+ const inst = await instantiateProvider(name, Ctor, cfg, proxyChannel);
274
300
  return { name, inst, sig };
275
301
  } catch (err) {
276
302
  const msg = err instanceof Error ? err.message : String(err);
@@ -25,6 +25,9 @@ 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
  'mimo-oauth': ['minimal', 'low', 'medium', 'high'],
28
+ // Command Code relays several vendors over Chat Completions; the common
29
+ // `reasoning_effort` levels are the fallback when no catalog row declares any.
30
+ 'command-code-oauth': ['low', 'medium', 'high'],
28
31
  };
29
32
  const EFFORT_BY_FAMILY = {
30
33
  opus: ['low', 'medium', 'high', 'xhigh', 'max'],
@@ -1,11 +1,8 @@
1
1
  // extension-scopes.mjs — plugin-aware layer over the shared scope model. A
2
2
  // plugin's scope cascades to the skills it owns and to the MCP server(s) it
3
- // installed, so limiting the plugin limits everything it brought along.
4
- import { readFileSync, statSync } from 'node:fs';
5
- import { join } from 'node:path';
6
-
7
- import { pluginMcpServerName, pluginServerMatcher } from './plugin-mcp.mjs';
8
- import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
3
+ // declares, so limiting the plugin limits everything it brought along. MCP
4
+ // ownership comes from the tool's `mcpPluginId`, set when the plugin's server
5
+ // connected, never from the server name.
9
6
  import { loadConfig } from '../config.mjs';
10
7
  import {
11
8
  extensionAllowedForCwd,
@@ -28,66 +25,29 @@ export {
28
25
  withExtensionScope,
29
26
  } from '../../../shared/extension-scopes.mjs';
30
27
 
31
- /** Plugin id that installed MCP server `serverName`, or ''. Plugin servers are
32
- * named `plugin-<name>` or `plugin-<name>--<suffix>` (plugin-mcp.mjs). */
33
- export function pluginIdForMcpServer(serverName, plugins) {
34
- const name = clean(serverName);
35
- if (!name) return '';
36
- for (const plugin of Array.isArray(plugins) ? plugins : []) {
37
- if (pluginServerMatcher(pluginMcpServerName(plugin))(name)) return clean(plugin?.id || plugin?.name);
38
- }
39
- return '';
40
- }
41
-
42
- export function mcpServerAllowedForCwd(scopes, serverName, cwd, { plugins = [] } = {}) {
28
+ /** Server scope ∩ owning plugin's scope (`pluginId` from owner metadata). */
29
+ export function mcpServerAllowedForCwd(scopes, serverName, cwd, { pluginId = '' } = {}) {
43
30
  if (!extensionAllowedForCwd(scopes, 'mcp', serverName, cwd)) return false;
44
- const pluginId = pluginIdForMcpServer(serverName, plugins);
45
- if (pluginId && !extensionAllowedForCwd(scopes, 'plugins', pluginId, cwd)) return false;
31
+ const owner = clean(pluginId);
32
+ if (owner && !extensionAllowedForCwd(scopes, 'plugins', owner, cwd)) return false;
46
33
  return true;
47
34
  }
48
35
 
49
36
  /** Drop tools of MCP servers whose scope excludes cwd; non-MCP tools pass. */
50
- export function filterMcpToolsForCwd(tools, scopes, cwd, options = {}) {
37
+ export function filterMcpToolsForCwd(tools, scopes, cwd) {
51
38
  const list = Array.isArray(tools) ? tools : [];
52
39
  const verdicts = new Map();
53
40
  return list.filter((tool) => {
54
- const server = mcpServerNameOfTool(tool?.name);
41
+ // A name shortened for provider limits carries its server explicitly.
42
+ const server = mcpServerNameOfTool(tool);
55
43
  if (!server) return true;
56
- if (!verdicts.has(server)) verdicts.set(server, mcpServerAllowedForCwd(scopes, server, cwd, options));
57
- return verdicts.get(server);
44
+ const pluginId = clean(tool?.mcpPluginId);
45
+ const key = `${server}\0${pluginId}`;
46
+ if (!verdicts.has(key)) verdicts.set(key, mcpServerAllowedForCwd(scopes, server, cwd, { pluginId }));
47
+ return verdicts.get(key);
58
48
  });
59
49
  }
60
50
 
61
- // `{ id, name }` of every registered plugin, from <data>/plugins/registry.json.
62
- // mtime-cached: this runs on every session create and MCP catalog fold.
63
- let _registryCache = { path: '', mtime: -1, plugins: [] };
64
- export function registeredPluginIdentities(dataDir = null) {
65
- let path = '';
66
- try {
67
- path = join(dataDir || resolvePluginData(), 'plugins', 'registry.json');
68
- } catch {
69
- return [];
70
- }
71
- let mtime = -1;
72
- try {
73
- mtime = statSync(path).mtimeMs;
74
- } catch {
75
- return [];
76
- }
77
- if (_registryCache.path === path && _registryCache.mtime === mtime) return _registryCache.plugins;
78
- let plugins = [];
79
- try {
80
- const parsed = JSON.parse(readFileSync(path, 'utf8'));
81
- plugins = (Array.isArray(parsed?.plugins) ? parsed.plugins : [])
82
- .map((entry) => ({ id: clean(entry?.id || entry?.name), name: clean(entry?.name || entry?.id) }))
83
- .filter((entry) => entry.id);
84
- } catch {
85
- plugins = [];
86
- }
87
- _registryCache = { path, mtime, plugins };
88
- return plugins;
89
- }
90
-
91
51
  /** The MCP tools a session at `cwd` may see. No scopes configured → input
92
52
  * returned as-is, so the global-only fast path costs one config read. */
93
53
  export function filterMcpToolsForSession(tools, cwd, config = null) {
@@ -95,5 +55,5 @@ export function filterMcpToolsForSession(tools, cwd, config = null) {
95
55
  if (!list.length) return list;
96
56
  const scopes = extensionScopesFromConfig(config || loadConfig({ secrets: false }));
97
57
  if (!hasExtensionScopes(scopes)) return list;
98
- return filterMcpToolsForCwd(list, scopes, cwd, { plugins: registeredPluginIdentities() });
58
+ return filterMcpToolsForCwd(list, scopes, cwd);
99
59
  }
@@ -19,7 +19,7 @@ import {
19
19
  stopActiveClock,
20
20
  validateObjective,
21
21
  } from './goal-state.mjs';
22
- import { assertExpectedGoal } from './goal-mutations.mjs';
22
+ import { assertExpectedGoal, goalFinished } from './goal-mutations.mjs';
23
23
  import { clean } from '../../../shared/clean.mjs';
24
24
 
25
25
  /** "12m remaining" for a budgeted Goal, "3m elapsed" for an open-ended one. */
@@ -109,6 +109,17 @@ const GOAL_CONTROL_ACTIONS = {
109
109
  const goal = await ctx.updateGoal(id, { status: 'complete' }, { user: true });
110
110
  return reply(action, goal, `Goal complete · ${goal.objective} · ${durationLabel(goal.timeUsedMs)} elapsed`);
111
111
  },
112
+ async dismiss(ctx, { id, goal, at, action }) {
113
+ // The user's acknowledgement of finished work: the same archive a new
114
+ // prompt applies, so the chrome lets go while the record stays.
115
+ if (!goalFinished(goal)) throw new Error('only a finished Goal can be dismissed; stop or complete it first');
116
+ if (!goal.archivedAt) {
117
+ goal.archivedAt = at;
118
+ goal.updatedAt = at;
119
+ await ctx.commit(id, goal);
120
+ }
121
+ return reply(action, null, `Goal dismissed · ${goal.objective}`);
122
+ },
112
123
  };
113
124
 
114
125
  export async function runGoalControl(ctx, sessionId, rawArgs = {}) {
@@ -146,7 +157,9 @@ export async function runGoalControl(ctx, sessionId, rawArgs = {}) {
146
157
  throw new Error('Goal changed while editing; reopen the editor before saving');
147
158
  }
148
159
  const at = ctx.now();
149
- if (goal.status === 'stopped') throw new Error('a stopped Goal cannot be changed; create a new Goal');
160
+ if (goal.status === 'stopped' && action !== 'dismiss') {
161
+ throw new Error('a stopped Goal cannot be changed; create a new Goal');
162
+ }
150
163
  const handler = GOAL_CONTROL_ACTIONS[action];
151
164
  if (!handler) throw new Error(`unknown Goal action: ${action}`);
152
165
  return handler(ctx, { id, goal, args, at, expectedGoalId, action });