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
@@ -20,12 +20,19 @@ const MODEL_FILES = ['model.onnx', 'tokenizer.json'];
20
20
  // The worker runs one judgment at a time, so requests are sent one by one:
21
21
  // each answer's deadline starts when it is sent, and the wait for earlier
22
22
  // requests is bounded separately so a burst of turns queues instead of
23
- // timing out behind each other.
23
+ // timing out behind each other. The next request is sent only once the worker
24
+ // has answered the previous one: an answer that missed its deadline still
25
+ // occupies the worker, and sending into it would start the next deadline
26
+ // while the worker is busy, failing it too.
27
+ // 500 ms covers the 18-layer judge's tail while parallel agents share the CPU.
24
28
  // MIXDOG_EFFORT_JUDGE_TIMEOUT_MS raises it on a host whose CPUs are shared
25
29
  // with heavy work (parallel benchmark containers).
26
30
  const WARM_TIMEOUT_MS =
27
- Number(process.env.MIXDOG_EFFORT_JUDGE_TIMEOUT_MS) > 0 ? Number(process.env.MIXDOG_EFFORT_JUDGE_TIMEOUT_MS) : 400;
31
+ Number(process.env.MIXDOG_EFFORT_JUDGE_TIMEOUT_MS) > 0 ? Number(process.env.MIXDOG_EFFORT_JUDGE_TIMEOUT_MS) : 500;
28
32
  const QUEUE_WAIT_MS = 1000;
33
+ // A worker that has not answered for this long is stuck; it is replaced so
34
+ // the queue does not stay blocked behind it.
35
+ const STUCK_MS = 10_000;
29
36
  let queueTail = Promise.resolve();
30
37
  // A turn that arrives while the judge is still loading (its first use, or the
31
38
  // first turn after a model update) waits at most this long; loading takes about 2 s on a desktop CPU.
@@ -107,7 +114,7 @@ function settle(id, fn) {
107
114
  const entry = pending.get(id);
108
115
  if (!entry) return;
109
116
  pending.delete(id);
110
- clearTimeout(entry.timer);
117
+ entry.done();
111
118
  fn(entry);
112
119
  }
113
120
 
@@ -151,11 +158,27 @@ function ensureWorker(dir) {
151
158
  return created;
152
159
  }
153
160
 
