mixdog 1.2.6 → 1.3.0

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 (246) hide show
  1. package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
  2. package/NOTICE.md +19 -0
  3. package/package.json +4 -1
  4. package/scripts/check-model-recommendations.mjs +89 -0
  5. package/scripts/gate-local.mjs +7 -0
  6. package/scripts/lib/isolated-test-env.mjs +16 -0
  7. package/src/agents/reviewer/AGENT.md +4 -2
  8. package/src/agents/reviewer/agent.json +1 -1
  9. package/src/defaults/shipped-history.json +7 -0
  10. package/src/defaults/skills/browser-use/SKILL.md +5 -0
  11. package/src/defaults/skills/memory-management/SKILL.md +4 -1
  12. package/src/defaults/skills/setup/references/actions.md +11 -4
  13. package/src/defaults/skills/setup-guide/SKILL.md +175 -0
  14. package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
  15. package/src/lib/rules-builder.cjs +55 -6
  16. package/src/rules/agent/AGENT.md +4 -0
  17. package/src/rules/agent-mode/AGENT_MODE.md +24 -0
  18. package/src/rules/agent-mode/MIXY.md +21 -0
  19. package/src/rules/agent-mode/ROUTINE.md +6 -0
  20. package/src/rules/lead/LEAD.md +7 -3
  21. package/src/rules/shared/70-delivery.md +3 -0
  22. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
  23. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
  24. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +182 -13
  25. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
  26. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +215 -0
  27. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
  28. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
  29. package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
  30. package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
  31. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
  32. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
  33. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
  34. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
  35. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
  36. package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
  39. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
  40. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
  41. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  42. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
  43. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
  44. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
  45. package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
  46. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
  47. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +12 -0
  48. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
  49. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
  50. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
  51. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -4
  52. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +4 -1
  53. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +50 -20
  54. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
  55. package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
  56. package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
  57. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
  58. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
  59. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
  60. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +162 -0
  61. package/src/runtime/agent/orchestrator/session/agent-room.mjs +641 -0
  62. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
  63. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
  64. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
  65. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +165 -0
  66. package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
  67. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
  68. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
  69. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
  70. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
  71. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
  72. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +16 -1
  73. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
  74. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
  75. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
  76. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
  77. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +7 -0
  78. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
  79. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
  80. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  81. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
  82. package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
  83. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
  84. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
  85. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
  86. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +27 -5
  87. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +55 -8
  88. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
  89. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
  90. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
  91. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
  92. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +24 -0
  93. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
  94. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +70 -0
  95. package/src/runtime/agent/orchestrator/session/routine-result.mjs +277 -0
  96. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
  97. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
  98. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
  99. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
  100. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
  101. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
  102. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +44 -1
  103. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
  105. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
  106. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
  107. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
  108. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
  109. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
  110. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
  111. package/src/runtime/browser-bridge/client.mjs +24 -0
  112. package/src/runtime/channels/lib/scheduler.mjs +40 -12
  113. package/src/runtime/channels/lib/schedules-db.mjs +71 -12
  114. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
  115. package/src/runtime/channels/lib/webhook.mjs +0 -1
  116. package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
  117. package/src/runtime/channels/lib/worker-main.mjs +17 -20
  118. package/src/runtime/computer-bridge/client.mjs +28 -1
  119. package/src/runtime/connectors/gmail-app-password.mjs +690 -0
  120. package/src/runtime/connectors/google-mcp.mjs +446 -0
  121. package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
  122. package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
  123. package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
  124. package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
  125. package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
  126. package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
  127. package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
  128. package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
  129. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
  130. package/src/runtime/memory/lib/memory.mjs +7 -1
  131. package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
  132. package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
  133. package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
  134. package/src/runtime/memory/tool-defs.mjs +5 -0
  135. package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
  136. package/src/runtime/shared/agent-avatar.d.mts +64 -0
  137. package/src/runtime/shared/agent-avatar.mjs +320 -0
  138. package/src/runtime/shared/agent-route-config.d.mts +3 -0
  139. package/src/runtime/shared/agent-route-config.mjs +16 -0
  140. package/src/runtime/shared/atomic-file.mjs +14 -1
  141. package/src/runtime/shared/config.mjs +8 -1
  142. package/src/runtime/shared/connector-accounts.mjs +15 -0
  143. package/src/runtime/shared/connector-catalog.d.mts +52 -0
  144. package/src/runtime/shared/connector-catalog.mjs +230 -0
  145. package/src/runtime/shared/connector-triggers.d.mts +3 -0
  146. package/src/runtime/shared/connector-triggers.mjs +43 -0
  147. package/src/runtime/shared/google-oauth-client.mjs +24 -0
  148. package/src/runtime/shared/held-request-host.d.mts +12 -0
  149. package/src/runtime/shared/held-request-host.mjs +71 -0
  150. package/src/runtime/shared/plain-preview.d.mts +1 -0
  151. package/src/runtime/shared/plain-preview.mjs +28 -0
  152. package/src/runtime/shared/routine-limits.d.mts +11 -0
  153. package/src/runtime/shared/routine-limits.mjs +21 -0
  154. package/src/runtime/shared/routine-trigger.d.mts +10 -0
  155. package/src/runtime/shared/routine-trigger.mjs +44 -0
  156. package/src/runtime/shared/schedule-model-ref.mjs +9 -10
  157. package/src/runtime/shared/slow-span.mjs +4 -12
  158. package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
  159. package/src/runtime/shared/transcript-metadata.mjs +10 -0
  160. package/src/runtime/shared/turn-changes.mjs +0 -8
  161. package/src/runtime/shared/usage-readout-totals.mjs +56 -0
  162. package/src/runtime/shared/user-data-guard.mjs +114 -36
  163. package/src/session-runtime/agent-home-delivery.mjs +459 -0
  164. package/src/session-runtime/agent-profile-changes.mjs +21 -0
  165. package/src/session-runtime/agent-room-delivery.mjs +92 -0
  166. package/src/session-runtime/agent-task-sessions.mjs +200 -0
  167. package/src/session-runtime/boot/apis.mjs +50 -18
  168. package/src/session-runtime/boot/facade.mjs +5 -8
  169. package/src/session-runtime/boot/infrastructure.mjs +28 -17
  170. package/src/session-runtime/boot/tools.mjs +3 -0
  171. package/src/session-runtime/channel-config-api.mjs +53 -5
  172. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
  173. package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
  174. package/src/session-runtime/held-requests.mjs +228 -0
  175. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
  176. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
  177. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
  178. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +31 -2
  179. package/src/session-runtime/internal-tool-executor.mjs +2 -0
  180. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
  181. package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
  182. package/src/session-runtime/lifecycle/shared.mjs +6 -0
  183. package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
  184. package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
  185. package/src/session-runtime/mcp-oauth-login.mjs +112 -15
  186. package/src/session-runtime/model-route/set-route.mjs +14 -3
  187. package/src/session-runtime/owned-automation-run.mjs +229 -0
  188. package/src/session-runtime/resource-api.mjs +5 -0
  189. package/src/session-runtime/resource-mcp-api.mjs +107 -13
  190. package/src/session-runtime/resource-skills-api.mjs +12 -0
  191. package/src/session-runtime/room-member-gate.mjs +48 -0
  192. package/src/session-runtime/routine-home-turn.mjs +156 -0
  193. package/src/session-runtime/routine-owner.mjs +48 -0
  194. package/src/session-runtime/schedule-session-run.mjs +30 -64
  195. package/src/session-runtime/services/agent-tool/helpers.mjs +10 -0
  196. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
  197. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
  198. package/src/session-runtime/services/channel-admin.mjs +93 -24
  199. package/src/session-runtime/services/memory-access.mjs +37 -0
  200. package/src/session-runtime/services/provider-admin.mjs +74 -39
  201. package/src/session-runtime/services/schedule-access.mjs +91 -0
  202. package/src/session-runtime/session-create/session-options.mjs +34 -7
  203. package/src/session-runtime/session-hooks.mjs +16 -8
  204. package/src/session-runtime/setup-tool/executor.mjs +68 -6
  205. package/src/session-runtime/setup-tool/extended-actions.mjs +72 -14
  206. package/src/session-runtime/setup-tool/settings-contract.mjs +8 -6
  207. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  208. package/src/session-runtime/skills-api.mjs +16 -1
  209. package/src/session-runtime/tool-policy-refresh.mjs +238 -29
  210. package/src/session-runtime/turn/session-ops.mjs +8 -1
  211. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
  212. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +3 -0
  213. package/src/session-runtime/webhook-session-run.mjs +49 -67
  214. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
  215. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +556 -22
  216. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
  217. package/src/session-runtime/workflow-agents-api.mjs +4 -1
  218. package/src/standalone/daemon-agent-control.mjs +1 -8
  219. package/src/standalone/daemon-session-control.mjs +11 -2
  220. package/src/standalone/daemon.mjs +18 -4
  221. package/src/standalone/session-protocol.mjs +27 -0
  222. package/src/standalone/session-service/session-calls/session-turns.mjs +9 -0
  223. package/src/standalone/session-service.mjs +19 -0
  224. package/src/tui/app/slash-commands.mjs +20 -0
  225. package/src/tui/dist/index.mjs +164 -86
  226. package/src/tui/session/context-state.mjs +6 -1
  227. package/src/tui/session/notice-surface.mjs +35 -4
  228. package/src/tui/session/queue-helpers.mjs +2 -1
  229. package/src/tui/session/session-api/extensions.mjs +4 -0
  230. package/src/tui/session/session-api/intake/submission.mjs +5 -0
  231. package/src/tui/session/session-api/integrations.mjs +8 -0
  232. package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
  233. package/src/tui/session/session-api/lifecycle.mjs +17 -1
  234. package/src/tui/session/session-api/routes.mjs +48 -3
  235. package/src/tui/session/session-flow/drain.mjs +12 -3
  236. package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
  237. package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
  238. package/src/tui/session/session-flow/steering.mjs +58 -24
  239. package/src/tui/session/tool-approval.mjs +26 -5
  240. package/src/tui/session/transcript-intake.mjs +6 -2
  241. package/src/tui/session/transcript-spill.mjs +5 -1
  242. package/src/tui/session/tui-steering-persist.mjs +65 -42
  243. package/src/tui/session/turn-live-call.mjs +56 -0
  244. package/src/tui/session/turn.mjs +22 -6
  245. package/src/session-runtime/automation-agents.mjs +0 -94
  246. package/src/session-runtime/automation-workflow.mjs +0 -59
