omnius 1.0.430 → 1.0.431

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -330392,7 +330392,7 @@ ${lanes.join("\n")}
330392
330392
  }
330393
330393
  return false;
330394
330394
  }
330395
- function createPropertyDescriptor(attributes, singleLine) {
330395
+ function createPropertyDescriptor(attributes, singleLine2) {
330396
330396
  const properties = [];
330397
330397
  tryAddPropertyAssignment(properties, "enumerable", asExpression(attributes.enumerable));
330398
330398
  tryAddPropertyAssignment(properties, "configurable", asExpression(attributes.configurable));
@@ -330401,7 +330401,7 @@ ${lanes.join("\n")}
330401
330401
  let isAccessor2 = tryAddPropertyAssignment(properties, "get", attributes.get);
330402
330402
  isAccessor2 = tryAddPropertyAssignment(properties, "set", attributes.set) || isAccessor2;
330403
330403
  Debug.assert(!(isData && isAccessor2), "A PropertyDescriptor may not be both an accessor descriptor and a data descriptor.");
330404
- return createObjectLiteralExpression(properties, !singleLine);
330404
+ return createObjectLiteralExpression(properties, !singleLine2);
330405
330405
  }
330406
330406
  function updateOuterExpression(outerExpression, expression) {
330407
330407
  switch (outerExpression.kind) {
@@ -343871,9 +343871,9 @@ ${lanes.join("\n")}
343871
343871
  }
343872
343872
  return;
343873
343873
  }
343874
- const singleLine = range.kind === 2 && singleLinePragmaRegEx.exec(text2);
343875
- if (singleLine) {
343876
- return addPragmaForMatch(pragmas, range, 2, singleLine);
343874
+ const singleLine2 = range.kind === 2 && singleLinePragmaRegEx.exec(text2);
343875
+ if (singleLine2) {
343876
+ return addPragmaForMatch(pragmas, range, 2, singleLine2);
343877
343877
  }
343878
343878
  if (range.kind === 3) {
343879
343879
  const multiLinePragmaRegEx = /@(\S+)(\s+(?:\S.*)?)?$/gm;
@@ -419394,7 +419394,7 @@ ${lanes.join("\n")}
419394
419394
  }
419395
419395
  function transformFunctionBody2(node) {
419396
419396
  let multiLine = false;
419397
- let singleLine = false;
419397
+ let singleLine2 = false;
419398
419398
  let statementsLocation;
419399
419399
  let closeBraceLocation;
419400
419400
  const prologue = [];
@@ -419436,7 +419436,7 @@ ${lanes.join("\n")}
419436
419436
  const equalsGreaterThanToken = node.equalsGreaterThanToken;
419437
419437
  if (!nodeIsSynthesized(equalsGreaterThanToken) && !nodeIsSynthesized(body)) {
419438
419438
  if (rangeEndIsOnSameLineAsRangeStart(equalsGreaterThanToken, body, currentSourceFile)) {
419439
- singleLine = true;
419439
+ singleLine2 = true;
419440
419440
  } else {
419441
419441
  multiLine = true;
419442
419442
  }
@@ -419465,7 +419465,7 @@ ${lanes.join("\n")}
419465
419465
  }
419466
419466
  const block = factory2.createBlock(setTextRange(factory2.createNodeArray(statements), statementsLocation), multiLine);
419467
419467
  setTextRange(block, node.body);
419468
- if (!multiLine && singleLine) {
419468
+ if (!multiLine && singleLine2) {
419469
419469
  setEmitFlags(
419470
419470
  block,
419471
419471
  1
@@ -603393,6 +603393,10 @@ function defaultConsensusModel(primaryModel) {
603393
603393
  if (model === "large-v3" || model === "large") return "small";
603394
603394
  return "base";
603395
603395
  }
603396
+ function asrConsensusEnabled() {
603397
+ const consensusEnv = String(process.env["OMNIUS_ASR_CONSENSUS"] ?? "").trim().toLowerCase();
603398
+ return !(consensusEnv === "0" || consensusEnv === "off" || consensusEnv === "false");
603399
+ }
603396
603400
  async function ensureVenvForTranscribeCli() {
603397
603401
  const bin = process.platform === "win32" ? "Scripts" : "bin";
603398
603402
  const exe = process.platform === "win32" ? "python.exe" : "python3";
@@ -603895,8 +603899,19 @@ var init_listen = __esm({
603895
603899
  updateListenLiveState({ phase: "error", lastStatus: "no mic capture tool (arecord/sox/ffmpeg)" });
603896
603900
  return "No microphone capture tool found. Install arecord (Linux), sox (macOS), or ffmpeg.";
603897
603901
  }
603898
- let tc = await this.loadTranscribeCli();
603899
- if (!tc) {
603902
+ const preferWhisperFallback = asrConsensusEnabled();
603903
+ if (preferWhisperFallback) {
603904
+ this.emit(
603905
+ "info",
603906
+ "ASR consensus enabled — using live-whisper.py (CUDA when available)."
603907
+ );
603908
+ updateListenLiveState({
603909
+ backend: "openai-whisper",
603910
+ lastStatus: "starting consensus Whisper backend..."
603911
+ });
603912
+ }
603913
+ let tc = preferWhisperFallback ? null : await this.loadTranscribeCli();
603914
+ if (!preferWhisperFallback && !tc) {
603900
603915
  if (_bgInstallPromise) {
603901
603916
  this.emit("info", "Waiting for transcribe-cli install...");
603902
603917
  await _bgInstallPromise;
@@ -604174,13 +604189,14 @@ transcribe-cli error: ${transcribeCliError}` : "";
604174
604189
  * Caller is responsible for calling .stop() when done.
604175
604190
  */
604176
604191
  async createCallTranscriber() {
604177
- let tc = await this.loadTranscribeCli();
604178
- if (!tc && _bgInstallPromise) {
604192
+ const requireConsensus = asrConsensusEnabled();
604193
+ let tc = requireConsensus ? null : await this.loadTranscribeCli();
604194
+ if (!requireConsensus && !tc && _bgInstallPromise) {
604179
604195
  await _bgInstallPromise;
604180
604196
  this.transcribeCliAvailable = null;
604181
604197
  tc = await this.loadTranscribeCli();
604182
604198
  }
604183
- if (!tc) {
604199
+ if (!requireConsensus && !tc) {
604184
604200
  try {
604185
604201
  await ensureManagedTranscribeCliNode();
604186
604202
  this.transcribeCliAvailable = null;
@@ -604188,7 +604204,7 @@ transcribe-cli error: ${transcribeCliError}` : "";
604188
604204
  } catch {
604189
604205
  }
604190
604206
  }
604191
- if (tc?.TranscribeLive && await ensureVenvForTranscribeCli()) {
604207
+ if (!requireConsensus && tc?.TranscribeLive && await ensureVenvForTranscribeCli()) {
604192
604208
  try {
604193
604209
  const transcriber = new tc.TranscribeLive({
604194
604210
  model: this.config.model,
@@ -617275,6 +617291,7 @@ let micStream = null;
617275
617291
  let scriptProcessor = null;
617276
617292
  let micActive = false;
617277
617293
  let playbackQueue = [];
617294
+ let pendingTtsHeader = null;
617278
617295
  let isPlaying = false;
617279
617296
  let reconnectDelay = 1000;
617280
617297
  let reconnectTimer = null;
@@ -617334,10 +617351,14 @@ function connect() {
617334
617351
  waveState = 'speaking';
617335
617352
  } else if (msg.type === 'speaking_end') {
617336
617353
  waveState = micActive ? 'listening' : 'idle';
617354
+ } else if (msg.type === 'tts_header') {
617355
+ pendingTtsHeader = { sampleRate: msg.sampleRate || 22050 };
617337
617356
  }
617338
617357
  } catch {}
617339
617358
  } else {
617340
- playbackQueue.push(new Int16Array(evt.data));
617359
+ const sampleRate = (pendingTtsHeader && pendingTtsHeader.sampleRate) || 22050;
617360
+ pendingTtsHeader = null;
617361
+ playbackQueue.push({ pcm: new Int16Array(evt.data), sampleRate });
617341
617362
  if (!isPlaying) drainPlayback();
617342
617363
  }
617343
617364
  };
@@ -617356,15 +617377,17 @@ function escapeHtml(s) {
617356
617377
  }
617357
617378
 
617358
617379
  async function drainPlayback() {
617359
- if (!playbackCtx) playbackCtx = new AudioContext({ sampleRate: 22050 });
617380
+ if (!playbackCtx) playbackCtx = new AudioContext();
617360
617381
  isPlaying = true;
617361
617382
  while (playbackQueue.length > 0) {
617362
- const int16 = playbackQueue.shift();
617383
+ const frame = playbackQueue.shift();
617384
+ const int16 = frame.pcm || frame;
617385
+ const sampleRate = frame.sampleRate || 22050;
617363
617386
  const float32 = new Float32Array(int16.length);
617364
617387
  for (let i = 0; i < int16.length; i++) {
617365
617388
  float32[i] = int16[i] < 0 ? int16[i] / 0x8000 : int16[i] / 0x7FFF;
617366
617389
  }
617367
- const buf = playbackCtx.createBuffer(1, float32.length, 22050);
617390
+ const buf = playbackCtx.createBuffer(1, float32.length, sampleRate);
617368
617391
  buf.getChannelData(0).set(float32);
617369
617392
  const src = playbackCtx.createBufferSource();
617370
617393
  src.buffer = buf;
@@ -618013,11 +618036,15 @@ var init_voice_session = __esm({
618013
618036
  /**
618014
618037
  * Send TTS audio (PCM Int16 @ model sample rate) to all connected clients.
618015
618038
  */
618016
- sendAudioToClients(pcmInt16) {
618039
+ sendAudioToClients(pcmInt16, sampleRate = 22050) {
618017
618040
  this.ttsSpeaking = true;
618041
+ const header = JSON.stringify({ type: "tts_header", sampleRate, bytes: pcmInt16.length });
618018
618042
  for (const ws of this.wsClients.values()) {
618019
618043
  try {
618020
- if (ws.readyState === import_websocket5.default.OPEN) ws.send(pcmInt16);
618044
+ if (ws.readyState === import_websocket5.default.OPEN) {
618045
+ ws.send(header);
618046
+ ws.send(pcmInt16);
618047
+ }
618021
618048
  } catch {
618022
618049
  }
618023
618050
  }
@@ -618025,11 +618052,15 @@ var init_voice_session = __esm({
618025
618052
  /**
618026
618053
  * Send TTS audio to a specific client by clientId.
618027
618054
  */
618028
- sendAudioToClient(clientId, pcmInt16) {
618055
+ sendAudioToClient(clientId, pcmInt16, sampleRate = 22050) {
618056
+ this.ttsSpeaking = true;
618029
618057
  const ws = this.wsClients.get(clientId);
618030
618058
  if (ws) {
618031
618059
  try {
618032
- if (ws.readyState === import_websocket5.default.OPEN) ws.send(pcmInt16);
618060
+ if (ws.readyState === import_websocket5.default.OPEN) {
618061
+ ws.send(JSON.stringify({ type: "tts_header", sampleRate, bytes: pcmInt16.length }));
618062
+ ws.send(pcmInt16);
618063
+ }
618033
618064
  } catch {
618034
618065
  }
618035
618066
  }
@@ -618038,6 +618069,7 @@ var init_voice_session = __esm({
618038
618069
  * Send speaking state change to a specific client.
618039
618070
  */
618040
618071
  sendSpeakingStateToClient(clientId, speaking) {
618072
+ this.ttsSpeaking = speaking;
618041
618073
  const ws = this.wsClients.get(clientId);
618042
618074
  if (ws) {
618043
618075
  const msg = JSON.stringify({ type: speaking ? "speaking_start" : "speaking_end" });
@@ -673216,6 +673248,147 @@ var init_emotion_engine = __esm({
673216
673248
  }
673217
673249
  });
673218
673250
 
673251
+ // packages/cli/src/tui/conversation-context.ts
673252
+ import { statfsSync as statfsSync8 } from "node:fs";
673253
+ function buildConversationRuntimeContext(now2 = /* @__PURE__ */ new Date(), repoRoot) {
673254
+ const date = new Intl.DateTimeFormat("en-US", {
673255
+ weekday: "long",
673256
+ year: "numeric",
673257
+ month: "long",
673258
+ day: "numeric"
673259
+ }).format(now2);
673260
+ const time = new Intl.DateTimeFormat("en-US", {
673261
+ hour: "numeric",
673262
+ minute: "2-digit",
673263
+ second: "2-digit",
673264
+ timeZoneName: "short"
673265
+ }).format(now2);
673266
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || process.env["TZ"] || "system";
673267
+ let diskLine = "";
673268
+ if (repoRoot) {
673269
+ try {
673270
+ const stats = statfsSync8(repoRoot);
673271
+ const totalBytes = Number(stats.blocks) * Number(stats.bsize);
673272
+ const freeBytes = Number(stats.bavail) * Number(stats.bsize);
673273
+ const usedBytes = totalBytes - freeBytes;
673274
+ const totalGB = Math.round(totalBytes / 1024 ** 3);
673275
+ const freeGB = Math.round(freeBytes / 1024 ** 3);
673276
+ const usedGB = Math.round(usedBytes / 1024 ** 3);
673277
+ const usedPct = totalBytes > 0 ? Math.round(usedBytes / totalBytes * 100) : 0;
673278
+ diskLine = `Disk: disk_available_gb=${freeGB} disk_used_gb=${usedGB} disk_total_gb=${totalGB} disk_used_pct=${usedPct} path=${repoRoot}`;
673279
+ } catch {
673280
+ }
673281
+ }
673282
+ return [
673283
+ `Current date: ${date}`,
673284
+ `Current time: ${time}`,
673285
+ `Current ISO timestamp: ${now2.toISOString()}`,
673286
+ `Timezone: ${timezone}`,
673287
+ repoRoot ? `Working directory: ${repoRoot}` : "",
673288
+ diskLine
673289
+ ].filter(Boolean).join("\n");
673290
+ }
673291
+ function quoteConversationBlock(value2, maxChars = 2400) {
673292
+ const text2 = String(value2 ?? "").replace(/\r\n/g, "\n").trim();
673293
+ if (!text2) return "> (empty)";
673294
+ const clipped = text2.length > maxChars ? `${text2.slice(0, Math.max(0, maxChars - 1))}...` : text2;
673295
+ return clipped.split("\n").map((line) => `> ${line}`).join("\n");
673296
+ }
673297
+ function buildScopedConversationMessages(turns, options2) {
673298
+ const pinned = [];
673299
+ const dynamic = [];
673300
+ let latestContextFrame = null;
673301
+ for (const turn of turns) {
673302
+ if (options2.isPinnedSystemTurn(turn) && !pinned.some((p2) => p2.content === turn.content)) {
673303
+ pinned.push(turn);
673304
+ } else if (options2.isContextFrameTurn?.(turn)) {
673305
+ latestContextFrame = turn;
673306
+ } else {
673307
+ dynamic.push(turn);
673308
+ }
673309
+ }
673310
+ if (latestContextFrame) {
673311
+ if (options2.placeContextFrameBeforeLastUser !== false) {
673312
+ let lastUserIdx = -1;
673313
+ for (let i2 = dynamic.length - 1; i2 >= 0; i2--) {
673314
+ if (dynamic[i2].role === "user") {
673315
+ lastUserIdx = i2;
673316
+ break;
673317
+ }
673318
+ }
673319
+ if (lastUserIdx >= 0) dynamic.splice(lastUserIdx, 0, latestContextFrame);
673320
+ else dynamic.push(latestContextFrame);
673321
+ } else {
673322
+ dynamic.push(latestContextFrame);
673323
+ }
673324
+ }
673325
+ const available = Math.max(0, options2.maxMessages - pinned.length);
673326
+ let windowed = dynamic.slice(-available);
673327
+ if (latestContextFrame && available > 0 && !windowed.includes(latestContextFrame)) {
673328
+ windowed = [latestContextFrame, ...windowed.slice(-(available - 1))];
673329
+ }
673330
+ return [...pinned, ...windowed];
673331
+ }
673332
+ function buildVoiceSessionContext(args) {
673333
+ const maxTranscriptTurns = Math.max(1, args.maxTranscriptTurns ?? 8);
673334
+ const voiceLines = (args.voiceTranscript ?? []).slice(-maxTranscriptTurns).map((turn) => `${formatClock(turn.ts)} ${turn.role}: ${singleLine(turn.content, 360)}`);
673335
+ const relayLines = (args.relayTranscript ?? []).slice(-maxTranscriptTurns).map((turn) => {
673336
+ const dir = turn.dir === "toMain" ? "voice->main" : "main->voice";
673337
+ return `${formatClock(turn.ts)} ${dir}: ${singleLine(turn.content, 360)}`;
673338
+ });
673339
+ const taskLine = args.activeTask ? `active=${args.activeTask.active ? "yes" : "no"} tool_calls=${args.activeTask.toolCalls ?? 0} files_touched=${args.activeTask.filesTouched ?? 0}` : "active=unknown";
673340
+ const sections = [
673341
+ `## Runtime Context
673342
+
673343
+ ${buildConversationRuntimeContext(/* @__PURE__ */ new Date(), args.repoRoot)}`,
673344
+ [
673345
+ "## Voice/Live Session Contract",
673346
+ "",
673347
+ `surface=voicechat session_id=${args.sessionId} turn=${args.turnCount}`,
673348
+ `main_agent_task=${taskLine}`,
673349
+ "Latest user utterance remains the active request. Earlier ASR transcript turns are conversational history, not new instructions.",
673350
+ "ASR text can be noisy. Prefer consensus-filtered final turns, resolve pronouns from recent context, and ask for clarification rather than filling gaps.",
673351
+ "Live perception is evidence for the current reply, not a topic to narrate unless the user asks about it."
673352
+ ].join("\n")
673353
+ ];
673354
+ if (voiceLines.length > 0) {
673355
+ sections.push(`## Recent Voice Conversation
673356
+
673357
+ ${quoteConversationBlock(voiceLines.join("\n"))}`);
673358
+ }
673359
+ if (relayLines.length > 0) {
673360
+ sections.push(`## Main Agent Relay Stream
673361
+
673362
+ ${quoteConversationBlock(relayLines.join("\n"))}`);
673363
+ }
673364
+ if (args.liveContext?.trim()) {
673365
+ sections.push(`## Live Context Frame
673366
+
673367
+ ${args.liveContext.trim()}`);
673368
+ }
673369
+ return sections.join("\n\n");
673370
+ }
673371
+ function singleLine(value2, maxChars) {
673372
+ const clean5 = value2.replace(/\s+/g, " ").trim();
673373
+ return clean5.length > maxChars ? `${clean5.slice(0, Math.max(0, maxChars - 1))}...` : clean5;
673374
+ }
673375
+ function formatClock(ts) {
673376
+ try {
673377
+ return new Date(ts).toLocaleTimeString("en-US", {
673378
+ hour: "2-digit",
673379
+ minute: "2-digit",
673380
+ second: "2-digit"
673381
+ });
673382
+ } catch {
673383
+ return "unknown-time";
673384
+ }
673385
+ }
673386
+ var init_conversation_context = __esm({
673387
+ "packages/cli/src/tui/conversation-context.ts"() {
673388
+ "use strict";
673389
+ }
673390
+ });
673391
+
673219
673392
  // packages/cli/src/tui/telegram-format.ts
673220
673393
  function escapeHtml2(text2) {
673221
673394
  return String(text2 ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -678572,7 +678745,6 @@ import {
678572
678745
  unlinkSync as unlinkSync34,
678573
678746
  readdirSync as readdirSync55,
678574
678747
  statSync as statSync56,
678575
- statfsSync as statfsSync8,
678576
678748
  readFileSync as readFileSync126,
678577
678749
  writeFileSync as writeFileSync82,
678578
678750
  appendFileSync as appendFileSync18
@@ -679829,42 +680001,7 @@ function telegramMemoryTags(text2, mediaSummary) {
679829
680001
  return tokens.filter((token) => !/^\d+$/.test(token)).slice(0, 10);
679830
680002
  }
679831
680003
  function buildTelegramRuntimeContext(now2 = /* @__PURE__ */ new Date(), repoRoot) {
679832
- const date = new Intl.DateTimeFormat("en-US", {
679833
- weekday: "long",
679834
- year: "numeric",
679835
- month: "long",
679836
- day: "numeric"
679837
- }).format(now2);
679838
- const time = new Intl.DateTimeFormat("en-US", {
679839
- hour: "numeric",
679840
- minute: "2-digit",
679841
- second: "2-digit",
679842
- timeZoneName: "short"
679843
- }).format(now2);
679844
- const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || process.env["TZ"] || "system";
679845
- let diskLine = "";
679846
- if (repoRoot) {
679847
- try {
679848
- const stats = statfsSync8(repoRoot);
679849
- const totalBytes = stats.blocks * stats.bsize;
679850
- const freeBytes = stats.bavail * stats.bsize;
679851
- const usedBytes = totalBytes - freeBytes;
679852
- const totalGB = Math.round(totalBytes / 1024 ** 3);
679853
- const freeGB = Math.round(freeBytes / 1024 ** 3);
679854
- const usedGB = Math.round(usedBytes / 1024 ** 3);
679855
- const usedPct = totalBytes > 0 ? Math.round(usedBytes / totalBytes * 100) : 0;
679856
- diskLine = `Disk: disk_available_gb=${freeGB} disk_used_gb=${usedGB} disk_total_gb=${totalGB} disk_used_pct=${usedPct} path=${repoRoot}`;
679857
- } catch {
679858
- }
679859
- }
679860
- return [
679861
- `Current date: ${date}`,
679862
- `Current time: ${time}`,
679863
- `Current ISO timestamp: ${now2.toISOString()}`,
679864
- `Timezone: ${timezone}`,
679865
- repoRoot ? `Working directory: ${repoRoot}` : "",
679866
- diskLine
679867
- ].filter(Boolean).join("\n");
680004
+ return buildConversationRuntimeContext(now2, repoRoot);
679868
680005
  }
679869
680006
  function telegramSessionIdFromKey(sessionKey) {
679870
680007
  return `telegram-${createHash42("sha1").update(sessionKey).digest("hex").slice(0, 16)}`;
@@ -681026,6 +681163,7 @@ var TELEGRAM_TOOL_ACTION_GROUPS, TELEGRAM_TOOL_ACTION_GROUP, TELEGRAM_TOOL_MUTAT
681026
681163
  var init_telegram_bridge = __esm({
681027
681164
  "packages/cli/src/tui/telegram-bridge.ts"() {
681028
681165
  "use strict";
681166
+ init_conversation_context();
681029
681167
  init_telegram_format();
681030
681168
  init_async_process();
681031
681169
  init_dist8();
@@ -697648,43 +697786,21 @@ function isContextSnapshotTurn(turn) {
697648
697786
  return turn.role === "system" && turn.content.startsWith(CONTEXT_SNAPSHOT_PREFIX);
697649
697787
  }
697650
697788
  function isPinnedSystemTurn(turn) {
697651
- return turn.role === "system" && (turn.content === SYSTEM_PROMPT2 || turn.content.startsWith("Available tools (emit one-line JSON:"));
697789
+ return turn.role === "system" && (turn.content === SYSTEM_PROMPT2 || turn.content.startsWith("Available tools."));
697652
697790
  }
697653
697791
  function buildRealtimeVoiceMessages(turns, maxMessages = MAX_REALTIME_MODEL_MESSAGES) {
697654
- const pinned = [];
697655
- const dynamic = [];
697656
- let latestSnapshot = null;
697657
- for (const turn of turns) {
697658
- if (isPinnedSystemTurn(turn) && !pinned.some((p2) => p2.content === turn.content)) {
697659
- pinned.push(turn);
697660
- } else if (isContextSnapshotTurn(turn)) {
697661
- latestSnapshot = turn;
697662
- } else {
697663
- dynamic.push(turn);
697664
- }
697665
- }
697666
- if (latestSnapshot) {
697667
- let lastUserIdx = -1;
697668
- for (let i2 = dynamic.length - 1; i2 >= 0; i2--) {
697669
- if (dynamic[i2].role === "user") {
697670
- lastUserIdx = i2;
697671
- break;
697672
- }
697673
- }
697674
- if (lastUserIdx >= 0) dynamic.splice(lastUserIdx, 0, latestSnapshot);
697675
- else dynamic.push(latestSnapshot);
697676
- }
697677
- const available = Math.max(0, maxMessages - pinned.length);
697678
- let windowed = dynamic.slice(-available);
697679
- if (latestSnapshot && available > 0 && !windowed.includes(latestSnapshot)) {
697680
- windowed = [latestSnapshot, ...windowed.slice(-(available - 1))];
697681
- }
697682
- return [...pinned, ...windowed];
697792
+ return buildScopedConversationMessages(turns, {
697793
+ maxMessages,
697794
+ isPinnedSystemTurn,
697795
+ isContextFrameTurn: isContextSnapshotTurn,
697796
+ placeContextFrameBeforeLastUser: true
697797
+ });
697683
697798
  }
697684
697799
  var VAD_SILENCE_MS, MAX_SEGMENT_MS, SUMMARY_INJECTION_INTERVAL, MAX_CONTEXT_TURNS, MAX_REALTIME_MODEL_MESSAGES, CONTEXT_SNAPSHOT_PREFIX, CONTEXT_SNAPSHOT_SCOPE, MAX_VOICE_REPLY_CHARS, AGENT_ECHO_WINDOW_MS, AGENT_ECHO_SIMILARITY, SYSTEM_PROMPT2, MIN_SIGNAL_SCORE, NOISE_ONLY_RE, VoiceChatSession;
697685
697800
  var init_voicechat = __esm({
697686
697801
  "packages/cli/src/tui/voicechat.ts"() {
697687
697802
  "use strict";
697803
+ init_conversation_context();
697688
697804
  VAD_SILENCE_MS = 3e3;
697689
697805
  MAX_SEGMENT_MS = 6500;
697690
697806
  SUMMARY_INJECTION_INTERVAL = 4;
@@ -697699,6 +697815,7 @@ var init_voicechat = __esm({
697699
697815
 
697700
697816
  Rules:
697701
697817
  - ALWAYS answer the user's most recent message directly. The live context snapshot is background perception — never the topic. Do not describe cameras, feeds, or surroundings unless the user's latest message asks about them.
697818
+ - Treat this as one continuous conversation thread. Resolve pronouns from recent voice turns and main-agent relay context, but let the latest user utterance override earlier transcript noise.
697702
697819
  - Never repeat your previous answer. If you already described the scene, don't describe it again unless explicitly asked again — respond to what the user just said.
697703
697820
  - Live perception: each turn you may receive a read-only "[ACTIVE CONTEXT FRAME]" system message containing what the camera currently sees (objects, people, classifications, recent visual events) and what is currently heard (sound scene, recent sounds, transcripts). Treat it as your own live sight and hearing. When the user asks what you SEE, answer from the camera fields; when the user asks what you HEAR, answer from the audio/sound fields. If the snapshot is missing or stale, say you can't see/hear clearly right now — do not invent observations.
697704
697821
  - Never invent environment facts (cwd, OS, specs, repo state). If you need a precise fact from the main agent, request a tool by outputting on a single line EXACTLY one JSON object: {"tool": string, "args": object} and nothing else. Then wait for the tool result before answering.
@@ -698022,13 +698139,19 @@ Rules:
698022
698139
  const snap = await Promise.resolve(this.toolRelay.contextSnapshot());
698023
698140
  if (snap && snap.trim()) {
698024
698141
  this.dropTransientSystemTurns();
698142
+ const contextFrame = buildVoiceSessionContext({
698143
+ sessionId: "voicechat",
698144
+ turnCount: this.turnCount,
698145
+ voiceTranscript: this.voiceTranscript,
698146
+ relayTranscript: this.relayTranscript,
698147
+ liveContext: `${CONTEXT_SNAPSHOT_SCOPE}
698148
+
698149
+ ${snap.trim()}`
698150
+ });
698025
698151
  this.context.push({
698026
698152
  role: "system",
698027
698153
  content: `${CONTEXT_SNAPSHOT_PREFIX}
698028
- turn: ${this.turnCount}
698029
- ${CONTEXT_SNAPSHOT_SCOPE}
698030
-
698031
- ${snap.trim()}`
698154
+ ${contextFrame}`
698032
698155
  });
698033
698156
  this.trimContext();
698034
698157
  }
@@ -737112,6 +737235,32 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
737112
737235
  sharedTranscriber: null
737113
737236
  };
737114
737237
  const engine = getListenEngine();
737238
+ const callConsensusRequired = (() => {
737239
+ const env2 = String(process.env["OMNIUS_ASR_CONSENSUS"] ?? "").trim().toLowerCase();
737240
+ return !(env2 === "0" || env2 === "off" || env2 === "false");
737241
+ })();
737242
+ const recentCallFinals = [];
737243
+ const normalizeCallTranscript = (value2) => value2.toLowerCase().replace(/[^\p{L}\p{N}\s']/gu, " ").replace(/\s+/g, " ").trim();
737244
+ const isLikelyCallAsrHallucination = (normalized) => /^(thanks|thank you) for watching(?: this video)?$/.test(normalized) || /^thanks for watching and/.test(normalized) || /\bdon't forget to (like|subscribe)\b/.test(normalized);
737245
+ const shouldDropCallFinalTranscript = (text2, evt) => {
737246
+ if (callConsensusRequired && evt.consensus !== true) {
737247
+ return evt.consensus === false ? "consensus rejected" : "missing consensus metadata";
737248
+ }
737249
+ const normalized = normalizeCallTranscript(text2);
737250
+ if (!normalized) return "empty transcript";
737251
+ if (isLikelyCallAsrHallucination(normalized)) {
737252
+ return "known whisper outro hallucination";
737253
+ }
737254
+ const now2 = Date.now();
737255
+ while (recentCallFinals.length > 0 && now2 - recentCallFinals[0].ts > 15e3) {
737256
+ recentCallFinals.shift();
737257
+ }
737258
+ if (recentCallFinals.some((entry) => entry.normalized === normalized)) {
737259
+ return "duplicate final transcript";
737260
+ }
737261
+ recentCallFinals.push({ normalized, ts: now2 });
737262
+ return null;
737263
+ };
737115
737264
  voiceSession.onUserAudio = (pcmChunk, userId) => {
737116
737265
  if (callState.sharedTranscriber)
737117
737266
  callState.sharedTranscriber.write(pcmChunk);
@@ -737142,7 +737291,7 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
737142
737291
  if (result && session?.isActive) {
737143
737292
  const { pcm, sampleRate } = result;
737144
737293
  session.sendSpeakingStateToClient(id2, true);
737145
- session.sendAudioToClient(id2, pcm);
737294
+ session.sendAudioToClient(id2, pcm, sampleRate);
737146
737295
  const durationMs = pcm.length / 2 / sampleRate * 1e3;
737147
737296
  setTimeout(
737148
737297
  () => session.sendSpeakingStateToClient(id2, false),
@@ -737177,17 +737326,25 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
737177
737326
  (evt) => {
737178
737327
  if (!evt.text?.trim() || !voiceSession?.isActive) return;
737179
737328
  const text2 = evt.text.trim();
737329
+ if (!evt.isFinal) return;
737330
+ const dropReason = shouldDropCallFinalTranscript(text2, evt);
737331
+ if (dropReason) {
737332
+ writeContent(
737333
+ () => renderInfo(
737334
+ `[call/asr] dropped ${dropReason}: "${text2.slice(0, 80)}"`
737335
+ )
737336
+ );
737337
+ return;
737338
+ }
737180
737339
  writeContent(
737181
737340
  () => renderVoiceSessionTranscript("user", text2)
737182
737341
  );
737183
737342
  voiceSession?.sendTranscript("user", text2);
737184
- if (evt.isFinal) {
737185
- for (const [clientId, agent] of callSubAgents) {
737186
- agent.handleTranscript(text2);
737187
- }
737188
- if (callSubAgents.size === 0) {
737189
- rl.feed(text2 + "\n");
737190
- }
737343
+ for (const [clientId, agent] of callSubAgents) {
737344
+ agent.handleTranscript(text2);
737345
+ }
737346
+ if (callSubAgents.size === 0) {
737347
+ rl.feed(text2 + "\n");
737191
737348
  }
737192
737349
  }
737193
737350
  );
@@ -737250,7 +737407,7 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
737250
737407
  voiceEngine.onPCMOutput = (pcm, sampleRate) => {
737251
737408
  if (session?.isActive) {
737252
737409
  session.sendSpeakingState(true);
737253
- session.sendAudioToClients(pcm);
737410
+ session.sendAudioToClients(pcm, sampleRate);
737254
737411
  const durationMs = pcm.length / 2 / sampleRate * 1e3;
737255
737412
  setTimeout(() => session.sendSpeakingState(false), durationMs);
737256
737413
  }
@@ -84,6 +84,76 @@ def emit_transcript(text: str, is_final: bool = False, doa=None, consensus=None)
84
84
  event["consensus"] = bool(consensus)
85
85
  emit(event)
86
86
 
87
+
88
+ def host_cuda_hardware_present() -> bool:
89
+ """Best-effort CUDA hardware hint, including Jetson/Orin nodes."""
90
+ for path in (
91
+ "/dev/nvidiactl",
92
+ "/dev/nvidia0",
93
+ "/dev/nvhost-gpu",
94
+ "/proc/driver/nvidia/gpus",
95
+ "/sys/devices/gpu.0",
96
+ ):
97
+ if os.path.exists(path):
98
+ return True
99
+ visible = os.environ.get("NVIDIA_VISIBLE_DEVICES", "").strip().lower()
100
+ return bool(visible and visible not in ("none", "void", "no"))
101
+
102
+
103
+ def select_whisper_device() -> str:
104
+ """Force Whisper onto CUDA whenever PyTorch can see a CUDA device.
105
+
106
+ CPU is only allowed when CUDA is genuinely unavailable to this Python
107
+ runtime. If CUDA is visible but unusable, fail loudly instead of silently
108
+ degrading into slow CPU transcription.
109
+ """
110
+ try:
111
+ import torch
112
+ except Exception as e:
113
+ if host_cuda_hardware_present():
114
+ emit_error(f"CUDA hardware appears present, but PyTorch could not be imported for CUDA Whisper ({e}); refusing CPU fallback.")
115
+ sys.exit(1)
116
+ emit_status(f"PyTorch unavailable for CUDA probe ({e}); using CPU because CUDA is not available to Whisper.")
117
+ return "cpu"
118
+
119
+ try:
120
+ cuda_count = int(torch.cuda.device_count())
121
+ cuda_available = bool(torch.cuda.is_available() and cuda_count > 0)
122
+ except Exception as e:
123
+ if host_cuda_hardware_present():
124
+ emit_error(f"CUDA hardware appears present, but PyTorch CUDA probe failed ({e}); refusing CPU fallback.")
125
+ sys.exit(1)
126
+ emit_status(f"CUDA probe failed ({e}); using CPU because CUDA is not available to Whisper.")
127
+ return "cpu"
128
+
129
+ if not cuda_available:
130
+ cuda_version = getattr(getattr(torch, "version", None), "cuda", None)
131
+ if host_cuda_hardware_present():
132
+ emit_error(f"CUDA hardware appears present, but PyTorch reports CUDA unavailable (torch.version.cuda={cuda_version}, device_count={cuda_count}); refusing CPU fallback.")
133
+ sys.exit(1)
134
+ emit_status(f"CUDA unavailable to PyTorch (torch.version.cuda={cuda_version}, device_count={cuda_count}); using CPU fallback.")
135
+ return "cpu"
136
+
137
+ raw_index = os.environ.get("OMNIUS_ASR_CUDA_DEVICE", "0").strip() or "0"
138
+ try:
139
+ device_index = int(raw_index)
140
+ except ValueError:
141
+ emit_error(f"Invalid OMNIUS_ASR_CUDA_DEVICE={raw_index!r}; refusing CPU fallback because CUDA is available.")
142
+ sys.exit(1)
143
+ if device_index < 0 or device_index >= cuda_count:
144
+ emit_error(f"OMNIUS_ASR_CUDA_DEVICE={device_index} outside available CUDA device range 0..{cuda_count - 1}; refusing CPU fallback.")
145
+ sys.exit(1)
146
+
147
+ try:
148
+ torch.cuda.set_device(device_index)
149
+ name = torch.cuda.get_device_name(device_index)
150
+ except Exception as e:
151
+ emit_error(f"CUDA is available but cuda:{device_index} could not be selected ({e}); refusing CPU fallback.")
152
+ sys.exit(1)
153
+
154
+ emit_status(f"CUDA available; forcing Whisper onto cuda:{device_index} ({name}).")
155
+ return f"cuda:{device_index}"
156
+
87
157
  # ---------------------------------------------------------------------------
88
158
  # Venv bootstrap
89
159
  # ---------------------------------------------------------------------------
@@ -344,18 +414,15 @@ def main():
344
414
 
345
415
  import whisper
346
416
 
347
- emit_status(f"Loading Whisper {args.model} model...")
417
+ device = select_whisper_device()
418
+ emit_status(f"Loading Whisper {args.model} model on {device}...")
348
419
  try:
349
- device = "cpu"
350
- try:
351
- import torch
352
- if torch.cuda.is_available():
353
- device = "cuda"
354
- except ImportError:
355
- pass
356
420
  model = whisper.load_model(args.model, device=device)
357
421
  except Exception as e:
358
- emit_error(f"Failed to load model: {e}")
422
+ if str(device).startswith("cuda"):
423
+ emit_error(f"Failed to load model on {device}; refusing CPU fallback because CUDA is available: {e}")
424
+ else:
425
+ emit_error(f"Failed to load model on CPU fallback: {e}")
359
426
  sys.exit(1)
360
427
 
361
428
  # Consensus model: hallucinations (mixed-language garbage on noisy or
@@ -363,7 +430,7 @@ def main():
363
430
  consensus_model = None
364
431
  consensus_name = (args.consensus_model or "off").strip().lower()
365
432
  if consensus_name not in ("off", "none", "", args.model):
366
- emit_status(f"Loading consensus Whisper {consensus_name} model...")
433
+ emit_status(f"Loading consensus Whisper {consensus_name} model on {device}...")
367
434
  try:
368
435
  consensus_model = whisper.load_model(consensus_name, device=device)
369
436
  except Exception as e:
@@ -377,7 +444,7 @@ def main():
377
444
 
378
445
  emit({"type": "ready"})
379
446
 
380
- fp16 = (device == "cuda")
447
+ fp16 = str(device).startswith("cuda")
381
448
 
382
449
  def run_transcribe(m, samples):
383
450
  return m.transcribe(
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omnius",
3
- "version": "1.0.430",
3
+ "version": "1.0.431",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omnius",
9
- "version": "1.0.430",
9
+ "version": "1.0.431",
10
10
  "bundleDependencies": [
11
11
  "image-to-ascii"
12
12
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnius",
3
- "version": "1.0.430",
3
+ "version": "1.0.431",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",