161
+ // `answer` settles with the probabilities or fails at the deadline; `done`
162
+ // resolves once the worker has answered (or exited), when it can take the next request.
154
163
  function request(target, { request: text, prev, prevRequest, step }) {
155
164
  const id = ++msgId;
156
- return new Promise((resolve, reject) => {
157
- const timer = setTimeout(() => settle(id, (entry) => entry.reject(new Error('timeout'))), WARM_TIMEOUT_MS);
158
- pending.set(id, { resolve, reject, timer });
165
+ let finish;
166
+ const done = new Promise((resolve) => {
167
+ finish = resolve;
168
+ });
169
+ const answer = new Promise((resolve, reject) => {
170
+ const timer = setTimeout(() => reject(new Error('timeout')), WARM_TIMEOUT_MS);
171
+ const stuck = setTimeout(() => void target.terminate().catch(() => {}), STUCK_MS);
172
+ stuck.unref();
173
+ pending.set(id, {
174
+ resolve,
175
+ reject,
176
+ done: () => {
177
+ clearTimeout(timer);
178
+ clearTimeout(stuck);
179
+ finish();
180
+ },
181
+ });
159
182
  // The worker formats the text the way its model was trained.
160
183
  target.postMessage({
161
184
  action: 'judge',
@@ -169,6 +192,9 @@ function request(target, { request: text, prev, prevRequest, step }) {
169
192
  ...(step ? { step } : {}),
170
193
  });
171
194
  });
195
+ // A caller that gave up while queued never reads its answer.
196
+ answer.catch(() => {});
197
+ return { answer, done };
172
198
  }
173
199
 
174
200
  // One install at a time (the settings install and the boot refresh can overlap).
@@ -257,19 +283,31 @@ export async function judgeTurn(input) {
257
283
  if (!ready && !(await waitReady(COLD_WAIT_MS))) {
258
284
  return { skipped: lastLoadError ? `load-error: ${lastLoadError}` : 'warming' };
259
285
  }
260
- const queuedAt = Date.now();
261
- const run = queueTail.then(async () => {
262
- if (worker !== target || !ready) return { skipped: 'effort judge exited' };
263
- if (Date.now() - queuedAt > QUEUE_WAIT_MS) return { skipped: 'busy' };
264
- const startedAt = Date.now();
265
- try {
266
- return { probs: await request(target, input), ms: Date.now() - startedAt };
267
- } catch (error) {
268
- return { skipped: String(error?.message || error) };
269
- }
286
+ let abandoned = false;
287
+ const turn = queueTail.then(() => {
288
+ if (abandoned) return null;
289
+ if (worker !== target || !ready) return { exited: true };
290
+ return { startedAt: Date.now(), ...request(target, input) };
270
291
  });
271
- queueTail = run;
272
- return run;
292
+ queueTail = turn.then((sent) => sent?.done);
293
+ let timer;
294
+ const sent = await Promise.race([
295
+ turn,
296
+ new Promise((resolve) => {
297
+ timer = setTimeout(resolve, QUEUE_WAIT_MS, null);
298
+ }),
299
+ ]);
300
+ clearTimeout(timer);
301
+ if (!sent) {
302
+ abandoned = true;
303
+ return { skipped: 'busy' };
304
+ }
305
+ if (sent.exited) return { skipped: 'effort judge exited' };
306
+ try {
307
+ return { probs: await sent.answer, ms: Date.now() - sent.startedAt };
308
+ } catch (error) {
309
+ return { skipped: String(error?.message || error) };
310
+ }
273
311
  }
274
312
 
275
313
  // MIXDOG_EFFORT_DECISIONS_LOG redirects the log, e.g. for benchmark runs whose
@@ -161,7 +161,7 @@ async function _migrateRecallIndexesIfStale(db) {
161
161
  }
162
162
 
163
163
  // One-time cleanup: attachment-only placeholder rows ('(attachment)' user
164
- // content, e.g. Discord provider discord.mjs:724) predate the
164
+ // content from the former channel provider) predate the
165
165
  // shouldExcludeIngestMessage() ingest-time filter (session-ingest.mjs).
166
166
  // Delete any already-persisted rows so they stop polluting recall/cycle1.
167
167
  // Idempotent (no-op once cleaned); best-effort so a failure never blocks boot.
@@ -201,8 +201,8 @@ function isExcludedUserIngestText(m, text) {
201
201
  if (metaSource === 'task-notification') return true;
202
202
  // `Reference files:` synthetic user rows (manager.mjs isReferenceFilesMessage).
203
203
  if (/^Reference files:\s*/i.test(trimmedStart)) return true;
204
- // Attachment-only placeholder rows (e.g. Discord provider discord.mjs:724
205
- // `"(attachment)"` fallback when a message carries no text, only files).
204
+ // Attachment-only placeholder rows (`"(attachment)"` fallback written by the
205
+ // former channel provider when a message carried no text, only files).
206
206
  if (text.trim() === '(attachment)') return true;
207
207
  // Compaction summary user rows (compact.mjs isSummaryMessage / SUMMARY_PREFIX).
208
208
  if (metaSource === 'compact-summary') return true;
@@ -21,7 +21,7 @@ import {
21
21
  } from 'node:fs/promises';
22
22
  import { dirname, basename, join } from 'node:path';
23
23
  import { randomBytes } from 'node:crypto';
24
- import { DEFAULT_BACKOFFS_MS, withFileLock, withFileLockSync } from './file-lock.mjs';
24
+ import { DEFAULT_BACKOFFS_MS, recordLockTiming, withFileLock, withFileLockSync } from './file-lock.mjs';
25
25
  import {
26
26
  enforceOwnerOnlyAclWin32 as _enforceOwnerOnlyAclWin32,
27
27
  enforceOwnerOnlyAclWin32Async as _enforceOwnerOnlyAclWin32Async,
@@ -321,9 +321,30 @@ export async function writeFileAtomicAsync(filePath, data, opts = {}) {
321
321
  };
322
322
  // Acquire the file lock before occupying an I/O slot. Otherwise a waiter can
323
323
  // exhaust the gate while the current lock owner needs it to finish its write.
324
- return opts.lock === true
325
- ? withFileLock(`${filePath}.lock`, () => _runAdaptiveFileIo(run), opts)
326
- : _runAdaptiveFileIo(run);
324
+ if (opts.lock !== true) return _runAdaptiveFileIo(run);
325
+ const lockPath = `${filePath}.lock`;
326
+ return withFileLock(
327
+ lockPath,
328
+ async () => {
329
+ const queuedAt = Date.now();
330
+ let startedAt = 0;
331
+ try {
332
+ return await _runAdaptiveFileIo(() => {
333
+ startedAt = Date.now();
334
+ return run();
335
+ });
336
+ } finally {
337
+ const now = Date.now();
338
+ // Recorded from this (lock-owner) context so the held warning can
339
+ // split the hold into gate wait vs. the write itself.
340
+ recordLockTiming(lockPath, {
341
+ ioGateWaitMs: (startedAt || now) - queuedAt,
342
+ writeMs: startedAt ? now - startedAt : 0,
343
+ });
344
+ }
345
+ },
346
+ opts
347
+ );
327
348
  }
328
349
 
329
350
  export function writeJsonAtomicAsync(filePath, value, opts = {}) {
@@ -16,6 +16,7 @@
16
16
  import { resolve, sep } from 'node:path';
17
17
  import { clean } from './clean.mjs';
18
18
  import { isPlainObject } from './object.mjs';
19
+ import { parseMcpToolName } from './tool-primitives.mjs';
19
20
 
20
21
  export const EXTENSION_SCOPE_KINDS = Object.freeze(['skills', 'mcp', 'plugins']);
21
22
 
@@ -123,9 +124,10 @@ export function skillAllowedForCwd(scopes, skill, cwd) {
123
124
  return true;
124
125
  }
125
126
 
126
- export function mcpServerNameOfTool(toolName) {
127
- const match = /^mcp__(.+?)__(.+)$/.exec(clean(toolName));
128
- return match ? match[1] : '';
127
+ /** Server of an MCP tool definition or name (metadata first; see parseMcpToolName). */
128
+ export function mcpServerNameOfTool(toolOrName) {
129
+ const value = toolOrName && typeof toolOrName === 'object' ? toolOrName : clean(toolOrName);
130
+ return clean(parseMcpToolName(value)?.server);
129
131
  }
130
132
 
131
133
  /** New config with one entry replaced; `projects` null/empty removes it (global). */
@@ -177,16 +177,38 @@ function warnLockRateLimited(warnedAt, lockPath, line) {
177
177
  }
178
178
  }
179
179
 
180
- function reportLockWait(lockPath, waitedMs, mode) {
180
+ function formatLockTiming(timing) {
181
+ return Object.entries(timing || {})
182
+ .filter(([, ms]) => Number.isFinite(ms))
183
+ .map(([name, ms]) => ` ${name}=${ms}ms`)
184
+ .join('');
185
+ }
186
+
187
+ // Lets a lock owner's callback attribute its time (io gate wait, write) to the
188
+ // held-warning of the lock it runs under.
189
+ export function recordLockTiming(lockPath, fields) {
190
+ const lease = heldLockPaths.getStore()?.get(lockPath);
191
+ if (lease?.active) Object.assign(lease.timing, fields);
192
+ }
193
+
194
+ function reportLockWait(lockPath, waitedMs, mode, timing) {
181
195
  if (waitedMs < LOCK_WAIT_WARN_MS) return;
182
- warnLockRateLimited(lockWaitWarnedAt, lockPath, `[atomic-file] ${mode} lock wait ${waitedMs}ms: ${lockPath}\n`);
196
+ warnLockRateLimited(
197
+ lockWaitWarnedAt,
198
+ lockPath,
199
+ `[atomic-file] ${mode} lock wait ${waitedMs}ms:${formatLockTiming(timing)} ${lockPath}\n`
200
+ );
183
201
  }
184
202
 
185
203
  // A lock held this long starves every waiter (they time out at 2s), so the
186
204
  // holder's duration is reported once released.
187
- function reportLockHeld(lockPath, heldMs, mode) {
205
+ function reportLockHeld(lockPath, heldMs, mode, timing) {
188
206
  if (heldMs < LOCK_HELD_WARN_MS) return;
189
- warnLockRateLimited(lockHeldWarnedAt, lockPath, `[atomic-file] ${mode} lock held ${heldMs}ms: ${lockPath}\n`);
207
+ warnLockRateLimited(
208
+ lockHeldWarnedAt,
209
+ lockPath,
210
+ `[atomic-file] ${mode} lock held ${heldMs}ms:${formatLockTiming(timing)} ${lockPath}\n`
211
+ );
190
212
  }
191
213
 
192
214
  function* tryAcquireReclaimGuard(lockPath) {
@@ -387,7 +409,7 @@ export function sweepStagedLockFiles(lockPath, now = Date.now()) {
387
409
  return removed;
388
410
  }
389
411
 
390
- function* acquireLock(lockPath, opts, mode, markHeld) {
412
+ function* acquireLock(lockPath, opts, mode, markHeld, timing = null) {
391
413
  const timeoutMs = lockTimeoutMs(opts);
392
414
  const staleMs = lockStaleMs(opts);
393
415
  const deadline = Date.now() + timeoutMs;
@@ -415,7 +437,9 @@ function* acquireLock(lockPath, opts, mode, markHeld) {
415
437
  attempt += 1;
416
438
  continue;
417
439
  }
418
- reportLockWait(lockPath, Date.now() - waitStartedAt, mode);
440
+ const osMs = Date.now() - waitStartedAt;
441
+ if (timing) timing.osAcquireMs = osMs;
442
+ reportLockWait(lockPath, osMs, mode, timing);
419
443
  return;
420
444
  }
421
445
  throw timeoutError(lockPath, timeoutMs, lastError, yield* describeLockHolder(lockPath));
@@ -444,13 +468,15 @@ export function withFileLockSync(lockPath, fn, opts = {}) {
444
468
  export async function withFileLock(lockPath, fn, opts = {}) {
445
469
  if (heldLockPaths.getStore()?.get(lockPath)?.active) return fn();
446
470
  if (lockTimeoutMs(opts) <= 0) return withOsFileLock(lockPath, fn, opts);
447
- return lockQueue(lockPath, () => withOsFileLock(lockPath, fn, opts));
471
+ const enqueuedAt = Date.now();
472
+ return lockQueue(lockPath, () => withOsFileLock(lockPath, fn, opts, Date.now() - enqueuedAt));
448
473
  }
449
474
 
450
- async function withOsFileLock(lockPath, fn, opts = {}) {
451
- await runAsync(acquireLock(lockPath, opts, 'async', true));
475
+ async function withOsFileLock(lockPath, fn, opts = {}, queueWaitMs = 0) {
476
+ const timing = { queueWaitMs };
477
+ await runAsync(acquireLock(lockPath, opts, 'async', true, timing));
452
478
  const heldSince = Date.now();
453
- const lease = { active: false };
479
+ const lease = { active: false, timing };
454
480
  try {
455
481
  if (opts.secret === true) await enforceOwnerOnlyAclWin32Async(lockPath, { fresh: true });
456
482
  // Async descendants inherit a reference to this lease, not permanent
@@ -464,7 +490,7 @@ async function withOsFileLock(lockPath, fn, opts = {}) {
464
490
  try {
465
491
  await runAsync(releaseLock(lockPath));
466
492
  } finally {
467
- reportLockHeld(lockPath, Date.now() - heldSince, 'async');
493
+ reportLockHeld(lockPath, Date.now() - heldSince, 'async', timing);
468
494
  // Unmark only once the lock file is gone: a sync waiter arriving while
469
495
  // the release is still pending must fail fast, not block its completion.
470
496
  unmarkOsHeld(lockPath);
@@ -96,6 +96,20 @@ function _catalogLevels(model) {
96
96
  return null;
97
97
  }
98
98
 
99
+ /**
100
+ * Effort levels the wire builder sends unchanged for `model` (reserved `max`
101
+ * excluded), from the same capability gates normalizeAnthropicEffortInput and
102
+ * applyAnthropicEffortToBody use: catalog record first, regex ladder for an
103
+ * absent record. A model without effort support, or whose catalog record
104
+ * declares none, accepts none.
105
+ */
106
+ export function anthropicEffortLevelsFor(model) {
107
+ if (!modelSupportsEffort(model)) return [];
108
+ const levels = ['low', 'medium', 'high'];
109
+ if (modelSupportsXhighEffort(model)) levels.push('xhigh');
110
+ return levels;
111
+ }
112
+
99
113
  export const EFFORT_BETA_HEADER = 'effort-2025-11-24';
100
114
 
101
115
  export const LEGACY_EFFORT_BUDGET = Object.freeze({
@@ -904,7 +904,6 @@ export function pricingCatalogRevisionSync() {
904
904
  }
905
905
 
906
906
  let lastAuditedRevision = null;
907
- let lastAudit = null;
908
907
  function auditCachedModelPricing() {
909
908
  const revision = pricingCatalogRevisionSync();
910
909
  const rows = Object.entries(cachedProviderModelListsSync()).flatMap(([provider, models]) =>
@@ -919,17 +918,6 @@ function auditCachedModelPricing() {
919
918
  );
920
919
  }
921
920
  lastAuditedRevision = revision;
922
- lastAudit = { revision, rows, unpriced };
923
- return lastAudit;
924
- }
925
-
926
- /** Catalog routes without a complete list price, from the latest audit (run on demand when none is cached). */
927
- export function unpricedModelsSync() {
928
- return (lastAudit ?? auditCachedModelPricing()).unpriced.map(({ provider, model, missingRates }) => ({
929
- provider,
930
- model,
931
- missingRates: [...missingRates],
932
- }));
933
921
  }
934
922
 
935
923
  /**
@@ -85,6 +85,12 @@ const PROVIDER_CATALOGS = Object.freeze({
85
85
  // Effort tiers live in the wire id; a row without levels has no effort
86
86
  // surface (resolveAntigravityWireModel drops the effort).
87
87
  'antigravity-oauth': { relay: true, rosterFiles: ['antigravity-oauth-models.json'], rosterLadderFinal: true },
88
+ // api.commandcode.ai/provider/v1 relays other vendors' models.
89
+ 'command-code-oauth': {
90
+ relay: true,
91
+ rosterFiles: ['command-code-oauth-models.json'],
92
+ rosterLimitsAuthoritative: true,
93
+ },
88
94
  'cursor-oauth': { relay: true },
89
95
  'cursor-api': { relay: true },
90
96
  'opencode-go': { relay: true },
@@ -7,10 +7,19 @@
7
7
  import { isAbsolute, join, relative, resolve } from 'node:path';
8
8
  import { readJsonSafe } from './json-file.mjs';
9
9
 
10
- /** `.codex-plugin/plugin.json` wins over a root-level `plugin.json`; a plugin
11
- * without either is an empty manifest, never an error. */
10
+ /** Manifest files in precedence order: `.codex-plugin/plugin.json`, a
11
+ * root-level `plugin.json`, then Claude Code's `.claude-plugin/plugin.json`. */
12
+ export const PLUGIN_MANIFEST_FILES = ['.codex-plugin/plugin.json', 'plugin.json', '.claude-plugin/plugin.json'];
13
+
14
+ /** First readable manifest in `PLUGIN_MANIFEST_FILES` order; a plugin without
15
+ * any is an empty manifest, never an error. A Claude Code manifest's `skills`
16
+ * is a plugin-relative path (e.g. `./.claude/skills/`). */
12
17
  export function pluginManifest(root) {
13
- return readJsonSafe(join(root, '.codex-plugin', 'plugin.json')) || readJsonSafe(join(root, 'plugin.json')) || {};
18
+ for (const rel of PLUGIN_MANIFEST_FILES) {
19
+ const manifest = readJsonSafe(join(root, ...rel.split('/')));
20
+ if (manifest) return manifest;
21
+ }
22
+ return {};
14
23
  }
15
24
 
16
25
  /** Resolve a manifest-relative path, refusing absolute paths and anything
@@ -21,22 +30,22 @@ export function resolveContainedPluginPath(root, rel) {
21
30
  const base = resolve(root);
22
31
  const abs = resolve(base, trimmed);
23
32
  const relToBase = relative(base, abs);
24
- if (relToBase.startsWith('..') || isAbsolute(relToBase)) return null;
33
+ if (relToBase.split(/[\\/]/)[0] === '..' || isAbsolute(relToBase)) return null;
25
34
  return abs;
26
35
  }
27
36
 
28
- /** Skill roots a plugin contributes: the conventional `<root>/skills/` plus an
29
- * optional manifest `skills` path. Deduplicated, absolute, root-contained. */
37
+ /** Skill roots a plugin contributes: the conventional `<root>/skills/` plus the
38
+ * manifest `skills` field, a path or a list of paths. Deduplicated, absolute,
39
+ * root-contained. */
30
40
  export function pluginSkillsRoots(root) {
31
41
  const manifest = pluginManifest(root);
32
42
  const roots = new Set();
33
43
  const add = (rel) => {
44
+ if (typeof rel !== 'string' || !rel.trim()) return;
34
45
  const abs = resolveContainedPluginPath(root, rel);
35
46
  if (abs) roots.add(abs);
36
47
  };
37
48
  add('./skills/');
38
- if (typeof manifest.skills === 'string' && manifest.skills.trim()) {
39
- add(manifest.skills.trim());
40
- }
49
+ for (const rel of Array.isArray(manifest.skills) ? manifest.skills : [manifest.skills]) add(rel);
41
50
  return [...roots];
42
51
  }
@@ -66,11 +66,18 @@
66
66
  },
67
67
  "muse-code-oauth": {
68
68
  "credentialFile": "muse-code-oauth.json",
69
- "credentialPathEnv": "MUSE_CODE_OAUTH_CREDENTIALS_PATH"
69
+ "credentialPathEnv": "MUSE_CODE_OAUTH_CREDENTIALS_PATH",
70
+ "modelCatalogFile": "muse-code-oauth-models.json"
71
+ },
72
+ "command-code-oauth": {
73
+ "credentialFile": "command-code-oauth.json",
74
+ "credentialPathEnv": "COMMAND_CODE_OAUTH_CREDENTIALS_PATH",
75
+ "modelCatalogFile": "command-code-oauth-models.json"
70
76
  },
71
77
  "mimo-oauth": {
72
78
  "credentialFile": "mimo-oauth.json",
73
- "credentialPathEnv": "MIMO_OAUTH_CREDENTIALS_PATH"
79
+ "credentialPathEnv": "MIMO_OAUTH_CREDENTIALS_PATH",
80
+ "modelCatalogFile": "mimo-oauth-models.json"
74
81
  },
75
82
  "cursor-oauth": {
76
83
  "credentialFile": "cursor-oauth.json",
@@ -8,6 +8,12 @@ import { ACCOUNT_PROVIDERS, providerAccountPath, readProviderAccountPool } from
8
8
  import { AGENT_PROVIDER_ENV_ALIASES, getAgentApiKey } from './provider-api-key.mjs';
9
9
  import { platformEntryKey } from './native-asset.mjs';
10
10
  import { clean } from './clean.mjs';
11
+ import { MODEL_PROXY_ENV, modelProxyClientMode } from './model-proxy-mode.mjs';
12
+ import { installModelProxyChannel } from '../agent/orchestrator/providers/model-proxy-channel.mjs';
13
+ import {
14
+ ModelProxyBridgeChannel,
15
+ readModelProxyBridgeDiscovery,
16
+ } from '../agent/orchestrator/providers/model-proxy-bridge.mjs';
11
17
 
12
18
  const contractPath = fileURLToPath(new URL('./pristine-execution-contract.json', import.meta.url));
13
19
 
@@ -205,6 +211,12 @@ export function createPristineExecutionBoundary({
205
211
 
206
212
  const contract = PRISTINE_EXECUTION_CONTRACT;
207
213
  const hostEnv = { ...env };
214
+ // Remote (SSH) mode: this server holds no model credentials, so the child
215
+ // runtime's model calls go through the daemon's model proxy bridge. Its
216
+ // endpoint is read from the HOST runtime root, before it is retargeted
217
+ // below; a missing or stale one fails closed (never a local credential).
218
+ const proxyMode = modelProxyClientMode(hostEnv);
219
+ const bridgeEndpoint = proxyMode ? readModelProxyBridgeDiscovery(hostEnv.MIXDOG_RUNTIME_ROOT) : null;
208
220
  const originalEnv = new Map();
209
221
  const touchedKeys = new Set();
210
222
  const rememberEnv = (name) => {
@@ -227,6 +239,8 @@ export function createPristineExecutionBoundary({
227
239
  mkdirSync(runtimeRoot, { recursive: true, mode: 0o700 });
228
240
  let cleaned = false;
229
241
  let restoreAuthBindings = () => {};
242
+ let bridgeChannel = null;
243
+ let restoreModelProxyChannel = () => {};
230
244
  // rmSync backs off linearly (retryDelay × attempt), so 50 retries at 100ms
231
245
  // is a ≈128s budget per attempt. Callers whose result is already complete
232
246
  // (headless exec) pass a smaller budget and leave stragglers to the sweep.
@@ -238,6 +252,8 @@ export function createPristineExecutionBoundary({
238
252
  else env[name] = value;
239
253
  }
240
254
  restoreAuthBindings();
255
+ restoreModelProxyChannel();
256
+ bridgeChannel?.dispose('the headless run finished');
241
257
  const removal = { recursive: true, force: true, maxRetries: rootRemovalRetries, retryDelay: 100 };
242
258
  // Windows releases file handles (pg.log, sockets) a beat after the owning
243
259
  // process exits; rmSync retries absorb that EBUSY/EPERM window.
@@ -280,7 +296,7 @@ export function createPristineExecutionBoundary({
280
296
  const oauth = contract.oauthProviders[selectedProvider];
281
297
  const apiKeyEnv = contract.apiKeyProviders[selectedProvider];
282
298
  let selectedApiKey = null;
283
- if (apiKeyEnv) {
299
+ if (apiKeyEnv && !proxyMode) {
284
300
  // Resolve before scrubbing so the selected provider may use its standard,
285
301
  // MIXDOG_AGENT_*, alias, or one provider-scoped keychain account. No
286
302
  // general config loader or all-provider credential scan is involved.
@@ -311,7 +327,15 @@ export function createPristineExecutionBoundary({
311
327
 
312
328
  let authMode = 'provider-managed';
313
329
  let catalogCount = 0;
314
- if (oauth) {
330
+ if (proxyMode) {
331
+ // No API key, OAuth account or credential binding: the PC behind the
332
+ // daemon runs every call. The runtime root stays the pristine one, for
333
+ // the services this run owns; only model calls reach the daemon.
334
+ setEnv(MODEL_PROXY_ENV, 'client');
335
+ bridgeChannel = new ModelProxyBridgeChannel(bridgeEndpoint);
336
+ restoreModelProxyChannel = installModelProxyChannel(bridgeChannel);
337
+ authMode = 'model-proxy-bridge';
338
+ } else if (oauth) {
315
339
  // Sign-in writes the selected account of the host's provider-accounts
316
340
  // pool; the single legacy file remains for hosts that predate the pool.
317
341
  // An explicit credential path still overrides both. Read the pool from
@@ -340,8 +364,8 @@ export function createPristineExecutionBoundary({
340
364
  [selectedProvider]: sourceCredential,
341
365
  });
342
366
  authMode = 'in-process-host-oauth-binding';
343
- const sourceCatalog = join(sourceDataDir, oauth.modelCatalogFile);
344
- if (oauth.modelCatalogFile && existsSync(sourceCatalog)) {
367
+ const sourceCatalog = oauth.modelCatalogFile && join(sourceDataDir, oauth.modelCatalogFile);
368
+ if (sourceCatalog && existsSync(sourceCatalog)) {
345
369
  copyFileSync(sourceCatalog, join(dataDir, oauth.modelCatalogFile));
346
370
  catalogCount = 1;
347
371
  }