@@ -27,7 +27,6 @@ import {
27
27
  export const TURN_CHANGE_FILE_PATCH_MAX_BYTES = 8 * 1024 * 1024;
28
28
  const APPLY_TIMEOUT_MS = 60_000;
29
29
  const APPLY_OUTPUT_MAX_BYTES = 4 * 1024 * 1024;
30
- const STATUSES = new Set(['A', 'M', 'D', 'R']);
31
30
  // Characters a patch header cannot carry unquoted; such a path is recorded but
32
31
  // never replayed.
33
32
  const UNSAFE_PATH_CHARACTERS = /[\t\n\r"\0]/;
@@ -37,13 +36,6 @@ export function changePathKey(value) {
37
36
  return process.platform === 'win32' ? text.toLowerCase() : text;
38
37
  }
39
38
 
40
- /** git status letter → record status (copies create a new file). */
41
- export function normalizeChangeStatus(code) {
42
- const letter = String(code || 'M').charAt(0);
43
- if (STATUSES.has(letter)) return letter;
44
- return letter === 'C' ? 'A' : 'M';
45
- }
46
-
47
39
  /** A root-relative forward-slash path that a patch may name, or ''. */
48
40
  function safeChangePath(value) {
49
41
  const text = clean(value).replace(/\\/g, '/');
@@ -0,0 +1,56 @@
1
+ // Session-cumulative figures behind the composer's usage readout, kept on the
2
+ // session record (`session.usageReadout`) so they survive restart, reload and
3
+ // compaction. One fold per model call of the session itself; calls of other
4
+ // sessions (delegated sub-agents have their own records) never enter.
5
+ //
6
+ // - cache hit = cacheRead / (input + cacheRead + cacheWrite) over every call
7
+ // whose prompt was measured. A call that could not measure its prompt
8
+ // (promptKnown === false) has unknown zeros, so it adds nothing.
9
+ // - speed = output tokens over generation ms (first visible token → end of the
10
+ // response) of the calls long enough to time. A reply too short says nothing
11
+ // about the model's speed, so each call is held to the floors below BEFORE it
12
+ // is summed.
13
+ // - total tokens = input + cacheRead + cacheWrite + output, from the same sums.
14
+ export const MIN_SPEED_OUTPUT_TOKENS = 20;
15
+ export const MIN_SPEED_MS = 500;
16
+
17
+ function num(value) {
18
+ const n = Number(value);
19
+ return Number.isFinite(n) && n > 0 ? n : 0;
20
+ }
21
+
22
+ /** The zeroed totals record. */
23
+ export function emptyUsageReadout() {
24
+ return { input: 0, cacheRead: 0, cacheWrite: 0, output: 0, speedOutputTokens: 0, speedMs: 0 };
25
+ }
26
+
27
+ /**
28
+ * Fold one `onUsageDelta` event (and the call's generation ms, 0 when it was
29
+ * not timed) into the totals; returns a new record, never mutates `totals`.
30
+ * Providers differ on whether input counts cache; the prompt total does not,
31
+ * so fresh input is the prompt less its cache read/write.
32
+ */
33
+ export function foldUsageReadout(totals, delta = {}, generationMs = 0) {
34
+ const base = totals || emptyUsageReadout();
35
+ const next = { ...base };
36
+ if (delta.promptKnown !== false) {
37
+ const cacheRead = num(delta.deltaCachedRead);
38
+ const cacheWrite = num(delta.deltaCacheWrite);
39
+ const prompt = num(delta.deltaPrompt) || num(delta.deltaInput) + cacheRead + cacheWrite;
40
+ next.input = num(base.input) + Math.max(0, prompt - cacheRead - cacheWrite);
41
+ next.cacheRead = num(base.cacheRead) + cacheRead;
42
+ next.cacheWrite = num(base.cacheWrite) + cacheWrite;
43
+ }
44
+ const output = num(delta.deltaOutput);
45
+ // Every call's output enters the session's total tokens, timed or not. A
46
+ // record saved before the field existed has no output sum to extend, and a
47
+ // partial one would pass for the whole: it stays without until the session
48
+ // starts over.
49
+ if (Number.isFinite(base.output)) next.output = base.output + output;
50
+ const ms = num(generationMs);
51
+ if (ms >= MIN_SPEED_MS && output >= MIN_SPEED_OUTPUT_TOKENS) {
52
+ next.speedOutputTokens = num(base.speedOutputTokens) + output;
53
+ next.speedMs = num(base.speedMs) + ms;
54
+ }
55
+ return next;
56
+ }
@@ -61,25 +61,92 @@ function scopeDirFor(dataDir) {
61
61
  let lastBackupMs = 0;
62
62
  let backupSeq = 0;
63
63
 
64
- // Claims `<iso-ms>-<10-digit seq>-<reason>` with an exclusive mkdir, so names
65
- // sort by timestamp, then sequence, and an existing backup is never reused
66
- // (EEXIST retries with the next sequence). The clamp keeps a backwards clock
67
- // from reordering this process's backups.
64
+ // `<iso-ms>-<10-digit seq>-<reason>`; an older build's name has no sequence.
65
+ const BACKUP_NAME_RE = /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z-(?:(\d{10})-)?/;
66
+
67
+ // The newest backup order among `names` (time, then sequence). An older
68
+ // build's name sorts after every sequenced name of its millisecond, so its
69
+ // sequence counts as unbounded. Names that are no backup's are ignored.
70
+ function newestBackupOrder(names) {
71
+ let ms = 0;
72
+ let seq = -1;
73
+ for (const name of names) {
74
+ const match = BACKUP_NAME_RE.exec(name);
75
+ if (!match) continue;
76
+ const time = Date.parse(`${match[1]}T${match[2]}:${match[3]}:${match[4]}.${match[5]}Z`);
77
+ if (!Number.isFinite(time)) continue;
78
+ const order = match[6] === undefined ? Number.POSITIVE_INFINITY : Number(match[6]);
79
+ if (time > ms || (time === ms && order > seq)) {
80
+ ms = time;
81
+ seq = order;
82
+ }
83
+ }
84
+ return { ms, seq };
85
+ }
86
+
87
+ // A name that sorts after every backup in `names`, whichever process wrote
88
+ // them and whatever its clock said: the backup a restore picks as the newest
89
+ // is then the one written last, and pruning never takes it.
90
+ function nextBackupName(names, reason) {
91
+ const newest = newestBackupOrder(names);
92
+ lastBackupMs = Math.max(Date.now(), lastBackupMs, newest.ms);
93
+ if (lastBackupMs === newest.ms) {
94
+ if (newest.seq === Number.POSITIVE_INFINITY) lastBackupMs += 1;
95
+ else backupSeq = Math.max(backupSeq, newest.seq + 1);
96
+ }
97
+ const time = new Date(lastBackupMs).toISOString().replace(/[:.]/g, '-');
98
+ return `${time}-${String(backupSeq++).padStart(10, '0')}-${safeReason(reason)}`;
99
+ }
100
+
101
+ // The scope's backup directory names. A reservation lists the scope first and
102
+ // a listing failure throws: an order guessed without it could sort before the
103
+ // newest backup.
104
+ function scopeBackupNames(entries) {
105
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
106
+ }
107
+
108
+ // Claims the next name with an exclusive mkdir (EEXIST retries after it).
109
+ // Returns the claimed dir and the scope's names including it, which pruning
110
+ // reuses instead of listing the scope again.
68
111
  function reserveBackupDir(scopeDir, reason) {
69
112
  mkdirSync(scopeDir, { recursive: true });
113
+ const names = scopeBackupNames(readdirSync(scopeDir, { withFileTypes: true }));
114
+ for (;;) {
115
+ const name = nextBackupName(names, reason);
116
+ names.push(name);
117
+ try {
118
+ mkdirSync(join(scopeDir, name));
119
+ return { backupDir: join(scopeDir, name), names };
120
+ } catch (error) {
121
+ if (error?.code !== 'EEXIST') throw error;
122
+ }
123
+ }
124
+ }
125
+
126
+ async function reserveBackupDirAsync(scopeDir, reason) {
127
+ await mkdirP(scopeDir, { recursive: true });
128
+ const names = scopeBackupNames(await readdirP(scopeDir, { withFileTypes: true }));
70
129
  for (;;) {
71
- lastBackupMs = Math.max(Date.now(), lastBackupMs);
72
- const time = new Date(lastBackupMs).toISOString().replace(/[:.]/g, '-');
73
- const backupDir = join(scopeDir, `${time}-${String(backupSeq++).padStart(10, '0')}-${safeReason(reason)}`);
130
+ const name = nextBackupName(names, reason);
131
+ names.push(name);
74
132
  try {
75
- mkdirSync(backupDir);
76
- return backupDir;
133
+ await mkdirP(join(scopeDir, name));
134
+ return { backupDir: join(scopeDir, name), names };
77
135
  } catch (error) {
78
136
  if (error?.code !== 'EEXIST') throw error;
79
137
  }
80
138
  }
81
139
  }
82
140
 
141
+ // The names past the newest `keep`, never the just-written backup.
142
+ function backupsToPrune(names, own, keep = 40) {
143
+ return [...names]
144
+ .sort()
145
+ .reverse()
146
+ .slice(keep)
147
+ .filter((name) => name !== own);
148
+ }
149
+
83
150
  function safeReason(reason) {
84
151
  return (
85
152
  String(reason || 'snapshot')
@@ -197,14 +264,30 @@ function isStructurallyCompleteMixdogConfigBackup(parsed) {
197
264
  return true;
198
265
  }
199
266
 
200
- /** Backup directory names in `dir`, newest first; empty when unreadable. */
201
- function backupDirsNewestFirst(dir) {
267
+ // A backup that is absent, not a backup, or not JSON is no backup; any other
268
+ // failure (EIO, EACCES, EMFILE…) is transient and must not pass for one.
269
+ function isAbsentBackupError(error) {
270
+ return error instanceof SyntaxError || error?.code === 'ENOENT' || error?.code === 'ENOTDIR';
271
+ }
272
+
273
+ /** Backup directory names in `dir`, newest first; empty when absent, throws on other failures. */
274
+ function listBackupDirsNewestFirst(dir) {
202
275
  try {
203
276
  return readdirSync(dir, { withFileTypes: true })
204
277
  .filter((entry) => entry.isDirectory())
205
278
  .map((entry) => entry.name)
206
279
  .sort()
207
280
  .reverse();
281
+ } catch (error) {
282
+ if (isAbsentBackupError(error)) return [];
283
+ throw error;
284
+ }
285
+ }
286
+
287
+ /** Backup directory names in `dir`, newest first; empty when unreadable. */
288
+ function backupDirsNewestFirst(dir) {
289
+ try {
290
+ return listBackupDirsNewestFirst(dir);
208
291
  } catch {
209
292
  return [];
210
293
  }
@@ -213,7 +296,8 @@ function backupDirsNewestFirst(dir) {
213
296
  /**
214
297
  * A backup is restorable only when its manifest names this data dir and this
215
298
  * incarnation of it. Legacy unscoped backups carry no manifest, so they stay
216
- * on disk but never match.
299
+ * on disk but never match. A transient read failure throws: skipping the
300
+ * backup would restore an older one instead.
217
301
  */
218
302
  function readRestorableConfig(backupDir, dataDirKey, instanceId) {
219
303
  try {
@@ -222,20 +306,21 @@ function readRestorableConfig(backupDir, dataDirKey, instanceId) {
222
306
  if (manifest.instanceId !== instanceId) return null;
223
307
  const parsed = JSON.parse(readFileSync(join(backupDir, 'mixdog-config.json'), 'utf8'));
224
308
  return isStructurallyCompleteMixdogConfigBackup(parsed) ? parsed : null;
225
- } catch {
226
- return null;
309
+ } catch (error) {
310
+ if (isAbsentBackupError(error)) return null;
311
+ throw error;
227
312
  }
228
313
  }
229
314
 
230
315
  function findRestorableConfig(dataDir, instanceId) {
231
316
  const key = normalizeDataDir(dataDir);
232
317
  const scopeDir = scopeDirFor(dataDir);
233
- for (const name of backupDirsNewestFirst(scopeDir)) {
318
+ for (const name of listBackupDirsNewestFirst(scopeDir)) {
234
319
  const parsed = readRestorableConfig(join(scopeDir, name), key, instanceId);
235
320
  if (parsed) return parsed;
236
321
  }
237
322
  const root = getBackupRoot();
238
- for (const name of backupDirsNewestFirst(root)) {
323
+ for (const name of listBackupDirsNewestFirst(root)) {
239
324
  if (name.startsWith(SCOPE_PREFIX)) continue;
240
325
  const parsed = readRestorableConfig(join(root, name), key, instanceId);
241
326
  if (parsed) return parsed;
@@ -245,13 +330,15 @@ function findRestorableConfig(dataDir, instanceId) {
245
330
 
246
331
  // Per-process restore lookup, keyed by data dir. A hit is reused while the
247
332
  // backup root and the marker's instanceId are unchanged; this process's own
248
- // backups invalidate it.
333
+ // backups invalidate it, and a `fresh` lookup (an RMW write, which another
334
+ // process's newer backup may precede) always rescans.
249
335
  const restoreLookupCache = new Map();
250
336
 
251
337
  /**
252
338
  * Newest structurally complete mixdog-config.json backed up from THIS data
253
339
  * dir (skips degenerate single-section snapshots from a prior failed RMW).
254
340
  * Never another data dir's config, and nothing for an uninitialized one.
341
+ * Throws on a transient read failure, which is never cached.
255
342
  */
256
343
  export function loadLatestMixdogConfigFromBackup(dataDir, options) {
257
344
  const instanceId = readInitInstanceId(dataDir);
@@ -259,7 +346,8 @@ export function loadLatestMixdogConfigFromBackup(dataDir, options) {
259
346
  const key = normalizeDataDir(dataDir);
260
347
  const root = getBackupRoot();
261
348
  let hit = restoreLookupCache.get(key);
262
- if (!hit || hit.root !== root || hit.instanceId !== instanceId) {
349
+ if (options?.fresh || !hit || hit.root !== root || hit.instanceId !== instanceId) {
350
+ restoreLookupCache.delete(key);
263
351
  hit = { root, instanceId, config: findRestorableConfig(dataDir, instanceId) };
264
352
  restoreLookupCache.set(key, hit);
265
353
  }
@@ -297,8 +385,8 @@ function copyTree(src, dst, copied) {
297
385
  }
298
386
 
299
387
  /** Prune one data dir's scope only; other scopes and legacy entries stay. */
300
- function pruneBackups(scopeDir, keep = 40) {
301
- for (const name of backupDirsNewestFirst(scopeDir).slice(keep)) {
388
+ function pruneBackups(scopeDir, names, own) {
389
+ for (const name of backupsToPrune(names, own)) {
302
390
  try {
303
391
  rmSync(join(scopeDir, name), { recursive: true, force: true });
304
392
  } catch {}
@@ -368,7 +456,7 @@ export function backupUserData(dataDir, reason = 'snapshot') {
368
456
  if (skipBackups()) return { dir: null, copied: [] };
369
457
  if (!dataDir || !existsSync(dataDir)) return { dir: null, copied: [] };
370
458
  const scopeDir = scopeDirFor(dataDir);
371
- const backupDir = reserveBackupDir(scopeDir, reason);
459
+ const { backupDir, names } = reserveBackupDir(scopeDir, reason);
372
460
  const copied = [];
373
461
  for (const rel of USER_DATA_FILES) {
374
462
  const src = join(dataDir, rel);
@@ -380,7 +468,7 @@ export function backupUserData(dataDir, reason = 'snapshot') {
380
468
  }
381
469
  if (copied.length > 0) {
382
470
  finishBackup(dataDir, backupDir, reason);
383
- pruneBackups(scopeDir);
471
+ pruneBackups(scopeDir, names, basename(backupDir));
384
472
  if (process.env.MIXDOG_SETUP_QUIET !== '1') {
385
473
  process.stderr.write(`[user-data-backup] ${reason}: copied ${copied.length} file(s) to ${backupDir}\n`);
386
474
  }
@@ -408,18 +496,8 @@ async function copyTreeAsync(src, dst, copied) {
408
496
  copied.push(dst);
409
497
  }
410
498
 
411
- async function pruneBackupsAsync(scopeDir, keep = 40) {
412
- let entries = [];
413
- try {
414
- entries = (await readdirP(scopeDir, { withFileTypes: true }))
415
- .filter((entry) => entry.isDirectory())
416
- .map((entry) => entry.name)
417
- .sort()
418
- .reverse();
419
- } catch {
420
- return;
421
- }
422
- for (const name of entries.slice(keep)) {
499
+ async function pruneBackupsAsync(scopeDir, names, own) {
500
+ for (const name of backupsToPrune(names, own)) {
423
501
  try {
424
502
  await rmP(join(scopeDir, name), { recursive: true, force: true });
425
503
  } catch {}
@@ -430,7 +508,7 @@ export async function backupUserDataAsync(dataDir, reason = 'snapshot') {
430
508
  if (skipBackups()) return { dir: null, copied: [] };
431
509
  if (!dataDir || !existsSync(dataDir)) return { dir: null, copied: [] };
432
510
  const scopeDir = scopeDirFor(dataDir);
433
- const backupDir = reserveBackupDir(scopeDir, reason);
511
+ const { backupDir, names } = await reserveBackupDirAsync(scopeDir, reason);
434
512
  const copied = [];
435
513
  for (const rel of USER_DATA_FILES) {
436
514
  const src = join(dataDir, rel);
@@ -442,7 +520,7 @@ export async function backupUserDataAsync(dataDir, reason = 'snapshot') {
442
520
  }
443
521
  if (copied.length > 0) {
444
522
  finishBackup(dataDir, backupDir, reason);
445
- await pruneBackupsAsync(scopeDir);
523
+ await pruneBackupsAsync(scopeDir, names, basename(backupDir));
446
524
  if (process.env.MIXDOG_SETUP_QUIET !== '1') {
447
525
  process.stderr.write(`[user-data-backup] ${reason}: copied ${copied.length} file(s) to ${backupDir}\n`);
448
526
  }