From f965e1c3ffddcce8397ce7c5972a3ee0163b99bb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 9 Dec 2025 01:17:01 +0100 Subject: [PATCH] chore: single-source working state from agent events --- apps/macos/Sources/Clawdis/AgentRPC.swift | 26 ---------------- .../Sources/Clawdis/ControlChannel.swift | 26 ++++++---------- .../CommandResolverTests.swift | 13 ++++++++ src/cli/program.ts | 31 ------------------- src/commands/agent.ts | 5 +++ 5 files changed, 28 insertions(+), 73 deletions(-) diff --git a/apps/macos/Sources/Clawdis/AgentRPC.swift b/apps/macos/Sources/Clawdis/AgentRPC.swift index a3155191e..d6f83a234 100644 --- a/apps/macos/Sources/Clawdis/AgentRPC.swift +++ b/apps/macos/Sources/Clawdis/AgentRPC.swift @@ -14,12 +14,6 @@ actor AgentRPC { let reason: String? } - struct JobStateEvent: Codable { - let id: String - let state: String - let durationMs: Double? - } - static let heartbeatNotification = Notification.Name("clawdis.rpc.heartbeat") private var process: Process? @@ -202,8 +196,6 @@ actor AgentRPC { } continue } - if self.parseJobStateEvent(from: line) != nil { continue } - if let waiter = waiters.first { self.waiters.removeFirst() waiter.resume(returning: line) @@ -229,24 +221,6 @@ actor AgentRPC { return try? decoder.decode(HeartbeatEvent.self, from: payloadData) } - private func parseJobStateEvent(from line: String) -> JobStateEvent? { - guard let data = line.data(using: .utf8) else { return nil } - guard - let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - let type = obj["type"] as? String, - type == "event", - let evt = obj["event"] as? String, - evt == "job-state", - let payload = obj["payload"] as? [String: Any] - else { - return nil - } - - let decoder = JSONDecoder() - guard let payloadData = try? JSONSerialization.data(withJSONObject: payload) else { return nil } - return try? decoder.decode(JobStateEvent.self, from: payloadData) - } - private func nextLine() async throws -> String { try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in self.waiters.append(cont) diff --git a/apps/macos/Sources/Clawdis/ControlChannel.swift b/apps/macos/Sources/Clawdis/ControlChannel.swift index 545066bf8..457bfa805 100644 --- a/apps/macos/Sources/Clawdis/ControlChannel.swift +++ b/apps/macos/Sources/Clawdis/ControlChannel.swift @@ -190,7 +190,7 @@ final class ControlChannel: ObservableObject { private var mode: Mode = .local private var localPort: UInt16 = 18789 private var pingTask: Task? - private var activeJobs: Int = 0 + private var jobStates: [String: String] = [:] @Published private(set) var state: ConnectionState = .disconnected @Published private(set) var lastPingMs: Double? @@ -208,13 +208,7 @@ final class ControlChannel: ObservableObject { { note in if let evt = note.object as? ControlAgentEvent { DispatchQueue.main.async { @MainActor in - let payload = ControlAgentEvent( - runId: evt.runId, - seq: evt.seq, - stream: evt.stream, - ts: evt.ts, - data: evt.data.mapValues { AnyCodable($0.value) }) - AgentEventStore.shared.append(payload) + AgentEventStore.shared.append(evt) } } } @@ -459,15 +453,15 @@ final class ControlChannel: ObservableObject { private func handleAgentEvent(_ event: ControlAgentEvent) { if event.stream == "job" { if let state = event.data["state"]?.value as? String { - switch state.lowercased() { - case "started", "streaming": - self.activeJobs &+= 1 - case "done", "error": - self.activeJobs = max(0, self.activeJobs - 1) - default: - break + let normalized = state.lowercased() + if normalized == "done" || normalized == "error" { + self.jobStates.removeValue(forKey: event.runId) + } else { + self.jobStates[event.runId] = normalized } - let working = self.activeJobs > 0 + + let workingStates: Set = ["started", "streaming", "running", "queued", "waiting"] + let working = self.jobStates.values.contains { workingStates.contains($0) } Task { @MainActor in AppStateStore.shared.setWorking(working) } diff --git a/apps/macos/Tests/ClawdisIPCTests/CommandResolverTests.swift b/apps/macos/Tests/ClawdisIPCTests/CommandResolverTests.swift index 7ad02970b..e4b5f80c3 100644 --- a/apps/macos/Tests/ClawdisIPCTests/CommandResolverTests.swift +++ b/apps/macos/Tests/ClawdisIPCTests/CommandResolverTests.swift @@ -1,3 +1,4 @@ +import Darwin import Foundation import Testing @testable import Clawdis @@ -35,8 +36,20 @@ import Testing let nodePath = tmp.appendingPathComponent("node_modules/.bin/node") let scriptPath = tmp.appendingPathComponent("bin/clawdis.js") try makeExec(at: nodePath) + try "#!/bin/sh\necho v22.0.0\n".write(to: nodePath, atomically: true, encoding: .utf8) + try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: nodePath.path) try makeExec(at: scriptPath) + let previous = getenv("CLAWDIS_RUNTIME").flatMap { String(validatingCString: $0) } + setenv("CLAWDIS_RUNTIME", "node", 1) + defer { + if let previous { + setenv("CLAWDIS_RUNTIME", previous, 1) + } else { + unsetenv("CLAWDIS_RUNTIME") + } + } + let cmd = CommandResolver.clawdisCommand(subcommand: "rpc") #expect(cmd.count >= 3) diff --git a/src/cli/program.ts b/src/cli/program.ts index 432230c4e..0e5f1a99d 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -1,4 +1,3 @@ -import { randomUUID } from "node:crypto"; import chalk from "chalk"; import { Command } from "commander"; import { agentCommand } from "../commands/agent.js"; @@ -273,14 +272,6 @@ Examples: return; } - const jobId = cmd.jobId ? String(cmd.jobId) : randomUUID(); - const startedAt = Date.now(); - respond({ - type: "event", - event: "job-state", - payload: { id: jobId, state: "started", startedAt }, - }); - const logs: string[] = []; const runtime: RuntimeEnv = { log: (msg: string) => logs.push(String(msg)), @@ -308,31 +299,9 @@ Examples: try { await agentCommand(opts, runtime, createDefaultDeps()); - const endedAt = Date.now(); - respond({ - type: "event", - event: "job-state", - payload: { - id: jobId, - state: "done", - durationMs: endedAt - startedAt, - endedAt, - }, - }); const payload = extractPayload(logs); respond({ type: "result", ok: true, payload }); } catch (err) { - const endedAt = Date.now(); - respond({ - type: "event", - event: "job-state", - payload: { - id: jobId, - state: "error", - durationMs: endedAt - startedAt, - endedAt, - }, - }); respond({ type: "error", error: String(err) }); } } catch (err) { diff --git a/src/commands/agent.ts b/src/commands/agent.ts index bc064306b..9f4cd8afd 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -300,6 +300,7 @@ export async function agentCommand( stream: "job", data: { state: "started", + startedAt, to: opts.to, sessionId, isNewSession, @@ -327,6 +328,8 @@ export async function agentCommand( stream: "job", data: { state: "done", + startedAt, + endedAt: Date.now(), to: opts.to, sessionId, durationMs: Date.now() - startedAt, @@ -338,6 +341,8 @@ export async function agentCommand( stream: "job", data: { state: "error", + startedAt, + endedAt: Date.now(), to: opts.to, sessionId, durationMs: Date.now() - startedAt,