Fix clipped server badge and client_hands hang across servers
This commit is contained in:
+1
-1
@@ -358,7 +358,7 @@ Implementer sessions get eight MCP tools for it (`internal/agents/clients.go`).
|
|||||||
- `client_edit`: a `read_file`, then a `write_file` of the text with `old_string` replaced by `new_string`, refused unless `old_string` appears exactly once. Nothing locks the file or checks it is unchanged between the two, so a write in between, whether another agent's `client_edit` of the same file or the human's or an app's, is lost.
|
- `client_edit`: a `read_file`, then a `write_file` of the text with `old_string` replaced by `new_string`, refused unless `old_string` appears exactly once. Nothing locks the file or checks it is unchanged between the two, so a write in between, whether another agent's `client_edit` of the same file or the human's or an app's, is lost.
|
||||||
|
|
||||||
`client_read`, `client_write` and `client_edit` wait at most 2 minutes for each reply, and their paths, like `client_download`'s `path`, are absolute or start with `~/`, which the app expands.
|
`client_read`, `client_write` and `client_edit` wait at most 2 minutes for each reply, and their paths, like `client_download`'s `path`, are absolute or start with `~/`, which the app expands.
|
||||||
- `client_hands`: `op: hands`, with `tool` and `arguments` as the `hands` tools take them (see "Screen and app control"). The app runs one `byre-hands`, bundled at `Contents/Helpers/byre-hands`, started on first use and again after it exits, writes each call to its stdin as a `tools/call` line and replies with the line's `result`, which byre returns to the agent as the tool's result, images included (`mcp.Tool.Rich`; Codex gets them as `inputImage` data URLs). The macOS privacy grants it needs (Screen Recording, Accessibility, Automation of Orion) belong to byre's app on that Mac, since the app is the responsible process; `NSAppleEventsUsageDescription` in `mac/Info.plist` is what lets macOS ask for the Automation grant. Each `byre-hands` process has its own pending calls, so one that exits fails only the calls sent to it.
|
- `client_hands`: `op: hands`, with `tool` and `arguments` as the `hands` tools take them (see "Screen and app control"). The app runs one `byre-hands`, bundled at `Contents/Helpers/byre-hands`, shared by its connections to every server (two run from that one path would cancel each other's ScreenCaptureKit connection, see "Screen and app control"), started on first use and again after it exits, writes each call to its stdin as a `tools/call` line and replies with the line's `result`, which byre returns to the agent as the tool's result, images included (`mcp.Tool.Rich`; Codex gets them as `inputImage` data URLs). The macOS privacy grants it needs (Screen Recording, Accessibility, Automation of Orion) belong to byre's app on that Mac, since the app is the responsible process; `NSAppleEventsUsageDescription` in `mac/Info.plist` is what lets macOS ask for the Automation grant. When `byre-hands` exits, the calls waiting on it fail. The agent's call waits for the reply 2 minutes plus the call's `wait_ms`, twice its `timeout_ms` (which `browser` and `chrome` spend once on the load and again on `wait_for`) and every `act` step's `wait`, then fails.
|
||||||
|
|
||||||
A byre restart drops every connection; each app reconnects 2s later, and calls in flight at the restart fail as disconnected. Updating a client's app goes through the tools themselves: `client_upload` the new `Byre.app`, then `client_bash` a command that quits the old app and opens the new one, which outlives the app and reconnects. An app that doesn't know the `upload` op can't be updated this way, so its update is by hand.
|
A byre restart drops every connection; each app reconnects 2s later, and calls in flight at the restart fail as disconnected. Updating a client's app goes through the tools themselves: `client_upload` the new `Byre.app`, then `client_bash` a command that quits the old app and opens the new one, which outlives the app and reconnects. An app that doesn't know the `upload` op can't be updated this way, so its update is by hand.
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const (
|
|||||||
replyGrace = 30 * time.Second
|
replyGrace = 30 * time.Second
|
||||||
copyTimeout = 10 * time.Minute
|
copyTimeout = 10 * time.Minute
|
||||||
fileTimeout = 2 * time.Minute
|
fileTimeout = 2 * time.Minute
|
||||||
handsTimeout = 11 * time.Minute
|
handsTimeout = 2 * time.Minute
|
||||||
defaultReadLimit = 2000
|
defaultReadLimit = 2000
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -528,7 +528,17 @@ func (m *Manager) clientHands(ctx context.Context, raw json.RawMessage) mcp.Resu
|
|||||||
args.Arguments = &map[string]any{}
|
args.Arguments = &map[string]any{}
|
||||||
}
|
}
|
||||||
|
|
||||||
cctx, cancel := context.WithTimeout(ctx, handsTimeout)
|
ms := func(m map[string]any, key string) time.Duration {
|
||||||
|
v, _ := m[key].(float64)
|
||||||
|
return time.Duration(v) * time.Millisecond
|
||||||
|
}
|
||||||
|
timeout := handsTimeout + ms(*args.Arguments, "wait_ms") + 2*ms(*args.Arguments, "timeout_ms")
|
||||||
|
steps, _ := (*args.Arguments)["steps"].([]any)
|
||||||
|
for _, s := range steps {
|
||||||
|
step, _ := s.(map[string]any)
|
||||||
|
timeout += ms(step, "wait")
|
||||||
|
}
|
||||||
|
cctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
reply, err := m.clients.Call(cctx, args.Client, api.ClientRequest{Op: api.Hands, Tool: &args.Tool, Arguments: args.Arguments})
|
reply, err := m.clients.Call(cctx, args.Client, api.ClientRequest{Op: api.Hands, Tool: &args.Tool, Arguments: args.Arguments})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -350,6 +350,7 @@ struct Badge: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
Text("\(count)")
|
Text("\(count)")
|
||||||
.font(Fonts.font(11, weight: 700))
|
.font(Fonts.font(11, weight: 700))
|
||||||
|
.fixedSize()
|
||||||
.foregroundStyle(Palette.danger)
|
.foregroundStyle(Palette.danger)
|
||||||
.padding(.horizontal, 7)
|
.padding(.horizontal, 7)
|
||||||
.background(Palette.dangerTint, in: Capsule())
|
.background(Palette.dangerTint, in: Capsule())
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ private let maxOutput = 64 << 10
|
|||||||
private let maxMessage = 256 << 20
|
private let maxMessage = 256 << 20
|
||||||
private let maxRead = 64 << 20
|
private let maxRead = 64 << 20
|
||||||
private let copyTimeoutMs = 10 * 60 * 1000
|
private let copyTimeoutMs = 10 * 60 * 1000
|
||||||
|
private let hands = Hands()
|
||||||
|
|
||||||
// The signature goes to a file, since a login shell's startup files can print to stdout.
|
// The signature goes to a file, since a login shell's startup files can print to stdout.
|
||||||
private let signScript = """
|
private let signScript = """
|
||||||
@@ -28,7 +29,6 @@ private let signScript = """
|
|||||||
@MainActor
|
@MainActor
|
||||||
final class Remote {
|
final class Remote {
|
||||||
let api: API
|
let api: API
|
||||||
private let hands = Hands()
|
|
||||||
private var ws: URLSessionWebSocketTask?
|
private var ws: URLSessionWebSocketTask?
|
||||||
private var stopped = false
|
private var stopped = false
|
||||||
private var heard = Date()
|
private var heard = Date()
|
||||||
@@ -63,9 +63,6 @@ final class Remote {
|
|||||||
checker?.cancel()
|
checker?.cancel()
|
||||||
ws?.cancel(with: .normalClosure, reason: nil)
|
ws?.cancel(with: .normalClosure, reason: nil)
|
||||||
ws = nil
|
ws = nil
|
||||||
Task { [hands] in
|
|
||||||
await hands.stop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A socket byre closed while this Mac slept stays open here until something is sent on it.
|
// A socket byre closed while this Mac slept stays open here until something is sent on it.
|
||||||
@@ -370,7 +367,6 @@ private struct HandsResponse: Decodable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private actor Hands {
|
private actor Hands {
|
||||||
private var process: Process?
|
|
||||||
private var input: FileHandle?
|
private var input: FileHandle?
|
||||||
private var next = 0
|
private var next = 0
|
||||||
private var pending: [Int: CheckedContinuation<OpenAPIObjectContainer, any Error>] = [:]
|
private var pending: [Int: CheckedContinuation<OpenAPIObjectContainer, any Error>] = [:]
|
||||||
@@ -392,10 +388,6 @@ private actor Hands {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop() {
|
|
||||||
process?.terminate()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func running() throws -> FileHandle {
|
private func running() throws -> FileHandle {
|
||||||
if let input {
|
if let input {
|
||||||
return input
|
return input
|
||||||
@@ -410,7 +402,6 @@ private actor Hands {
|
|||||||
p.standardError = FileHandle.nullDevice
|
p.standardError = FileHandle.nullDevice
|
||||||
let exit = try start(p)
|
let exit = try start(p)
|
||||||
log.info("byre-hands started: pid \(p.processIdentifier)")
|
log.info("byre-hands started: pid \(p.processIdentifier)")
|
||||||
process = p
|
|
||||||
input = stdin.fileHandleForWriting
|
input = stdin.fileHandleForWriting
|
||||||
Task {
|
Task {
|
||||||
await serve(p, stdout.fileHandleForReading, exit)
|
await serve(p, stdout.fileHandleForReading, exit)
|
||||||
@@ -435,7 +426,6 @@ private actor Hands {
|
|||||||
for await _ in exit {}
|
for await _ in exit {}
|
||||||
let status = p.terminationStatus
|
let status = p.terminationStatus
|
||||||
log.info("byre-hands exited: status \(status)")
|
log.info("byre-hands exited: status \(status)")
|
||||||
process = nil
|
|
||||||
input = nil
|
input = nil
|
||||||
let waiting = pending
|
let waiting = pending
|
||||||
pending = [:]
|
pending = [:]
|
||||||
|
|||||||
Reference in New Issue
Block a user