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
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { parseJsonText, updateJsonAtomicSync } from './atomic-file.mjs';
4
+ import { parseJsonText, updateJsonAtomic } from './atomic-file.mjs';
5
5
  import { resolvePluginData } from './plugin-paths.mjs';
6
6
 
7
7
  export const ACCOUNT_PROVIDERS = Object.freeze([
@@ -9,6 +9,7 @@ export const ACCOUNT_PROVIDERS = Object.freeze([
9
9
  'anthropic-oauth',
10
10
  'grok-oauth',
11
11
  'muse-code-oauth',
12
+ 'command-code-oauth',
12
13
  'mimo-oauth',
13
14
  'cursor-oauth',
14
15
  'antigravity-oauth',
@@ -74,16 +75,39 @@ function normalizeLabels(pool) {
74
75
  return { ...pool, accounts };
75
76
  }
76
77
 
78
+ // How the roster picks an account. `manual` serves from the selected account
79
+ // only; `limit` keeps the selected account until it reaches a limit and then
80
+ // moves to the account with the most headroom; `headroom` starts every new
81
+ // session on the account with the most headroom and keeps it for that session.
82
+ export const ACCOUNT_POLICIES = Object.freeze(['manual', 'limit', 'headroom']);
83
+
84
+ // `auto` predates `policy` and is still written beside it, so a build that
85
+ // knows only `auto` reads the same switching on/off. Whenever the two
86
+ // disagree (an older build toggled `auto`), `auto` decides whether switching
87
+ // is on at all.
88
+ export function providerAccountPolicy(pool) {
89
+ if (pool?.auto === false) return 'manual';
90
+ return pool?.policy === 'headroom' ? 'headroom' : 'limit';
91
+ }
92
+
93
+ function normalizePool(pool) {
94
+ if (!pool) return pool;
95
+ const policy = providerAccountPolicy(pool);
96
+ return { ...normalizeLabels(pool), policy, auto: policy !== 'manual' };
97
+ }
98
+
77
99
  export function readProviderAccountPool(provider, dataDir = resolvePluginData()) {
78
100
  requireAccountProvider(provider);
79
- return normalizeLabels(read(dataDir).providers[provider]) || { accounts: [], selectedId: null, auto: true };
101
+ return (
102
+ normalizePool(read(dataDir).providers[provider]) || { accounts: [], selectedId: null, auto: true, policy: 'limit' }
103
+ );
80
104
  }
81
105
 
82
- function update(provider, mutate) {
106
+ async function update(provider, mutate) {
83
107
  requireAccountProvider(provider);
84
108
  // Read failures must not be interpreted as an empty account roster.
85
109
  read();
86
- const result = updateJsonAtomicSync(
110
+ const result = await updateJsonAtomic(
87
111
  file(),
88
112
  (raw) => {
89
113
  const state = validate(raw);
@@ -94,7 +118,7 @@ function update(provider, mutate) {
94
118
  },
95
119
  { mode: 0o600, secret: true, fsyncDir: true }
96
120
  );
97
- return normalizeLabels(result.providers[provider]);
121
+ return normalizePool(result.providers[provider]);
98
122
  }
99
123
 
100
124
  /**
@@ -117,7 +141,7 @@ export function newProviderAccountId() {
117
141
  return randomUUID();
118
142
  }
119
143
 
120
- export function registerProviderAccount(provider, id, { label = '', includeDefault = false } = {}) {
144
+ export async function registerProviderAccount(provider, id, { label = '', includeDefault = false } = {}) {
121
145
  requireAccountId(id);
122
146
  return update(provider, (pool) => {
123
147
  if (includeDefault && !pool.accounts.some((row) => row.id === 'default')) {
@@ -131,12 +155,12 @@ export function registerProviderAccount(provider, id, { label = '', includeDefau
131
155
  });
132
156
  }
133
157
 
134
- export function changeProviderAccounts(provider, change) {
158
+ export async function changeProviderAccounts(provider, change) {
135
159
  if (
136
160
  !change ||
137
161
  typeof change !== 'object' ||
138
162
  Array.isArray(change) ||
139
- Object.keys(change).some((key) => !['selectedId', 'order', 'auto', 'useCredits', 'rename'].includes(key))
163
+ Object.keys(change).some((key) => !['selectedId', 'order', 'auto', 'policy', 'useCredits', 'rename'].includes(key))
140
164
  ) {
141
165
  throw new TypeError('Invalid provider account change.');
142
166
  }
@@ -175,8 +199,19 @@ export function changeProviderAccounts(provider, change) {
175
199
  }
176
200
  if (change.auto !== undefined) {
177
201
  if (typeof change.auto !== 'boolean') throw new TypeError('Automatic account switching must be boolean.');
202
+ // Turning switching on keeps a non-manual policy; on from manual is the
203
+ // classic switch-at-limit behaviour.
204
+ const current = providerAccountPolicy(pool);
205
+ if (!change.auto) pool.policy = 'manual';
206
+ else if (current === 'manual') pool.policy = 'limit';
207
+ else pool.policy = current;
178
208
  pool.auto = change.auto;
179
209
  }
210
+ if (change.policy !== undefined) {
211
+ if (!ACCOUNT_POLICIES.includes(change.policy)) throw new TypeError('Unknown account switching policy.');
212
+ pool.policy = change.policy;
213
+ pool.auto = change.policy !== 'manual';
214
+ }
180
215
  if (change.useCredits !== undefined) {
181
216
  if (typeof change.useCredits !== 'boolean') throw new TypeError('Credit use must be boolean.');
182
217
  pool.useCredits = change.useCredits;
@@ -184,7 +219,24 @@ export function changeProviderAccounts(provider, change) {
184
219
  });
185
220
  }
186
221
 
187
- export function removeProviderAccount(provider, id) {
222
+ // Selects `id` only while the selection is still `expectedSelectedId`, checked
223
+ // under the write's own lock, so a newer selection (a manual switch) wins. A
224
+ // given `expectedPolicy` must also still hold: a policy change while the
225
+ // request ran (e.g. to Manual) cancels the commit.
226
+ // Returns the pool when the selection moved, otherwise null.
227
+ export async function selectProviderAccountIf(provider, expectedSelectedId, id, expectedPolicy) {
228
+ requireAccountId(id);
229
+ let moved = false;
230
+ const pool = await update(provider, (pool) => {
231
+ if (pool.selectedId !== expectedSelectedId || !pool.accounts.some((row) => row.id === id)) return;
232
+ if (expectedPolicy && providerAccountPolicy(pool) !== expectedPolicy) return;
233
+ pool.selectedId = id;
234
+ moved = true;
235
+ });
236
+ return moved ? pool : null;
237
+ }
238
+
239
+ export async function removeProviderAccount(provider, id) {
188
240
  requireAccountId(id);
189
241
  return update(provider, (pool) => {
190
242
  pool.accounts = pool.accounts.filter((row) => row.id !== id);
@@ -192,7 +244,7 @@ export function removeProviderAccount(provider, id) {
192
244
  });
193
245
  }
194
246
 
195
- export function recordProviderAccountUsage(provider, id, snapshot) {
247
+ export async function recordProviderAccountUsage(provider, id, snapshot) {
196
248
  if (!snapshot || !Array.isArray(snapshot.quotaWindows)) return;
197
249
  const windows = snapshot.quotaWindows.slice(0, 12).map((row) => ({
198
250
  label: String(row.label || '').slice(0, 40),
@@ -200,7 +252,7 @@ export function recordProviderAccountUsage(provider, id, snapshot) {
200
252
  resetAt: typeof row.resetAt === 'number' && Number.isFinite(row.resetAt) ? row.resetAt : null,
201
253
  }));
202
254
  if (!readProviderAccountPool(provider).accounts.some((row) => row.id === id)) return;
203
- update(provider, (pool) => {
255
+ await update(provider, (pool) => {
204
256
  const row = pool.accounts.find((entry) => entry.id === id);
205
257
  if (!row) return;
206
258
  row.usage = { windows, checkedAt: Date.now() };
@@ -225,7 +277,7 @@ const MAX_BLOCK_MS = 6 * 60 * 60_000;
225
277
  // early can tell us anything.
226
278
  // `options.quota` records that the block came from a typed quota refusal or an
227
279
  // exhausted usage window, so later sends still count it as quota evidence.
228
- export function blockProviderAccount(provider, id, until, model = '', source = 'inferred', options = {}) {
280
+ export async function blockProviderAccount(provider, id, until, model = '', source = 'inferred', options = {}) {
229
281
  return update(provider, (pool) => {
230
282
  const row = pool.accounts.find((entry) => entry.id === id);
231
283
  if (!row) return;
@@ -245,7 +297,7 @@ export function blockProviderAccount(provider, id, until, model = '', source = '
245
297
  // the refusal window and the meter recorded against the OLD subscription would
246
298
  // otherwise outlive it and refuse a request the provider would have served.
247
299
  // Both are dropped; the next usage reading refills them.
248
- export function clearProviderAccountQuotaState(provider, id) {
300
+ export async function clearProviderAccountQuotaState(provider, id) {
249
301
  return update(provider, (pool) => {
250
302
  const row = pool.accounts.find((entry) => entry.id === id);
251
303
  if (!row) return;
@@ -318,14 +370,53 @@ export function providerAccountsResetAt(rows, now = Date.now(), model = '', opti
318
370
  return known.length ? Math.min(...known) : null;
319
371
  }
320
372
 
373
+ function windowFull(row, window, now) {
374
+ return window.usedPct >= 100 && (window.resetAt ? window.resetAt > now : now - row.usage.checkedAt < 60_000);
375
+ }
376
+
321
377
  export function providerAccountExhausted(row, now = Date.now(), model = '', options = {}) {
322
378
  if (row?.blockedUntil > now && blockAppliesToModel(row, model)) return true;
323
379
  if (options.useCredits === true) return false;
324
380
  return (row?.usage?.windows || []).some(
325
- (window) =>
326
- window.usedPct >= 100 &&
327
- quotaWindowAppliesToModel(window, model) &&
328
- (window.resetAt ? window.resetAt > now : now - row.usage.checkedAt < 60_000)
381
+ (window) => quotaWindowAppliesToModel(window, model) && windowFull(row, window, now)
382
+ );
383
+ }
384
+
385
+ function quotaWindowFamily(window) {
386
+ return FAMILY_QUOTA_WINDOW.exec(String(window?.label || '').trim())?.[1] || '';
387
+ }
388
+
389
+ // Which limit currently holds the account, independent of any one model:
390
+ // `{ scope: 'account' }` when every request is refused (an unscoped window is
391
+ // full, or a block with no narrower meter to attribute it to), `{ scope:
392
+ // 'model', families }` when only model-family meters ("7D Fable") or a block
393
+ // attributed to one are spent, so other models still run — else null.
394
+ export function providerAccountLimit(row, now = Date.now(), options = {}) {
395
+ if (!row) return null;
396
+ const windows = row.usage?.windows || [];
397
+ const blocked = row.blockedUntil > now;
398
+ const scope = String(row.blockedModel || '').trim();
399
+ const scopedWindows = windows.filter((window) => quotaWindowFamily(window));
400
+ // A block counts as family-scoped only when a family meter covers the model
401
+ // that earned it; any other refusal is treated as the account's.
402
+ const covering = scope ? scopedWindows.filter((window) => quotaWindowAppliesToModel(window, scope)) : [];
403
+ if (blocked && !covering.length) return { scope: 'account' };
404
+ const credits = options.useCredits === true;
405
+ if (!credits && windows.some((window) => !quotaWindowFamily(window) && windowFull(row, window, now))) {
406
+ return { scope: 'account' };
407
+ }
408
+ const families = new Set(blocked ? covering.map(quotaWindowFamily) : []);
409
+ if (!credits) {
410
+ for (const window of scopedWindows) if (windowFull(row, window, now)) families.add(quotaWindowFamily(window));
411
+ }
412
+ return families.size ? { scope: 'model', families: [...families] } : null;
413
+ }
414
+
415
+ // The account refuses `model` only through a model-scoped limit: it still
416
+ // serves other models, so it remains the provider's account of record.
417
+ export function providerAccountExhaustedForModelOnly(row, now = Date.now(), model = '', options = {}) {
418
+ return (
419
+ providerAccountExhausted(row, now, model, options) && providerAccountLimit(row, now, options)?.scope === 'model'
329
420
  );
330
421
  }
331
422
 
@@ -347,15 +438,28 @@ function accountHeadroom(row, now, model) {
347
438
  return used.length ? 100 - Math.max(...used) : null;
348
439
  }
349
440
 
350
- export function chooseProviderAccount(pool, excluded = new Set(), now = Date.now(), model = '') {
441
+ // `sessionAccountId` is the account the requesting session already runs on
442
+ // (headroom policy only): kept while usable, so a session switches only on a
443
+ // limit. Without one, the session's first request takes the account with the
444
+ // most headroom.
445
+ export function chooseProviderAccount(
446
+ pool,
447
+ excluded = new Set(),
448
+ now = Date.now(),
449
+ model = '',
450
+ { sessionAccountId } = {}
451
+ ) {
351
452
  const options = { useCredits: pool.useCredits === true };
352
453
  const usable = (row) => row && !excluded.has(row.id) && !providerAccountExhausted(row, now, model, options);
353
454
  const selected = pool.accounts.find((row) => row.id === pool.selectedId);
354
- if (pool.auto === false) {
455
+ const policy = providerAccountPolicy(pool);
456
+ if (policy === 'manual') {
355
457
  if (excluded.has(selected?.id)) return null;
356
458
  return options.useCredits || usable(selected) ? selected || null : null;
357
459
  }
358
- if (usable(selected)) return selected;
460
+ const preferred =
461
+ policy === 'headroom' ? pool.accounts.find((row) => sessionAccountId && row.id === sessionAccountId) : selected;
462
+ if (usable(preferred)) return preferred;
359
463
  // Falling back in roster order is blind: the account after the one that just
360
464
  // refused may itself be nearly spent, so the request pays a second switch to
361
465
  // learn what a recorded meter already knew. Ranking needs evidence for EVERY
@@ -69,6 +69,18 @@ export function getAgentApiKey(provider) {
69
69
  return null;
70
70
  }
71
71
 
72
+ /** The one API-key resolver for key-based providers: an explicit config key
73
+ * wins, otherwise the environment/keychain (getAgentApiKey). */
74
+ export function resolveProviderApiKey(provider, config) {
75
+ return config?.apiKey || getAgentApiKey(provider) || '';
76
+ }
77
+
78
+ /** Auth-error reload: the stored/environment key is re-read first so a
79
+ * rotated key beats the stale one captured in config. */
80
+ export function reloadProviderApiKey(provider, config) {
81
+ return getAgentApiKey(provider) || config?.apiKey || '';
82
+ }
83
+
72
84
  /** One provider's key read on demand, warming no other account: the
73
85
  * environment first (getAgentApiKey's precedence), then only this provider's
74
86
  * keychain entry through the async path (no whole-keyring snapshot). */
@@ -11,9 +11,15 @@
11
11
  export const POWERSHELL_UTF8_PREFIX =
12
12
  '[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; $OutputEncoding=New-Object System.Text.UTF8Encoding $false;';
13
13
 
14
+ // The `mixdog-skill-node` definition PowerShell commands carry next
15
+ // (renderPowerShellSkillFunction) is the same kind of execution detail.
16
+ const SKILL_FUNCTION_LINE = /^[ \t]*function mixdog-skill-node \{[^\r\n]*\r?\n/;
17
+
14
18
  export function displayShellCommand(value) {
15
19
  const text = String(value ?? '');
16
- const trimmed = text.trimStart();
17
- if (!trimmed.startsWith(POWERSHELL_UTF8_PREFIX)) return text;
18
- return trimmed.slice(POWERSHELL_UTF8_PREFIX.length).replace(/^[ \t]*\r?\n/, '');
20
+ let rest = text.trimStart();
21
+ const prefixed = rest.startsWith(POWERSHELL_UTF8_PREFIX);
22
+ if (prefixed) rest = rest.slice(POWERSHELL_UTF8_PREFIX.length).replace(/^[ \t]*\r?\n/, '');
23
+ const withoutFunction = rest.replace(SKILL_FUNCTION_LINE, '');
24
+ return prefixed || withoutFunction !== rest ? withoutFunction : text;
19
25
  }
@@ -0,0 +1,10 @@
1
+ export interface SkillMention {
2
+ name: string;
3
+ start: number;
4
+ end: number;
5
+ }
6
+ export function isReservedMentionName(name: unknown): boolean;
7
+ export function isInMarkdownCode(text: unknown, offset: number): boolean;
8
+ export function skillMentionCandidates(text: unknown): SkillMention[];
9
+ export function skillMentions(text: unknown, isSkill: (name: string) => boolean): SkillMention[];
10
+ export function mentionedSkillNames(text: unknown, isSkill: (name: string) => boolean): string[];
@@ -0,0 +1,133 @@
1
+ // Inline `$name` skill mentions: a skill can be named anywhere in a prompt.
2
+ // A candidate is `$` at the start of the text or after whitespace or an
3
+ // opening bracket/quote, followed by a skill-name token (the same
4
+ // lowercase-letters, digits and hyphens rule, 64 characters at most, that
5
+ // skill-document.mjs enforces). It selects a skill only when the token is
6
+ // exactly a known skill name, so `$HOME`, `$300` and unknown names stay prose.
7
+ // The token ends where no ASCII identifier character follows (`$pptx_env`,
8
+ // `$pptxUPPER` are not `pptx`); any other character, Korean particles
9
+ // included (`$pptx로`), ends it.
10
+ const MENTION = /(^|[\s([{"'`])\$([a-z0-9][a-z0-9-]{0,63})(?![A-Za-z0-9_-])/g;
11
+
12
+ // Common environment-variable names (compared case-insensitively) and
13
+ // numeric-only tokens are never inline mentions, even when a skill has that
14
+ // name: `$path` or `$300` in prose means the variable or the amount. Choosing
15
+ // such a skill explicitly (the `Skill:` header chip) still works.
16
+ const RESERVED_NAMES = new Set([
17
+ 'path',
18
+ 'home',
19
+ 'user',
20
+ 'shell',
21
+ 'pwd',
22
+ 'tmpdir',
23
+ 'temp',
24
+ 'tmp',
25
+ 'lang',
26
+ 'term',
27
+ 'xdg_config_home',
28
+ 'userprofile',
29
+ 'appdata',
30
+ 'localappdata',
31
+ 'programfiles',
32
+ 'systemroot',
33
+ 'windir',
34
+ 'comspec',
35
+ ]);
36
+
37
+ /** True when `name` can never be an inline mention (env var or number). */
38
+ export function isReservedMentionName(name) {
39
+ const value = String(name ?? '');
40
+ return /^[0-9]+$/.test(value) || RESERVED_NAMES.has(value.toLowerCase());
41
+ }
42
+
43
+ // Markdown code is never a mention: fenced blocks (``` or ~~~, 3+ chars,
44
+ // optional info string, closed by a fence of the same char at least as long,
45
+ // else running to the end) and inline code spans (a backtick run closed by
46
+ // the next run of exactly the same length; an unmatched run is literal).
47
+ // Deliberate Mixdog difference: Codex (codex-rs/skills/src/mentions.rs) scans
48
+ // raw bytes with no code handling. Offsets stay those of the original text,
49
+ // so desktop highlighting lines up (and never lands inside code).
50
+ const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})(.*)$/;
51
+ const FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
52
+
53
+ function inlineCodeRanges(source, from, to, ranges) {
54
+ const runs = [];
55
+ const re = /`+/g;
56
+ re.lastIndex = from;
57
+ for (let match = re.exec(source); match && match.index < to; match = re.exec(source)) {
58
+ runs.push({ start: match.index, end: Math.min(to, match.index + match[0].length) });
59
+ }
60
+ for (let index = 0; index < runs.length; index += 1) {
61
+ const length = runs[index].end - runs[index].start;
62
+ const close = runs.findIndex((run, at) => at > index && run.end - run.start === length);
63
+ if (close < 0) continue;
64
+ ranges.push([runs[index].start, runs[close].end]);
65
+ index = close;
66
+ }
67
+ }
68
+
69
+ /** [start, end) offsets of Markdown code (fenced blocks and inline spans). */
70
+ function codeRanges(source) {
71
+ const ranges = [];
72
+ let open = null;
73
+ let proseStart = 0;
74
+ for (let at = 0; at <= source.length; ) {
75
+ const newline = source.indexOf('\n', at);
76
+ const end = newline < 0 ? source.length : newline;
77
+ const next = newline < 0 ? source.length + 1 : newline + 1;
78
+ const line = source.slice(at, end).replace(/\r$/, '');
79
+ if (!open) {
80
+ const fence = FENCE_OPEN.exec(line);
81
+ if (fence && !(fence[1][0] === '`' && fence[2].includes('`'))) {
82
+ inlineCodeRanges(source, proseStart, at, ranges);
83
+ open = { char: fence[1][0], length: fence[1].length, start: at };
84
+ }
85
+ } else {
86
+ const fence = FENCE_CLOSE.exec(line);
87
+ if (fence && fence[1][0] === open.char && fence[1].length >= open.length) {
88
+ ranges.push([open.start, Math.min(next, source.length)]);
89
+ proseStart = Math.min(next, source.length);
90
+ open = null;
91
+ }
92
+ }
93
+ at = next;
94
+ }
95
+ if (open) ranges.push([open.start, source.length]);
96
+ else inlineCodeRanges(source, proseStart, source.length, ranges);
97
+ return ranges;
98
+ }
99
+
100
+ /** True when `offset` in `text` lies inside Markdown code (a fenced block or
101
+ * an inline code span) by the same rules mention recognition uses, so a
102
+ * composer can tell whether a `$` being typed can become a mention. */
103
+ export function isInMarkdownCode(text, offset) {
104
+ const source = typeof text === 'string' ? text : '';
105
+ if (!Number.isInteger(offset) || (!source.includes('`') && !source.includes('~~~'))) return false;
106
+ return codeRanges(source).some(([from, to]) => offset >= from && offset < to);
107
+ }
108
+
109
+ /** Every syntactically valid `$name` in `text` outside Markdown code, in
110
+ * order: `start` is the `$`. */
111
+ export function skillMentionCandidates(text) {
112
+ const source = typeof text === 'string' ? text : '';
113
+ const found = [];
114
+ let code = null;
115
+ for (const match of source.matchAll(MENTION)) {
116
+ if (isReservedMentionName(match[2])) continue;
117
+ const start = match.index + match[1].length;
118
+ code ??= source.includes('`') || source.includes('~~~') ? codeRanges(source) : [];
119
+ if (code.some(([from, to]) => start >= from && start < to)) continue;
120
+ found.push({ name: match[2], start, end: start + 1 + match[2].length });
121
+ }
122
+ return found;
123
+ }
124
+
125
+ /** Candidates whose name `isSkill(name)` accepts (all occurrences, in order). */
126
+ export function skillMentions(text, isSkill) {
127
+ return skillMentionCandidates(text).filter((mention) => isSkill(mention.name));
128
+ }
129
+
130
+ /** Distinct mentioned skill names in first-mention order. */
131
+ export function mentionedSkillNames(text, isSkill) {
132
+ return [...new Set(skillMentions(text, isSkill).map((mention) => mention.name))];
133
+ }