@livx.cc/agentx 0.96.16 → 0.96.18

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.
@@ -34,9 +34,16 @@ FileHandle.standardError.write("mic access granted: \(granted)\n".data(using: .u
34
34
 
35
35
  let engine = AVAudioEngine()
36
36
  let input = engine.inputNode
37
- do { try input.setVoiceProcessingEnabled(true) } catch {
38
- FileHandle.standardError.write("AEC unavailable: \(error)\n".data(using: .utf8)!)
39
- exit(1)
37
+ // VPIO gives hardware echo cancellation, but on some Macs (observed: macOS 26.5.x + a built-in mic that
38
+ // VPIO exposes as a multichannel format) the VP input tap NEVER delivers buffers — capture is dead. The
39
+ // host detects that (no audio) and respawns us with MIC_NO_VPIO=1: plain capture, no AEC (heuristic-echo
40
+ // tier, gated below). Skipping VPIO trades echo cancellation for a mic that actually works.
41
+ let useVPIO = ProcessInfo.processInfo.environment["MIC_NO_VPIO"] != "1"
42
+ if useVPIO {
43
+ do { try input.setVoiceProcessingEnabled(true) } catch {
44
+ FileHandle.standardError.write("AEC unavailable: \(error)\n".data(using: .utf8)!)
45
+ exit(1)
46
+ }
40
47
  }
41
48
  // the VP I/O unit is full-duplex: input stays muted unless the output side renders too. The mixer→
42
49
  // output format must match the VP output's hardware format (nil/default fails kAUInitialize -10875).
@@ -44,9 +51,10 @@ let outHw = engine.outputNode.outputFormat(forBus: 0)
44
51
  engine.connect(engine.mainMixerNode, to: engine.outputNode, format: outHw)
45
52
  engine.mainMixerNode.outputVolume = 1 // playback is audible now (was 0 when this path was only a keep-alive)
46
53
  // VP ducks all other system audio by default — disable (we ARE the audio).
47
- if #available(macOS 14.0, *) {
54
+ if useVPIO, #available(macOS 14.0, *) {
48
55
  input.voiceProcessingOtherAudioDuckingConfiguration = .init(enableAdvancedDucking: false, duckingLevel: .min)
49
56
  }
57
+ FileHandle.standardError.write("mode: \(useVPIO ? "vpio" : "no-vpio")\n".data(using: .utf8)!)
50
58
 
51
59
  // ---- playback: framed s16le 44.1k mono on stdin → player node (the VPIO reference signal) ----
52
60
  let playFmt = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 1, interleaved: false)!
@@ -165,6 +173,17 @@ do { try engine.start() } catch {
165
173
  }
166
174
  player.play()
167
175
 
176
+ // Non-VPIO mode only: the input tap won't fire unless the output graph is actively rendering. Keep the
177
+ // MAIN player warm with a continuous silent loop WHEN IDLE — when real TTS arrives the stdin handler
178
+ // stops this loop, plays the TTS, then resumes the loop on FLUSH. (A separate node does NOT wake the
179
+ // tap — empirically only the main player does.) Pure zeros → inaudible; gate envelope untouched.
180
+ let keepSilence = AVAudioPCMBuffer(pcmFormat: playFmt, frameCapacity: 4410)!; keepSilence.frameLength = 4410
181
+ var loopingSilence = false
182
+ func armIdleKeepAlive() { if !useVPIO { player.scheduleBuffer(keepSilence, at: nil, options: .loops, completionHandler: nil); loopingSilence = true } }
183
+ // Clear the idle loop so a real TTS buffer can play (a .loops buffer never yields the queue otherwise).
184
+ func stopIdleKeepAlive() { if loopingSilence { player.stop(); player.play(); loopingSilence = false } }
185
+ armIdleKeepAlive()
186
+
168
187
  // stdin reader (background): framed playback + in-band FLUSH. EOF = parent gone → clean exit.
169
188
  DispatchQueue.global(qos: .userInteractive).async {
170
189
  let stdin = FileHandle.standardInput
@@ -184,6 +203,7 @@ DispatchQueue.global(qos: .userInteractive).async {
184
203
  if len == 0 { // FLUSH: stop drops every scheduled buffer; restart for the next turn
185
204
  player.stop()
186
205
  player.play()
206
+ armIdleKeepAlive() // non-VPIO: resume the idle silence loop so the input tap stays awake between turns
187
207
  pausedAt = 0
188
208
  gateLock.lock(); pausedSince = 0; audibleUntil = CACurrentMediaTime(); schedEnv.removeAll(); gateLock.unlock() // gate tail only; envelope timeline resets with playerTime
189
209
  continue
@@ -237,6 +257,7 @@ DispatchQueue.global(qos: .userInteractive).async {
237
257
  }
238
258
  gateLock.unlock()
239
259
  }
260
+ stopIdleKeepAlive() // non-VPIO: drop the idle silence loop before the first real TTS buffer
240
261
  player.scheduleBuffer(buf, completionHandler: nil)
241
262
  // extend the audible window by this buffer's duration (+0.8s tail covers device latency)
242
263
  gateLock.lock()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livx.cc/agentx",
3
- "version": "0.96.16",
3
+ "version": "0.96.18",
4
4
  "description": "Edge-native AI agent runtime — drives a virtual filesystem via any LLM (ai.libx.js). Same bytes run in node, browser, or edge.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",