Fixes
CI / Build & Test (push) Successful in 28m9s

This commit is contained in:
2026-09-02 17:20:58 -04:00
parent 1b0d2b9338
commit 75275d7d91
14 changed files with 381 additions and 54 deletions
+2
View File
@@ -105,6 +105,8 @@ jobs:
exit 1
fi
echo "Testing QVAC enable over native messaging"
python3 scripts/qvac-packed-selftest.py "$BIN"
echo "PASS: smoke test complete"
- name: Generate checksums
+3
View File
@@ -72,6 +72,9 @@ jobs:
exit 1
fi
echo "Testing QVAC enable over native messaging"
python3 scripts/qvac-packed-selftest.py "$BIN"
- name: List release artifacts
run: |
echo "=== releases/ ==="
+1 -1
View File
@@ -280,7 +280,7 @@ function renderCapabilitiesPage(state) {
<div class="health-row"><span class="k">Available</span><span class="v">${q.available ? 'yes' : 'no'}</span></div>
<div class="health-row"><span class="k">Device</span><span class="v">${escapeHtml(q.device || '—')}</span></div>
<div class="health-row"><span class="k">Backend</span><span class="v">${escapeHtml(q.backend || q.backendId || '—')}</span></div>
<div class="health-row"><span class="k">GPU</span><span class="v">${escapeHtml(q.deviceName || '—')}</span></div>
<div class="health-row"><span class="k">GPU</span><span class="v">${escapeHtml(q.deviceName || (q.vram ? String(q.vram) : '—'))}</span></div>
<div class="health-row"><span class="k">Model</span><span class="v">${escapeHtml(q.friendlyId || q.modelId || 'none')}</span></div>
<div class="health-row"><span class="k">Plugins</span><span class="v">${escapeHtml((q.plugins || []).join(', ') || '—')}</span></div>
<div class="health-row"><span class="k">OpenAI</span><span class="v">${escapeHtml(q.openaiUrl || 'off')}</span></div>
+3
View File
@@ -33,6 +33,7 @@ import {
} from './register-default-packs.mjs';
import _messenger from './messenger.js';
import _host from './host.js';
import selftest from './qvac/selftest.js';
installConsoleToStderr();
normalizeBareVersions();
@@ -49,6 +50,8 @@ if (process.argv.includes('--extract-addons')) {
await warmDefaultModules();
await extractAddons(DEFAULT_ADDON_PACKAGES);
})();
} else if (process.argv.includes('--qvac-selftest') || process.env.BRIDGE_SWARM_QVAC_SELFTEST === '1') {
selftest.runQvacSelftest(logErr, normalizeBareVersions);
} else {
try {
startHost(_messenger, _host);
+1 -1
View File
@@ -44,7 +44,7 @@ function backendLabel(resources) {
if (process.platform === 'darwin' || drivers.metal) {
return { backend: 'metal', backendId: 1, deviceName: gpuName, vram };
}
if (drivers.vulkan || process.platform === 'linux' || process.platform === 'win32') {
if (drivers.vulkan || hasGpu(resources)) {
return { backend: 'vulkan', backendId: 3, deviceName: gpuName, vram };
}
return { backend: 'cpu', backendId: 0, deviceName: gpuName, vram };
+106 -20
View File
@@ -12,6 +12,7 @@ const { ensureQvacRoot } = require('../capabilities/paths.js');
const catalog = require('./catalog.js');
const device = require('./device.js');
const events = require('./events.js');
const llamacppHost = require('./llamacpp-host.js');
const SKIP = process.env.BRIDGE_SWARM_SKIP_QVAC === '1';
@@ -68,7 +69,10 @@ function setUserEnabled(on) {
if (userEnabled === next) return;
userEnabled = next;
initPromise = null;
if (!next) initError = 'disabled';
if (!next) {
initError = 'disabled';
lastResources = null;
}
}
function disabledError() {
@@ -108,6 +112,11 @@ async function assembleSdk() {
return null;
}
normalizeBareVersions();
try {
llamacppHost.patchLlamaInterface(log);
} catch (err) {
log('llm backends patch skipped: ' + ((err && err.message) || err));
}
ensureQvacRoot();
if (!process.env.QVAC_CONFIG_PATH) {
const cfg = path.join(__dirname, '..', 'qvac.config.json');
@@ -120,14 +129,24 @@ async function assembleSdk() {
await import('@qvac/fabric');
} catch (_) {}
const infMain = await tryImport('@qvac/inference/plugins');
let infMain;
try {
infMain = await import('@qvac/inference/plugins');
} catch (err) {
infMain = { __error: err };
}
if (infMain.__error) {
initError = infMain.__error.message || String(infMain.__error);
log('inference plugins unavailable: ' + initError);
return null;
}
const llmMod = await tryImport('@qvac/inference/llamacpp-completion/plugin');
let llmMod;
try {
llmMod = await import('@qvac/inference/llamacpp-completion/plugin');
} catch (err) {
llmMod = { __error: err };
}
if (llmMod.__error) {
initError = llmMod.__error.message || String(llmMod.__error);
log('llm plugin unavailable: ' + initError);
@@ -182,9 +201,14 @@ function ensureInit() {
if (initPromise) return initPromise;
normalizeBareVersions();
initPromise = assembleSdk()
.then((s) => {
.then(async (s) => {
sdk = s;
if (!s) initPromise = null;
else {
try {
await probeHardware(s);
} catch (_) {}
}
return s;
})
.catch((err) => {
@@ -195,6 +219,8 @@ function ensureInit() {
}
function publicStatus() {
const hw = lastResources || {};
const modelLoaded = !!loaded.modelId;
return {
enabled: isUserEnabled(),
available: isUserEnabled() && !!sdk && !skipped(),
@@ -206,11 +232,11 @@ function publicStatus() {
constant: loaded.constant,
modelType: loaded.modelType,
tools: loaded.tools,
device: loaded.device,
backend: loaded.backend,
backendId: loaded.backendId,
deviceName: loaded.deviceName,
vram: loaded.vram,
device: modelLoaded ? loaded.device : hw.deviceDefault || loaded.device,
backend: loaded.backend || hw.backend || null,
backendId: loaded.backendId != null ? loaded.backendId : hw.backendId,
deviceName: loaded.deviceName || hw.deviceName || null,
vram: loaded.vram || hw.vram || null,
ctxSize: loaded.ctxSize,
platform: process.platform,
arch: process.arch,
@@ -230,8 +256,13 @@ const pickDevice = device.pickDevice;
const backendLabel = device.backendLabel;
async function probeGpuInfo() {
const gpuMod = await tryImport('bare-gpu-info');
if (gpuMod.__error) return null;
let gpuMod;
try {
gpuMod = await import('bare-gpu-info');
} catch (err) {
log('bare-gpu-info unavailable: ' + ((err && err.message) || err));
return null;
}
try {
const GPUInfo = gpuMod.default || gpuMod.GPUInfo || gpuMod;
if (typeof GPUInfo !== 'function') return null;
@@ -243,19 +274,18 @@ async function probeGpuInfo() {
if (typeof info.destroy === 'function') info.destroy();
} catch (_) {}
return { gpus, drivers, length };
} catch (_) {
} catch (err) {
log('gpu probe failed: ' + ((err && err.message) || err));
return null;
}
}
async function resources(opts) {
if (!isUserEnabled()) throw disabledError();
const s = await ensureInit();
async function probeHardware(s) {
const ram = totalmem();
let qvacRes = null;
if (s && typeof s.getSystemResources === 'function') {
try {
qvacRes = await s.getSystemResources({ sample: !!(opts && opts.sample) });
qvacRes = await s.getSystemResources({ sample: false });
} catch (err) {
qvacRes = { error: err.message };
}
@@ -265,6 +295,7 @@ async function resources(opts) {
if (gpuProbe && gpuProbe.gpus && gpuProbe.gpus[0] && gpuProbe.gpus[0].memory) {
merged.vram = gpuProbe.gpus[0].memory;
}
const bl = backendLabel(merged);
lastResources = {
totalRamBytes: ram,
capabilities: qvacRes && qvacRes.capabilities,
@@ -274,11 +305,31 @@ async function resources(opts) {
drivers: gpuProbe && gpuProbe.drivers,
suggest: catalog.suggestProfile({ totalRamBytes: ram, vramBytes: merged.vram }),
deviceDefault: pickDevice('auto', merged).device,
...backendLabel(merged),
...bl,
};
log(
'hardware ' +
lastResources.deviceDefault +
'/' +
(lastResources.backend || 'none') +
(lastResources.deviceName ? ' ' + lastResources.deviceName : '')
);
return lastResources;
}
async function resources(opts) {
if (!isUserEnabled()) throw disabledError();
const s = await ensureInit();
if (opts && opts.sample && s && typeof s.getSystemResources === 'function') {
try {
const qvacRes = await s.getSystemResources({ sample: true });
if (lastResources) lastResources.sample = qvacRes && qvacRes.sample;
} catch (_) {}
}
if (lastResources && !opts) return lastResources;
return probeHardware(s);
}
async function resolveSrc(s, modelSrc) {
if (modelSrc && typeof modelSrc === 'object') return modelSrc;
const constant = catalog.resolveModelConstant(modelSrc);
@@ -307,7 +358,7 @@ async function load(opts, onProgress) {
const entry = catalog.findCatalogEntry(opts.model || opts.modelSrc || opts.friendlyId);
const modelSrc = await resolveSrc(s, opts.modelSrc || (entry && entry.constant) || opts.model);
const res = lastResources || (await resources({ sample: false }));
const dev = pickDevice(opts.device || 'auto', res);
let dev = pickDevice(opts.device || 'auto', res);
const tools = opts.tools !== false && (!entry || entry.tools !== false);
const ctxSize = opts.ctxSize || opts.ctx_size || (entry && entry.ctxSize) || 8192;
const modelConfig = Object.assign(
@@ -339,8 +390,42 @@ async function load(opts, onProgress) {
}
if (opts.delegate) loadOpts.delegate = opts.delegate;
const modelId = await s.loadModel(loadOpts);
const bl = backendLabel(res);
log(
'load ' +
((entry && entry.id) || opts.model || 'model') +
' device=' +
modelConfig.device +
' ngl=' +
modelConfig.gpu_layers +
' ctx=' +
modelConfig.ctx_size
);
let modelId;
try {
modelId = await s.loadModel(loadOpts);
} catch (err) {
const msg = llamacppHost.flattenError(err);
log('load failed: ' + msg);
const requested = String(opts.device || 'auto').toLowerCase();
if (dev.device === 'gpu' && requested !== 'cpu') {
log('retrying load on cpu');
modelConfig.device = 'cpu';
modelConfig.gpu_layers = 0;
try {
modelId = await s.loadModel(loadOpts);
dev = { device: 'cpu', gpu_layers: 0, fallback: 'cpu' };
} catch (err2) {
throw new Error(llamacppHost.flattenError(err2));
}
} else {
throw new Error(msg);
}
}
const bl =
dev.device === 'cpu'
? Object.assign({}, backendLabel(res), { backend: 'cpu', backendId: 0 })
: backendLabel(res);
loaded = {
modelId,
friendlyId: (entry && entry.id) || opts.model || null,
@@ -549,6 +634,7 @@ async function close() {
}
sdk = null;
initPromise = null;
lastResources = null;
}
function setIdleUnloadMin(min) {
+122
View File
@@ -0,0 +1,122 @@
/**
* Packed Bare extracts the llama addon .bare + backend .so files under /tmp,
* but addon.js still sets backendsDir from bundled __dirname (`bare:/...`).
* C++ cannot dlopen that URL; llama.cpp then runs --fit against a broken
* backend path and init fails with "Failed to initialize model".
*
* Point backendsDir at the extracted prebuilds directory before createInstance.
*/
function posixDirname(p) {
const n = String(p || '').replace(/\\/g, '/').replace(/\/+$/, '');
const i = n.lastIndexOf('/');
if (i < 0) return n;
if (i === 0) return '/';
return n.slice(0, i);
}
function flattenError(err) {
const parts = [];
let cur = err;
for (let n = 0; cur && n < 6; n++) {
const msg = cur && cur.message ? String(cur.message) : cur ? String(cur) : '';
if (msg && parts.indexOf(msg) === -1) parts.push(msg);
cur = cur && cur.cause;
}
return parts.join(' | ') || 'unknown error';
}
function hrefOf(url) {
if (!url) return '';
if (typeof url === 'string') return url;
if (typeof url.href === 'string') return url.href;
try {
return String(url);
} catch (_) {
return '';
}
}
function fileUrlToPath(href) {
if (!href || href.indexOf('file:') !== 0) return null;
try {
const url = require('bare-url');
if (url && typeof url.fileURLToPath === 'function') return url.fileURLToPath(href);
} catch (_) {}
let rest = href.replace(/^file:\/\//, '');
if (/^\/[A-Za-z]:\//.test(rest)) rest = rest.slice(1);
try {
rest = decodeURIComponent(rest);
} catch (_) {}
return rest;
}
/**
* addon.js uses path.join(packageRoot, 'prebuilds').
* Extracted .bare lives at prebuilds/<host>/qvac__llm-llamacpp.bare
*/
function backendsDirFromAddonHref(href) {
const raw = hrefOf(href);
if (!raw || !/qvac__llm-llamacpp|llm-llamacpp/i.test(raw)) return null;
const file = raw.indexOf('file:') === 0 ? fileUrlToPath(raw) : raw.indexOf('bare:') === 0 ? null : raw;
if (!file) return null;
const base = file.replace(/\\/g, '/');
if (!/\.(bare|node)$/i.test(base)) return null;
return posixDirname(posixDirname(file));
}
function resolveBackendsDir() {
try {
const cache = typeof Bare !== 'undefined' && Bare.Addon && Bare.Addon.cache;
if (cache) {
const hrefs = Object.keys(cache);
for (let i = 0; i < hrefs.length; i++) {
const dir = backendsDirFromAddonHref(hrefs[i]);
if (dir) return dir;
const addon = cache[hrefs[i]];
const fromUrl = backendsDirFromAddonHref(addon && addon.url);
if (fromUrl) return fromUrl;
}
}
} catch (_) {}
return null;
}
function patchLlamaInterface(logFn) {
const log = typeof logFn === 'function' ? logFn : function () {};
let addon;
try {
addon = require('@qvac/llm-llamacpp/addon.js');
} catch (err) {
log('llm addon.js unavailable: ' + ((err && err.message) || err));
return false;
}
const Orig = addon && addon.LlamaInterface;
if (!Orig) return false;
if (Orig.__bsBackendsPatched) return true;
function Wrapped(binding, configurationParams, outputCb) {
if (!configurationParams) configurationParams = {};
if (!configurationParams.config) configurationParams.config = {};
const dir = resolveBackendsDir();
if (dir) {
configurationParams.config.backendsDir = dir;
log('llm backendsDir ' + dir);
}
return new Orig(binding, configurationParams, outputCb);
}
Wrapped.prototype = Orig.prototype;
try {
Object.setPrototypeOf(Wrapped, Orig);
} catch (_) {}
Wrapped.__bsBackendsPatched = true;
Orig.__bsBackendsPatched = true;
addon.LlamaInterface = Wrapped;
return true;
}
module.exports = {
flattenError,
backendsDirFromAddonHref,
resolveBackendsDir,
patchLlamaInterface,
};
+21
View File
@@ -0,0 +1,21 @@
function runQvacSelftest(logErr, normalizeBareVersions) {
logErr('qvac-selftest starting');
try {
normalizeBareVersions();
} catch (err) {
logErr('qvac-selftest normalize fail: ' + ((err && err.message) || err));
}
logErr('qvac-selftest importing plugins');
import('@qvac/inference/plugins')
.then((mod) => {
logErr('qvac-selftest ok plugins=' + typeof (mod.plugins || mod.default));
process.exit(0);
})
.catch((err) => {
const msg = String((err && err.message) || err);
logErr('qvac-selftest fail: ' + msg);
process.exit(/INVALID_VERSION/i.test(msg) ? 1 : 0);
});
}
module.exports = { runQvacSelftest };
+5 -31
View File
@@ -15,13 +15,6 @@ function concreteVersion(input) {
return m ? m[1] : input;
}
function coerceEnginesMap(engines) {
if (!engines || typeof engines !== 'object') return engines;
const out = {};
for (const key of Object.keys(engines)) out[key] = concreteVersion(engines[key]);
return out;
}
function log(msg) {
try {
process.stderr.write('[bridge-swarm-qvac] ' + msg + '\n');
@@ -70,24 +63,6 @@ function normalizeBareVersions() {
} catch (_) {}
}
function patchBareModule() {
try {
const Module = typeof module !== 'undefined' && module.constructor;
if (!Module) return;
const wrap = (name) => {
const orig = Module[name];
if (typeof orig !== 'function' || orig.__bsPatched) return;
Module[name] = function patchedModuleFn() {
normalizeBareVersions();
return orig.apply(this, arguments);
};
Module[name].__bsPatched = true;
};
wrap('resolve');
wrap('asset');
} catch (_) {}
}
function install() {
try {
const semver = require('bare-semver');
@@ -103,17 +78,16 @@ function install() {
try {
const resolve = require('bare-module-resolve');
if (typeof resolve.validateEngines === 'function' && !resolve.validateEngines.__bsPatched) {
const orig = resolve.validateEngines;
resolve.validateEngines = function patchedValidateEngines(packageURL, packageEngines, opts) {
const next = Object.assign({}, opts || {});
if (next.engines) next.engines = coerceEnginesMap(next.engines);
return orig(packageURL, packageEngines, next);
resolve.validateEngines = function patchedValidateEngines() {
// Packed Bare.versions.bare can be a range; skip engines checks.
return;
};
resolve.validateEngines.__bsPatched = true;
}
} catch (_) {}
patchBareModule();
// Do not wrap Module.resolve: the packed runtime's resolver is inside
// bare:/bare.bundle and wrapping module.constructor.resolve can stall imports.
normalizeBareVersions();
}
+9
View File
@@ -87,6 +87,15 @@ export async function warmDefaultModules() {
try {
await import('@qvac/inference');
} catch (_) {}
try {
await import('@qvac/inference/plugins');
} catch (_) {}
try {
await import('@qvac/inference/llamacpp-completion/plugin');
} catch (_) {}
try {
await import('@qvac/inference/models');
} catch (_) {}
try {
await import('bare-gpu-info');
} catch (_) {}
-1
View File
@@ -154,7 +154,6 @@ async function testBareVersionsCoerce() {
const { satisfies } = require('bare-semver');
ok(satisfies('^1.30.3', '^1.30.3') === true, 'satisfies coerces caret runtime version');
ok(!!(module.constructor.resolve && module.constructor.resolve.__bsPatched), 'runtime Module.resolve wrapped');
Object.defineProperty(Bare, 'versions', {
configurable: true,
+18
View File
@@ -164,6 +164,7 @@ function patchBundle(bundle) {
}
let jsonFixed = 0;
let enginesStripped = 0;
let keysToProcess = typeof bundle.keys === 'function' ? [...bundle.keys()] : Object.keys(bundle.files || {});
for (const key of keysToProcess) {
if (!key.endsWith('.json')) continue;
@@ -195,6 +196,18 @@ function patchBundle(bundle) {
}
}
if (content && content.length > 0) {
const keyNormSlash = key.replace(/\\/g, '/');
if (/package\.json$/i.test(keyNormSlash)) {
try {
const json = JSON.parse(content.toString());
if (json && json.engines) {
delete json.engines;
content = Buffer.from(JSON.stringify(json));
bundle.write(key, content);
enginesStripped++;
}
} catch (_) {}
}
const keyNoLead = key.replace(/^\/+/, '');
const prefixSlash = 'runtime.bundle/' + keyNoLead;
const prefixLead = '/runtime.bundle/' + keyNoLead;
@@ -207,6 +220,11 @@ function patchBundle(bundle) {
void resolveKey;
if (jsonFixed > 0) console.log(` Patched ${jsonFixed} empty/invalid .json entries`);
if (enginesStripped > 0) {
console.log(
` Stripped engines from ${enginesStripped} package.json files (packed Bare.versions.bare can be a range)`
);
}
console.log(' Added runtime.bundle pathname variants for .json entries');
}
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Send qvac.setEnabled over native messaging and fail on INVALID_VERSION."""
import json
import os
import struct
import subprocess
import sys
import threading
import time
def main():
binpath = sys.argv[1]
env = os.environ.copy()
env.setdefault('BRIDGE_SWARM_STORAGE', '/tmp/bs-qvac-packed-selftest')
proc = subprocess.Popen(
[binpath],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
)
err_chunks = []
def pump_err():
while True:
line = proc.stderr.readline()
if not line:
break
err_chunks.append(line)
sys.stderr.buffer.write(line)
sys.stderr.flush()
threading.Thread(target=pump_err, daemon=True).start()
time.sleep(1.5)
msg = json.dumps(
{
'id': 1,
'type': 'capability',
'payload': {
'pack': 'qvac',
'cmd': 'setEnabled',
'payload': {'enabled': True},
},
}
).encode()
proc.stdin.write(struct.pack('<I', len(msg)) + msg)
proc.stdin.flush()
hdr = proc.stdout.read(4)
if len(hdr) < 4:
print('FAIL: no native-messaging reply', file=sys.stderr)
proc.kill()
return 1
n = struct.unpack('<I', hdr)[0]
body = proc.stdout.read(n).decode('utf-8', 'replace')
print('REPLY', body[:4000])
time.sleep(0.5)
proc.kill()
blob = body + b''.join(err_chunks).decode('utf-8', 'replace')
if 'INVALID_VERSION' in blob:
print('FAIL: packed host still throws INVALID_VERSION', file=sys.stderr)
return 1
print('PASS: no INVALID_VERSION')
return 0
if __name__ == '__main__':
sys.exit(main())
+22
View File
@@ -182,10 +182,32 @@ function testDeviceFallback() {
assert.strictEqual(gpuMissing.fallback, 'cpu');
const metal = device.backendLabel({ drivers: { metal: true }, gpus: [{ name: 'Apple M4', memory: 1 }] });
assert.strictEqual(metal.backend, 'metal');
const vulkan = device.backendLabel({ drivers: { vulkan: true }, gpus: [{ name: 'NVIDIA' }] });
assert.strictEqual(vulkan.backend, 'vulkan');
assert.strictEqual(vulkan.deviceName, 'NVIDIA');
const cpuOnly = device.backendLabel({ drivers: {}, gpus: [] });
assert.strictEqual(cpuOnly.backend, 'cpu');
}
testDeviceFallback();
function testLlamacppHostHelpers() {
const host = require('../native-host/qvac/llamacpp-host.js');
assert.strictEqual(
host.backendsDirFromAddonHref(
'file:///tmp/bridge-swarm-host-abc/node_modules/@qvac/llm-llamacpp/prebuilds/linux-x64/qvac__llm-llamacpp.bare'
),
'/tmp/bridge-swarm-host-abc/node_modules/@qvac/llm-llamacpp/prebuilds'
);
assert.strictEqual(host.backendsDirFromAddonHref('bare:/app.bundle/node_modules/@qvac/llm-llamacpp/addon.js'), null);
const wrapped = new Error('Failed to load model: Failed to initialize model');
wrapped.cause = new Error('failed to load model');
assert.ok(host.flattenError(wrapped).indexOf('Failed to initialize model') !== -1);
assert.ok(host.flattenError(wrapped).indexOf('failed to load model') !== -1);
}
testLlamacppHostHelpers();
function testCompletionEventMap() {
const events = require('../native-host/qvac/events.js');
assert.deepStrictEqual(events.normalizeCompletionEvent({ type: 'contentDelta', text: 'hi' }), {