Updates
Rolling release / release (push) Successful in 9m26s

This commit is contained in:
2026-09-11 21:21:26 -04:00
parent b39a60132a
commit 9e76928045
10 changed files with 315 additions and 63 deletions
+4 -1
View File
@@ -25,6 +25,7 @@ export async function serveOnSessionBus(daemon) {
async Say(text) { await daemon.say?.(text); }
async Ask(text) { await daemon.ask(text); }
async ResetContext() { await daemon.resetContext(); }
Confirm(jobId, toolCallId, decision) { daemon.confirmPermission?.(jobId, toolCallId, decision); }
Cancel() { daemon.cancel(); }
SetMode(mode) { daemon.mode = mode; daemon.emit('ModeChanged', mode); }
GetState() { return daemon.state; }
@@ -42,6 +43,7 @@ export async function serveOnSessionBus(daemon) {
// private $emitter. Returning the payload is sufficient; calling
// `this.emit` here crashes because Interface does not expose that method.
ConfirmationRequired(tool, args, pattern) { return [String(tool), String(args), String(pattern)]; }
ContextReset() { return; }
StateChanged(state) { return String(state); }
Reply(text) { return String(text); }
Token(text) { return String(text); }
@@ -68,6 +70,7 @@ export async function serveOnSessionBus(daemon) {
Say: { inSignature: 's', outSignature: '', method: 'Say' },
Ask: { inSignature: 's', outSignature: '', method: 'Ask' },
ResetContext: { inSignature: '', outSignature: '', method: 'ResetContext' },
Confirm: { inSignature: 'sss', outSignature: '', method: 'Confirm' },
Cancel: { inSignature: '', outSignature: '', method: 'Cancel' },
SetMode: { inSignature: 's', outSignature: '', method: 'SetMode' },
GetState: { inSignature: '', outSignature: 's', method: 'GetState' },
@@ -141,7 +144,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'));
daemon.on('ConfirmationRequired', (event) => iface.ConfirmationRequired(event?.tool || 'action', JSON.stringify({ jobId: event?.jobId || '', toolCallId: event?.toolCallId || '', args: event?.args || {} }), event?.pattern || 'explicit confirmation required'));
for (const signal of ['ContextReset', 'WakeHeard', 'PartialTranscript', 'FinalTranscript', 'SpeakingLevel', 'ListeningLevel', 'ChipOffered', 'JobProgress', 'ComputerStep', 'ComputerHighlight', 'Thinking', 'ToolCall', 'ToolResult']) {
daemon.on(signal, (...args) => iface[signal](...args));
}
+10 -3
View File
@@ -46,15 +46,22 @@ export class HarnessBridge extends EventEmitter {
// omit it from the final envelope after a tool call. Keep the current
// post-tool stream as a fallback so the daemon can still speak the reply.
let streamed = '';
const onChunk = (payload) => { streamed += String(payload?.text || payload?.delta || ''); };
const onToolCall = () => { streamed = ''; };
let postTool = '';
let sawTool = false;
const onChunk = (payload) => {
const chunk = String(payload?.text || payload?.delta || '');
streamed += chunk;
if (sawTool) postTool += chunk;
};
const onToolCall = () => { sawTool = true; postTool = ''; streamed = ''; };
const chunkSubscription = this.session.on('agent_message_chunk', onChunk);
const toolSubscription = this.session.on('tool_call', onToolCall);
const offChunk = typeof chunkSubscription === 'function' ? chunkSubscription : () => this.session.off?.('agent_message_chunk', onChunk);
const offToolCall = typeof toolSubscription === 'function' ? toolSubscription : () => this.session.off?.('tool_call', onToolCall);
try {
const reply = await this.session.prompt(text);
if (reply && !reply.text && streamed.trim()) return { ...reply, text: streamed };
const spoken = String(reply?.text || '').trim() || postTool.trim() || streamed.trim();
if (reply && spoken && spoken !== String(reply.text || '').trim()) return { ...reply, text: spoken };
return reply;
} finally {
offChunk?.();
+4
View File
@@ -68,6 +68,7 @@ export class JarvisDaemon extends EventEmitter {
const reply = await job;
this.telemetry.record('llm', startedAt, { success: true });
const spoken = spokenReply(reply);
if (!spoken) { this.setState('LISTENING'); return ''; }
this._beginSpeech(); this.lastReply = spoken; this.emit('Reply', spoken); this._speakReply(spoken); return spoken;
} catch (error) {
this.telemetry.record('llm', Date.now(), { success: false }); this.emit('Error', 'QVAC', error.message); this.voice.cancel(); this.setState('ARMED'); throw error;
@@ -86,6 +87,9 @@ export class JarvisDaemon extends EventEmitter {
this.setState('ARMED');
this.emit('ContextReset');
}
confirmPermission(jobId, toolCallId, decision) {
this.harness.session?.permit?.(String(jobId || ''), String(toolCallId || ''), String(decision || 'deny'));
}
_beginSpeech() {
try {
if (this.voice.state === 'ARMED' || this.voice.state === 'SLEEPING') this.voice.wake();
+6 -3
View File
@@ -5,9 +5,12 @@ import os from 'node:os';
export function voiceSettings() {
let config = {};
try { config = JSON.parse(fs.readFileSync(path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'jarvis/config.json'), 'utf8')); } catch {}
const phrase = config.wakePhrase || config.wake_phrase || 'hey jarvis';
const aliases = Array.isArray(config.aliases) ? config.aliases : ['jarvis', 'okay jarvis'];
return {
command: process.env.JARVIS_WAKE_COMMAND || config.wake_command || '',
phrases: [config.wake_phrase || 'hey jarvis', 'jarvis', 'okay jarvis'],
ttsEnabled: config.tts_enabled !== false,
command: process.env.JARVIS_WAKE_COMMAND || config.wakeCommand || config.wake_command || '',
phrases: [phrase, ...aliases].map((item) => String(item || '').trim()).filter(Boolean),
ttsEnabled: config.ttsEnabled !== false && config.tts_enabled !== false,
modelProfile: config.modelProfile || config.model_profile || 'laptop-16gb',
};
}