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.
- package/package.json +2 -1
- package/scripts/lib/run-node-tests.mjs +13 -0
- package/scripts/session-efficiency-diag.mjs +283 -0
- package/src/defaults/skills/computer-use/SKILL.md +3 -3
- package/src/defaults/skills/local-provider/SKILL.md +2 -1
- package/src/defaults/skills/setup/SKILL.md +4 -4
- package/src/defaults/skills/setup/references/actions.md +1 -3
- package/src/defaults/skills/setup/references/surfaces.md +2 -3
- package/src/headless-exec.mjs +1 -0
- package/src/rules/shared/70-delivery.md +2 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +8 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +18 -3
- package/src/runtime/agent/orchestrator/providers/client-version-store.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +27 -4
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +9 -5
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +18 -4
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-models.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +16 -0
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +0 -20
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +12 -10
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +23 -9
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +15 -6
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +7 -0
- package/src/runtime/channels/lib/voice-transcription.mjs +30 -4
- package/src/runtime/computer-bridge/action-schema.mjs +3 -3
- package/src/runtime/computer-bridge/actions.mjs +4 -0
- package/src/runtime/computer-bridge/bridge-env.test-support.mjs +22 -0
- package/src/runtime/computer-bridge/client.mjs +10 -183
- package/src/runtime/computer-bridge/core-actions.mjs +2 -1
- package/src/runtime/computer-bridge/error-recovery.mjs +46 -34
- package/src/runtime/computer-bridge/result-canonical.mjs +181 -0
- package/src/runtime/local-provider/asset-installer.mjs +20 -10
- package/src/runtime/local-provider/catalog.mjs +4 -7
- package/src/runtime/local-provider/data/manifest.json +14 -1
- package/src/runtime/local-provider/hardware.mjs +36 -5
- package/src/runtime/local-provider/resumable-installations.mjs +2 -2
- package/src/runtime/local-provider/server.mjs +1 -1
- package/src/runtime/memory/index.mjs +2 -4
- package/src/runtime/memory/lib/core-memory-file.mjs +3 -8
- package/src/runtime/memory/lib/core-memory-store.mjs +14 -61
- package/src/runtime/memory/lib/cycle-scheduler/backlog-probe.mjs +5 -9
- package/src/runtime/memory/lib/cycle1/cycle1-plan.mjs +7 -0
- package/src/runtime/memory/lib/cycle1/cycle1-rows.mjs +43 -16
- package/src/runtime/memory/lib/cycle1/cycle1-window.mjs +11 -2
- package/src/runtime/memory/lib/embedding-provider.mjs +2 -5
- package/src/runtime/memory/lib/embedding-reindex.mjs +1 -5
- package/src/runtime/memory/lib/http-router/lifecycle-routes.mjs +0 -1
- package/src/runtime/memory/lib/memory-action-handlers/maintenance-actions.mjs +1 -1
- package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +4 -22
- package/src/runtime/memory/lib/memory.mjs +79 -32
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +0 -2
- package/src/runtime/office/com/com-adapter.mjs +3 -0
- package/src/runtime/office/pdf/pdf-adapter.mjs +50 -14
- package/src/runtime/office/pdf/pdf-render.mjs +2 -6
- package/src/runtime/office/pdf/pdf-security.mjs +8 -2
- package/src/runtime/office/portable/font-provisioner.mjs +28 -0
- package/src/runtime/office/portable/portable-docx-edits.mjs +28 -1
- package/src/runtime/office/portable/portable-soffice.mjs +44 -7
- package/src/runtime/shared/llm/anthropic-betas.mjs +7 -1
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +1 -1
- package/src/runtime/shared/llm/default-context-window.mjs +42 -0
- package/src/runtime/shared/llm/model-catalog-projection.mjs +2 -0
- package/src/runtime/shared/llm/model-catalog.mjs +2 -0
- package/src/runtime/shared/plugin-paths.mjs +9 -2
- package/src/runtime/shared/worker-exec-argv.mjs +25 -0
- package/src/runtime/web-search/lib/formatter.mjs +43 -18
- package/src/session-runtime/automation-agents.mjs +88 -0
- package/src/session-runtime/automation-workflow.mjs +19 -6
- package/src/session-runtime/boot/apis.mjs +0 -1
- package/src/session-runtime/cwd-plugins/core-memory-context.mjs +14 -12
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +4 -25
- package/src/session-runtime/model-recency.mjs +10 -2
- package/src/session-runtime/model-route/set-route.mjs +7 -1
- package/src/session-runtime/resource-api.mjs +8 -2
- package/src/session-runtime/resource-plugins-api.mjs +14 -2
- package/src/session-runtime/route-state.mjs +6 -2
- package/src/session-runtime/schedule-session-run.mjs +7 -1
- package/src/session-runtime/services/agent-tool/render.mjs +0 -5
- package/src/session-runtime/settings-builtin-tools-api.mjs +0 -16
- package/src/session-runtime/setup-tool/executor.mjs +7 -13
- package/src/session-runtime/setup-tool/tool-defs.mjs +2 -3
- package/src/session-runtime/tool-policy-refresh.mjs +0 -2
- package/src/session-runtime/webhook-session-run.mjs +10 -11
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +5 -0
- package/src/standalone/daemon-agent-control.mjs +8 -1
- package/src/standalone/daemon.mjs +4 -0
- package/src/tui/dist/index.mjs +3 -2
- package/src/tui/session/agent-job-feed/notification-router.mjs +7 -0
- package/src/tui/session/live-share.mjs +16 -1
- package/src/tui/session/notification-plan.mjs +4 -0
- 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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
77
|
-
(
|
|
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 {
|
|
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 =
|
|
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
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
198
|
-
status = 'active', archived_at = NULL, updated_at = $
|
|
199
|
-
WHERE id = $
|
|
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,
|
|
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,
|
|
208
|
-
VALUES ($1, $2, $3, $4, $5
|
|
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,
|
|
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`
|
|
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
|
-
|
|
318
|
-
|
|
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 {
|
|
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
|
|
38
|
-
AND
|
|
39
|
-
|
|
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.
|