mixdog 1.0.8 → 1.0.9

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 (109) hide show
  1. package/package.json +2 -1
  2. package/scripts/lib/run-node-tests.mjs +13 -0
  3. package/scripts/session-efficiency-diag.mjs +283 -0
  4. package/src/defaults/skills/computer-use/SKILL.md +3 -3
  5. package/src/defaults/skills/local-provider/SKILL.md +2 -1
  6. package/src/defaults/skills/setup/SKILL.md +4 -4
  7. package/src/defaults/skills/setup/references/actions.md +1 -3
  8. package/src/defaults/skills/setup/references/surfaces.md +2 -3
  9. package/src/headless-exec.mjs +1 -0
  10. package/src/rules/shared/70-delivery.md +2 -2
  11. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +10 -0
  12. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +1 -1
  13. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  14. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +8 -4
  15. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +10 -1
  16. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +18 -3
  18. package/src/runtime/agent/orchestrator/providers/client-version-store.mjs +50 -0
  19. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +27 -4
  20. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +9 -5
  21. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +18 -4
  22. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +2 -2
  23. package/src/runtime/agent/orchestrator/providers/lib/anthropic-models.mjs +7 -1
  24. package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +25 -2
  25. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +7 -1
  26. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +4 -0
  27. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +8 -0
  28. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +17 -3
  29. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +7 -1
  30. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +16 -0
  31. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +0 -20
  32. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +12 -10
  33. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +3 -1
  34. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +1 -1
  35. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +15 -3
  36. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +11 -2
  37. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +5 -1
  38. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +23 -9
  39. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +15 -6
  40. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -4
  41. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +7 -0
  42. package/src/runtime/channels/lib/voice-transcription.mjs +30 -4
  43. package/src/runtime/computer-bridge/action-schema.mjs +3 -3
  44. package/src/runtime/computer-bridge/actions.mjs +4 -0
  45. package/src/runtime/computer-bridge/bridge-env.test-support.mjs +22 -0
  46. package/src/runtime/computer-bridge/client.mjs +10 -183
  47. package/src/runtime/computer-bridge/core-actions.mjs +2 -1
  48. package/src/runtime/computer-bridge/error-recovery.mjs +46 -34
  49. package/src/runtime/computer-bridge/result-canonical.mjs +181 -0
  50. package/src/runtime/local-provider/asset-installer.mjs +20 -10
  51. package/src/runtime/local-provider/catalog.mjs +4 -7
  52. package/src/runtime/local-provider/data/manifest.json +14 -1
  53. package/src/runtime/local-provider/hardware.mjs +36 -5
  54. package/src/runtime/local-provider/resumable-installations.mjs +2 -2
  55. package/src/runtime/local-provider/server.mjs +1 -1
  56. package/src/runtime/memory/index.mjs +2 -4
  57. package/src/runtime/memory/lib/core-memory-file.mjs +3 -8
  58. package/src/runtime/memory/lib/core-memory-store.mjs +14 -61
  59. package/src/runtime/memory/lib/cycle-scheduler/backlog-probe.mjs +5 -9
  60. package/src/runtime/memory/lib/cycle1/cycle1-plan.mjs +7 -0
  61. package/src/runtime/memory/lib/cycle1/cycle1-rows.mjs +43 -16
  62. package/src/runtime/memory/lib/cycle1/cycle1-window.mjs +11 -2
  63. package/src/runtime/memory/lib/embedding-provider.mjs +2 -5
  64. package/src/runtime/memory/lib/embedding-reindex.mjs +1 -5
  65. package/src/runtime/memory/lib/http-router/lifecycle-routes.mjs +0 -1
  66. package/src/runtime/memory/lib/memory-action-handlers/maintenance-actions.mjs +1 -1
  67. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +4 -22
  68. package/src/runtime/memory/lib/memory.mjs +79 -32
  69. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +0 -2
  70. package/src/runtime/office/com/com-adapter.mjs +3 -0
  71. package/src/runtime/office/pdf/pdf-adapter.mjs +50 -14
  72. package/src/runtime/office/pdf/pdf-render.mjs +2 -6
  73. package/src/runtime/office/pdf/pdf-security.mjs +8 -2
  74. package/src/runtime/office/portable/font-provisioner.mjs +28 -0
  75. package/src/runtime/office/portable/portable-docx-edits.mjs +28 -1
  76. package/src/runtime/office/portable/portable-soffice.mjs +44 -7
  77. package/src/runtime/shared/llm/anthropic-betas.mjs +7 -1
  78. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +1 -1
  79. package/src/runtime/shared/llm/default-context-window.mjs +42 -0
  80. package/src/runtime/shared/llm/model-catalog-projection.mjs +2 -0
  81. package/src/runtime/shared/llm/model-catalog.mjs +2 -0
  82. package/src/runtime/shared/plugin-paths.mjs +9 -2
  83. package/src/runtime/shared/worker-exec-argv.mjs +25 -0
  84. package/src/runtime/web-search/lib/formatter.mjs +43 -18
  85. package/src/session-runtime/automation-agents.mjs +88 -0
  86. package/src/session-runtime/automation-workflow.mjs +19 -6
  87. package/src/session-runtime/boot/apis.mjs +0 -1
  88. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +14 -12
  89. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +4 -25
  90. package/src/session-runtime/model-recency.mjs +10 -2
  91. package/src/session-runtime/model-route/set-route.mjs +7 -1
  92. package/src/session-runtime/resource-api.mjs +8 -2
  93. package/src/session-runtime/resource-plugins-api.mjs +14 -2
  94. package/src/session-runtime/route-state.mjs +6 -2
  95. package/src/session-runtime/schedule-session-run.mjs +7 -1
  96. package/src/session-runtime/services/agent-tool/render.mjs +0 -5
  97. package/src/session-runtime/settings-builtin-tools-api.mjs +0 -16
  98. package/src/session-runtime/setup-tool/executor.mjs +7 -13
  99. package/src/session-runtime/setup-tool/tool-defs.mjs +2 -3
  100. package/src/session-runtime/tool-policy-refresh.mjs +0 -2
  101. package/src/session-runtime/webhook-session-run.mjs +10 -11
  102. package/src/session-runtime/workflow-agents-api/agent-route.mjs +5 -0
  103. package/src/standalone/daemon-agent-control.mjs +8 -1
  104. package/src/standalone/daemon.mjs +4 -0
  105. package/src/tui/dist/index.mjs +3 -2
  106. package/src/tui/session/agent-job-feed/notification-router.mjs +7 -0
  107. package/src/tui/session/live-share.mjs +16 -1
  108. package/src/tui/session/notification-plan.mjs +4 -0
  109. package/src/session-runtime/bridge-first-use-gate.mjs +0 -72
