Fix QVAC Worker
CI / test (push) Successful in 2m40s
Release rolling / release (push) Successful in 11m42s

This commit is contained in:
Raven Scott
2026-07-30 16:01:34 -04:00
parent 726dbdfd56
commit 2007ff6ad3
5 changed files with 291 additions and 16 deletions
+15
View File
@@ -100,6 +100,21 @@ npm run ensure:qvac-bare-runtimes # force-install all QVAC host binaries
npm run make:client:darwin-arm64
```
### Worker RPC timeout (`RPC initialization timed out after 30000ms`)
`bundleSdk` writes **absolute** `file:///build-machine/...` imports into
`qvac/worker.entry.mjs`. Off the build host Bare cannot load them, so the worker
never handshakes.
Fix (already wired into forge + `build-qvac-worker`):
```bash
node scripts/rewrite-qvac-worker-entry.cjs --force-portable
```
That rewrites the entry to **relative** `../node_modules/@qvac/sdk/...` imports
so the packaged app is self-contained.
### Native host matrix (LLM prebuilds)
`@qvac/llm-llamacpp` ships Bare prebuilds for:
+61
View File
@@ -483,6 +483,16 @@ module.exports = {
`[forge] failed to ensure bare-runtime-${packageHost} for QVAC: ${err?.message || err}`
)
}
// bundleSdk (QvacForgePlugin) may regenerate absolute file:// imports —
// always re-write a portable entry before packaging copies files.
try {
require('./scripts/rewrite-qvac-worker-entry.cjs').ensurePortableWorkerEntry({
root: __dirname,
forcePortable: true,
})
} catch (err) {
console.warn('[forge] prePackage portable worker rewrite:', err?.message || err)
}
}
if (process.env.PEARDATA_SKIP_PREPACKAGE_BUNDLE === '1') {
@@ -533,6 +543,23 @@ module.exports = {
)
}
// Portable worker.entry.mjs: strip absolute file:///build-machine paths
// (bundleSdk emits those; Bare then dies → RPC init timeout 30s)
if (qvacEnabled) {
try {
const { ensurePortableWorkerEntry } = require('./scripts/rewrite-qvac-worker-entry.cjs')
const wr = ensurePortableWorkerEntry({
root: buildPath,
forcePortable: true,
})
console.log(
`[forge] portable QVAC worker entry: ${wr.reason || 'ok'}${wr.path}`
)
} catch (err) {
console.warn('[forge] rewrite-qvac-worker-entry failed:', err?.message || err)
}
}
// --- bare-runtime platform binary (QVAC issue #1492) -------------------
// If the platform package was missed by ignore/prune, copy it from the
// project tree. Fail hard when QVAC is enabled so CI never ships a
@@ -565,6 +592,40 @@ module.exports = {
}
console.log(`[forge] verified bare-runtime-${host} binary in package`)
}
// Fail if worker still has build-machine absolute imports
if (qvacEnabled) {
const entry = path.join(buildPath, 'qvac', 'worker.entry.mjs')
if (fs.existsSync(entry)) {
const body = fs.readFileSync(entry, 'utf8')
if (/file:\/\/\/Users\/|file:\/\/\/home\/|file:\/\/\/[A-Za-z]:\//.test(body)) {
throw new Error(
`[forge] qvac/worker.entry.mjs still has absolute file:// imports — ` +
`Bare worker will time out on other machines. Run: ` +
`node scripts/rewrite-qvac-worker-entry.cjs --force-portable`
)
}
console.log('[forge] verified portable qvac/worker.entry.mjs (no absolute file://)')
} else {
throw new Error(
`[forge] missing qvac/worker.entry.mjs in package — Bare RPC cannot start`
)
}
// Ensure @qvac/sdk paths the worker imports exist
const needFiles = [
'node_modules/@qvac/sdk/dist/server/worker-core.js',
'node_modules/@qvac/sdk/dist/server/plugins/index.js',
'node_modules/@qvac/sdk/dist/logging/index.js',
'node_modules/@qvac/sdk/dist/server/bare/plugins/llamacpp-completion/plugin.js',
]
for (const rel of needFiles) {
if (!fs.existsSync(path.join(buildPath, rel))) {
throw new Error(
`[forge] QVAC package missing ${rel} (required by portable worker entry)`
)
}
}
}
},
postPackage: async (_forgeConfig, options) => {
const platform = options.platform || process.platform
+15 -16
View File
@@ -1,26 +1,25 @@
/**
* QVAC SDK Worker Entry (auto-generated)
* Generated by: @qvac/sdk/commands bundleSdk
* Plugins: 1
* QVAC SDK Worker Entry (portable)
* Relative imports only — safe for packaged Electron / CI clients.
* Plugins: 1 — @qvac/sdk/llamacpp-completion/plugin
*
* - @qvac/sdk/llamacpp-completion/plugin
* Regenerated by scripts/rewrite-qvac-worker-entry.cjs
* (bundleSdk emits absolute file:// URLs that break off the build machine)
*/
import { initializeWorkerCore, ensureRPCSetup } from "file:///Users/raven/dev/peardata/node_modules/@qvac/sdk/dist/server/worker-core.js";
import { registerPlugin } from "file:///Users/raven/dev/peardata/node_modules/@qvac/sdk/dist/server/plugins/index.js";
import { getServerLogger } from "file:///Users/raven/dev/peardata/node_modules/@qvac/sdk/dist/logging/index.js";
import { initializeWorkerCore, ensureRPCSetup } from '../node_modules/@qvac/sdk/dist/server/worker-core.js'
import { registerPlugin } from '../node_modules/@qvac/sdk/dist/server/plugins/index.js'
import { getServerLogger } from '../node_modules/@qvac/sdk/dist/logging/index.js'
import { llmPlugin } from '../node_modules/@qvac/sdk/dist/server/bare/plugins/llamacpp-completion/plugin.js'
import { llmPlugin } from "file:///Users/raven/dev/peardata/node_modules/@qvac/sdk/dist/server/bare/plugins/llamacpp-completion/plugin.js";
const { hasRPCConfig } = initializeWorkerCore()
const { hasRPCConfig } = initializeWorkerCore();
const logger = getServerLogger()
logger.info('🐻 QVAC Worker (portable bundle)')
logger.info('📦 Plugins: 1 (llamacpp-completion)')
const logger = getServerLogger();
logger.info("🐻 QVAC Worker (custom bundle)");
logger.info("📦 Plugins: 1");
registerPlugin(llmPlugin)
registerPlugin(llmPlugin);
// Auto-setup RPC if config present
if (hasRPCConfig) {
ensureRPCSetup();
ensureRPCSetup()
}
+3
View File
@@ -51,6 +51,9 @@ async function main() {
`[build-qvac-worker] ok addons=${(result.addons || []).length} bundle=${result.bundlePath || '(default)'}`
)
// bundleSdk writes absolute file:// imports — rewrite for packaged / CI clients
require('./rewrite-qvac-worker-entry.cjs').ensurePortableWorkerEntry({ root })
if (typeof commands.verifyBundle === 'function') {
const verify = await commands.verifyBundle({
projectRoot: root,
+197
View File
@@ -0,0 +1,197 @@
#!/usr/bin/env node
/**
* Make qvac/worker.entry.mjs portable for packaged Electron / CI clients.
*
* `@qvac/sdk` bundleSdk writes absolute `file:///…/node_modules/@qvac/…` imports
* so bare-pack can resolve addons on the *build* machine. Those paths break as
* soon as the app is shipped elsewhere → Bare worker dies →
* "RPC initialization timed out after 30000ms — the worker process may have failed to start"
*
* This rewrites those URLs to relative paths from qvac/worker.entry.mjs, e.g.
* ../node_modules/@qvac/sdk/dist/server/worker-core.js
*
* Usage:
* node scripts/rewrite-qvac-worker-entry.cjs
* node scripts/rewrite-qvac-worker-entry.cjs --entry /path/to/qvac/worker.entry.mjs
* node scripts/rewrite-qvac-worker-entry.cjs --root /path/to/app
*/
'use strict'
const fs = require('fs')
const path = require('path')
const { pathToFileURL, fileURLToPath } = require('url')
const rootDefault = path.resolve(__dirname, '..')
/**
* @param {string} entryPath absolute path to worker.entry.mjs
* @returns {{ rewritten: number, path: string, ok: boolean, reason?: string }}
*/
function rewriteWorkerEntry(entryPath) {
if (!fs.existsSync(entryPath)) {
return { rewritten: 0, path: entryPath, ok: false, reason: 'missing' }
}
const original = fs.readFileSync(entryPath, 'utf8')
const entryDir = path.dirname(entryPath)
let count = 0
const next = original.replace(/file:\/\/\/[^\s"'`]+|file:\/\/[^\s"'`]+/g, (href) => {
try {
let filePath
try {
filePath = fileURLToPath(href)
} catch {
return href
}
if (!fs.existsSync(filePath)) {
// Still rewrite if it points under a known node_modules layout
// (CI may rewrite before files are fully staged)
}
let rel = path.relative(entryDir, filePath)
if (!rel || rel === path.basename(filePath)) {
// same dir
rel = `./${path.basename(filePath)}`
} else if (!rel.startsWith('.') && !path.isAbsolute(rel)) {
rel = `./${rel}`
}
// ESM wants POSIX separators
rel = rel.split(path.sep).join('/')
// Prefer bare relative specifier (no file://) — Bare resolves these
count++
return rel
} catch {
return href
}
})
if (count === 0 && !/file:\/\//.test(original)) {
// Already portable — ensure relative @qvac paths exist or write canonical entry
if (
original.includes('../node_modules/@qvac/sdk/') ||
original.includes('@qvac/sdk/')
) {
return { rewritten: 0, path: entryPath, ok: true, reason: 'already-portable' }
}
}
if (next !== original) {
fs.writeFileSync(entryPath, next, 'utf8')
}
// Guard: no absolute build-machine paths left
if (/file:\/\/\/Users\/|file:\/\/\/home\/|file:\/\/\/[A-Za-z]:\//.test(next)) {
return {
rewritten: count,
path: entryPath,
ok: false,
reason: 'absolute file:// paths remain after rewrite',
}
}
return { rewritten: count, path: entryPath, ok: true }
}
/**
* Write a known-good portable LLM-only worker entry (matches qvac.config.json).
* Used when rewrite fails or entry is missing.
* @param {string} entryPath
*/
function writePortableLlmWorkerEntry(entryPath) {
const content = `/**
* QVAC SDK Worker Entry (portable)
* Relative imports only — safe for packaged Electron / CI clients.
* Plugins: 1 — @qvac/sdk/llamacpp-completion/plugin
*
* Regenerated by scripts/rewrite-qvac-worker-entry.cjs
* (bundleSdk emits absolute file:// URLs that break off the build machine)
*/
import { initializeWorkerCore, ensureRPCSetup } from '../node_modules/@qvac/sdk/dist/server/worker-core.js'
import { registerPlugin } from '../node_modules/@qvac/sdk/dist/server/plugins/index.js'
import { getServerLogger } from '../node_modules/@qvac/sdk/dist/logging/index.js'
import { llmPlugin } from '../node_modules/@qvac/sdk/dist/server/bare/plugins/llamacpp-completion/plugin.js'
const { hasRPCConfig } = initializeWorkerCore()
const logger = getServerLogger()
logger.info('🐻 QVAC Worker (portable bundle)')
logger.info('📦 Plugins: 1 (llamacpp-completion)')
registerPlugin(llmPlugin)
if (hasRPCConfig) {
ensureRPCSetup()
}
`
fs.mkdirSync(path.dirname(entryPath), { recursive: true })
fs.writeFileSync(entryPath, content, 'utf8')
return entryPath
}
/**
* @param {{ root?: string, entry?: string, forcePortable?: boolean }} [opts]
*/
function ensurePortableWorkerEntry(opts = {}) {
const root = opts.root || rootDefault
const entryPath =
opts.entry || path.join(root, 'qvac', 'worker.entry.mjs')
if (opts.forcePortable || !fs.existsSync(entryPath)) {
writePortableLlmWorkerEntry(entryPath)
console.log(`[rewrite-qvac-worker] wrote portable entry: ${entryPath}`)
return { ok: true, path: entryPath, rewritten: -1, reason: 'wrote-portable' }
}
const result = rewriteWorkerEntry(entryPath)
if (!result.ok && result.reason !== 'missing') {
console.warn(
`[rewrite-qvac-worker] rewrite incomplete (${result.reason}) — writing portable LLM entry`
)
writePortableLlmWorkerEntry(entryPath)
return { ok: true, path: entryPath, rewritten: result.rewritten, reason: 'fallback-portable' }
}
if (result.reason === 'missing') {
writePortableLlmWorkerEntry(entryPath)
return { ok: true, path: entryPath, rewritten: -1, reason: 'wrote-portable' }
}
console.log(
`[rewrite-qvac-worker] ${entryPath}: rewritten=${result.rewritten} (${result.reason || 'ok'})`
)
return result
}
function parseArgs(argv) {
/** @type {{ root?: string, entry?: string, forcePortable?: boolean }} */
const opts = {}
for (let i = 0; i < argv.length; i++) {
const a = argv[i]
if (a === '--root' && argv[i + 1]) opts.root = path.resolve(argv[++i])
else if (a.startsWith('--root=')) opts.root = path.resolve(a.slice(7))
else if (a === '--entry' && argv[i + 1]) opts.entry = path.resolve(argv[++i])
else if (a.startsWith('--entry=')) opts.entry = path.resolve(a.slice(8))
else if (a === '--force-portable') opts.forcePortable = true
else if (a === '--help' || a === '-h') {
console.log(
'Usage: node scripts/rewrite-qvac-worker-entry.cjs [--root DIR] [--entry FILE] [--force-portable]'
)
process.exit(0)
}
}
return opts
}
if (require.main === module) {
const result = ensurePortableWorkerEntry(parseArgs(process.argv.slice(2)))
if (!result.ok) {
console.error('[rewrite-qvac-worker] FAILED', result)
process.exit(1)
}
}
module.exports = {
rewriteWorkerEntry,
writePortableLlmWorkerEntry,
ensurePortableWorkerEntry,
pathToFileURL,
}