Repository navigation
acpmux: pause two agents that keep messaging each other without progress #21975
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
6478f11
acpmux: test that two agents messaging each other without progress ar…
teamleaderleo 78326ba
acpmux: pause two agents that keep messaging each other without progress
teamleaderleo aaac37d
acpmux: tests for the agent pair guard's review blockers
teamleaderleo d3209ce
acpmux: agent pair guard fixes from review
teamleaderleo 00d5771
Merge remote-tracking branch 'origin/feat-cmux-next' into lead/21975
teamleaderleo 09b6a7d
test(acpmux): agent pair guard misses cmux-subsessions messages
teamleaderleo 35ef39d
fix(acpmux): guard cmux-subsessions messages and keep held ones owed
teamleaderleo 80f386f
Merge remote-tracking branch 'origin/feat-cmux-next' into wt/21975
teamleaderleo 8e35008
chore(next): PR autofix at 00d57718d07d
856e586
Merge remote-tracking branch 'origin/chat/agent-pingpong-guard' into …
teamleaderleo ca001b9
test(acpmux): a restart during a resume holds the rest again
teamleaderleo f78dc28
fix(acpmux): a restart during a resume keeps delivering what was held
teamleaderleo c49a339
feat(next): the agent pane may resume a paused agent pair of a chat i…
teamleaderleo 3309231
acpmux: cap agent pair messages and keep held messages through a release
teamleaderleo 01e88e8
acpmux: a dead release is not accepted, and a resent held message kee…
teamleaderleo 87af37e
Merge remote-tracking branch 'origin/chat/agent-pingpong-guard' into …
teamleaderleo c944981
Merge commit 'refs/wt21975/base' into wt/21975
teamleaderleo 7224dd5
acpmux: leave the pause row to #22309
teamleaderleo 3374b54
Merge remote-tracking branch 'origin/feat-cmux-next' into lead/21975
teamleaderleo f8b5588
acpmux: a released message waits its turn, and holds keep their order
teamleaderleo df60fb9
test(acpmux): the sub-session pair's tenth exchange goes back to the …
teamleaderleo 7e6da00
refactor(acpmux): keep the agent pair guard out of the over-budget files
teamleaderleo 0648dd3
feat(agent-pane): the pane policy and page know _acpmux/agent_pair_re…
teamleaderleo fd79911
Merge origin/chat/agent-pingpong-guard (f8b55884c96) into lead/21975
teamleaderleo 290a99a
fix(acpmux): a steer-only message into a paused pair is held
teamleaderleo 91d456e
test(agent-pane): the agent_pair_resume scope cases use the runner's …
teamleaderleo ed07b2a
Merge branch 'feat-cmux-next' into chat/agent-pingpong-guard
teamleaderleo 0802b88
chore(next): PR autofix at ed07b2a8de0f
55ce7f7
Merge branch 'feat-cmux-next' into chat/agent-pingpong-guard
teamleaderleo ee5b57d
chore(next): PR autofix at 55ce7f7f223f
93fae49
Merge branch 'feat-cmux-next' into chat/agent-pingpong-guard
teamleaderleo cdf7908
Merge branch 'feat-cmux-next' into chat/agent-pingpong-guard
teamleaderleo a4a7090
Merge branch 'feat-cmux-next' into chat/agent-pingpong-guard
teamleaderleo 51294c4
Merge branch 'feat-cmux-next' into chat/agent-pingpong-guard
teamleaderleo e975ea5
Merge origin/feat-cmux-next into chat/agent-pingpong-guard
teamleaderleo ebdba19
acpmux agent pairs: a resume waits for the logs; god files shrink
teamleaderleo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
64 changes: 64 additions & 0 deletions
64
Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AcpmuxPaneMethods+ReplyShapes.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import Foundation | ||
|
|
||
| /// The replies the relay filters itself (ad349), of ``AcpmuxPaneMethods``: read-only requests | ||
| /// whose reply reaches the pane cut to the fields it renders. | ||
| nonisolated extension AcpmuxPaneMethods { | ||
| /// The shape of a reply the relay filters itself, without relying on the daemon's redaction. | ||
| public nonisolated indirect enum ReplyShape: Sendable { | ||
| case string | ||
| case object([String: ReplyShape]) | ||
| case list(ReplyShape) | ||
| /// Only the scheme of the URL in the sibling field `source` (`ssh`, `ws`, `wss`), so the | ||
| /// page learns a peer's transport without its URL (which can carry a user or a token). | ||
| case scheme(from: String) | ||
| } | ||
|
|
||
| /// The URL schemes a `.scheme` field may report; any other scheme is dropped. | ||
| static let reportedSchemes: Set<String> = ["ssh", "ws", "wss"] | ||
|
|
||
| /// Replies filtered to the fields the pane renders; everything else is dropped, at every depth. | ||
| /// `_acpmux/status`: the pane reads only `peers[].name` and each peer's `transport`, the | ||
| /// scheme of its `url` (direct.ts, after initialize; environments.ts peerKindsFromStatus). | ||
| public static let replyShapes: [String: ReplyShape] = [ | ||
| "_acpmux/status": .object([ | ||
| "peers": .list(.object(["name": .string, "transport": .scheme(from: "url")])), | ||
| ]), | ||
| ] | ||
|
|
||
| /// `value` cut to `shape`: nil when it does not fit (a list keeps only the items that fit). | ||
| static func filtered(_ value: Any, to shape: ReplyShape) -> Any? { | ||
| switch shape { | ||
| case .string: | ||
| return value as? String | ||
| case .scheme: | ||
| guard let scheme = (value as? String).flatMap({ URLComponents(string: $0)?.scheme?.lowercased() }), | ||
| reportedSchemes.contains(scheme) else { return nil } | ||
| return scheme | ||
| case .list(let item): | ||
| return (value as? [Any])?.compactMap { filtered($0, to: item) } | ||
| case .object(let fields): | ||
| guard let object = value as? [String: Any] else { return nil } | ||
| let kept = fields.compactMap { key, field -> (String, Any)? in | ||
| let source: String | ||
| if case .scheme(let from) = field { source = from } else { source = key } | ||
| return object[source].flatMap { filtered($0, to: field) }.map { (key, $0) } | ||
| } | ||
| // An object with none of its fields is dropped (a peer with no name renders nothing). | ||
| return kept.isEmpty ? nil : Dictionary(uniqueKeysWithValues: kept) | ||
| } | ||
| } | ||
|
|
||
| /// A reply to a filtered request, rebuilt with the page's id (raw JSON) and its filtered | ||
| /// result. An error keeps only its code and message. | ||
| static func filteredReply(_ object: [String: Any], shape: ReplyShape, pageID: String) -> String { | ||
| let id = (try? JSONSerialization.jsonObject(with: Data(pageID.utf8), options: [.fragmentsAllowed])) ?? NSNull() | ||
| var reply: [String: Any] = ["jsonrpc": "2.0", "id": id] | ||
| if let error = object["error"] as? [String: Any] { | ||
| reply["error"] = ["code": error["code"] as? Int ?? -32603, "message": error["message"] as? String ?? ""] | ||
| } else { | ||
| reply["result"] = object["result"].flatMap { filtered($0, to: shape) } ?? [String: Any]() | ||
| } | ||
| let data = (try? JSONSerialization.data(withJSONObject: reply, options: [.sortedKeys])) ?? Data() | ||
| return String(decoding: data, as: UTF8.self) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneTunables+AgentPairGuard.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| public import CmuxNextDesign | ||
| import CmuxNextPages | ||
|
|
||
| nonisolated extension AgentPaneTunables { | ||
| /// The runaway guard for agent-to-agent messages (acpmux `hub/agent_pairs.rs`): two agent | ||
| /// sessions that keep messaging each other without a tool call or file change in between, or | ||
| /// that keep sending the same message, are paused until the person resumes them. Off, the | ||
| /// daemon this app starts gets `ACPMUX_AGENT_PAIR_GUARD=0`. Read at each daemon start, so a | ||
| /// toggle applies to the next one. | ||
| public static let agentPairGuard = Tunable<Bool>.toggle( | ||
| "agent.pairGuard", PageTunables.section, "Pause agents messaging in a loop", | ||
| help: "Two agents that keep messaging each other without making progress are paused until you resume them.", | ||
| default: true, code: "AgentPaneTunables.agentPairGuard") | ||
|
|
||
| /// The guard's cap: two agents that exchange more than this many messages within | ||
| /// ``agentPairWindow`` with no prompt from the person are paused (`ACPMUX_AGENT_PAIR_MAX_MESSAGES`). | ||
| public static let agentPairCap = Tunable<Double>.number( | ||
| "agent.pairCap", PageTunables.section, "Agent message cap", | ||
| help: "Messages two agents may exchange without you. 0 is off.", | ||
| default: 20, range: 0...200, step: 1, unit: .count, code: "AgentPaneTunables.agentPairCap") | ||
|
|
||
| /// The window ``agentPairCap`` counts over (`ACPMUX_AGENT_PAIR_WINDOW_SECS`). | ||
| public static let agentPairWindow = Tunable<Double>.number( | ||
| "agent.pairWindow", PageTunables.section, "Agent message window", | ||
| help: "How long the cap counts. 0 is off.", | ||
| default: 600, range: 0...3600, step: 60, unit: .seconds, code: "AgentPaneTunables.agentPairWindow") | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new
agent.pairGuardsetting defaults to on. This PR adds user-visible daemon behavior with more than 40 runtime lines, so the repository's high-tier rule applies: “Ship user-visible high-tier behavior behind a default-off setting.”Default the setting and its launch behavior to off until the user opts in.
Context Used: CLAUDE.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kept on by default. That was Leo's call for this guard: a runaway pair spends usage silently, and the pause is undone by one Resume or by the person's next prompt. The Debug Settings toggle
agent.pairGuardturns it off, andagent.pairCap/agent.pairWindow(0 = off) tune the cap.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That context makes sense. Given this guard prevents runaway agent-to-agent usage by default, while still providing an explicit Debug Settings opt-out and a direct resume path, the default-on choice is appropriate here. I’m withdrawing the default-off concern.