Updates
jarvis-ci / node (push) Failing after 2m33s

This commit is contained in:
2026-09-11 14:00:44 -04:00
parent 8f6884ac42
commit 68d7a40605
12 changed files with 204 additions and 29 deletions
+3
View File
@@ -21,6 +21,7 @@ export async function serveOnSessionBus(daemon) {
ComputerGrant(persist) { daemon.computerGrant(persist); }
ComputerRevoke() { daemon.computerRevoke(); }
ComputerStatus() { return JSON.stringify(daemon.computer.status()); }
ConfirmationRequired(tool, args, pattern) { this.emit('ConfirmationRequired', String(tool), String(args), String(pattern)); }
StateChanged(state) { this.emit('StateChanged', state); }
Reply(text) { this.emit('Reply', String(text)); }
Token(text) { this.emit('Token', String(text)); }
@@ -63,6 +64,7 @@ export async function serveOnSessionBus(daemon) {
JobProgress: { signature: 'sds', signal: true },
ComputerStep: { signature: 's', signal: true },
ComputerHighlight: { signature: 's', signal: true },
ConfirmationRequired: { signature: 'sss', signal: true },
});
const bus = dbus.sessionBus();
await bus.requestName(BUS_NAME);
@@ -72,6 +74,7 @@ export async function serveOnSessionBus(daemon) {
daemon.on('Reply', (reply) => iface.Reply(reply));
daemon.on('Token', (token) => iface.Token(token));
daemon.on('Error', (code, message) => iface.Error(code, message));
daemon.on('ConfirmationRequired', (event) => iface.ConfirmationRequired(event?.tool || 'action', JSON.stringify(event?.args || {}), event?.pattern || 'explicit confirmation required'));
for (const signal of ['WakeHeard', 'PartialTranscript', 'FinalTranscript', 'SpeakingLevel', 'ListeningLevel', 'ChipOffered', 'JobProgress', 'ComputerStep', 'ComputerHighlight']) {
daemon.on(signal, (...args) => iface[signal](...args));
}
+2
View File
@@ -1,4 +1,5 @@
import { Agent, assertSdkVersion } from './qvac-master.js';
import { MODEL_PROFILES } from './model-profiles.js';
try {
const sdkVersion = assertSdkVersion();
@@ -11,6 +12,7 @@ try {
gpus: resources.gpus || [],
vramBytes: resources.vramBytes || 0,
sdkVersion,
profiles: MODEL_PROFILES,
}, null, 2));
if (!hasGpu) {
console.error('gpu-doctor: QVAC did not observe a usable GPU; Jarvis will refuse CPU inference');
+11 -3
View File
@@ -4,6 +4,7 @@ import { ComputerUseSession } from '../computer-use/session.js';
import { VoiceStateMachine } from './voice-state.js';
import { QvacScheduler } from './qvac-scheduler.js';
import { cancelQvac, resumeQvac, suspendQvac } from './qvac-master.js';
import { PrivacyLog } from './privacy-log.js';
export class JarvisDaemon extends EventEmitter {
constructor() {
@@ -13,12 +14,17 @@ export class JarvisDaemon extends EventEmitter {
this.scheduler = new QvacScheduler({ concurrency: 1 });
this.computer = new ComputerUseSession();
this.harness = new HarnessBridge({ cwd: process.cwd(), computer: this.computer });
this.log = new PrivacyLog();
this.locked = false;
this._idleTimer = setInterval(() => this.tickIdle(), 30_000);
this._idleTimer.unref?.();
this.harness.on('agent_message_chunk', (ev) => this.emit('Token', ev?.text || ev?.delta || ''));
this.harness.on('permission', (ev) => this.emit('ConfirmationRequired', ev));
this.harness.on('hud_sidecar', (ev) => this.emit('ChipOffered', 'sidecar', ev?.title || 'Suggested action', JSON.stringify(ev || {})));
}
setState(state) { this.state = state; this.emit('StateChanged', state); }
async arm() { await resumeQvac().catch(() => {}); this.voice.wake(); this.setState('LISTENING'); }
setState(state) { this.state = state; this.emit('StateChanged', state); this.log.record('state', { state }).catch(() => {}); }
async arm() { if (this.locked) return; await resumeQvac().catch(() => {}); this.voice.wake(); this.setState('LISTENING'); }
async sleep() { this.voice.sleep(); this.setState('SLEEPING'); await suspendQvac().catch((error) => this.emit('Error', 'QVAC_SUSPEND', error.message)); }
say(text) { this.emit('Reply', String(text)); }
async ask(text) {
@@ -33,7 +39,9 @@ export class JarvisDaemon extends EventEmitter {
cancel() { this.harness.cancel(); this.scheduler.cancelQueued((job) => job.lane === 'voice'); this.computer.revoke(); this.voice.cancel(); this.setState('ARMED'); cancelQvac().catch((error) => this.emit('Error', 'QVAC_CANCEL', error.message)); }
computerGrant(persist = false) { const result = this.computer.grant({ persist }); this.emit('ComputerStep', JSON.stringify({ action: 'grant', ...result })); return result; }
computerRevoke() { this.computer.revoke(); this.emit('ComputerStep', JSON.stringify({ action: 'revoke' })); }
async close() { this.computerRevoke(); await this.harness.close(); }
handleLockScreen(locked) { this.locked = Boolean(locked); if (this.locked) { this.cancel(); this.setState('ARMED'); } this.emit('LockScreenChanged', this.locked); }
tickIdle() { if (!this.locked && this.voice.expireIdle() === 'SLEEPING' && this.state !== 'SLEEPING') this.sleep(); }
async close() { clearInterval(this._idleTimer); this.computerRevoke(); await this.harness.close(); }
}
if (import.meta.url === `file://${process.argv[1]}`) {
+35
View File
@@ -0,0 +1,35 @@
import net from 'node:net';
import fs from 'node:fs';
import path from 'node:path';
import { mkdir, rm } from 'node:fs/promises';
const MAX_LINE = 64 * 1024;
export async function createEventSocket({ socketPath, onMessage }) {
await mkdir(path.dirname(socketPath), { recursive: true });
try { await rm(socketPath, { force: true }); } catch {}
const server = net.createServer((socket) => {
let buffer = '';
socket.setEncoding('utf8');
socket.on('data', (chunk) => {
buffer += chunk;
if (buffer.length > MAX_LINE * 2) { socket.destroy(new Error('IPC buffer too large')); return; }
let index;
while ((index = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, index); buffer = buffer.slice(index + 1);
if (line.length > MAX_LINE) { socket.destroy(new Error('IPC message too large')); return; }
try { onMessage?.(JSON.parse(line), socket); } catch { socket.write(JSON.stringify({ error: 'invalid IPC message' }) + '\n'); }
}
});
});
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(socketPath, resolve); });
return { server, socketPath, close: () => new Promise((resolve) => server.close(() => fs.unlink(socketPath, () => resolve()))) };
}
export function sendEvent(socket, event, payload = {}) {
const message = JSON.stringify({ event, ...payload });
if (Buffer.byteLength(message) > MAX_LINE) throw new Error('IPC event too large; use a file path for bulk data');
socket.write(`${message}\n`);
}
export { MAX_LINE };
+11
View File
@@ -0,0 +1,11 @@
export const MODEL_PROFILES = Object.freeze({
'laptop-8gb': { model: 'qwen3-1.7b', minRamGb: 8, vision: false },
'laptop-16gb': { model: 'qwen3.5-4b', minRamGb: 10, vision: true },
'desktop-gpu': { model: 'qwen3.5-9b', minRamGb: 16, vision: true },
});
export function profile(name = 'laptop-16gb') {
const selected = MODEL_PROFILES[name];
if (!selected) throw new Error(`unknown Jarvis model profile: ${name}`);
return { name, ...selected };
}
+22
View File
@@ -0,0 +1,22 @@
import { appendFile, mkdir } from 'node:fs/promises';
import path from 'node:path';
const ALLOWED = new Set(['event', 'state', 'code', 'lane', 'jobId', 'durationMs', 'success', 'reason']);
export class PrivacyLog {
constructor({ file = path.join(process.env.XDG_STATE_HOME || path.join(process.env.HOME || '/tmp', '.local/state'), 'jarvis/events.jsonl') } = {}) {
this.file = file;
}
async record(event, fields = {}) {
const safe = { ts: new Date().toISOString(), event };
for (const [key, value] of Object.entries(fields)) {
if (ALLOWED.has(key) && (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')) safe[key] = value;
}
await mkdir(path.dirname(this.file), { recursive: true });
await appendFile(this.file, `${JSON.stringify(safe)}\n`, 'utf8');
return safe;
}
}
export { ALLOWED };
+7
View File
@@ -67,6 +67,13 @@ export async function cancelQvac() {
await Agent.engine.cancel();
}
export async function cancelQvacRequest({ requestId, modelId, kind } = {}) {
if (!requestId && !modelId) throw new Error('requestId or modelId is required');
const sdk = await Agent.engine.ensureInit();
if (typeof sdk.cancel !== 'function') throw new Error('QVAC runtime does not expose cancel()');
await sdk.cancel(requestId ? { requestId } : { modelId, kind });
}
export async function suspendQvac() {
const sdk = await Agent.engine.ensureInit();
if (typeof sdk.suspend !== 'function') throw new Error('QVAC runtime does not expose suspend()');