@@ -0,0 +1,181 @@
1
+ import { computerResultRecovery } from './error-recovery.mjs';
2
+
3
+ const ACT_STEP_STATUSES = new Set(['succeeded', 'failed', 'skipped', 'pending', 'uncertain']);
4
+
5
+ function canonicalizeActResult(value, args) {
6
+ value.completed_actions = value.completed_steps;
7
+ value.total_actions = value.total_steps;
8
+ const canonicalStep = (row, index) => {
9
+ const normalized = { ...row };
10
+ normalized.type = args.input?.actions?.[index]?.type || normalized.action;
11
+ if (!ACT_STEP_STATUSES.has(normalized.status)) {
12
+ normalized.status = normalized.ok === false ? 'failed' : 'succeeded';
13
+ }
14
+ delete normalized.action;
15
+ delete normalized.ok;
16
+ return normalized;
17
+ };
18
+ value.actions = Array.isArray(value.steps) ? value.steps.map(canonicalStep) : value.steps;
19
+ // A single-window act reports the same transition on the act and its step.
20
+ const transition = JSON.stringify(value.window_transition);
21
+ for (const row of Array.isArray(value.actions) ? value.actions : []) {
22
+ if (transition && JSON.stringify(row?.window_transition) === transition) delete row.window_transition;
23
+ }
24
+ delete value.completed_steps;
25
+ delete value.total_steps;
26
+ delete value.steps;
27
+ }
28
+
29
+ // Host diagnostics stay in the host's run record; the model acts on none of them.
30
+ const HOST_DIAGNOSTIC_FIELDS = ['timings_ms', 'capture_attempts'];
31
+
32
+ function dropHostDiagnostics(value) {
33
+ if (!value || typeof value !== 'object') return;
34
+ if (Array.isArray(value)) {
35
+ for (const item of value) dropHostDiagnostics(item);
36
+ return;
37
+ }
38
+ for (const field of HOST_DIAGNOSTIC_FIELDS) delete value[field];
39
+ for (const item of Object.values(value)) dropHostDiagnostics(item);
40
+ }
41
+
42
+ // Elements and OCR text reach the model one line each, the shape browser
43
+ // snapshots use, because a field name costs more than the value it labels:
44
+ // `#mark [ref] Role "name" value="…" state=… source=… disabled focused
45
+ // @x,y,width,height actions`. Flags appear only when they differ from the
46
+ // common case the capture description states (an enabled UIA element), and
47
+ // the geometry a som frame repeats as x/y/width/height, center and
48
+ // screen_bounds collapses into its one bounds box.
49
+ const DERIVED_GEOMETRY_FIELDS = new Set([
50
+ 'x',
51
+ 'y',
52
+ 'width',
53
+ 'height',
54
+ 'center_x',
55
+ 'center_y',
56
+ 'center',
57
+ 'screen_bounds',
58
+ ]);
59
+
60
+ const quotedText = (text) => JSON.stringify(String(text));
61
+ const bareText = (text) => (/^[^\s"]+$/.test(String(text)) ? String(text) : quotedText(text));
62
+ const isUnsetField = (field) =>
63
+ field === undefined || field === null || field === '' || field === false || (Array.isArray(field) && !field.length);
64
+
65
+ function boundsText(box) {
66
+ return box.length === 4 && box.every((edge) => Number.isFinite(edge)) ? `@${box.join(',')}` : null;
67
+ }
68
+
69
+ function extraFieldTexts(rest) {
70
+ return Object.entries(rest)
71
+ .filter(([key, field]) => !DERIVED_GEOMETRY_FIELDS.has(key) && !isUnsetField(field))
72
+ .map(([key, field]) => `${key}=${typeof field === 'string' ? bareText(field) : JSON.stringify(field)}`);
73
+ }
74
+
75
+ function elementLine(element) {
76
+ if (!element || typeof element !== 'object') return element;
77
+ const { mark, ref, role, name, value, state, source, enabled, has_keyboard_focus, bounds, actions, ...rest } =
78
+ element;
79
+ return [
80
+ isUnsetField(mark) ? null : `#${mark}`,
81
+ isUnsetField(ref) ? null : `[${ref}]`,
82
+ bareText(role || 'Unknown'),
83
+ quotedText(name ?? ''),
84
+ isUnsetField(value) ? null : `value=${quotedText(value)}`,
85
+ isUnsetField(state) || state === source ? null : `state=${bareText(state)}`,
86
+ isUnsetField(source) || source === 'uia' ? null : `source=${bareText(source)}`,
87
+ enabled === false ? 'disabled' : null,
88
+ has_keyboard_focus === true ? 'focused' : null,
89
+ boundsText(Array.isArray(bounds) ? bounds : [rest.x, rest.y, rest.width, rest.height]),
90
+ Array.isArray(actions) && actions.length ? actions.join(',') : null,
91
+ ...extraFieldTexts(rest),
92
+ ]
93
+ .filter(Boolean)
94
+ .join(' ');
95
+ }
96
+
97
+ function ocrTextLine(row) {
98
+ if (!row || typeof row !== 'object') return row;
99
+ const { mark, text, line, x, y, width, height, ...rest } = row;
100
+ return [
101
+ isUnsetField(mark) ? null : `#${mark}`,
102
+ quotedText(text ?? ''),
103
+ boundsText([x, y, width, height]),
104
+ isUnsetField(line) ? null : `line=${line}`,
105
+ ...extraFieldTexts(rest),
106
+ ]
107
+ .filter(Boolean)
108
+ .join(' ');
109
+ }
110
+
111
+ function renderObservationLines(frame) {
112
+ if (!frame || typeof frame !== 'object') return;
113
+ if (Array.isArray(frame.elements)) frame.elements = frame.elements.map(elementLine);
114
+ for (const key of ['lines', 'words']) {
115
+ if (Array.isArray(frame.ocr?.[key])) frame.ocr[key] = frame.ocr[key].map(ocrTextLine);
116
+ }
117
+ }
118
+
119
+ export function canonicalComputerResultText(text, args) {
120
+ if (args.action === 'clipboard' && args.input?.operation === 'read') return text;
121
+ let value;
122
+ try {
123
+ value = JSON.parse(text);
124
+ } catch {
125
+ return text;
126
+ }
127
+ if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.action !== 'string') {
128
+ return text;
129
+ }
130
+ value.action = args.action;
131
+ if (args.action === 'list') value.kind = args.input?.kind;
132
+ if (args.action === 'capture') value.mode = args.input?.mode || 'state';
133
+ if (args.action === 'window') value.operation = args.input?.operation;
134
+ if (args.action === 'clipboard') value.operation = args.input?.operation;
135
+ if (args.action === 'act') canonicalizeActResult(value, args);
136
+ if (value.capture_after && value.observation === undefined) {
137
+ value.observation = value.capture_after;
138
+ delete value.capture_after;
139
+ }
140
+ dropHostDiagnostics(value);
141
+ renderObservationLines(value);
142
+ renderObservationLines(value.observation);
143
+ if (value.ok === false && value.recovery === undefined) {
144
+ const recovery = computerResultRecovery(value, args);
145
+ if (recovery) value.recovery = recovery;
146
+ }
147
+ return JSON.stringify(value);
148
+ }
149
+
150
+ // A capture payload reports the frame's real pixel size (`width`/`height`,
151
+ // nested under `observation` for action replies). Carrying it on the image
152
+ // block lets the context estimator bill a screenshot at its true vision cost
153
+ // instead of the flat unknown-image allowance. Provider normalizers rebuild
154
+ // the wire block from `type`/`source` alone, so these fields never reach an
155
+ // API and never shift the cached prefix.
156
+ export function computerImageDimensions(text) {
157
+ let value;
158
+ try {
159
+ value = JSON.parse(text);
160
+ } catch {
161
+ return null;
162
+ }
163
+ for (const frame of [value, value?.observation]) {
164
+ const width = Number(frame?.width);
165
+ const height = Number(frame?.height);
166
+ if (width > 0 && height > 0 && Number.isFinite(width) && Number.isFinite(height)) return { width, height };
167
+ }
168
+ return null;
169
+ }
170
+
171
+ export function canonicalComputerResultIsError(text, args) {
172
+ if (args.action === 'clipboard' && args.input?.operation === 'read') return false;
173
+ try {
174
+ const value = JSON.parse(text);
175
+ return Boolean(
176
+ value && typeof value === 'object' && !Array.isArray(value) && value.action === args.action && value.ok === false
177
+ );
178
+ } catch {
179
+ return false;
180
+ }
181
+ }
@@ -143,18 +143,26 @@ export async function downloadVerifiedLocalAsset(
143
143
  return destination;
144
144
  }
145
145
 
146
- async function extractZip(zipPath, destination, signal) {
147
- signal?.throwIfAborted();
146
+ // The Windows builds ship as zips (PowerShell's Expand-Archive); the macOS
147
+ // build is a gzipped tarball, which the system tar unpacks.
148
+ function extractCommand(archive, destination) {
149
+ if (!archive.endsWith('.zip')) return ['tar', ['-xzf', archive, '-C', destination], process.env];
148
150
  const command = [
149
151
  "$ErrorActionPreference = 'Stop'",
150
152
  'Expand-Archive -LiteralPath $env:MIXDOG_LOCAL_ARCHIVE -DestinationPath $env:MIXDOG_LOCAL_DESTINATION -Force',
151
153
  ].join('; ');
152
- const child = spawn('powershell', ['-NoProfile', '-NonInteractive', '-Command', command], {
153
- env: {
154
- ...process.env,
155
- MIXDOG_LOCAL_ARCHIVE: zipPath,
156
- MIXDOG_LOCAL_DESTINATION: destination,
157
- },
154
+ return [
155
+ 'powershell',
156
+ ['-NoProfile', '-NonInteractive', '-Command', command],
157
+ { ...process.env, MIXDOG_LOCAL_ARCHIVE: archive, MIXDOG_LOCAL_DESTINATION: destination },
158
+ ];
159
+ }
160
+
161
+ async function extractArchive(archive, destination, signal) {
162
+ signal?.throwIfAborted();
163
+ const [command, args, env] = extractCommand(archive, destination);
164
+ const child = spawn(command, args, {
165
+ env,
158
166
  windowsHide: true,
159
167
  signal,
160
168
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -184,7 +192,9 @@ async function installRuntimeInternal({ dataDir, onProgress, fetchFn, signal })
184
192
  signal?.throwIfAborted();
185
193
  const entry = localProviderRuntimePlatformEntry();
186
194
  if (!entry || !localProviderCatalogStatus({ dataDir, hardware }).available) {
187
- throw new Error('[local-provider] Windows x64 with a compatible NVIDIA RTX GPU is required');
195
+ throw new Error(
196
+ '[local-provider] Windows x64 with a compatible NVIDIA RTX GPU, or an Apple Silicon Mac with enough memory, is required'
197
+ );
188
198
  }
189
199
  const target = localProviderRuntimeDirectory(dataDir);
190
200
  const executable = join(target, entry.executable);
@@ -217,7 +227,7 @@ async function installRuntimeInternal({ dataDir, onProgress, fetchFn, signal })
217
227
  totalBytes: entry.downloadBytes,
218
228
  percent: Math.min(99, Math.round(((completedBytes + asset.size) / entry.downloadBytes) * 100)),
219
229
  });
220
- await extractZip(archive, staging, signal);
230
+ await extractArchive(archive, staging, signal);
221
231
  completedBytes += asset.size;
222
232
  }
223
233
  if (!existsSync(join(staging, entry.executable))) {
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
 
5
5
  import { resolvePluginData } from '../shared/plugin-paths.mjs';
6
6
  import { localProviderDiskStatus, partialAssetBytes } from './asset-storage.mjs';
7
- import { localProviderHardwareStatus } from './hardware.mjs';
7
+ import { localProviderHardwareStatus, localProviderPlatformKey } from './hardware.mjs';
8
8
  import { registeredLocalModels } from './registered-models.mjs';
9
9
  import { localModelState } from './model-state.mjs';
10
10
  import { localContextSettings } from './context-settings.mjs';
@@ -24,12 +24,8 @@ export function localProviderModelRoot(dataDir = resolvePluginData()) {
24
24
  return join(dataDir, 'local-provider', 'models');
25
25
  }
26
26
 
27
- function localProviderRuntimePlatformKey(platform = process.platform, arch = process.arch) {
28
- return platform === 'win32' && arch === 'x64' ? 'win32-x64-nvidia' : '';
29
- }
30
-
31
27
  export function localProviderRuntimePlatformEntry(options = {}) {
32
- const key = localProviderRuntimePlatformKey(options.platform, options.arch);
28
+ const key = localProviderPlatformKey(options.platform, options.arch);
33
29
  const entry = key ? LOCAL_PROVIDER_MANIFEST.runtime?.platforms?.[key] : null;
34
30
  return entry ? { key, ...entry } : null;
35
31
  }
@@ -109,7 +105,8 @@ export function localProviderCatalogStatus({
109
105
  const executable = localProviderRuntimeExecutable(dataDir, { platform, arch });
110
106
  const models = localProviderModelEntries(dataDir).map((entry) => publicModel(entry, dataDir, hardware));
111
107
  return {
112
- available: Boolean(platformEntry && hardware?.gpu?.vendor === 'NVIDIA' && models.some((entry) => entry.compatible)),
108
+ platformSupported: Boolean(platformEntry),
109
+ available: Boolean(platformEntry && hardware?.supported && models.some((entry) => entry.compatible)),
113
110
  runtime: {
114
111
  installed: Boolean(executable && existsSync(executable)),
115
112
  version: LOCAL_PROVIDER_MANIFEST.runtime.version,
@@ -23,6 +23,19 @@
23
23
  "sha256": "8c79a9b226de4b3cacfd1f83d24f962d0773be79f1e7b75c6af4ded7e32ae1d6"
24
24
  }
25
25
  ]
26
+ },
27
+ "darwin-arm64-metal": {
28
+ "executable": "llama-b10621/llama-server",
29
+ "backend": "Metal",
30
+ "downloadBytes": 10954823,
31
+ "assets": [
32
+ {
33
+ "name": "llama-b10621-bin-macos-arm64.tar.gz",
34
+ "url": "https://github.com/ggml-org/llama.cpp/releases/download/b10621/llama-b10621-bin-macos-arm64.tar.gz",
35
+ "size": 10954823,
36
+ "sha256": "429c8270608600188035e5e92f7d78dffb7900904fe7dd7e6a84f48068cd13cf"
37
+ }
38
+ ]
26
39
  }
27
40
  }
28
41
  },
@@ -30,7 +43,7 @@
30
43
  {
31
44
  "id": "qwen3.8-27b-q4-k-m",
32
45
  "name": "Qwen3.8 27B Q4_K_M",
33
- "description": "Recommended coding and tool-use model for NVIDIA RTX GPUs with 24 GB VRAM.",
46
+ "description": "Recommended coding and tool-use model for GPUs with 24 GB of memory.",
34
47
  "filename": "Qwen3.8-27B-Q4_K_M.gguf",
35
48
  "url": "https://huggingface.co/ggml-org/Qwen3.8-27B-GGUF/resolve/0669b98607d47046c7c2b3f801011d54a08cfccf/Qwen3.8-27B-Q4_K_M.gguf?download=true",
36
49
  "revision": "0669b98607d47046c7c2b3f801011d54a08cfccf",
@@ -1,10 +1,37 @@
1
1
  import { execFile } from 'node:child_process';
2
+ import { cpus, totalmem } from 'node:os';
2
3
  import { promisify } from 'node:util';
3
4
 
4
5
  const execFileAsync = promisify(execFile);
5
6
  const GPU_FIELDS = 'index,uuid,name,memory.total,memory.free';
6
7
  const CACHE_MS = 5_000;
7
8
 
9
+ /** The manifest runtime this host runs: CUDA on Windows x64, Metal on Apple
10
+ * Silicon; '' where no llama.cpp build is pinned. */
11
+ export function localProviderPlatformKey(platform = process.platform, arch = process.arch) {
12
+ if (platform === 'win32' && arch === 'x64') return 'win32-x64-nvidia';
13
+ if (platform === 'darwin' && arch === 'arm64') return 'darwin-arm64-metal';
14
+ return '';
15
+ }
16
+
17
+ /** Apple Silicon has one GPU on unified memory. Metal caps what it wires for
18
+ * the GPU: the `iogpu.wired_limit_mb` override when set, else two thirds of
19
+ * RAM — exactly what llama.cpp's Metal backend reports on a 16 GB M4; larger
20
+ * Macs may allow more, which this leaves unused. Metal reports that whole
21
+ * working set as free, so free equals total here as it does in llama.cpp. */
22
+ export function appleSiliconGpu({ name, totalBytes, wiredLimitMb }) {
23
+ const memoryBytes = wiredLimitMb > 0 ? wiredLimitMb * 1024 ** 2 : Math.floor((totalBytes * 2) / 3);
24
+ return { index: 0, uuid: 'MTL0', vendor: 'Apple', name, memoryBytes, freeMemoryBytes: memoryBytes };
25
+ }
26
+
27
+ async function queryAppleSilicon() {
28
+ const limit = await execFileAsync('/usr/sbin/sysctl', ['-n', 'iogpu.wired_limit_mb'], {
29
+ encoding: 'utf8',
30
+ timeout: 5_000,
31
+ }).catch(() => ({ stdout: '0' })); // macOS before 14 has no such key
32
+ return { name: cpus()[0]?.model || 'Apple Silicon', totalBytes: totalmem(), wiredLimitMb: Number(limit.stdout) || 0 };
33
+ }
34
+
8
35
  export function parseNvidiaGpus(output) {
9
36
  return String(output || '')
10
37
  .split(/\r?\n/)
@@ -40,12 +67,14 @@ export function createHardwareProbe({
40
67
  maxBuffer: 64 * 1024,
41
68
  })
42
69
  ).stdout,
70
+ appleQueryFn = queryAppleSilicon,
43
71
  platform = process.platform,
44
72
  arch = process.arch,
45
73
  now = Date.now,
46
74
  cacheMs = CACHE_MS,
47
75
  } = {}) {
48
- const platformSupported = platform === 'win32' && arch === 'x64';
76
+ const platformSupported = Boolean(localProviderPlatformKey(platform, arch));
77
+ const apple = platform === 'darwin';
49
78
  let value = {
50
79
  platform,
51
80
  arch,
@@ -70,12 +99,14 @@ export function createHardwareProbe({
70
99
  return value;
71
100
  };
72
101
  pending = Promise.resolve()
73
- .then(queryFn)
102
+ .then(apple ? appleQueryFn : queryFn)
74
103
  .then(
75
104
  (output) => {
76
- const gpus = parseNvidiaGpus(output).sort(
77
- (a, b) => b.memoryBytes - a.memoryBytes || b.freeMemoryBytes - a.freeMemoryBytes || a.index - b.index
78
- );
105
+ const gpus = apple
106
+ ? [appleSiliconGpu(output)]
107
+ : parseNvidiaGpus(output).sort(
108
+ (a, b) => b.memoryBytes - a.memoryBytes || b.freeMemoryBytes - a.freeMemoryBytes || a.index - b.index
109
+ );
79
110
  return settle({
80
111
  gpu: gpus[0] || null,
81
112
  gpus,
@@ -1,5 +1,5 @@
1
1
  import { join } from 'node:path';
2
- import { LOCAL_PROVIDER_MANIFEST, exactLocalProviderFile, localProviderRuntimeRoot } from './catalog.mjs';
2
+ import { exactLocalProviderFile, localProviderRuntimePlatformEntry, localProviderRuntimeRoot } from './catalog.mjs';
3
3
  import { partialAssetBytes } from './asset-storage.mjs';
4
4
 
5
5
  // Partial files are durable evidence after a process restart. They do not
@@ -27,7 +27,7 @@ export function resumableLocalInstallations(catalog, live, dataDir) {
27
27
  entries.push(pausedEntry('model', model.id, receivedBytes, model.sizeBytes));
28
28
  }
29
29
  if (!catalog.runtime.installed && !entries.some((entry) => entry.phase === 'runtime')) {
30
- const assets = LOCAL_PROVIDER_MANIFEST.runtime.platforms['win32-x64-nvidia'].assets;
30
+ const assets = localProviderRuntimePlatformEntry()?.assets || [];
31
31
  const receivedBytes = assets.reduce((sum, asset) => {
32
32
  const path = join(localProviderRuntimeRoot(dataDir), '.downloads', asset.name);
33
33
  return sum + (exactLocalProviderFile(path, asset.size) ? asset.size : partialAssetBytes(path, asset.size));
@@ -83,7 +83,7 @@ export async function ensureLocalProviderServer(modelId, { dataDir = resolvePlug
83
83
  const hardware = await detectLocalProviderHardware({ refresh: true });
84
84
  startupSignal?.throwIfAborted();
85
85
  const gpu = selectLocalProviderGpu(hardware, entry);
86
- return { gpu, env: { CUDA_VISIBLE_DEVICES: gpu.uuid } };
86
+ return { gpu, env: gpu.vendor === 'NVIDIA' ? { CUDA_VISIBLE_DEVICES: gpu.uuid } : {} };
87
87
  },
88
88
  onReady: async ({ baseURL, apiKey, loadTimeMs }, startupSignal) => {
89
89
  let props = {};
@@ -59,7 +59,6 @@ import {
59
59
  resolveCoalesceMaxRetries,
60
60
  scheduleCoalescedCycleRetry,
61
61
  } from './lib/memory-cycle-requests.mjs';
62
- import { backfillCoreEmbeddings } from './lib/core-memory-store.mjs';
63
62
  import { drainEmbeddingReindex } from './lib/embedding-reindex.mjs';
64
63
  import { refreshCoreMemoryFile } from './lib/core-memory-file.mjs';
65
64
  import { resolveProjectId, resolveProjectScope } from './lib/project-id-resolver.mjs';
@@ -420,14 +419,13 @@ function _startEmbeddingReindex() {
420
419
  _embeddingReindexController = controller;
421
420
  const promise = drainEmbeddingReindex({
422
421
  flushEntries: ({ signal }) => flushEmbeddingDirty(db, { signal }),
423
- backfillCore: ({ signal }) => backfillCoreEmbeddings(DATA_DIR, { signal }),
424
422
  signal: controller.signal,
425
423
  })
426
424
  .then((summary) => {
427
- if (summary.attempted > 0 || summary.coreFilled > 0) {
425
+ if (summary.attempted > 0) {
428
426
  __mixdogMemoryLog(
429
427
  `[memory-service] embedding reindex passes=${summary.passes} attempted=${summary.attempted} ` +
430
- `ok=${summary.succeeded} failed=${summary.failed} core=${summary.coreFilled}\n`
428
+ `ok=${summary.succeeded} failed=${summary.failed}\n`
431
429
  );
432
430
  }
433
431
  })
@@ -30,15 +30,10 @@ function normalizeSummary(value) {
30
30
  .trim();
31
31
  }
32
32
 
33
+ // The prompt carries the user's text only; the memory tool addresses entries
34
+ // through its own list results, not through injected ids.
33
35
  export function formatCuratedCoreMemoryLine(row) {
34
- const summary = normalizeSummary(row?.summary);
35
- if (!summary) return '';
36
- const id = Number(row?.id);
37
- const scope = row?.project_id ?? row?.projectId ?? 'common';
38
- const version = row?.index_revision ?? row?.indexRevision;
39
- return Number.isInteger(id) && id > 0 && version
40
- ? `[project=${scope} id=${id} index_revision=${version}] ${summary}`
41
- : summary;
36
+ return normalizeSummary(row?.summary);
42
37
  }
43
38
 
44
39
  function normalizeCuratedEntry(row) {
@@ -1,15 +1,14 @@
1
- import { __mixdogMemoryLog } from './memory-log.mjs';
2
1
 
3
2
  // User-curated core memory store — native PG-backed via core_entries table.
4
3
  // Per-project entries distinguished by project_id column (NULL = COMMON).
5
4
  // Explicit add/edit/delete operations affect only the requested entry.
6
5
  // Generated conversation history never enters this store automatically.
7
6
 
8
- import { getDatabase, embeddingToSql } from './memory.mjs';
7
+ // Entries are injected verbatim and recalled by term match, so no write here
8
+ // embeds: saving never waits on the embedding model.
9
+ import { getDatabase } from './memory.mjs';
9
10
  import { VALID_CATEGORY } from './memory-categories.mjs';
10
- import { cachedEmbedTextBatch } from './memory-embed.mjs';
11
11
  import { checkedConnect } from './pg/adapter.mjs';
12
- import { throwIfAborted } from './memory-cycle-shared.mjs';
13
12
  import { findCoreKeyRows } from './core-memory-uniqueness.mjs';
14
13
 
15
14
  const CORE_ELEMENT_DERIVE_LENGTH = 40;
@@ -75,46 +74,6 @@ function _getDb(dataDir) {
75
74
  return db;
76
75
  }
77
76
 
78
- async function _embedFor(db, element, summary) {
79
- const text = `${element}\n${summary || ''}`.trim();
80
- if (!text) return null;
81
- const [vec] = await cachedEmbedTextBatch(db, [text]);
82
- return Array.isArray(vec) ? vec : null;
83
- }
84
-
85
- // Lazy repair of NULL embeddings on existing rows. Runs once per boot or
86
- // whenever a NULL slips back in via direct SQL. SELECT WHERE embedding IS NULL
87
- // returns 0 rows on a fully-populated table, so this is a fast no-op.
88
- async function _backfillNullEmbeddings(db, options = {}) {
89
- const signal = options?.signal;
90
- throwIfAborted(signal);
91
- // Only refill live cores; legacy archived entries stay inactive.
92
- const r = await db.query(
93
- `SELECT id, element, summary FROM core_entries WHERE embedding IS NULL AND (status IS NULL OR status = 'active')`
94
- );
95
- if (r.rows.length === 0) return 0;
96
- let filled = 0;
97
- for (const row of r.rows) {
98
- throwIfAborted(signal);
99
- const vec = await _embedFor(db, row.element, row.summary);
100
- throwIfAborted(signal);
101
- if (!vec) continue;
102
- await db.query(`UPDATE core_entries SET embedding = $1::halfvec WHERE id = $2 AND embedding IS NULL`, [
103
- embeddingToSql(vec),
104
- row.id,
105
- ]);
106
- filled++;
107
- }
108
- if (filled > 0) {
109
- __mixdogMemoryLog(`[core-memory] backfilled ${filled} NULL embedding(s) on core_entries\n`);
110
- }
111
- return filled;
112
- }
113
-
114
- export async function backfillCoreEmbeddings(dataDir, options = {}) {
115
- const db = _getDb(dataDir);
116
- return await _backfillNullEmbeddings(db, options);
117
- }
118
77
 
119
78
  // NOTE: boot-time core id compaction was REMOVED. Resequencing core_entries.id
120
79
  // to 1..N rewrote primary keys that are handed out as stable `core:N`
@@ -177,8 +136,6 @@ async function _addCoreImpl(dataDir, input, projectId) {
177
136
  if (errors.length) throw new Error(errors.join('; '));
178
137
  const db = _getDb(dataDir);
179
138
  const now = Date.now();
180
- await _backfillNullEmbeddings(db);
181
- const embedding = await _embedFor(db, el, sm);
182
139
 
183
140
  const client = await checkedConnect(db._pool, 'memory');
184
141
  try {
@@ -194,20 +151,20 @@ async function _addCoreImpl(dataDir, input, projectId) {
194
151
  );
195
152
  }
196
153
  const revived = await client.query(
197
- `UPDATE core_entries SET summary = $1, category = $2, embedding = $3::halfvec,
198
- status = 'active', archived_at = NULL, updated_at = $4
199
- WHERE id = $5
154
+ `UPDATE core_entries SET summary = $1, category = $2,
155
+ status = 'active', archived_at = NULL, updated_at = $3
156
+ WHERE id = $4
200
157
  RETURNING id, element, summary, category, project_id, created_at, updated_at`,
201
- [sm, cat, embedding ? embeddingToSql(embedding) : null, now, collisions[0].id]
158
+ [sm, cat, now, collisions[0].id]
202
159
  );
203
160
  await client.query('COMMIT');
204
161
  return { ...revived.rows[0], revived_from_archived: true };
205
162
  }
206
163
  const r = await client.query(
207
- `INSERT INTO core_entries(element, summary, category, project_id, embedding, created_at, updated_at)
208
- VALUES ($1, $2, $3, $4, $5::halfvec, $6, $7)
164
+ `INSERT INTO core_entries(element, summary, category, project_id, created_at, updated_at)
165
+ VALUES ($1, $2, $3, $4, $5, $6)
209
166
  RETURNING id, element, summary, category, project_id, created_at, updated_at`,
210
- [el, sm, cat, projectId, embedding ? embeddingToSql(embedding) : null, now, now]
167
+ [el, sm, cat, projectId, now, now]
211
168
  );
212
169
  await client.query('COMMIT');
213
170
  return r.rows[0];
@@ -276,8 +233,6 @@ async function _editCoreImpl(dataDir, id, patch) {
276
233
  return { ...cur, element: newElement, summary: newSummary, category: newCategory, updated_at: now };
277
234
  }
278
235
 
279
- await _backfillNullEmbeddings(db);
280
- const embedding = await _embedFor(db, newElement, newSummary);
281
236
  const client = await checkedConnect(db._pool, 'memory');
282
237
  try {
283
238
  await client.query('BEGIN');
@@ -289,8 +244,7 @@ async function _editCoreImpl(dataDir, id, patch) {
289
244
  for (const poolKey of new Set(poolKeys)) {
290
245
  await client.query(`SELECT pg_advisory_xact_lock(hashtext($1))`, [poolKey]);
291
246
  }
292
- // Revalidate under the lock. `cur` (and the embedding derived from it) was
293
- // read before any locking, so without this check two concurrent edits both
247
+ // Revalidate under the lock. `cur` was read before any locking, so without this check two concurrent edits both
294
248
  // computed a patch from the same snapshot and the slower one silently
295
249
  // overwrote the newer content.
296
250
  const fresh = (await client.query(`SELECT * FROM core_entries WHERE id = $1 FOR UPDATE`, [numId])).rows[0];
@@ -313,10 +267,9 @@ async function _editCoreImpl(dataDir, id, patch) {
313
267
  }
314
268
  await client.query(
315
269
  `UPDATE core_entries
316
- SET element = $1, summary = $2, category = $3, project_id = $4,
317
- embedding = $5::halfvec, updated_at = $6
318
- WHERE id = $7`,
319
- [newElement, newSummary, newCategory, newProjectId, embedding ? embeddingToSql(embedding) : null, now, numId]
270
+ SET element = $1, summary = $2, category = $3, project_id = $4, updated_at = $5
271
+ WHERE id = $6`,
272
+ [newElement, newSummary, newCategory, newProjectId, now, numId]
320
273
  );
321
274
  await client.query('COMMIT');
322
275
  return {
@@ -1,4 +1,4 @@
1
- import { CYCLE1_OMITTED_COOLDOWN_MS } from '../cycle1/cycle1-plan.mjs';
1
+ import { cycle1RowDueSql, cycle1UnchunkedSql } from '../cycle1/cycle1-rows.mjs';
2
2
 
3
3
  const BACKLOG_WARN_PENDING = 500;
4
4
 
@@ -27,17 +27,13 @@ export function createBacklogProbe({ getDb, ledger, log, flushRawEmbeddings }) {
27
27
  async function probe(now) {
28
28
  const db = getDb();
29
29
  try {
30
- const unchunked = await countRows(
31
- db,
32
- `SELECT COUNT(*) c FROM entries WHERE chunk_root IS NULL AND NULLIF(btrim(session_id), '') IS NOT NULL`
33
- );
30
+ const unchunked = await countRows(db, `SELECT COUNT(*) c FROM entries WHERE ${cycle1UnchunkedSql()}`);
34
31
  const unchunkedEligible = await countRows(
35
32
  db,
36
33
  `SELECT COUNT(*) c FROM entries
37
- WHERE chunk_root IS NULL
38
- AND NULLIF(btrim(session_id), '') IS NOT NULL
39
- AND (reviewed_at IS NULL OR reviewed_at < $1)`,
40
- [now - CYCLE1_OMITTED_COOLDOWN_MS]
34
+ WHERE ${cycle1UnchunkedSql()}
35
+ AND ${cycle1RowDueSql('$1')}`,
36
+ [now]
41
37
  );
42
38
  ledger.setBacklog({ unchunked, unchunked_eligible: unchunkedEligible, at: now });
43
39
  if (unchunked > BACKLOG_WARN_PENDING) {
@@ -9,6 +9,13 @@ const CYCLE1_SESSION_CAP = 10;
9
9
  export const CYCLE1_PACKET_MAX_ROWS = 50;
10
10
  export const CYCLE1_MAX_PACKETS = 4;
11
11
  export const CYCLE1_OMITTED_COOLDOWN_MS = 60 * 60 * 1000;
12
+ // A row the classifier leaves raw again waits twice as long each time, up to
13
+ // this. Most such rows are one- or two-row leftovers too short for any summary
14
+ // to come out shorter than they are, so the same prompt yields the same miss:
15
+ // at a flat hourly retry they filled every packet slot of every run (runtime
16
+ // log 2026-10-04..05: 1768 of 1832 classifier calls committed nothing; one
17
+ // two-character row had been sent 278 times).
18
+ export const CYCLE1_OMITTED_COOLDOWN_MAX_MS = 30 * 24 * 60 * 60 * 1000;
12
19
  // A session is chunked only after it has been quiet this long, so one task's
13
20
  // request, work, result and correction reach the classifier together instead
14
21
  // of being split at every scheduler tick.