Mac: clarify runtime comments
This commit is contained in:
@@ -162,6 +162,7 @@ actor AgentRPC {
|
||||
}
|
||||
|
||||
Task.detached { [weak self] in
|
||||
// Ensure all waiters are failed if the worker dies (e.g., crash or SIGTERM).
|
||||
process.waitUntilExit()
|
||||
await self?.stop()
|
||||
}
|
||||
@@ -188,7 +189,7 @@ actor AgentRPC {
|
||||
self.buffer.removeSubrange(self.buffer.startIndex...range.lowerBound)
|
||||
guard let line = String(data: lineData, encoding: .utf8) else { continue }
|
||||
|
||||
// Handle event envelopes (unsolicited)
|
||||
// Event frames are pushed without request/response pairing (e.g., heartbeats).
|
||||
if let event = self.parseHeartbeatEvent(from: line) {
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: Self.heartbeatNotification, object: event)
|
||||
|
||||
@@ -20,6 +20,8 @@ actor ConnectionWaiter {
|
||||
private var pendingResult: Result<Void, Error>?
|
||||
|
||||
func wait() async throws {
|
||||
// Acts like a one-shot Future; if the connection resolves before wait() is called,
|
||||
// stash the result so the waiter resumes immediately.
|
||||
try await withCheckedThrowingContinuation { (c: CheckedContinuation<Void, Error>) in
|
||||
if let pending = pendingResult {
|
||||
pendingResult = nil
|
||||
@@ -292,7 +294,7 @@ final class ControlChannel: ObservableObject {
|
||||
proc.standardOutput = outPipe
|
||||
proc.standardError = errPipe
|
||||
try proc.run()
|
||||
// Give ssh a brief moment; if it exits immediately, surface the error.
|
||||
// Give ssh a brief moment; if it exits immediately we surface stderr instead of silently failing.
|
||||
Thread.sleep(forTimeInterval: 0.2) // 200ms
|
||||
if !proc.isRunning {
|
||||
let err = String(data: errPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
@@ -381,6 +383,7 @@ final class ControlChannel: ObservableObject {
|
||||
getsockname(socket, withUnsafeMutablePointer(to: &addr) {
|
||||
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) { $0 }
|
||||
}, &len)
|
||||
// Asking the kernel for port 0 yields an ephemeral free port; reuse it for the SSH tunnel.
|
||||
port = UInt16(bigEndian: addr.sin_port)
|
||||
return port
|
||||
}
|
||||
|
||||
@@ -654,6 +654,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSXPCListenerDelegate
|
||||
private var state: AppState?
|
||||
private let xpcLogger = Logger(subsystem: "com.steipete.clawdis", category: "xpc")
|
||||
private let webChatAutoLogger = Logger(subsystem: "com.steipete.clawdis", category: "WebChat")
|
||||
// Only clients signed with this team ID may talk to the XPC service (hard-fails if mismatched).
|
||||
private let allowedTeamIDs: Set<String> = ["Y5PE65HELJ"]
|
||||
let updaterController: UpdaterProviding = makeUpdaterController()
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ final class RelayProcessManager: ObservableObject {
|
||||
workingDirectory: FilePath(cwd))
|
||||
{ execution, stdin, stdout, stderr in
|
||||
self.didStart(execution)
|
||||
// Consume stdout/stderr eagerly so the relay can't block on full pipes.
|
||||
async let out: Void = self.stream(output: stdout, label: "stdout")
|
||||
async let err: Void = self.stream(output: stderr, label: "stderr")
|
||||
try await stdin.finish()
|
||||
@@ -143,6 +144,7 @@ final class RelayProcessManager: ObservableObject {
|
||||
|
||||
self.status = .restarting
|
||||
self.logger.warning("relay crashed (code \(code)); restarting")
|
||||
// Slight backoff to avoid hammering the system in case of immediate crash-on-start.
|
||||
try? await Task.sleep(nanoseconds: 750_000_000)
|
||||
self.startIfNeeded()
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ enum ShellRunner {
|
||||
_ = await waitTask.value // drain pipes after termination
|
||||
return Response(ok: false, message: "timeout")
|
||||
}
|
||||
// Whichever completes first (process exit or timeout) wins; cancel the other branch.
|
||||
let first = await group.next()!
|
||||
group.cancelAll()
|
||||
return first
|
||||
|
||||
@@ -257,6 +257,7 @@ enum CommandResolver {
|
||||
extras.insert(relay.appendingPathComponent("node_modules/.bin").path, at: 0)
|
||||
}
|
||||
var seen = Set<String>()
|
||||
// Preserve order while stripping duplicates so PATH lookups remain deterministic.
|
||||
return (extras + current).filter { seen.insert($0).inserted }
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ actor VoicePushToTalk {
|
||||
self.triggerChimePlayed = true
|
||||
await MainActor.run { VoiceWakeChimePlayer.play(config.triggerChime) }
|
||||
}
|
||||
// Pause the always-on wake word recognizer so both pipelines don't fight over the mic tap.
|
||||
await VoiceWakeRuntime.shared.pauseForPushToTalk()
|
||||
await MainActor.run {
|
||||
VoiceWakeOverlayController.shared.showPartial(transcript: "")
|
||||
@@ -189,6 +190,7 @@ actor VoicePushToTalk {
|
||||
}
|
||||
let transcript = result?.bestTranscription.formattedString
|
||||
let isFinal = result?.isFinal ?? false
|
||||
// Hop to a Task so UI updates stay off the Speech callback thread.
|
||||
Task.detached { [weak self, transcript, isFinal] in
|
||||
guard let self else { return }
|
||||
await self.handle(transcript: transcript, isFinal: isFinal)
|
||||
|
||||
@@ -290,6 +290,7 @@ enum VoiceWakeForwarder {
|
||||
let nanos = UInt64(max(timeout, 0.1) * 1_000_000_000)
|
||||
try? await Task.sleep(nanoseconds: nanos)
|
||||
if process.isRunning {
|
||||
// SIGTERM is enough to stop ssh; keeps stdout/stderr readable for diagnostics.
|
||||
process.terminate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +165,7 @@ final class VoiceWakeOverlayController: ObservableObject {
|
||||
guard let window else { return }
|
||||
if !self.model.isVisible {
|
||||
self.model.isVisible = true
|
||||
// Keep the status item in “listening” mode until we explicitly dismiss the overlay.
|
||||
AppStateStore.shared.triggerVoiceEars(ttl: nil)
|
||||
let start = target.offsetBy(dx: 0, dy: -6)
|
||||
window.setFrame(start, display: true)
|
||||
@@ -276,7 +277,6 @@ final class VoiceWakeOverlayController: ObservableObject {
|
||||
}
|
||||
|
||||
private func scheduleAutoSend(after delay: TimeInterval, sendChime: VoiceWakeChime) {
|
||||
guard let forwardConfig, forwardConfig.enabled else { return }
|
||||
self.autoSendTask?.cancel()
|
||||
self.autoSendTask = Task<Void, Never> { [weak self, sendChime] in
|
||||
let nanos = UInt64(max(0, delay) * 1_000_000_000)
|
||||
|
||||
@@ -125,7 +125,7 @@ actor VoiceWakeRuntime {
|
||||
|
||||
self.currentConfig = config
|
||||
self.lastHeard = Date()
|
||||
self.cooldownUntil = nil
|
||||
// Preserve any existing cooldownUntil so the debounce after send isn't wiped by a restart.
|
||||
|
||||
self.recognitionTask = recognizer.recognitionTask(with: request) { [weak self] result, error in
|
||||
guard let self else { return }
|
||||
@@ -253,6 +253,7 @@ actor VoiceWakeRuntime {
|
||||
VoiceWakeOverlayController.shared.showPartial(transcript: snapshot, attributed: attributed)
|
||||
}
|
||||
|
||||
// Keep the "ears" boosted for the capture window so the status icon animates while recording.
|
||||
await MainActor.run { AppStateStore.shared.triggerVoiceEars(ttl: nil) }
|
||||
|
||||
self.captureTask?.cancel()
|
||||
@@ -269,6 +270,7 @@ actor VoiceWakeRuntime {
|
||||
while self.isCapturing {
|
||||
let now = Date()
|
||||
if now >= hardStop {
|
||||
// Hard-stop after a maximum duration so we never leave the recognizer pinned open.
|
||||
await self.finalizeCapture(config: config)
|
||||
return
|
||||
}
|
||||
@@ -337,6 +339,7 @@ actor VoiceWakeRuntime {
|
||||
self.lastHeard = Date()
|
||||
}
|
||||
|
||||
// Normalize against the adaptive threshold so the UI meter stays roughly 0...1 across devices.
|
||||
let clamped = min(1.0, max(0.0, rms / max(self.minSpeechRMS, threshold)))
|
||||
Task { @MainActor in
|
||||
VoiceWakeOverlayController.shared.updateLevel(clamped)
|
||||
|
||||
@@ -135,6 +135,7 @@ final class WebChatServer: @unchecked Sendable {
|
||||
}
|
||||
let fileURL = root.appendingPathComponent(path)
|
||||
webChatServerLogger.debug("WebChatServer resolved file=\(fileURL.path, privacy: .public)")
|
||||
// Simple directory traversal guard: served files must live under the bundled web root.
|
||||
guard fileURL.path.hasPrefix(root.path) else {
|
||||
self.send(status: 403, mime: "text/plain", body: Data("Forbidden".utf8), over: connection)
|
||||
return
|
||||
|
||||
@@ -137,6 +137,7 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate {
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
do {
|
||||
// Recreate the tunnel silently so the window keeps working without user intervention.
|
||||
let base = try await self.startOrRestartTunnel()
|
||||
self.loadPage(baseURL: base)
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user