Fix QVAC Worker
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user