fix(agents): one host-routed agent-detection entry point (#27054)

* fix(agents): one host-routed agent-detection entry point

preflight.detectAgents / refreshAgents take an optional { worktreeId } and the
host resolves that workspace's project runtime (WSL included) or SSH target.
Gated by preflight.workspace-scoped.v1; old peers keep the host default.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(agents): route detection by the worktree's own host record

A repo id shared by local and SSH rows made the first row pick the detection
host. Resolve the worktree from the host's list and apply the shared
execution-host routing rule; ambiguous rows refuse. Dashboard falls back to the
host-default list until a workspace's own list loads.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(agents): refuse an ambiguous workspace instead of probing the host

Only selector_not_found keeps the host default; selector_ambiguous now fails
closed. Adds runtime-level coverage of the lookup.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(agents): refuse an ambiguously owned folder workspace

Co-Authored-By: Claude <noreply@anthropic.com>

* test(agents): quick-launch runtime target carries the workspace

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
OrcaWin
2026-10-09 18:39:13 -07:00
committed by GitHub
co-authored by Claude m4air
parent 8b3de5bbfb
commit d3e3b173e6
39 changed files with 2283 additions and 1314 deletions
@@ -327,6 +327,11 @@
"message": "Couldn't start the agent."
}
},
"742db574bda3": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\",\"params\":{\"worktreeId\":\"repo-9::/w\"}}"
},
"7cdbda15d605": {
"name": "agent.launchReplay#1",
"ordinal": 7,
@@ -419,38 +424,6 @@
"status": "pending",
"startedAt": 0
},
"9c16efc31903": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex"]
}
}
},
"a4273b38df83": {
"launched": "unlaunched"
},
@@ -742,6 +715,38 @@
"ordinal": 3,
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
},
"e5392ac00e18": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-9::/w"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex"]
}
}
},
"eaa398d8c3aa": {
"launched": {
"kind": "unconfirmed",
@@ -760,11 +765,6 @@
"launched": {
"kind": "sent"
}
},
"f49d184679d1": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\"}"
}
},
"recording": {
@@ -785,8 +785,8 @@
{
"id": "pr-triage-launch.normal:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "9c16efc31903", "ba3fac1eb199"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "28339abecf78"],
"sender": ["3e6a0417d2ee", "02188f091419", "e5392ac00e18", "ba3fac1eb199"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3", "28339abecf78"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -797,8 +797,8 @@
{
"id": "pr-triage-launch.result-absent:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "9c16efc31903", "8638ab90602a"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "28339abecf78"],
"sender": ["3e6a0417d2ee", "02188f091419", "e5392ac00e18", "8638ab90602a"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3", "28339abecf78"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -809,8 +809,8 @@
{
"id": "pr-triage-launch.result-null:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "9c16efc31903", "c39600966772"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "28339abecf78"],
"sender": ["3e6a0417d2ee", "02188f091419", "e5392ac00e18", "c39600966772"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3", "28339abecf78"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -821,8 +821,8 @@
{
"id": "pr-triage-launch.inner-ok-missing:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "9c16efc31903", "ab12d545c466"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "28339abecf78"],
"sender": ["3e6a0417d2ee", "02188f091419", "e5392ac00e18", "ab12d545c466"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3", "28339abecf78"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -833,8 +833,8 @@
{
"id": "pr-triage-launch.inner-false-string-error:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "9c16efc31903", "5b817dac4c7d"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "28339abecf78"],
"sender": ["3e6a0417d2ee", "02188f091419", "e5392ac00e18", "5b817dac4c7d"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3", "28339abecf78"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -845,8 +845,8 @@
{
"id": "pr-triage-launch.inner-false-object-error:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "9c16efc31903", "7cdbda15d605"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "28339abecf78"],
"sender": ["3e6a0417d2ee", "02188f091419", "e5392ac00e18", "7cdbda15d605"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3", "28339abecf78"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -857,8 +857,8 @@
{
"id": "pr-triage-launch.outer-refused:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "9c16efc31903", "a8f475e8a3a8"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "28339abecf78"],
"sender": ["3e6a0417d2ee", "02188f091419", "e5392ac00e18", "a8f475e8a3a8"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3", "28339abecf78"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -869,8 +869,8 @@
{
"id": "pr-triage-launch.outer-refused-no-message:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "9c16efc31903", "4a304acaf84f"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "28339abecf78"],
"sender": ["3e6a0417d2ee", "02188f091419", "e5392ac00e18", "4a304acaf84f"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3", "28339abecf78"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -881,8 +881,8 @@
{
"id": "pr-triage-launch.method-not-found:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "9c16efc31903", "b613a82cee3d"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "28339abecf78"],
"sender": ["3e6a0417d2ee", "02188f091419", "e5392ac00e18", "b613a82cee3d"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3", "28339abecf78"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -896,14 +896,14 @@
"sender": [
"3e6a0417d2ee",
"02188f091419",
"9c16efc31903",
"e5392ac00e18",
"3b9fb3dfed63",
"b81eb9edc2b2"
],
"payloads": [
"e0eef7c9fafe",
"1c2564f1daca",
"f49d184679d1",
"742db574bda3",
"28339abecf78",
"dca101029f70"
],
@@ -920,14 +920,14 @@
"sender": [
"3e6a0417d2ee",
"02188f091419",
"9c16efc31903",
"e5392ac00e18",
"675c01bc8b85",
"b81eb9edc2b2"
],
"payloads": [
"e0eef7c9fafe",
"1c2564f1daca",
"f49d184679d1",
"742db574bda3",
"28339abecf78",
"dca101029f70"
],
@@ -41,7 +41,7 @@
}
}
},
"0220267a7bca": {
"039b2277b75a": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
@@ -52,42 +52,7 @@
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"error": {
"code": "method_not_found",
"message": "Unknown method"
},
"id": "frame-3",
"ok": false
}
}
},
"084fe2afb0ce": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
"worktreeId": "repo-9::/w"
}
},
{
@@ -110,6 +75,40 @@
}
}
},
"1469e05f35f7": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-9::/w"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"$rpc": "null"
}
}
}
},
"1963ee2edae9": {
"launched": {
"kind": "not-started",
@@ -228,7 +227,7 @@
"startedAt": 0
}
},
"570f5efcd445": {
"67471f63b6f7": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
@@ -239,215 +238,7 @@
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"error": {
"code": "refused",
"message": "outer refused"
},
"id": "frame-3",
"ok": false
}
}
},
"57f451cceda3": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "rejected",
"startedAt": 0,
"settledAt": 0,
"error": {
"category": "Error",
"message": "",
"isRpcDeliveryUnknown": true
}
}
},
"585dfba86809": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true
}
}
},
"6dd8724be3b1": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"$rpc": "null"
}
}
}
},
"9270aeb7d9c6": {
"status": "pending",
"startedAt": 0
},
"9c16efc31903": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex"]
}
}
},
"a4273b38df83": {
"launched": "unlaunched"
},
"a9705081551e": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"error": {
"message": "inner refused"
},
"ok": false
}
}
}
},
"ac08a3ca5c67": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
"worktreeId": "repo-9::/w"
}
},
{
@@ -471,6 +262,148 @@
}
}
},
"742db574bda3": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\",\"params\":{\"worktreeId\":\"repo-9::/w\"}}"
},
"786221fb36b0": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-9::/w"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "rejected",
"startedAt": 0,
"settledAt": 0,
"error": {
"category": "Error",
"message": "",
"isRpcDeliveryUnknown": true
}
}
},
"880c4a7837af": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-9::/w"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"error": {
"code": "method_not_found",
"message": "Unknown method"
},
"id": "frame-3",
"ok": false
}
}
},
"9270aeb7d9c6": {
"status": "pending",
"startedAt": 0
},
"96aa7f51b001": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-9::/w"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "rejected",
"startedAt": 0,
"settledAt": 0,
"error": {
"category": "Error",
"message": "transport failure",
"isRpcDeliveryUnknown": true
}
}
},
"a4273b38df83": {
"launched": "unlaunched"
},
"a55db9667146": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-9::/w"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true
}
}
},
"b13ddf18a62e": {
"launched": {
"kind": "not-started",
@@ -535,12 +468,7 @@
}
}
},
"e0eef7c9fafe": {
"name": "repo.list#1",
"ordinal": 3,
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
},
"e1b544c0b6ec": {
"bfd08cc3060a": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
@@ -551,7 +479,7 @@
{
"name": "params",
"value": {
"$rpc": "absent"
"worktreeId": "repo-9::/w"
}
},
{
@@ -562,36 +490,22 @@
}
],
"settlement": {
"status": "rejected",
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"error": {
"category": "Error",
"message": "transport failure",
"isRpcDeliveryUnknown": true
"value": {
"id": "frame-3",
"ok": true,
"result": {
"error": {
"message": "inner refused"
},
"ok": false
}
}
}
},
"e35d7003f64c": {
"launched": {
"kind": "not-started",
"message": "transport failure"
}
},
"eb79a9b3682a": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"$rpc": "undefined"
}
},
"eb8d47f90956": {
"launched": {
"kind": "sent"
}
},
"efd2ac432554": {
"d85a8cd9155e": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
@@ -602,7 +516,7 @@
{
"name": "params",
"value": {
"$rpc": "absent"
"worktreeId": "repo-9::/w"
}
},
{
@@ -626,10 +540,96 @@
}
}
},
"f49d184679d1": {
"e0eef7c9fafe": {
"name": "repo.list#1",
"ordinal": 3,
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
},
"e35d7003f64c": {
"launched": {
"kind": "not-started",
"message": "transport failure"
}
},
"e5392ac00e18": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\"}"
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-9::/w"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex"]
}
}
},
"e69d09287c64": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-9::/w"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"error": {
"code": "refused",
"message": "outer refused"
},
"id": "frame-3",
"ok": false
}
}
},
"eb79a9b3682a": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"$rpc": "undefined"
}
},
"eb8d47f90956": {
"launched": {
"kind": "sent"
}
}
},
"recording": {
@@ -650,8 +650,8 @@
{
"id": "pr-triage-launch.normal:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "9c16efc31903", "ba3fac1eb199"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "28339abecf78"],
"sender": ["3e6a0417d2ee", "02188f091419", "e5392ac00e18", "ba3fac1eb199"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3", "28339abecf78"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -662,8 +662,8 @@
{
"id": "pr-triage-launch.result-absent:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "585dfba86809"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["3e6a0417d2ee", "02188f091419", "a55db9667146"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -674,8 +674,8 @@
{
"id": "pr-triage-launch.result-null:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "6dd8724be3b1"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["3e6a0417d2ee", "02188f091419", "1469e05f35f7"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -686,8 +686,8 @@
{
"id": "pr-triage-launch.inner-ok-missing:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "084fe2afb0ce"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["3e6a0417d2ee", "02188f091419", "039b2277b75a"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -698,8 +698,8 @@
{
"id": "pr-triage-launch.inner-false-string-error:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "ac08a3ca5c67"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["3e6a0417d2ee", "02188f091419", "67471f63b6f7"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -710,8 +710,8 @@
{
"id": "pr-triage-launch.inner-false-object-error:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "a9705081551e"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["3e6a0417d2ee", "02188f091419", "bfd08cc3060a"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -722,8 +722,8 @@
{
"id": "pr-triage-launch.outer-refused:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "570f5efcd445"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["3e6a0417d2ee", "02188f091419", "e69d09287c64"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -734,8 +734,8 @@
{
"id": "pr-triage-launch.outer-refused-no-message:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "efd2ac432554"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["3e6a0417d2ee", "02188f091419", "d85a8cd9155e"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -746,8 +746,8 @@
{
"id": "pr-triage-launch.method-not-found:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "0220267a7bca"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["3e6a0417d2ee", "02188f091419", "880c4a7837af"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -758,8 +758,8 @@
{
"id": "pr-triage-launch.transport-rejection:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "e1b544c0b6ec"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["3e6a0417d2ee", "02188f091419", "96aa7f51b001"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -770,8 +770,8 @@
{
"id": "pr-triage-launch.transport-rejection-no-message:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "57f451cceda3"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["3e6a0417d2ee", "02188f091419", "786221fb36b0"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -329,6 +329,11 @@
}
}
},
"742db574bda3": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\",\"params\":{\"worktreeId\":\"repo-9::/w\"}}"
},
"747c556da67a": {
"name": "repo.list#1",
"ordinal": 1,
@@ -399,38 +404,6 @@
"status": "pending",
"startedAt": 0
},
"9c16efc31903": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex"]
}
}
},
"a4273b38df83": {
"launched": "unlaunched"
},
@@ -578,6 +551,38 @@
"message": "transport failure"
}
},
"e5392ac00e18": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-9::/w"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex"]
}
}
},
"eb79a9b3682a": {
"status": "fulfilled",
"startedAt": 0,
@@ -625,11 +630,6 @@
"ok": false
}
}
},
"f49d184679d1": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\"}"
}
},
"recording": {
@@ -650,8 +650,8 @@
{
"id": "pr-triage-launch.normal:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "9c16efc31903", "ba3fac1eb199"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "28339abecf78"],
"sender": ["3e6a0417d2ee", "02188f091419", "e5392ac00e18", "ba3fac1eb199"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3", "28339abecf78"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -358,6 +358,11 @@
}
}
},
"742db574bda3": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\",\"params\":{\"worktreeId\":\"repo-9::/w\"}}"
},
"753cc167886c": {
"launched": {
"kind": "not-started",
@@ -440,38 +445,6 @@
"message": "Cannot read properties of undefined (reading 'settings')"
}
},
"9c16efc31903": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex"]
}
}
},
"a4273b38df83": {
"launched": "unlaunched"
},
@@ -585,6 +558,38 @@
"message": "transport failure"
}
},
"e5392ac00e18": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-9::/w"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex"]
}
}
},
"eb79a9b3682a": {
"status": "fulfilled",
"startedAt": 0,
@@ -598,11 +603,6 @@
"kind": "sent"
}
},
"f49d184679d1": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\"}"
},
"fb9f08890f70": {
"name": "settings.get#1",
"ordinal": 2,
@@ -656,8 +656,8 @@
{
"id": "pr-triage-launch.normal:launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "9c16efc31903", "ba3fac1eb199"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "28339abecf78"],
"sender": ["3e6a0417d2ee", "02188f091419", "e5392ac00e18", "ba3fac1eb199"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3", "28339abecf78"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -668,8 +668,8 @@
{
"id": "pr-triage-launch.result-absent:launched",
"observation": {
"sender": ["3e6a0417d2ee", "98bd3a5fda2b", "9c16efc31903"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["3e6a0417d2ee", "98bd3a5fda2b", "e5392ac00e18"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -680,8 +680,8 @@
{
"id": "pr-triage-launch.result-null:launched",
"observation": {
"sender": ["3e6a0417d2ee", "69518e60477b", "9c16efc31903"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["3e6a0417d2ee", "69518e60477b", "e5392ac00e18"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -692,8 +692,8 @@
{
"id": "pr-triage-launch.inner-ok-missing:launched",
"observation": {
"sender": ["3e6a0417d2ee", "fb9f08890f70", "9c16efc31903", "ba3fac1eb199"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "28339abecf78"],
"sender": ["3e6a0417d2ee", "fb9f08890f70", "e5392ac00e18", "ba3fac1eb199"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3", "28339abecf78"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -704,8 +704,8 @@
{
"id": "pr-triage-launch.inner-false-string-error:launched",
"observation": {
"sender": ["3e6a0417d2ee", "86798e4821d1", "9c16efc31903", "ba3fac1eb199"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "28339abecf78"],
"sender": ["3e6a0417d2ee", "86798e4821d1", "e5392ac00e18", "ba3fac1eb199"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3", "28339abecf78"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -716,8 +716,8 @@
{
"id": "pr-triage-launch.inner-false-object-error:launched",
"observation": {
"sender": ["3e6a0417d2ee", "1c5359ea3f7d", "9c16efc31903", "ba3fac1eb199"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "28339abecf78"],
"sender": ["3e6a0417d2ee", "1c5359ea3f7d", "e5392ac00e18", "ba3fac1eb199"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3", "28339abecf78"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -728,8 +728,8 @@
{
"id": "pr-triage-launch.outer-refused:launched",
"observation": {
"sender": ["3e6a0417d2ee", "ceb5fe72c101", "9c16efc31903"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["3e6a0417d2ee", "ceb5fe72c101", "e5392ac00e18"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -740,8 +740,8 @@
{
"id": "pr-triage-launch.outer-refused-no-message:launched",
"observation": {
"sender": ["3e6a0417d2ee", "34f3a5b8bd78", "9c16efc31903"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["3e6a0417d2ee", "34f3a5b8bd78", "e5392ac00e18"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -752,8 +752,8 @@
{
"id": "pr-triage-launch.method-not-found:launched",
"observation": {
"sender": ["3e6a0417d2ee", "0ba58e9fadf7", "9c16efc31903"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["3e6a0417d2ee", "0ba58e9fadf7", "e5392ac00e18"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -764,8 +764,8 @@
{
"id": "pr-triage-launch.transport-rejection:launched",
"observation": {
"sender": ["3e6a0417d2ee", "566d8202b582", "9c16efc31903"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["3e6a0417d2ee", "566d8202b582", "e5392ac00e18"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -776,8 +776,8 @@
{
"id": "pr-triage-launch.transport-rejection-no-message:launched",
"observation": {
"sender": ["3e6a0417d2ee", "0ca529648be4", "9c16efc31903"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["3e6a0417d2ee", "0ca529648be4", "e5392ac00e18"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -41,7 +41,7 @@
}
}
},
"0220267a7bca": {
"1782d7da7125": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
@@ -52,7 +52,7 @@
{
"name": "params",
"value": {
"$rpc": "absent"
"worktreeId": "repo-1::/folder"
}
},
{
@@ -68,48 +68,14 @@
"settledAt": 0,
"value": {
"error": {
"code": "method_not_found",
"message": "Unknown method"
"code": "refused",
"message": ""
},
"id": "frame-3",
"ok": false
}
}
},
"084fe2afb0ce": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"error": "refused"
}
}
}
},
"1c2564f1daca": {
"name": "settings.get#1",
"ordinal": 4,
@@ -152,6 +118,73 @@
}
},
"44136fa355b3": {},
"4b4eff46e2be": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-1::/folder"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"error": {
"code": "method_not_found",
"message": "Unknown method"
},
"id": "frame-3",
"ok": false
}
}
},
"4b7de31b7ca7": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-1::/folder"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex", "claude"]
}
}
},
"51d4cb56be85": {
"name": "settings.get#1",
"ordinal": 2,
@@ -178,137 +211,10 @@
"startedAt": 0
}
},
"570f5efcd445": {
"584e8a48c9d1": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"error": {
"code": "refused",
"message": "outer refused"
},
"id": "frame-3",
"ok": false
}
}
},
"57f451cceda3": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "rejected",
"startedAt": 0,
"settledAt": 0,
"error": {
"category": "Error",
"message": "",
"isRpcDeliveryUnknown": true
}
}
},
"585dfba86809": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true
}
}
},
"6dd8724be3b1": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"$rpc": "null"
}
}
}
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\",\"params\":{\"worktreeId\":\"repo-1::/folder\"}}"
},
"70e0675ab2a9": {
"name": "repo.list#1",
@@ -351,7 +257,7 @@
}
}
},
"89b73b1e13d1": {
"8f2549f081ef": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
@@ -362,7 +268,7 @@
{
"name": "params",
"value": {
"$rpc": "absent"
"worktreeId": "repo-1::/folder"
}
},
{
@@ -373,13 +279,13 @@
}
],
"settlement": {
"status": "fulfilled",
"status": "rejected",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex", "claude"]
"error": {
"category": "Error",
"message": "",
"isRpcDeliveryUnknown": true
}
}
},
@@ -408,7 +314,7 @@
"isRpcDeliveryUnknown": true
}
},
"a9705081551e": {
"ad56d2b94b5e": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
@@ -419,7 +325,7 @@
{
"name": "params",
"value": {
"$rpc": "absent"
"worktreeId": "repo-1::/folder"
}
},
{
@@ -430,53 +336,13 @@
}
],
"settlement": {
"status": "fulfilled",
"status": "rejected",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"error": {
"message": "inner refused"
},
"ok": false
}
}
}
},
"ac08a3ca5c67": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"error": "inner refused",
"ok": false
}
"error": {
"category": "Error",
"message": "transport failure",
"isRpcDeliveryUnknown": true
}
}
},
@@ -505,22 +371,7 @@
"isRpcDeliveryUnknown": false
}
},
"c7584e82c72f": {
"status": "rejected",
"startedAt": 0,
"settledAt": 0,
"error": {
"category": "Error",
"message": "",
"isRpcDeliveryUnknown": true
}
},
"e0eef7c9fafe": {
"name": "repo.list#1",
"ordinal": 3,
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
},
"e1b544c0b6ec": {
"c0260c98bf87": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
@@ -531,39 +382,7 @@
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "rejected",
"startedAt": 0,
"settledAt": 0,
"error": {
"category": "Error",
"message": "transport failure",
"isRpcDeliveryUnknown": true
}
}
},
"efd2ac432554": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
"worktreeId": "repo-1::/folder"
}
},
{
@@ -580,13 +399,134 @@
"value": {
"error": {
"code": "refused",
"message": ""
"message": "outer refused"
},
"id": "frame-3",
"ok": false
}
}
},
"c581f563511c": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-1::/folder"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"error": "inner refused",
"ok": false
}
}
}
},
"c7584e82c72f": {
"status": "rejected",
"startedAt": 0,
"settledAt": 0,
"error": {
"category": "Error",
"message": "",
"isRpcDeliveryUnknown": true
}
},
"d0f8e1b60093": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-1::/folder"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"error": {
"message": "inner refused"
},
"ok": false
}
}
}
},
"e0eef7c9fafe": {
"name": "repo.list#1",
"ordinal": 3,
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
},
"ec9c870f9c3a": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-1::/folder"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"error": "refused"
}
}
}
},
"f3b516f62081": {
"status": "rejected",
"startedAt": 0,
@@ -597,10 +537,70 @@
"isRpcDeliveryUnknown": false
}
},
"f49d184679d1": {
"fea75e1dbba4": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\"}"
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-1::/folder"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": {
"$rpc": "null"
}
}
}
},
"ff00bccaaf1f": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-1::/folder"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true
}
}
}
},
"recording": {
@@ -621,8 +621,8 @@
{
"id": "new-tab-local-agents.normal:settled",
"observation": {
"sender": ["70e0675ab2a9", "02188f091419", "89b73b1e13d1"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "02188f091419", "4b7de31b7ca7"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "b27c85677730"
},
@@ -633,8 +633,8 @@
{
"id": "new-tab-local-agents.result-absent:settled",
"observation": {
"sender": ["70e0675ab2a9", "02188f091419", "585dfba86809"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "02188f091419", "ff00bccaaf1f"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "98b298cb3858"
},
@@ -645,8 +645,8 @@
{
"id": "new-tab-local-agents.result-null:settled",
"observation": {
"sender": ["70e0675ab2a9", "02188f091419", "6dd8724be3b1"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "02188f091419", "fea75e1dbba4"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "98b298cb3858"
},
@@ -657,8 +657,8 @@
{
"id": "new-tab-local-agents.inner-ok-missing:settled",
"observation": {
"sender": ["70e0675ab2a9", "02188f091419", "084fe2afb0ce"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "02188f091419", "ec9c870f9c3a"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "98b298cb3858"
},
@@ -669,8 +669,8 @@
{
"id": "new-tab-local-agents.inner-false-string-error:settled",
"observation": {
"sender": ["70e0675ab2a9", "02188f091419", "ac08a3ca5c67"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "02188f091419", "c581f563511c"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "98b298cb3858"
},
@@ -681,8 +681,8 @@
{
"id": "new-tab-local-agents.inner-false-object-error:settled",
"observation": {
"sender": ["70e0675ab2a9", "02188f091419", "a9705081551e"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "02188f091419", "d0f8e1b60093"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "98b298cb3858"
},
@@ -693,8 +693,8 @@
{
"id": "new-tab-local-agents.outer-refused:settled",
"observation": {
"sender": ["70e0675ab2a9", "02188f091419", "570f5efcd445"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "02188f091419", "c0260c98bf87"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "32a7c0ae7918"
},
@@ -705,8 +705,8 @@
{
"id": "new-tab-local-agents.outer-refused-no-message:settled",
"observation": {
"sender": ["70e0675ab2a9", "02188f091419", "efd2ac432554"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "02188f091419", "1782d7da7125"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "f3b516f62081"
},
@@ -717,8 +717,8 @@
{
"id": "new-tab-local-agents.method-not-found:settled",
"observation": {
"sender": ["70e0675ab2a9", "02188f091419", "0220267a7bca"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "02188f091419", "4b4eff46e2be"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "b948e8307e81"
},
@@ -729,8 +729,8 @@
{
"id": "new-tab-local-agents.transport-rejection:settled",
"observation": {
"sender": ["70e0675ab2a9", "02188f091419", "e1b544c0b6ec"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "02188f091419", "ad56d2b94b5e"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "a947768bc0ed"
},
@@ -741,8 +741,8 @@
{
"id": "new-tab-local-agents.transport-rejection-no-message:settled",
"observation": {
"sender": ["70e0675ab2a9", "02188f091419", "57f451cceda3"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "02188f091419", "8f2549f081ef"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "c7584e82c72f"
},
@@ -218,6 +218,38 @@
}
}
},
"4b7de31b7ca7": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-1::/folder"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex", "claude"]
}
}
},
"51d4cb56be85": {
"name": "settings.get#1",
"ordinal": 2,
@@ -244,6 +276,11 @@
"startedAt": 0
}
},
"584e8a48c9d1": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\",\"params\":{\"worktreeId\":\"repo-1::/folder\"}}"
},
"624ec4e5082c": {
"name": "repo.list#1",
"ordinal": 1,
@@ -351,38 +388,6 @@
}
}
},
"89b73b1e13d1": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex", "claude"]
}
}
},
"8a72d3d14d44": {
"name": "repo.list#1",
"ordinal": 1,
@@ -596,11 +601,6 @@
"message": "",
"isRpcDeliveryUnknown": false
}
},
"f49d184679d1": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\"}"
}
},
"recording": {
@@ -621,8 +621,8 @@
{
"id": "new-tab-local-agents.normal:settled",
"observation": {
"sender": ["70e0675ab2a9", "02188f091419", "89b73b1e13d1"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "02188f091419", "4b7de31b7ca7"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "b27c85677730"
},
@@ -257,6 +257,38 @@
]
},
"44136fa355b3": {},
"4b7de31b7ca7": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-1::/folder"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex", "claude"]
}
}
},
"51d4cb56be85": {
"name": "settings.get#1",
"ordinal": 2,
@@ -315,6 +347,11 @@
}
}
},
"584e8a48c9d1": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\",\"params\":{\"worktreeId\":\"repo-1::/folder\"}}"
},
"69518e60477b": {
"name": "settings.get#1",
"ordinal": 2,
@@ -425,38 +462,6 @@
}
}
},
"89b73b1e13d1": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex", "claude"]
}
}
},
"9270aeb7d9c6": {
"status": "pending",
"startedAt": 0
@@ -587,11 +592,6 @@
"isRpcDeliveryUnknown": false
}
},
"f49d184679d1": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\"}"
},
"fb9f08890f70": {
"name": "settings.get#1",
"ordinal": 2,
@@ -645,8 +645,8 @@
{
"id": "new-tab-local-agents.normal:settled",
"observation": {
"sender": ["70e0675ab2a9", "02188f091419", "89b73b1e13d1"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "02188f091419", "4b7de31b7ca7"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "b27c85677730"
},
@@ -657,8 +657,8 @@
{
"id": "new-tab-local-agents.result-absent:settled",
"observation": {
"sender": ["70e0675ab2a9", "98bd3a5fda2b", "89b73b1e13d1"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "98bd3a5fda2b", "4b7de31b7ca7"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "1c10eabc36a4"
},
@@ -669,8 +669,8 @@
{
"id": "new-tab-local-agents.result-null:settled",
"observation": {
"sender": ["70e0675ab2a9", "69518e60477b", "89b73b1e13d1"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "69518e60477b", "4b7de31b7ca7"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "1a04332d2ee1"
},
@@ -681,8 +681,8 @@
{
"id": "new-tab-local-agents.inner-ok-missing:settled",
"observation": {
"sender": ["70e0675ab2a9", "fb9f08890f70", "89b73b1e13d1"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "fb9f08890f70", "4b7de31b7ca7"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "3c911d72c9be"
},
@@ -693,8 +693,8 @@
{
"id": "new-tab-local-agents.inner-false-string-error:settled",
"observation": {
"sender": ["70e0675ab2a9", "86798e4821d1", "89b73b1e13d1"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "86798e4821d1", "4b7de31b7ca7"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "3c911d72c9be"
},
@@ -705,8 +705,8 @@
{
"id": "new-tab-local-agents.inner-false-object-error:settled",
"observation": {
"sender": ["70e0675ab2a9", "1c5359ea3f7d", "89b73b1e13d1"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "1c5359ea3f7d", "4b7de31b7ca7"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "3c911d72c9be"
},
@@ -717,8 +717,8 @@
{
"id": "new-tab-local-agents.outer-refused:settled",
"observation": {
"sender": ["70e0675ab2a9", "ceb5fe72c101", "89b73b1e13d1"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "ceb5fe72c101", "4b7de31b7ca7"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "32a7c0ae7918"
},
@@ -729,8 +729,8 @@
{
"id": "new-tab-local-agents.outer-refused-no-message:settled",
"observation": {
"sender": ["70e0675ab2a9", "34f3a5b8bd78", "89b73b1e13d1"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "34f3a5b8bd78", "4b7de31b7ca7"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "f3b516f62081"
},
@@ -741,8 +741,8 @@
{
"id": "new-tab-local-agents.method-not-found:settled",
"observation": {
"sender": ["70e0675ab2a9", "0ba58e9fadf7", "89b73b1e13d1"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "0ba58e9fadf7", "4b7de31b7ca7"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "b948e8307e81"
},
@@ -753,8 +753,8 @@
{
"id": "new-tab-local-agents.transport-rejection:settled",
"observation": {
"sender": ["70e0675ab2a9", "566d8202b582", "89b73b1e13d1"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "566d8202b582", "4b7de31b7ca7"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "a947768bc0ed"
},
@@ -765,8 +765,8 @@
{
"id": "new-tab-local-agents.transport-rejection-no-message:settled",
"observation": {
"sender": ["70e0675ab2a9", "0ca529648be4", "89b73b1e13d1"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "0ca529648be4", "4b7de31b7ca7"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "c7584e82c72f"
},
@@ -73,6 +73,38 @@
}
},
"44136fa355b3": {},
"4b7de31b7ca7": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-1::/folder"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex", "claude"]
}
}
},
"51d4cb56be85": {
"name": "settings.get#1",
"ordinal": 2,
@@ -99,6 +131,11 @@
"startedAt": 0
}
},
"584e8a48c9d1": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\",\"params\":{\"worktreeId\":\"repo-1::/folder\"}}"
},
"70e0675ab2a9": {
"name": "repo.list#1",
"ordinal": 1,
@@ -140,38 +177,6 @@
}
}
},
"89b73b1e13d1": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex", "claude"]
}
}
},
"9270aeb7d9c6": {
"status": "pending",
"startedAt": 0
@@ -195,11 +200,6 @@
"name": "repo.list#1",
"ordinal": 3,
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
},
"f49d184679d1": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\"}"
}
},
"recording": {
@@ -220,8 +220,8 @@
{
"id": "settled",
"observation": {
"sender": ["70e0675ab2a9", "02188f091419", "89b73b1e13d1"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1"],
"sender": ["70e0675ab2a9", "02188f091419", "4b7de31b7ca7"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "584e8a48c9d1"],
"settlements": {
"load": "b27c85677730"
},
@@ -89,37 +89,10 @@
}
}
},
"9c16efc31903": {
"742db574bda3": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex"]
}
}
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\",\"params\":{\"worktreeId\":\"repo-9::/w\"}}"
},
"c15f33d27bd2": {
"name": "agent.launchReplay#1",
@@ -171,6 +144,38 @@
"ordinal": 3,
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
},
"e5392ac00e18": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-9::/w"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex"]
}
}
},
"eaa398d8c3aa": {
"launched": {
"kind": "unconfirmed",
@@ -184,11 +189,6 @@
"value": {
"$rpc": "undefined"
}
},
"f49d184679d1": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\"}"
}
},
"recording": {
@@ -197,8 +197,8 @@
{
"id": "unconfirmed",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "9c16efc31903", "c15f33d27bd2"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "28339abecf78"],
"sender": ["3e6a0417d2ee", "02188f091419", "e5392ac00e18", "c15f33d27bd2"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3", "28339abecf78"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -141,42 +141,15 @@
"startedAt": 0
}
},
"742db574bda3": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\",\"params\":{\"worktreeId\":\"repo-9::/w\"}}"
},
"9270aeb7d9c6": {
"status": "pending",
"startedAt": 0
},
"9c16efc31903": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex"]
}
}
},
"a4273b38df83": {
"launched": "unlaunched"
},
@@ -243,6 +216,38 @@
"ordinal": 3,
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
},
"e5392ac00e18": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "repo-9::/w"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["codex"]
}
}
},
"eb79a9b3682a": {
"status": "fulfilled",
"startedAt": 0,
@@ -255,11 +260,6 @@
"launched": {
"kind": "sent"
}
},
"f49d184679d1": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\"}"
}
},
"recording": {
@@ -280,8 +280,8 @@
{
"id": "launched",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "9c16efc31903", "ba3fac1eb199"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "28339abecf78"],
"sender": ["3e6a0417d2ee", "02188f091419", "e5392ac00e18", "ba3fac1eb199"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3", "28339abecf78"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -153,7 +153,17 @@
"prompt": "Fix the failing checks"
}
},
"9c16efc31903": {
"742db574bda3": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\",\"params\":{\"worktreeId\":\"repo-9::/w\"}}"
},
"e0eef7c9fafe": {
"name": "repo.list#1",
"ordinal": 3,
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
},
"e5392ac00e18": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
@@ -164,7 +174,7 @@
{
"name": "params",
"value": {
"$rpc": "absent"
"worktreeId": "repo-9::/w"
}
},
{
@@ -185,11 +195,6 @@
}
}
},
"e0eef7c9fafe": {
"name": "repo.list#1",
"ordinal": 3,
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
},
"eb79a9b3682a": {
"status": "fulfilled",
"startedAt": 0,
@@ -197,11 +202,6 @@
"value": {
"$rpc": "undefined"
}
},
"f49d184679d1": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\"}"
}
},
"recording": {
@@ -210,8 +210,8 @@
{
"id": "prompt-not-sent",
"observation": {
"sender": ["3e6a0417d2ee", "02188f091419", "9c16efc31903", "29436402d701"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "28339abecf78"],
"sender": ["3e6a0417d2ee", "02188f091419", "e5392ac00e18", "29436402d701"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "742db574bda3", "28339abecf78"],
"settlements": {
"launch": "eb79a9b3682a"
},
@@ -9,6 +9,11 @@
"ordinal": 4,
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
},
"2398eeb5ec74": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\",\"params\":{\"worktreeId\":\"workspace-1\"}}"
},
"5ee9e7b49320": {
"name": "agent.launchReplay#1",
"ordinal": 8,
@@ -91,6 +96,38 @@
}
}
},
"bca41ddb4705": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"worktreeId": "workspace-1"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["claude"]
}
}
},
"bcb5111e6012": {
"name": "agent.launchReplay#1",
"ordinal": 7,
@@ -149,11 +186,6 @@
"$rpc": "undefined"
}
},
"f49d184679d1": {
"name": "preflight.detectAgents#1",
"ordinal": 6,
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\"}"
},
"fc1691a7deca": {
"actionError": "Workspace not found",
"busyAction": {
@@ -186,38 +218,6 @@
"sendSheet": {
"$rpc": "null"
}
},
"fc3974e714de": {
"name": "preflight.detectAgents#1",
"ordinal": 5,
"args": [
{
"name": "method",
"value": "preflight.detectAgents"
},
{
"name": "params",
"value": {
"$rpc": "absent"
}
},
{
"name": "options",
"value": {
"$rpc": "absent"
}
}
],
"settlement": {
"status": "fulfilled",
"startedAt": 0,
"settledAt": 0,
"value": {
"id": "frame-3",
"ok": true,
"result": ["claude"]
}
}
}
},
"recording": {
@@ -226,8 +226,8 @@
{
"id": "refused",
"observation": {
"sender": ["9239a4f232b8", "96a9a954e28f", "fc3974e714de", "bcb5111e6012"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "f49d184679d1", "5ee9e7b49320"],
"sender": ["9239a4f232b8", "96a9a954e28f", "bca41ddb4705", "bcb5111e6012"],
"payloads": ["e0eef7c9fafe", "1c2564f1daca", "2398eeb5ec74", "5ee9e7b49320"],
"settlements": {
"create-and-send": "eb79a9b3682a"
},
+5 -5
View File
@@ -9268,7 +9268,7 @@
{
"complete": "preflight.detectAgents#1",
"params": {
"$undefined": true
"worktreeId": "repo-9::/w"
},
"reply": {
"ok": true,
@@ -9361,7 +9361,7 @@
{
"complete": "preflight.detectAgents#1",
"params": {
"$undefined": true
"worktreeId": "repo-9::/w"
},
"reply": {
"ok": true,
@@ -9454,7 +9454,7 @@
{
"complete": "preflight.detectAgents#1",
"params": {
"$undefined": true
"worktreeId": "repo-9::/w"
},
"reply": {
"ok": true,
@@ -18538,7 +18538,7 @@
{
"complete": "preflight.detectAgents#1",
"params": {
"$undefined": true
"worktreeId": "workspace-1"
},
"reply": {
"ok": true,
@@ -23149,7 +23149,7 @@
{
"complete": "preflight.detectAgents#1",
"params": {
"$undefined": true
"worktreeId": "repo-1::/folder"
},
"reply": {
"ok": true,
@@ -89,7 +89,9 @@ async function loadDetectedAgents(
repo
}
: {
reply: await preflightDetectAgentsRead.request(client),
// Why the workspace: the host resolves its project runtime (a WSL distro on Windows). An
// older host discards the params and answers with its own default, as it always has.
reply: await preflightDetectAgentsRead.request(client, { worktreeId }),
interpret: preflightDetectAgentsRead.interpret,
repo
}
+5 -12
View File
@@ -1,11 +1,9 @@
import { ipcMain } from 'electron'
import {
detectInstalledAgentsWithShellPathHydration,
detectRemoteAgents,
detectRemoteWindowsTerminalCapabilities,
refreshShellPathAndDetectAgents,
runPreflightCheck
} from '../preflight/agent-detection'
import { detectAgentsOnHost, refreshAgentsOnHost } from '../preflight/workspace-agent-detection'
import type {
PreflightRuntimeContext,
PreflightStatus,
@@ -30,7 +28,7 @@ export function registerPreflightHandlers(): void {
)
ipcMain.handle('preflight:detectAgents', async (_event, args?: PreflightRuntimeContext) =>
detectInstalledAgentsWithShellPathHydration(args)
detectAgentsOnHost({ kind: 'local', context: args })
)
// Why here: this is the one place that already answers "what can the installed agent CLIs
@@ -40,18 +38,13 @@ export function registerPreflightHandlers(): void {
)
ipcMain.handle('preflight:refreshAgents', async (_event, args?: PreflightRuntimeContext) => {
return refreshShellPathAndDetectAgents(args)
return refreshAgentsOnHost({ kind: 'local', context: args })
})
// Why: remote worktrees need agent detection on the SSH host, not the local
// machine. This handler forwards the same KNOWN_AGENT_COMMANDS list to the
// relay's preflight.detectAgents RPC, whose lookup command is selected on
// the remote host so native Windows OpenSSH does not require a POSIX shell.
ipcMain.handle(
'preflight:detectRemoteAgents',
async (_event, args: { connectionId: string }): Promise<string[]> => {
return detectRemoteAgents(args)
}
async (_event, args: { connectionId: string }): Promise<string[]> =>
detectAgentsOnHost({ kind: 'ssh', connectionId: args.connectionId })
)
ipcMain.handle(
+25 -1
View File
@@ -3,6 +3,7 @@ import type { GlobalSettings } from '../shared/global-settings-types'
import type { Project } from '../shared/project-types'
import type { Repo } from '../shared/repo-types'
import {
getWorkspaceRuntimePreference,
resolveProjectExecutionRuntime,
type ProjectExecutionRuntimeResolution
} from '../shared/project-execution-runtime'
@@ -49,7 +50,7 @@ function canResolveProjectRuntimeForWorktreeId(
function resolveLocalProjectRuntime(
store: ResolvableStore,
project: Project,
project: Pick<Project, 'id' | 'localWindowsRuntimePreference'>,
settings: ReturnType<ResolvableStore['getSettings']> = store.getSettings()
): ProjectExecutionRuntimeResolution {
const wslAvailable = hasCachedWslAvailability()
@@ -83,6 +84,29 @@ export function resolveLocalProjectRuntimeForRepo(
return resolveLocalProjectRuntime(store, project)
}
/** Same choice the renderer makes for a workspace: the project's runtime, else the distro of a
* WSL-share path, else the global default. Works for a repo with no project too. */
export function resolveLocalWorkspaceRuntime(
store: ProjectRuntimeResolutionStore,
repo: Repo,
workspacePath: string | null | undefined
): ProjectExecutionRuntimeResolution | undefined {
if (
getRepoExecutionHostId(repo) !== LOCAL_EXECUTION_HOST_ID ||
!canResolveProjectRuntimeForRepo(store)
) {
return undefined
}
const project = store.getProjects().find((entry) => entry.sourceRepoIds.includes(repo.id))
return resolveLocalProjectRuntime(store, {
id: project?.id ?? repo.id,
localWindowsRuntimePreference: getWorkspaceRuntimePreference(
project?.localWindowsRuntimePreference,
workspacePath
)
})
}
export function resolveLocalProjectRuntimesForRepos(
store: ProjectRuntimeResolutionStore,
repos: readonly Repo[]
@@ -0,0 +1,299 @@
import '../runtime/rpc/unused-default-rpc-methods.test-fixture'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { RpcDispatcher } from '../runtime/rpc/dispatcher'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import { PREFLIGHT_METHODS } from '../runtime/rpc/methods/preflight'
import type { FolderWorkspace } from '../../shared/folder-workspace-types'
import type { GlobalWindowsRuntimeDefault } from '../../shared/project-execution-runtime'
import type { Project } from '../../shared/project-types'
import type { ProjectGroup } from '../../shared/project-group-types'
import type { Repo } from '../../shared/repo-types'
import { folderWorkspaceKey, parseWorkspaceKey } from '../../shared/workspace-scope'
import { splitWorktreeId } from '../../shared/worktree/id'
import {
resolveWorkspaceAgentDetectionHost,
type WorkspaceAgentDetectionStore
} from './workspace-agent-detection'
const { detectInstalledMock, detectRemoteMock, refreshMock } = vi.hoisted(() => ({
detectInstalledMock: vi.fn(),
detectRemoteMock: vi.fn(),
refreshMock: vi.fn()
}))
vi.mock('./agent-detection', () => ({
detectInstalledAgentsWithShellPathHydration: detectInstalledMock,
detectRemoteAgents: detectRemoteMock,
refreshShellPathAndDetectAgents: refreshMock,
detectRemoteWindowsTerminalCapabilities: vi.fn(),
runPreflightCheck: vi.fn()
}))
const WSL_REPO_PATH = '\\\\wsl.localhost\\Ubuntu-24.04\\home\\me\\project\\repo'
const originalPlatform = process.platform
function repo(overrides: Partial<Repo> & Pick<Repo, 'id' | 'path'>): Repo {
return { displayName: overrides.id, badgeColor: 'blue', addedAt: 1, ...overrides }
}
function project(
id: string,
repoIds: string[],
preference?: Project['localWindowsRuntimePreference']
): Project {
return {
id,
displayName: id,
badgeColor: 'blue',
sourceRepoIds: repoIds,
createdAt: 1,
updatedAt: 1,
...(preference ? { localWindowsRuntimePreference: preference } : {})
}
}
const GROUP: ProjectGroup = {
id: 'g1',
name: 'g1',
parentPath: null,
connectionId: null,
parentGroupId: null,
createdFrom: 'manual',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const FOLDER: FolderWorkspace = {
id: 'f1',
projectGroupId: 'g1',
name: 'f1',
folderPath: WSL_REPO_PATH,
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 1,
createdAt: 1,
updatedAt: 1
}
function store(args: {
repos: Repo[]
projects?: Project[]
globalDefault?: GlobalWindowsRuntimeDefault
folderWorkspaces?: FolderWorkspace[]
projectGroups?: ProjectGroup[]
}): WorkspaceAgentDetectionStore {
return {
getRepos: () => args.repos,
getRepo: (id: string) => args.repos.find((entry) => entry.id === id),
getProjects: () => args.projects ?? [],
getSettings: () => ({ localWindowsRuntimeDefault: args.globalDefault }),
getFolderWorkspaces: () => args.folderWorkspaces ?? [],
getProjectGroups: () => args.projectGroups ?? []
}
}
function wslContext(distro: string, projectId: string, reason: string) {
return {
projectRuntime: {
status: 'resolved',
runtime: expect.objectContaining({ kind: 'wsl', distro, projectId, reason })
}
}
}
// Stands in for the runtime, which reads the worktree's host and path from its own list.
function resolveFor(s: WorkspaceAgentDetectionStore, workspaceId: string, hostId?: string) {
const scope = parseWorkspaceKey(workspaceId)
if (scope?.type === 'folder') {
return resolveWorkspaceAgentDetectionHost(s, {
kind: 'folder',
folderWorkspaceId: scope.folderWorkspaceId
})
}
const parsed = splitWorktreeId(workspaceId)
if (!parsed) {
throw new Error(`not a worktree id: ${workspaceId}`)
}
return resolveWorkspaceAgentDetectionHost(s, {
kind: 'worktree',
repoId: parsed.repoId,
path: parsed.worktreePath,
hostId
})
}
describe('resolveWorkspaceAgentDetectionHost on a Windows host', () => {
beforeEach(() => {
Object.defineProperty(process, 'platform', { value: 'win32' })
})
afterEach(() => {
Object.defineProperty(process, 'platform', { value: originalPlatform })
})
it('keeps the host default for a workspace no repo row owns', () => {
const s = store({ repos: [repo({ id: 'r1', path: 'C:\\r1' })] })
expect(resolveFor(s, 'missing::C:\\x')).toEqual({ kind: 'local' })
})
it('routes by the worktree owner, not the first row sharing its repo id', () => {
const sshRow = repo({ id: 'r1', path: '/srv/r1', executionHostId: 'ssh:build-host' })
const localRow = repo({ id: 'r1', path: 'C:\\src\\r1', executionHostId: 'local' })
for (const repos of [
[sshRow, localRow],
[localRow, sshRow]
]) {
const s = store({ repos })
expect(resolveFor(s, 'r1::C:\\src\\r1', 'local')).toEqual({
kind: 'local',
context: {
projectRuntime: {
status: 'resolved',
runtime: expect.objectContaining({ kind: 'windows-host' })
}
}
})
expect(resolveFor(s, 'r1::/srv/r1', 'ssh:build-host')).toEqual({
kind: 'ssh',
connectionId: 'build-host'
})
// Rival rows and no worktree host name no single owner: refuse rather than guess.
expect(() => resolveFor(s, 'r1::/srv/r1')).toThrow('worktree_execution_host_unresolved')
}
})
it('probes inside the distro of a WSL-share workspace with no saved runtime', () => {
const s = store({ repos: [repo({ id: 'r1', path: WSL_REPO_PATH })] })
expect(resolveFor(s, `r1::${WSL_REPO_PATH}`)).toEqual({
kind: 'local',
context: wslContext('Ubuntu-24.04', 'r1', 'project-override')
})
})
it('follows the global WSL default for a project that inherits it', () => {
const s = store({
repos: [repo({ id: 'r1', path: 'C:\\src\\r1' })],
projects: [project('p1', ['r1'])],
globalDefault: { kind: 'wsl', distro: 'Ubuntu' }
})
expect(resolveFor(s, 'r1::C:\\src\\r1')).toEqual({
kind: 'local',
context: wslContext('Ubuntu', 'p1', 'global-default')
})
})
it("lets the project's own runtime win over a WSL-share path", () => {
const s = store({
repos: [repo({ id: 'r1', path: WSL_REPO_PATH })],
projects: [project('p1', ['r1'], { kind: 'windows-host' })]
})
expect(resolveFor(s, `r1::${WSL_REPO_PATH}`)).toEqual({
kind: 'local',
context: {
projectRuntime: {
status: 'resolved',
runtime: expect.objectContaining({ kind: 'windows-host', reason: 'project-override' })
}
}
})
})
it('routes an SSH workspace to its target, under either ownership spelling', () => {
const s = store({
repos: [
repo({ id: 'legacy', path: '/srv/a', connectionId: 'ssh-a' }),
repo({ id: 'unified', path: '/srv/b', executionHostId: 'ssh:ssh-b' })
]
})
expect(resolveFor(s, 'legacy::/srv/a')).toEqual({
kind: 'ssh',
connectionId: 'ssh-a'
})
expect(resolveFor(s, 'unified::/srv/b')).toEqual({
kind: 'ssh',
connectionId: 'ssh-b'
})
})
it('resolves a local folder workspace through its only repo', () => {
const s = store({
repos: [repo({ id: 'r1', path: WSL_REPO_PATH, projectGroupId: 'g1' })],
projectGroups: [GROUP],
folderWorkspaces: [FOLDER]
})
expect(resolveFor(s, folderWorkspaceKey('f1'))).toEqual({
kind: 'local',
context: wslContext('Ubuntu-24.04', 'r1', 'project-override')
})
})
it('refuses a folder whose candidate repos live on different SSH hosts', () => {
const s = store({
repos: [
repo({ id: 'a', path: '/srv/a', executionHostId: 'ssh:one', projectGroupId: 'g1' }),
repo({ id: 'b', path: '/srv/b', executionHostId: 'ssh:two', projectGroupId: 'g1' })
],
projectGroups: [GROUP],
folderWorkspaces: [{ ...FOLDER, folderPath: '/srv' }]
})
expect(() => resolveFor(s, folderWorkspaceKey('f1'))).toThrow(
'worktree_execution_host_unresolved'
)
})
})
describe('preflight.detectAgents resolves the workspace on the host', () => {
beforeEach(() => {
Object.defineProperty(process, 'platform', { value: 'win32' })
detectInstalledMock.mockReset().mockResolvedValue(['claude', 'codex'])
})
afterEach(() => {
Object.defineProperty(process, 'platform', { value: originalPlatform })
})
function dispatcherFor(s: WorkspaceAgentDetectionStore) {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these methods read only resolveAgentDetectionHost and getRuntimeId from the runtime.
const runtime = {
getRuntimeId: () => 'host',
resolveAgentDetectionHost: async (worktreeId?: string) =>
worktreeId ? resolveFor(s, worktreeId) : { kind: 'local' }
} as unknown as OrcaRuntimeService
return new RpcDispatcher({ runtime, methods: PREFLIGHT_METHODS })
}
it('#19885: a paired client naming a WSL-hosted workspace gets the distro probe', async () => {
const dispatcher = dispatcherFor(store({ repos: [repo({ id: 'r1', path: WSL_REPO_PATH })] }))
const response = await dispatcher.dispatch({
id: '1',
authToken: 't',
method: 'preflight.detectAgents',
params: { worktreeId: `r1::${WSL_REPO_PATH}` }
})
expect(detectInstalledMock).toHaveBeenCalledWith(
wslContext('Ubuntu-24.04', 'r1', 'project-override')
)
expect(response).toMatchObject({ ok: true, result: ['claude', 'codex'] })
})
it('#18666: a project on the global WSL default is probed in that distro', async () => {
const dispatcher = dispatcherFor(
store({
repos: [repo({ id: 'r1', path: 'C:\\src\\r1' })],
projects: [project('p1', ['r1'])],
globalDefault: { kind: 'wsl', distro: 'Ubuntu' }
})
)
await dispatcher.dispatch({
id: '1',
authToken: 't',
method: 'preflight.refreshAgents',
params: { worktreeId: 'r1::C:\\src\\r1' }
})
expect(refreshMock).toHaveBeenCalledWith(wslContext('Ubuntu', 'p1', 'global-default'))
})
})
@@ -0,0 +1,142 @@
import {
getRepoExecutionHostId,
getSshTargetIdForExecutionHost,
LOCAL_EXECUTION_HOST_ID
} from '../../shared/execution-host'
import {
findFolderWorkspaceCandidateRepos,
resolveFolderWorkspaceHost
} from '../../shared/folder-workspace-execution-host'
import type { FolderWorkspace } from '../../shared/folder-workspace-types'
import type { ProjectGroup } from '../../shared/project-group-types'
import type { Repo } from '../../shared/repo-types'
import {
resolveLocalWorkspaceRuntime,
type ProjectRuntimeResolutionStore
} from '../local-project-runtime-resolution'
import type { PreflightRuntimeContext } from '../ipc/preflight-runtime-target'
import { resolveWorktreeHostRouting } from '../runtime/worktree-launch-host-repo'
import {
detectInstalledAgentsWithShellPathHydration,
detectRemoteAgents,
refreshShellPathAndDetectAgents,
type RefreshAgentsResult
} from './agent-detection'
/** Where agent CLIs are probed: this machine (optionally inside a WSL distro) or an SSH target. */
export type AgentDetectionHost =
| { kind: 'local'; context?: PreflightRuntimeContext }
| { kind: 'ssh'; connectionId: string }
export type WorkspaceAgentDetectionStore = ProjectRuntimeResolutionStore & {
getRepos?: () => Repo[]
getFolderWorkspaces?: () => FolderWorkspace[]
getProjectGroups?: () => ProjectGroup[]
}
export const HOST_DEFAULT_AGENT_DETECTION: AgentDetectionHost = { kind: 'local' }
/** A workspace as this host knows it: a folder workspace, or a worktree read from its own list. */
export type AgentDetectionWorkspace =
| { kind: 'folder'; folderWorkspaceId: string }
| { kind: 'worktree'; repoId: string; path: string; hostId?: string | null }
type ResolvedWorkspace =
| { kind: 'ssh'; connectionId: string }
| { kind: 'local'; repo: Repo; path: string }
/**
* The host a workspace's agents run on, resolved on this machine from its own project settings.
* A workspace this store cannot place keeps the host default older clients rely on.
*/
export function resolveWorkspaceAgentDetectionHost(
store: WorkspaceAgentDetectionStore,
workspace: AgentDetectionWorkspace
): AgentDetectionHost {
const resolved =
workspace.kind === 'folder'
? resolveFolderWorkspace(store, workspace.folderWorkspaceId)
: resolveWorktree(store, workspace)
if (!resolved) {
return HOST_DEFAULT_AGENT_DETECTION
}
if (resolved.kind === 'ssh') {
return resolved
}
const projectRuntime = resolveLocalWorkspaceRuntime(store, resolved.repo, resolved.path)
return projectRuntime
? { kind: 'local', context: { projectRuntime } }
: HOST_DEFAULT_AGENT_DETECTION
}
function resolveWorktree(
store: WorkspaceAgentDetectionStore,
worktree: Extract<AgentDetectionWorkspace, { kind: 'worktree' }>
): ResolvedWorkspace | null {
// Why the shared rule: one repo id can name rows on several hosts, and the first row is not
// necessarily this worktree's owner.
const routing = resolveWorktreeHostRouting(store.getRepos?.() ?? [], worktree)
if (routing.kind === 'ambiguous') {
throw new Error('worktree_execution_host_unresolved')
}
if (routing.kind === 'unowned') {
return null
}
const sshTargetId = getSshTargetIdForExecutionHost(routing.hostId)
if (sshTargetId) {
return { kind: 'ssh', connectionId: sshTargetId }
}
// A worktree another runtime owns is not this host's to probe; keep the host default.
return routing.hostId === LOCAL_EXECUTION_HOST_ID && routing.repo
? { kind: 'local', repo: routing.repo, path: worktree.path }
: null
}
function resolveFolderWorkspace(
store: WorkspaceAgentDetectionStore,
folderWorkspaceId: string
): ResolvedWorkspace | null {
const state = {
folderWorkspaces: store.getFolderWorkspaces?.() ?? [],
projectGroups: store.getProjectGroups?.() ?? [],
repos: store.getRepos?.() ?? []
}
const host = resolveFolderWorkspaceHost(state, folderWorkspaceId)
if (host.kind === 'ssh') {
return { kind: 'ssh', connectionId: host.targetId }
}
if (host.kind === 'ambiguous') {
throw new Error('worktree_execution_host_unresolved')
}
const folder = state.folderWorkspaces.find((entry) => entry.id === folderWorkspaceId)
const candidates = findFolderWorkspaceCandidateRepos(state, folderWorkspaceId)
// Why one candidate: a folder spanning several repos has no single project runtime to pick.
const repo = candidates.length === 1 ? candidates[0] : undefined
return host.kind === 'local' &&
folder &&
repo &&
getRepoExecutionHostId(repo) === LOCAL_EXECUTION_HOST_ID
? { kind: 'local', repo, path: folder.folderPath }
: null
}
/** The one agent-detection entry point: IPC, runtime RPC, and SSH all probe through here. */
export function detectAgentsOnHost(host: AgentDetectionHost): Promise<string[]> {
return host.kind === 'ssh'
? detectRemoteAgents({ connectionId: host.connectionId })
: detectInstalledAgentsWithShellPathHydration(host.context)
}
export async function refreshAgentsOnHost(host: AgentDetectionHost): Promise<RefreshAgentsResult> {
if (host.kind === 'local') {
return refreshShellPathAndDetectAgents(host.context)
}
// Why: an SSH probe already reads the target's fresh login PATH; there is no local PATH to merge.
return {
agents: await detectRemoteAgents({ connectionId: host.connectionId }),
addedPathSegments: [],
shellHydrationOk: true,
pathSource: 'sync_seed_only',
pathFailureReason: 'none'
}
}
@@ -0,0 +1,97 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const electronMocks = vi.hoisted(() => {
const ipcMain = {
on: vi.fn(() => ipcMain),
removeListener: vi.fn(() => ipcMain),
emit: vi.fn(() => true)
}
return {
BrowserWindow: { fromId: vi.fn((): unknown => null) },
webContents: { fromId: vi.fn((): unknown => null) },
ipcMain,
app: { getPath: vi.fn(() => '/tmp'), isPackaged: false }
}
})
vi.mock('electron', () => electronMocks)
import { OrcaRuntimeService } from './orca-runtime'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants'
import type { Repo } from '../../shared/repo-types'
import type { ResolvedWorktree } from './runtime-worktree-path-identity'
const originalPlatform = process.platform
function makeRuntime(repos: Repo[]) {
const store = {
getRepo: (id: string) => repos.find((repo) => repo.id === id),
getRepos: () => repos,
getProjects: () => [],
getSettings: () => ({ localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' } })
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: detection resolution reads only the repo, project and settings members supplied here.
const runtime = new OrcaRuntimeService(store as never)
const selector = vi.fn<(selector: string) => Promise<ResolvedWorktree>>()
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: replaces the protected worktree lookup this method awaits; its signature is restated above.
;(runtime as unknown as { resolveWorktreeSelector: typeof selector }).resolveWorktreeSelector =
selector
return { runtime, selector }
}
function worktree(fields: Partial<ResolvedWorktree>): ResolvedWorktree {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the method reads only repoId, path and hostId.
return fields as ResolvedWorktree
}
describe('OrcaRuntimeService.resolveAgentDetectionHost', () => {
beforeEach(() => {
Object.defineProperty(process, 'platform', { value: 'win32' })
})
afterEach(() => {
Object.defineProperty(process, 'platform', { value: originalPlatform })
})
const local: Repo = {
id: 'r1',
path: 'C:\\src\\r1',
displayName: 'r1',
badgeColor: 'blue',
addedAt: 1,
executionHostId: 'local'
}
it('keeps the host default for no, floating, or unknown workspaces', async () => {
const { runtime, selector } = makeRuntime([local])
selector.mockRejectedValue(new Error('selector_not_found'))
expect(await runtime.resolveAgentDetectionHost(undefined)).toEqual({ kind: 'local' })
expect(await runtime.resolveAgentDetectionHost(FLOATING_TERMINAL_WORKTREE_ID)).toEqual({
kind: 'local'
})
expect(await runtime.resolveAgentDetectionHost('r1::C:\\gone')).toEqual({ kind: 'local' })
expect(selector).toHaveBeenCalledTimes(1)
})
it('refuses a workspace id two hosts share instead of probing this one', async () => {
const { runtime, selector } = makeRuntime([local])
selector.mockRejectedValue(new Error('selector_ambiguous'))
await expect(runtime.resolveAgentDetectionHost('r1::/srv/r1')).rejects.toThrow(
'selector_ambiguous'
)
})
it("routes by the host's own worktree record", async () => {
const ssh: Repo = { ...local, path: '/srv/r1', executionHostId: 'ssh:build-host' }
const { runtime, selector } = makeRuntime([ssh, local])
selector.mockResolvedValueOnce(worktree({ repoId: 'r1', path: 'C:\\src\\r1', hostId: 'local' }))
expect(await runtime.resolveAgentDetectionHost('r1::C:\\src\\r1')).toEqual({
kind: 'local',
context: {
projectRuntime: {
status: 'resolved',
runtime: expect.objectContaining({ kind: 'wsl', distro: 'Ubuntu' })
}
}
})
expect(selector).toHaveBeenCalledWith('id:r1::C:\\src\\r1')
})
})
@@ -13,6 +13,14 @@ import { RECENT_PTY_OUTPUT_LIMIT, RecentPtyOutputBuffer } from './recent-pty-out
import { appendRecentPtyPathCandidates } from './terminal-output-path-candidates'
import type { ProjectExecutionRuntimeResolution } from '../../shared/project-execution-runtime'
import { resolveLocalProjectRuntimeForWorktreeId } from '../local-project-runtime-resolution'
import {
HOST_DEFAULT_AGENT_DETECTION,
resolveWorkspaceAgentDetectionHost,
type AgentDetectionHost
} from '../preflight/workspace-agent-detection'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants'
import { parseWorkspaceKey } from '../../shared/workspace-scope'
import type { ResolvedWorktree } from './runtime-worktree-path-identity'
import type { RuntimePtyWorktreeRecord } from './runtime-terminal-state-records'
import {
localOrchestrationCliCommand,
@@ -248,6 +256,41 @@ export class OrcaRuntimeWithGetOrchestrationDispatchAuthority extends OrcaRuntim
: undefined
}
// Why the host's own worktree record: it carries the owning host and the real path, where the
// client's id alone could pick the wrong row for a repo id shared across hosts.
async resolveAgentDetectionHost(
worktreeId: string | null | undefined
): Promise<AgentDetectionHost> {
if (!this.store || !worktreeId || worktreeId === FLOATING_TERMINAL_WORKTREE_ID) {
return HOST_DEFAULT_AGENT_DETECTION
}
const store = this.requireStore()
const scope = parseWorkspaceKey(worktreeId)
if (scope?.type === 'folder') {
return resolveWorkspaceAgentDetectionHost(store, {
kind: 'folder',
folderWorkspaceId: scope.folderWorkspaceId
})
}
let worktree: ResolvedWorktree
try {
worktree = await this.resolveWorktreeSelector(`id:${worktreeId}`)
} catch (error) {
// Only an unknown workspace keeps the answer older clients always got; an ambiguous one
// has no single host, and probing this one would list another machine's agents.
if (error instanceof Error && error.message === 'selector_not_found') {
return HOST_DEFAULT_AGENT_DETECTION
}
throw error
}
return resolveWorkspaceAgentDetectionHost(store, {
kind: 'worktree',
repoId: worktree.repoId,
path: worktree.path,
hostId: worktree.hostId
})
}
getOrchestrationFleetAgentStatusSnapshot(): readonly FleetAgentStatusEvidence[] {
return readOrchestrationFleetAgentStatusSnapshot(this)
}
+51 -4
View File
@@ -58,14 +58,22 @@ describe('preflight RPC methods', () => {
pathSource: 'shell_hydrate',
pathFailureReason: 'none'
})
const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService
const resolveAgentDetectionHost = vi.fn(() => ({ kind: 'local' }) as const)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these methods read only resolveAgentDetectionHost and getRuntimeId from the runtime.
const runtime = {
getRuntimeId: () => 'test-runtime',
resolveAgentDetectionHost
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: PREFLIGHT_METHODS })
const detected = await dispatcher.dispatch(makeRequest('preflight.detectAgents'))
const refreshed = await dispatcher.dispatch(makeRequest('preflight.refreshAgents'))
const refreshed = await dispatcher.dispatch(makeRequest('preflight.refreshAgents', null))
expect(detectInstalledAgentsWithShellPathHydrationMock).toHaveBeenCalled()
expect(refreshShellPathAndDetectAgentsMock).toHaveBeenCalled()
// An older client sends no workspace and keeps the host-default probe.
expect(resolveAgentDetectionHost).toHaveBeenNthCalledWith(1, undefined)
expect(resolveAgentDetectionHost).toHaveBeenNthCalledWith(2, undefined)
expect(detectInstalledAgentsWithShellPathHydrationMock).toHaveBeenCalledWith(undefined)
expect(refreshShellPathAndDetectAgentsMock).toHaveBeenCalledWith(undefined)
expect(detected).toMatchObject({ ok: true, result: ['codex'] })
expect(refreshed).toMatchObject({
ok: true,
@@ -73,6 +81,45 @@ describe('preflight RPC methods', () => {
})
})
it('probes the runtime the host resolves for the requested workspace (#19885)', async () => {
const projectRuntime = {
status: 'resolved',
runtime: {
kind: 'wsl',
hostPlatform: 'wsl',
projectId: 'p1',
distro: 'Ubuntu',
reason: 'project-override',
cacheKey: 'p1:wsl:Ubuntu'
}
} as const
detectInstalledAgentsWithShellPathHydrationMock.mockResolvedValueOnce(['codex'])
detectRemoteAgentsMock.mockResolvedValueOnce(['claude'])
const resolveAgentDetectionHost = vi.fn((worktreeId?: string) =>
worktreeId === 'repo-ssh::/srv/w'
? ({ kind: 'ssh', connectionId: 'ssh-1' } as const)
: ({ kind: 'local', context: { projectRuntime } } as const)
)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these methods read only resolveAgentDetectionHost and getRuntimeId from the runtime.
const runtime = {
getRuntimeId: () => 'test-runtime',
resolveAgentDetectionHost
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: PREFLIGHT_METHODS })
const wsl = await dispatcher.dispatch(
makeRequest('preflight.detectAgents', { worktreeId: 'repo-1::\\\\wsl.localhost\\Ubuntu\\w' })
)
const ssh = await dispatcher.dispatch(
makeRequest('preflight.detectAgents', { worktreeId: 'repo-ssh::/srv/w' })
)
expect(detectInstalledAgentsWithShellPathHydrationMock).toHaveBeenCalledWith({ projectRuntime })
expect(wsl).toMatchObject({ ok: true, result: ['codex'] })
expect(detectRemoteAgentsMock).toHaveBeenCalledWith({ connectionId: 'ssh-1' })
expect(ssh).toMatchObject({ ok: true, result: ['claude'] })
})
it('detects agents on remote SSH connections through runtime RPC', async () => {
detectRemoteAgentsMock.mockResolvedValueOnce(['claude'])
const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService
+14 -8
View File
@@ -1,12 +1,14 @@
import { defineMethod } from '../core'
import {
detectRemoteAgents,
detectRemoteWindowsTerminalCapabilities,
detectInstalledAgentsWithShellPathHydration,
refreshShellPathAndDetectAgents,
runPreflightCheck
} from '../../../preflight/agent-detection'
import {
detectAgentsOnHost,
refreshAgentsOnHost
} from '../../../preflight/workspace-agent-detection'
import {
PreflightAgentDetection,
PreflightCheck,
PreflightDetectRemoteAgents,
PreflightDetectRemoteWindowsTerminalCapabilities
@@ -22,14 +24,17 @@ export const PREFLIGHT_METHODS = [
defineMethod({
name: 'preflight.detectAgents',
permission: 'workspace',
params: null,
handler: async () => detectInstalledAgentsWithShellPathHydration()
params: PreflightAgentDetection,
// Why the host resolves: only it knows the workspace's project runtime, including WSL.
handler: async (params, { runtime }) =>
detectAgentsOnHost(await runtime.resolveAgentDetectionHost(params.worktreeId))
}),
defineMethod({
name: 'preflight.detectRemoteAgents',
permission: 'workspace',
params: PreflightDetectRemoteAgents,
handler: async (params) => detectRemoteAgents(params)
handler: async (params) =>
detectAgentsOnHost({ kind: 'ssh', connectionId: params.connectionId })
}),
defineMethod({
name: 'preflight.detectRemoteWindowsTerminalCapabilities',
@@ -40,7 +45,8 @@ export const PREFLIGHT_METHODS = [
defineMethod({
name: 'preflight.refreshAgents',
permission: 'workspace',
params: null,
handler: async () => refreshShellPathAndDetectAgents()
params: PreflightAgentDetection,
handler: async (params, { runtime }) =>
refreshAgentsOnHost(await runtime.resolveAgentDetectionHost(params.worktreeId))
})
]
@@ -14,10 +14,7 @@ import {
launchHostProvesAgentInFront,
nameLocalTypedLineShell
} from './agent-launch-typed-line-shell'
import {
detectInstalledAgentsWithShellPathHydration,
detectRemoteAgents
} from '../preflight/agent-detection'
import { detectAgentsOnHost } from '../preflight/workspace-agent-detection'
import type { RuntimeStore } from './runtime-store-contract'
import type { RuntimeManagedWorktreeCreateArgs } from './runtime-managed-worktree-create-types'
@@ -61,9 +58,9 @@ export async function resolveWorktreeStartupDraftAgent(
const sshConnectionId = getRepoSshConnectionId(repo)
try {
// Why: startup-draft fallback can run from sparse runtime launch envs too.
detected = sshConnectionId
? await detectRemoteAgents({ connectionId: sshConnectionId })
: await detectInstalledAgentsWithShellPathHydration()
detected = await detectAgentsOnHost(
sshConnectionId ? { kind: 'ssh', connectionId: sshConnectionId } : { kind: 'local' }
)
} catch {
detected = []
}
@@ -12,6 +12,7 @@ import {
} from '../../../../shared/tui-agent-selection'
import type { TuiAgent } from '../../../../shared/tui-agent'
import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
import { getRuntimeAgentInventoryKey } from '@/store/slices/runtime-agent-inventory-key'
export type DashboardLaunchDetectionState = Pick<
AppState,
@@ -46,6 +47,20 @@ function buildDashboardLaunchCatalog(state: DashboardLaunchOptionState): Dashboa
}
}
// Why the host-default fallback: a card can render before any launch surface probed its workspace.
function runtimeAgentsForWorkspace(
state: DashboardLaunchOptionState,
environmentId: string,
worktreeId: string
): readonly TuiAgent[] {
const agents = state.runtimeDetectedAgentIds
return (
agents?.[getRuntimeAgentInventoryKey(environmentId, worktreeId)] ??
agents?.[environmentId] ??
[]
)
}
function detectedAgentsForWorktree(
state: DashboardLaunchOptionState,
worktreeId: string,
@@ -58,7 +73,7 @@ function detectedAgentsForWorktree(
const group = folder ? catalog.groupsById.get(folder.projectGroupId) : undefined
const host = parseExecutionHostId(group?.executionHostId)
if (host?.kind === 'runtime') {
return state.runtimeDetectedAgentIds?.[host.environmentId] ?? []
return runtimeAgentsForWorkspace(state, host.environmentId, worktreeId)
}
const connectionId = folder?.connectionId ?? group?.connectionId
return connectionId ? (state.remoteDetectedAgentIds?.[connectionId] ?? []) : []
@@ -68,7 +83,7 @@ function detectedAgentsForWorktree(
const repo = catalog.reposById.get(worktree?.repoId ?? repoId)
const host = parseExecutionHostId(worktree?.hostId ?? repo?.executionHostId)
if (host?.kind === 'runtime') {
return state.runtimeDetectedAgentIds?.[host.environmentId] ?? []
return runtimeAgentsForWorkspace(state, host.environmentId, worktreeId)
}
const connectionId = host?.kind === 'ssh' ? host.targetId : repo?.connectionId
return connectionId
@@ -159,7 +159,8 @@ describe('QuickLaunchAgentMenuItems', () => {
expect(useDetectedAgentsMock).toHaveBeenLastCalledWith({
kind: 'runtime',
environmentId: 'env-1'
environmentId: 'env-1',
worktreeId: 'worktree-1'
})
})
@@ -179,7 +180,8 @@ describe('QuickLaunchAgentMenuItems', () => {
expect(useDetectedAgentsMock).toHaveBeenLastCalledWith({
kind: 'runtime',
environmentId: 'env-1'
environmentId: 'env-1',
worktreeId: 'worktree-1'
})
})
@@ -70,7 +70,7 @@ describe('getAgentDetectionTargetKeyForWorktree', () => {
} as Parameters<typeof getAgentDetectionTargetKeyForWorktree>[0]
expect(getAgentDetectionTargetKeyForWorktree(state, folderWorkspaceKey('runtime-folder'))).toBe(
'runtime:owner-env'
`runtime:owner-env:${encodeURIComponent(folderWorkspaceKey('runtime-folder'))}`
)
expect(projectGroupReads).toBe(0)
})
@@ -116,7 +116,14 @@ describe('getAgentDetectionTargetKeyForWorktree', () => {
}
} as unknown as Parameters<typeof getAgentDetectionTargetKeyForWorktree>[0]
expect(getAgentDetectionTargetKeyForWorktree(state, 'repo-1::worktree-1')).toBe('runtime:env-1')
const key = getAgentDetectionTargetKeyForWorktree(state, 'repo-1::worktree-1')
// The workspace rides along so the paired host resolves its runtime (#19885).
expect(key).toBe('runtime:env-1:repo-1%3A%3Aworktree-1')
expect(parseAgentDetectionTargetKey(key)).toEqual({
kind: 'runtime',
environmentId: 'env-1',
worktreeId: 'repo-1::worktree-1'
})
})
it('builds one owner index per cold worktree and repo snapshot', () => {
@@ -14,6 +14,11 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
export const AGENT_DETECTION_LOCAL_TARGET_KEY = 'local'
// Why the workspace rides along: the paired host resolves that workspace's runtime (e.g. WSL).
function getRuntimeAgentDetectionTargetKey(environmentId: string, worktreeId: string): string {
return `runtime:${encodeURIComponent(environmentId)}:${encodeURIComponent(worktreeId)}`
}
function getLocalAgentDetectionTargetKey(worktreeId: string): string {
return worktreeId === FLOATING_TERMINAL_WORKTREE_ID
? `${AGENT_DETECTION_LOCAL_TARGET_KEY}:${encodeURIComponent(worktreeId)}:host`
@@ -45,7 +50,7 @@ export function getAgentDetectionTargetKeyForWorktree(
worktreeId
)
if (explicitRuntimeEnvironmentId) {
return `runtime:${explicitRuntimeEnvironmentId}`
return getRuntimeAgentDetectionTargetKey(explicitRuntimeEnvironmentId, worktreeId)
}
// Why: a hostless folder can span local and SSH children, so keep the
// ambiguity gate before applying its focused-runtime fallback.
@@ -62,7 +67,7 @@ export function getAgentDetectionTargetKeyForWorktree(
return `ssh:${executionHost.targetId}`
}
if (executionHost?.kind === 'runtime') {
return `runtime:${executionHost.environmentId}`
return getRuntimeAgentDetectionTargetKey(executionHost.environmentId, worktreeId)
}
return getLocalAgentDetectionTargetKey(worktreeId)
}
@@ -97,7 +102,15 @@ export function parseAgentDetectionTargetKey(
return { kind: 'ssh', connectionId: key.slice('ssh:'.length) }
}
if (key.startsWith('runtime:')) {
return { kind: 'runtime', environmentId: key.slice('runtime:'.length) }
const [encodedEnvironmentId = '', encodedWorktreeId] = key.slice('runtime:'.length).split(':')
try {
const environmentId = decodeURIComponent(encodedEnvironmentId)
return encodedWorktreeId
? { kind: 'runtime', environmentId, worktreeId: decodeURIComponent(encodedWorktreeId) }
: { kind: 'runtime', environmentId }
} catch {
return { kind: 'runtime', environmentId: encodedEnvironmentId }
}
}
return { kind: 'local' }
}
+23 -14
View File
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef } from 'react'
import { useAppStore } from '@/store'
import type { TuiAgent } from '../../../shared/tui-agent'
import { getRuntimeAgentInventoryKey } from '@/store/slices/runtime-agent-inventory-key'
export type UseDetectedAgentsResult = {
/** Null while detection is in flight on first load. */
@@ -19,7 +20,8 @@ export type UseDetectedAgentsResult = {
export type AgentDetectionTarget =
| { kind: 'local'; worktreeId?: string | null; contextKey?: string }
| { kind: 'ssh'; connectionId: string }
| { kind: 'runtime'; environmentId: string }
// worktreeId: the host resolves that workspace's runtime; absent means the host default.
| { kind: 'runtime'; environmentId: string; worktreeId?: string | null }
function normalizeAgentDetectionTarget(
target: AgentDetectionTarget | string | null | undefined
@@ -66,12 +68,18 @@ export function useDetectedAgents(
? target.environmentId
: null
const localWorktreeId = target?.kind === 'local' ? target.worktreeId : undefined
const runtimeWorktreeId = target?.kind === 'runtime' ? target.worktreeId : undefined
// Records for a runtime host are per workspace; ssh/local records keep their own keys.
const recordKey =
targetKind === 'runtime' && targetId
? getRuntimeAgentInventoryKey(targetId, runtimeWorktreeId)
: targetId
const localContextKey = target?.kind === 'local' ? target.contextKey : undefined
const remoteTargetKey =
targetKind === 'ssh' && targetId
? `ssh:${targetId}`
: targetKind === 'runtime' && targetId
? `runtime:${targetId}`
: targetKind === 'runtime' && recordKey
? `runtime:${recordKey}`
: null
const detectedIds = useAppStore((s) => {
@@ -81,8 +89,8 @@ export function useDetectedAgents(
if (targetKind === 'ssh' && targetId) {
return s.remoteDetectedAgentIds[targetId] ?? null
}
if (targetKind === 'runtime' && targetId) {
return s.runtimeDetectedAgentIds[targetId] ?? null
if (targetKind === 'runtime' && recordKey) {
return s.runtimeDetectedAgentIds[recordKey] ?? null
}
return localContextKey
? (s.localDetectedAgentIdsByContext[localContextKey] ?? null)
@@ -95,16 +103,16 @@ export function useDetectedAgents(
if (targetKind === 'ssh' && targetId) {
return s.isDetectingRemoteAgents[targetId] ?? false
}
if (targetKind === 'runtime' && targetId) {
return s.isDetectingRuntimeAgents[targetId] ?? false
if (targetKind === 'runtime' && recordKey) {
return s.isDetectingRuntimeAgents[recordKey] ?? false
}
return localContextKey
? (s.isDetectingLocalAgentsByContext[localContextKey] ?? false)
: s.isDetectingAgents
})
const isRefreshing = useAppStore((s) => {
if (targetKind === 'runtime' && targetId) {
return s.isRefreshingRuntimeAgents[targetId] ?? false
if (targetKind === 'runtime' && recordKey) {
return s.isRefreshingRuntimeAgents[recordKey] ?? false
}
if (targetKind === 'ssh' && targetId) {
return s.isDetectingRemoteAgents[targetId] ?? false
@@ -132,13 +140,13 @@ export function useDetectedAgents(
// no-op Zustand subscriptions per hook during unrelated store churn.
const state = useAppStore.getState()
if (targetKind === 'runtime' && targetId) {
return state.refreshRuntimeDetectedAgents(targetId)
return state.refreshRuntimeDetectedAgents(targetId, runtimeWorktreeId)
}
if (targetKind === 'ssh' && targetId) {
return state.refreshRemoteDetectedAgents(targetId)
}
return state.refreshDetectedAgents(localWorktreeId)
}, [isUnknown, localWorktreeId, targetKind, targetId])
}, [isUnknown, localWorktreeId, runtimeWorktreeId, targetKind, targetId])
useEffect(() => {
if (isUnknown) {
@@ -165,14 +173,14 @@ export function useDetectedAgents(
}
} else if (targetKind === 'runtime' && targetId) {
if (detectedIds === null) {
void state.ensureRuntimeDetectedAgents(targetId)
void state.ensureRuntimeDetectedAgents(targetId, runtimeWorktreeId)
} else if (isNewRemoteTarget && detectedIds.length > 0) {
// Why: a host can install an agent after its cached list was populated;
// refresh once when a new launch surface first observes that host.
void state.refreshRuntimeDetectedAgents(targetId)
void state.refreshRuntimeDetectedAgents(targetId, runtimeWorktreeId)
} else if (isNewRemoteTarget) {
// Empty results are intentionally retryable through the normal probe.
void state.ensureRuntimeDetectedAgents(targetId)
void state.ensureRuntimeDetectedAgents(targetId, runtimeWorktreeId)
}
} else {
if (detectedIds === null) {
@@ -186,6 +194,7 @@ export function useDetectedAgents(
remoteTargetKey,
detectedIds,
localWorktreeId,
runtimeWorktreeId,
localContextKey
])
@@ -0,0 +1,48 @@
import type { AppState } from '@/store/types'
import type { AgentDetectionTarget } from '@/hooks/useDetectedAgents'
import { getRuntimeAgentInventoryKey } from '@/store/slices/runtime-agent-inventory-key'
import type { TuiAgent } from '../../../shared/tui-agent'
type DetectionReadState = Pick<
AppState,
| 'detectedAgentIds'
| 'localDetectedAgentIdsByContext'
| 'remoteDetectedAgentIds'
| 'runtimeDetectedAgentIds'
>
type DetectionActionState = Pick<
AppState,
'ensureDetectedAgents' | 'ensureRemoteDetectedAgents' | 'ensureRuntimeDetectedAgents'
>
/** The agent list a detection target's host last reported; null while it has not loaded. */
export function readDetectedAgentsForTarget(
state: DetectionReadState,
target: AgentDetectionTarget
): TuiAgent[] | null {
if (target.kind === 'ssh') {
return state.remoteDetectedAgentIds[target.connectionId] ?? null
}
if (target.kind === 'runtime') {
const key = getRuntimeAgentInventoryKey(target.environmentId, target.worktreeId)
return state.runtimeDetectedAgentIds[key] ?? null
}
return target.contextKey
? (state.localDetectedAgentIdsByContext[target.contextKey] ?? null)
: state.detectedAgentIds
}
/** Loads a target's list from the host that owns it. */
export function ensureDetectedAgentsForTarget(
state: DetectionActionState,
target: AgentDetectionTarget
): Promise<TuiAgent[]> {
if (target.kind === 'ssh') {
return state.ensureRemoteDetectedAgents(target.connectionId)
}
if (target.kind === 'runtime') {
return state.ensureRuntimeDetectedAgents(target.environmentId, target.worktreeId)
}
return state.ensureDetectedAgents(target.worktreeId)
}
@@ -1,6 +1,5 @@
import { isNativeChatEnabled } from '../../../shared/structured-native-chat-launch-route'
import { pickTuiAgent } from '../../../shared/tui-agent-selection'
import type { TuiAgent } from '../../../shared/tui-agent'
import { withTimeout } from '../../../shared/promise-timeout-fallback'
import { useAppStore } from '@/store'
import type { AppState } from '@/store/types'
@@ -9,6 +8,10 @@ import {
parseAgentDetectionTargetKey
} from '@/hooks/useAgentDetectionTarget'
import type { AgentDetectionTarget } from '@/hooks/useDetectedAgents'
import {
ensureDetectedAgentsForTarget,
readDetectedAgentsForTarget
} from '@/lib/agent-detection-target-inventory'
import { workspaceKindForWorktreeId } from '@/lib/agent-launch-route-input'
import { planAgentSessionLaunch } from '@/lib/agent-session-launch-plan'
import { newAgentLaunchRequestId } from '@/lib/agent-launch-request-id'
@@ -33,24 +36,11 @@ function defaultChatDetectionTarget(
return parseAgentDetectionTargetKey(getAgentDetectionTargetKeyForWorktree(state, worktreeId))
}
/** Same host lists the tab bar reads; null means that host's detection has not loaded. */
function readDetectedAgents(state: AppState, target: AgentDetectionTarget): TuiAgent[] | null {
if (target.kind === 'ssh') {
return state.remoteDetectedAgentIds[target.connectionId] ?? null
}
if (target.kind === 'runtime') {
return state.runtimeDetectedAgentIds[target.environmentId] ?? null
}
return target.contextKey
? (state.localDetectedAgentIdsByContext[target.contextKey] ?? null)
: state.detectedAgentIds
}
/** True when the default chat could open here but the workspace host's agent list has not loaded. */
export function emptyWorkspaceDefaultChatAwaitsDetection(worktreeId: string): boolean {
const state = useAppStore.getState()
const target = defaultChatDetectionTarget(state, worktreeId)
return target !== undefined && readDetectedAgents(state, target) === null
return target !== undefined && readDetectedAgentsForTarget(state, target) === null
}
/** Loads the workspace host's agent list; resolves on success, failure, or timeout. */
@@ -60,13 +50,11 @@ export async function loadEmptyWorkspaceDefaultChatDetection(worktreeId: string)
if (!target) {
return
}
const detection =
target.kind === 'ssh'
? state.ensureRemoteDetectedAgents(target.connectionId)
: target.kind === 'runtime'
? state.ensureRuntimeDetectedAgents(target.environmentId)
: state.ensureDetectedAgents(target.worktreeId)
await withTimeout(detection, DEFAULT_CHAT_DETECTION_TIMEOUT_MS, [])
await withTimeout(
ensureDetectedAgentsForTarget(state, target),
DEFAULT_CHAT_DETECTION_TIMEOUT_MS,
[]
)
}
/**
@@ -85,7 +73,7 @@ export function openDefaultAgentChatInEmptyWorkspace(
}
const agent = pickTuiAgent(
state.settings?.defaultTuiAgent,
readDetectedAgents(state, target) ?? [],
readDetectedAgentsForTarget(state, target) ?? [],
state.settings?.disabledTuiAgents
)
if (!agent) {
@@ -28,7 +28,7 @@ export async function detectAgentSessionContinuationAgents(
return connectionId
? state.ensureRemoteDetectedAgents(connectionId)
: runtimeEnvironmentId
? state.ensureRuntimeDetectedAgents(runtimeEnvironmentId)
? state.ensureRuntimeDetectedAgents(runtimeEnvironmentId, worktreeId)
: state.ensureDetectedAgents(worktreeId)
}
@@ -3,6 +3,7 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
import { parseWslUncPath } from '../../../shared/wsl-paths'
import {
deriveGlobalWindowsRuntimeDefaultFromLegacySettings,
getWorkspaceRuntimePreference,
resolveProjectExecutionRuntime,
type ProjectExecutionRuntimeResolution
} from '../../../shared/project-execution-runtime'
@@ -81,11 +82,10 @@ export function getLocalProjectExecutionRuntimeContext(
}
const projectId = getLocalPreflightProjectId(state, worktreeId)
const project = getLocalRuntimeProject(state, projectId, repo.id)
const localPath = worktree?.path ?? repo?.path
const worktreeWslDistro = getWslDistroFromPath(localPath)
const projectRuntimePreference =
project?.localWindowsRuntimePreference ??
(worktreeWslDistro ? { kind: 'wsl', distro: worktreeWslDistro } : { kind: 'inherit-global' })
const projectRuntimePreference = getWorkspaceRuntimePreference(
project?.localWindowsRuntimePreference,
worktree?.path ?? repo?.path
)
return resolveProjectExecutionRuntime({
appPlatform,
@@ -140,10 +140,10 @@ export function getLocalRepoProjectExecutionRuntimeContext(
}
const project = getLocalRuntimeProject(state, repoId, repo.id)
const projectId = project?.id ?? repoId
const repoWslDistro = getWslDistroFromPath(repo?.path)
const projectRuntimePreference =
project?.localWindowsRuntimePreference ??
(repoWslDistro ? { kind: 'wsl', distro: repoWslDistro } : { kind: 'inherit-global' })
const projectRuntimePreference = getWorkspaceRuntimePreference(
project?.localWindowsRuntimePreference,
repo?.path
)
return resolveProjectExecutionRuntime({
appPlatform,
@@ -0,0 +1,15 @@
const WORKSPACE_SEPARATOR = '\u0000'
/** A paired host's agent list is per workspace: two projects on one Windows host can run in
* different runtimes (native vs a WSL distro). No workspace means the host-default list. */
export function getRuntimeAgentInventoryKey(
environmentId: string,
worktreeId?: string | null
): string {
return worktreeId ? `${environmentId}${WORKSPACE_SEPARATOR}${worktreeId}` : environmentId
}
export function getRuntimeAgentInventoryEnvironmentId(inventoryKey: string): string {
const separatorIndex = inventoryKey.indexOf(WORKSPACE_SEPARATOR)
return separatorIndex === -1 ? inventoryKey : inventoryKey.slice(0, separatorIndex)
}
@@ -1,20 +1,37 @@
import type { StateCreator } from 'zustand'
import type { AppState } from '../types'
import type { TuiAgent } from '../../../../shared/tui-agent'
import { callRuntimeRpc, RuntimeRpcCallError } from '@/runtime/runtime-rpc-client'
import {
callRuntimeRpc,
RuntimeRpcCallError,
runtimeEnvironmentSupportsCapability
} from '@/runtime/runtime-rpc-client'
import { PREFLIGHT_WORKSPACE_SCOPED_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
import {
getRuntimeAgentInventoryEnvironmentId,
getRuntimeAgentInventoryKey
} from './runtime-agent-inventory-key'
// Why: remote runtime hosts are not SSH connections, but their launch surfaces
// (tab bar, quick launch, Settings → Agents under an Active Server) still have
// to probe the host where the workspace actually runs.
// Records are keyed by getRuntimeAgentInventoryKey: the environment alone for
// the host-default list, or environment + workspace for a workspace's list.
export type RuntimeDetectedAgentsSlice = {
runtimeDetectedAgentIds: Record<string, TuiAgent[] | null>
isDetectingRuntimeAgents: Record<string, boolean>
isRefreshingRuntimeAgents: Record<string, boolean>
ensureRuntimeDetectedAgents: (environmentId: string) => Promise<TuiAgent[]>
ensureRuntimeDetectedAgents: (
environmentId: string,
worktreeId?: string | null
) => Promise<TuiAgent[]>
/** Forces a re-detect on the runtime host via `preflight.refreshAgents`
* (login-shell PATH re-read), falling back to `preflight.detectAgents` for
* servers that predate the refresh RPC. */
refreshRuntimeDetectedAgents: (environmentId: string) => Promise<TuiAgent[]>
refreshRuntimeDetectedAgents: (
environmentId: string,
worktreeId?: string | null
) => Promise<TuiAgent[]>
clearRuntimeDetectedAgents: (environmentId: string) => void
/** Drops runtime detected-agent caches for environments not in the kept set.
* Wired into setRuntimeEnvironments so removed environments don't leak their
@@ -31,208 +48,201 @@ function isRuntimeMethodNotFoundError(error: unknown): boolean {
return error instanceof RuntimeRpcCallError && error.code === 'method_not_found'
}
/**
* Sends `preflight.detectAgents` / `refreshAgents` scoped to a workspace. A host without the
* workspace-scoped capability would discard the params and answer with its own default, so it is
* asked for exactly that. The host default goes out synchronously, as it always has.
*/
export function callRuntimeAgentDetection<T>(
environmentId: string,
worktreeId: string | null | undefined,
call: (params: { worktreeId: string } | undefined) => Promise<T>
): Promise<T> {
if (!worktreeId) {
return call(undefined)
}
return runtimeEnvironmentSupportsCapability(
environmentId,
PREFLIGHT_WORKSPACE_SCOPED_RUNTIME_CAPABILITY
).then((supported) => call(supported ? { worktreeId } : undefined))
}
export function _getRuntimeDetectPromiseCountForTest(): number {
return runtimeDetectPromises.size
}
function withoutKeys<T>(record: Record<string, T>, drop: (key: string) => boolean) {
let changed = false
const next: Record<string, T> = {}
for (const [key, value] of Object.entries(record)) {
if (drop(key)) {
changed = true
} else {
next[key] = value
}
}
return changed ? next : record
}
export const createRuntimeDetectedAgentsSlice: StateCreator<
AppState,
[],
[],
RuntimeDetectedAgentsSlice
> = (set, get) => ({
runtimeDetectedAgentIds: {},
isDetectingRuntimeAgents: {},
isRefreshingRuntimeAgents: {},
ensureRuntimeDetectedAgents: (environmentId: string) => {
const inflightRefresh = runtimeRefreshPromises.get(environmentId)
if (inflightRefresh) {
return inflightRefresh
}
const existing = get().runtimeDetectedAgentIds[environmentId]
// Why: an empty result ([]) is truthy, so a prior "no agents found" detection
// must not be treated as cached — re-detect so a later install / PATH fix is
// picked up without a reconnect. Non-empty results still short-circuit.
if (existing?.length) {
return Promise.resolve(existing)
}
const inflight = runtimeDetectPromises.get(environmentId)
if (inflight) {
return inflight
}
set((s) => ({
isDetectingRuntimeAgents: { ...s.isDetectingRuntimeAgents, [environmentId]: true }
}))
const pending = callRuntimeRpc<TuiAgent[]>(
{ kind: 'environment', environmentId },
'preflight.detectAgents'
)
.then((ids) => {
const typed = ids as TuiAgent[]
// Why: skip committing if the environment was removed (retained out)
// while the detect was in flight — otherwise it re-adds a stale entry
// that retainRuntimeDetectedAgents just pruned.
if (runtimeDetectPromises.get(environmentId) === pending) {
set((s) => ({
runtimeDetectedAgentIds: { ...s.runtimeDetectedAgentIds, [environmentId]: typed },
isDetectingRuntimeAgents: { ...s.isDetectingRuntimeAgents, [environmentId]: false }
}))
> = (set, get) => {
// Drops every workspace's inventory for the environments `drop` selects.
const dropEnvironments = (drop: (environmentId: string) => boolean): void => {
const dropKey = (key: string) => drop(getRuntimeAgentInventoryEnvironmentId(key))
for (const promises of [runtimeDetectPromises, runtimeRefreshPromises]) {
for (const key of promises.keys()) {
if (dropKey(key)) {
promises.delete(key)
}
return typed
})
.catch(() => {
// Why: a remote runtime may be disconnected or version-incompatible.
// Keep the menu retryable instead of pinning a failed probe forever.
// Same in-flight guard as the .then() above: if the environment was
// retained out mid-detect, don't re-add the isDetecting entry that
// retainRuntimeDetectedAgents just pruned (and don't clobber a freshly
// started detect's spinner).
if (runtimeDetectPromises.get(environmentId) === pending) {
set((s) => ({
isDetectingRuntimeAgents: { ...s.isDetectingRuntimeAgents, [environmentId]: false }
}))
}
return [] as TuiAgent[]
})
.finally(() => {
if (runtimeDetectPromises.get(environmentId) === pending) {
runtimeDetectPromises.delete(environmentId)
}
})
runtimeDetectPromises.set(environmentId, pending)
return pending
},
refreshRuntimeDetectedAgents: (environmentId: string) => {
const inflight = runtimeRefreshPromises.get(environmentId)
if (inflight) {
return inflight
}
// Why: a refresh is newer and authoritative; detach an older detect so its
// late result cannot overwrite the freshly hydrated PATH result.
runtimeDetectPromises.delete(environmentId)
set((s) => ({
isRefreshingRuntimeAgents: { ...s.isRefreshingRuntimeAgents, [environmentId]: true }
}))
const pending = callRuntimeRpc<{ agents: TuiAgent[] }>(
{ kind: 'environment', environmentId },
'preflight.refreshAgents'
)
.then((result) => result.agents)
.catch((error) => {
if (!isRuntimeMethodNotFoundError(error)) {
throw error
}
// Why: only older servers need the fallback; retrying disconnects and
// runtime failures doubles remote work without any chance of recovery.
return callRuntimeRpc<TuiAgent[]>(
{ kind: 'environment', environmentId },
'preflight.detectAgents'
)
})
.then((ids) => {
const typed = ids as TuiAgent[]
// Why: same guard as ensureRuntimeDetectedAgents — if the environment
// was retained out mid-refresh, don't re-add a pruned entry.
if (runtimeRefreshPromises.get(environmentId) === pending) {
set((s) => ({
runtimeDetectedAgentIds: { ...s.runtimeDetectedAgentIds, [environmentId]: typed },
isDetectingRuntimeAgents: {
...s.isDetectingRuntimeAgents,
[environmentId]: false
},
isRefreshingRuntimeAgents: { ...s.isRefreshingRuntimeAgents, [environmentId]: false }
}))
}
return typed
})
.catch(() => {
// Why: a disconnected runtime must keep Refresh retryable and must not
// wipe the last known agent list.
if (runtimeRefreshPromises.get(environmentId) === pending) {
set((s) => ({
isDetectingRuntimeAgents: {
...s.isDetectingRuntimeAgents,
[environmentId]: false
},
isRefreshingRuntimeAgents: { ...s.isRefreshingRuntimeAgents, [environmentId]: false }
}))
}
return get().runtimeDetectedAgentIds[environmentId] ?? []
})
.finally(() => {
if (runtimeRefreshPromises.get(environmentId) === pending) {
runtimeRefreshPromises.delete(environmentId)
}
})
runtimeRefreshPromises.set(environmentId, pending)
return pending
},
clearRuntimeDetectedAgents: (environmentId: string) => {
runtimeDetectPromises.delete(environmentId)
runtimeRefreshPromises.delete(environmentId)
set((s) => {
const { [environmentId]: _, ...restAgents } = s.runtimeDetectedAgentIds
const { [environmentId]: __, ...restLoading } = s.isDetectingRuntimeAgents
const { [environmentId]: ___, ...restRefreshing } = s.isRefreshingRuntimeAgents
return {
runtimeDetectedAgentIds: restAgents,
isDetectingRuntimeAgents: restLoading,
isRefreshingRuntimeAgents: restRefreshing
}
})
},
retainRuntimeDetectedAgents: (environmentIds: Iterable<string>) => {
const keep = new Set(environmentIds)
for (const id of runtimeDetectPromises.keys()) {
if (!keep.has(id)) {
runtimeDetectPromises.delete(id)
}
}
for (const id of runtimeRefreshPromises.keys()) {
if (!keep.has(id)) {
runtimeRefreshPromises.delete(id)
}
}
set((s) => {
let changed = false
const nextAgents = { ...s.runtimeDetectedAgentIds }
const nextLoading = { ...s.isDetectingRuntimeAgents }
const nextRefreshing = { ...s.isRefreshingRuntimeAgents }
for (const id of Object.keys(nextAgents)) {
if (!keep.has(id)) {
delete nextAgents[id]
changed = true
}
}
for (const id of Object.keys(nextLoading)) {
if (!keep.has(id)) {
delete nextLoading[id]
changed = true
}
}
for (const id of Object.keys(nextRefreshing)) {
if (!keep.has(id)) {
delete nextRefreshing[id]
changed = true
}
}
return changed
? {
runtimeDetectedAgentIds: nextAgents,
isDetectingRuntimeAgents: nextLoading,
isRefreshingRuntimeAgents: nextRefreshing
}
: s
const runtimeDetectedAgentIds = withoutKeys(s.runtimeDetectedAgentIds, dropKey)
const isDetectingRuntimeAgents = withoutKeys(s.isDetectingRuntimeAgents, dropKey)
const isRefreshingRuntimeAgents = withoutKeys(s.isRefreshingRuntimeAgents, dropKey)
return runtimeDetectedAgentIds === s.runtimeDetectedAgentIds &&
isDetectingRuntimeAgents === s.isDetectingRuntimeAgents &&
isRefreshingRuntimeAgents === s.isRefreshingRuntimeAgents
? s
: { runtimeDetectedAgentIds, isDetectingRuntimeAgents, isRefreshingRuntimeAgents }
})
}
})
return {
runtimeDetectedAgentIds: {},
isDetectingRuntimeAgents: {},
isRefreshingRuntimeAgents: {},
ensureRuntimeDetectedAgents: (environmentId: string, worktreeId?: string | null) => {
const key = getRuntimeAgentInventoryKey(environmentId, worktreeId)
const inflightRefresh = runtimeRefreshPromises.get(key)
if (inflightRefresh) {
return inflightRefresh
}
const existing = get().runtimeDetectedAgentIds[key]
// Why: an empty result ([]) is truthy, so a prior "no agents found" detection
// must not be treated as cached — re-detect so a later install / PATH fix is
// picked up without a reconnect. Non-empty results still short-circuit.
if (existing?.length) {
return Promise.resolve(existing)
}
const inflight = runtimeDetectPromises.get(key)
if (inflight) {
return inflight
}
set((s) => ({
isDetectingRuntimeAgents: { ...s.isDetectingRuntimeAgents, [key]: true }
}))
const target = { kind: 'environment', environmentId } as const
const pending = callRuntimeAgentDetection(environmentId, worktreeId, (params) =>
callRuntimeRpc<TuiAgent[]>(target, 'preflight.detectAgents', params)
)
.then((typed) => {
// Why: skip committing if the environment was removed (retained out)
// while the detect was in flight — otherwise it re-adds a stale entry
// that retainRuntimeDetectedAgents just pruned.
if (runtimeDetectPromises.get(key) === pending) {
set((s) => ({
runtimeDetectedAgentIds: { ...s.runtimeDetectedAgentIds, [key]: typed },
isDetectingRuntimeAgents: { ...s.isDetectingRuntimeAgents, [key]: false }
}))
}
return typed
})
.catch((): TuiAgent[] => {
// Why: a remote runtime may be disconnected or version-incompatible.
// Keep the menu retryable instead of pinning a failed probe forever.
// Same in-flight guard as the .then() above.
if (runtimeDetectPromises.get(key) === pending) {
set((s) => ({
isDetectingRuntimeAgents: { ...s.isDetectingRuntimeAgents, [key]: false }
}))
}
return []
})
.finally(() => {
if (runtimeDetectPromises.get(key) === pending) {
runtimeDetectPromises.delete(key)
}
})
runtimeDetectPromises.set(key, pending)
return pending
},
refreshRuntimeDetectedAgents: (environmentId: string, worktreeId?: string | null) => {
const key = getRuntimeAgentInventoryKey(environmentId, worktreeId)
const inflight = runtimeRefreshPromises.get(key)
if (inflight) {
return inflight
}
// Why: a refresh is newer and authoritative; detach an older detect so its
// late result cannot overwrite the freshly hydrated PATH result.
runtimeDetectPromises.delete(key)
set((s) => ({
isRefreshingRuntimeAgents: { ...s.isRefreshingRuntimeAgents, [key]: true }
}))
const target = { kind: 'environment', environmentId } as const
const pending = callRuntimeAgentDetection(environmentId, worktreeId, (params) =>
callRuntimeRpc<{ agents: TuiAgent[] }>(target, 'preflight.refreshAgents', params)
.then((result) => result.agents)
.catch((error) => {
if (!isRuntimeMethodNotFoundError(error)) {
throw error
}
// Why: only older servers need the fallback; retrying disconnects and
// runtime failures doubles remote work without any chance of recovery.
return callRuntimeRpc<TuiAgent[]>(target, 'preflight.detectAgents', params)
})
)
.then((typed) => {
// Why: same guard as ensureRuntimeDetectedAgents — if the environment
// was retained out mid-refresh, don't re-add a pruned entry.
if (runtimeRefreshPromises.get(key) === pending) {
set((s) => ({
runtimeDetectedAgentIds: { ...s.runtimeDetectedAgentIds, [key]: typed },
isDetectingRuntimeAgents: { ...s.isDetectingRuntimeAgents, [key]: false },
isRefreshingRuntimeAgents: { ...s.isRefreshingRuntimeAgents, [key]: false }
}))
}
return typed
})
.catch(() => {
// Why: a disconnected runtime must keep Refresh retryable and must not
// wipe the last known agent list.
if (runtimeRefreshPromises.get(key) === pending) {
set((s) => ({
isDetectingRuntimeAgents: { ...s.isDetectingRuntimeAgents, [key]: false },
isRefreshingRuntimeAgents: { ...s.isRefreshingRuntimeAgents, [key]: false }
}))
}
return get().runtimeDetectedAgentIds[key] ?? []
})
.finally(() => {
if (runtimeRefreshPromises.get(key) === pending) {
runtimeRefreshPromises.delete(key)
}
})
runtimeRefreshPromises.set(key, pending)
return pending
},
clearRuntimeDetectedAgents: (environmentId: string) => {
dropEnvironments((id) => id === environmentId)
},
retainRuntimeDetectedAgents: (environmentIds: Iterable<string>) => {
const keep = new Set(environmentIds)
dropEnvironments((id) => !keep.has(id))
}
}
}
+14
View File
@@ -1,3 +1,5 @@
import { parseWslUncPath } from './wsl-paths'
export type LocalWindowsRuntimePreference =
| { kind: 'inherit-global' }
| { kind: 'windows-host' }
@@ -107,6 +109,18 @@ export function normalizeProjectRuntimePreference(value: unknown): LocalWindowsR
return { kind: 'inherit-global' }
}
/** A project with no saved runtime runs a workspace that lives in a WSL share inside that distro. */
export function getWorkspaceRuntimePreference(
projectPreference: LocalWindowsRuntimePreference | undefined,
workspacePath: string | null | undefined
): LocalWindowsRuntimePreference {
if (projectPreference) {
return projectPreference
}
const distro = workspacePath ? parseWslUncPath(workspacePath)?.distro : undefined
return distro ? { kind: 'wsl', distro } : { kind: 'inherit-global' }
}
export function normalizeGlobalWindowsRuntimeDefault(value: unknown): GlobalWindowsRuntimeDefault {
if (!isRecord(value)) {
return { kind: 'windows-host' }
+5
View File
@@ -320,6 +320,10 @@ export const WORKTREE_VISIBILITY_SOURCE_DEFAULTS_RUNTIME_CAPABILITY =
// would re-show that row as a normal workspace, so the host leaves such rows out of its listings.
export const WORKTREE_BACKGROUND_REMOVAL_RUNTIME_CAPABILITY =
'worktree.background-removal.v1' as const
// Why: older hosts discard preflight.detectAgents params and probe their own PATH, so a client
// must not read that answer as the workspace's (a WSL project on a Windows host differs).
export const PREFLIGHT_WORKSPACE_SCOPED_RUNTIME_CAPABILITY =
'preflight.workspace-scoped.v1' as const
// Hosts without this capability have no notifications.registerPush RPC.
export const NOTIFICATIONS_REMOTE_PUSH_RUNTIME_CAPABILITY = 'notifications.remote-push.v1' as const
@@ -425,6 +429,7 @@ export const RUNTIME_CAPABILITIES = [
...SKILL_RUNTIME_CAPABILITIES,
...AUTOMATION_RUNTIME_CAPABILITIES,
NOTIFICATIONS_REMOTE_PUSH_RUNTIME_CAPABILITY,
PREFLIGHT_WORKSPACE_SCOPED_RUNTIME_CAPABILITY,
...AGENT_LAUNCH_RUNTIME_CAPABILITIES
] as const
@@ -11,3 +11,8 @@ export const PreflightDetectRemoteAgents = z.object({
export const PreflightDetectRemoteWindowsTerminalCapabilities = z.object({
connectionId: z.string().min(1)
})
/** Why optional: a client that predates it sends nothing and keeps the host-default probe. */
export const PreflightAgentDetection = z.object({
worktreeId: z.string().min(1).optional()
})
+3 -2
View File
@@ -433,6 +433,7 @@ import {
PluginsPanelActionParams
} from './plugins-params'
import {
PreflightAgentDetection,
PreflightCheck,
PreflightDetectRemoteAgents,
PreflightDetectRemoteWindowsTerminalCapabilities
@@ -1110,11 +1111,11 @@ export const RPC_PARAMS_BY_METHOD = {
'plugins.readPanelEntry': PluginReadPanelEntryParams,
'plugins.setEnabled': PluginSetEnabledParams,
'preflight.check': PreflightCheck,
'preflight.detectAgents': null,
'preflight.detectAgents': PreflightAgentDetection,
'preflight.detectRemoteAgents': PreflightDetectRemoteAgents,
'preflight.detectRemoteWindowsTerminalCapabilities':
PreflightDetectRemoteWindowsTerminalCapabilities,
'preflight.refreshAgents': null,
'preflight.refreshAgents': PreflightAgentDetection,
'project.list': null,
'project.update': ProjectUpdate,
'projectGroup.create': ProjectGroupCreate,
@@ -0,0 +1,187 @@
// Workspace-scoped agent detection, paired across two builds: current code against the newest
// published release.
//
// `preflight.detectAgents` / `refreshAgents` gained an optional `{ worktreeId }` so the host probes
// the workspace's own runtime (a WSL distro on a Windows host). Each skew must keep today's answer:
//
// - an old client sends nothing, and a new host answers with its own default as it always did;
// - a new client asks an old host only for its default, because the capability is absent — and an
// old host that is sent the field anyway (mobile does not gate) discards it rather than refusing.
//
// The old side's capability list is read from its checkout and the capability removed from it, so
// this stays exercised after a release ships the capability.
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import {
PREFLIGHT_WORKSPACE_SCOPED_RUNTIME_CAPABILITY,
RUNTIME_CAPABILITIES
} from '../../../src/shared/protocol-version'
import { RPC_PARAMS_BY_METHOD } from '../../../src/shared/rpc-contract/rpc-params-catalog.generated'
import { parseRpcRequestParams } from '../../../src/main/runtime/rpc/dispatcher-request-parsing'
import type { RpcAnyMethodDeclaration, RpcRequest } from '../../../src/main/runtime/rpc/core'
import { getRuntimeAgentInventoryKey } from '../../../src/renderer/src/store/slices/runtime-agent-inventory-key'
import {
importReleaseCheckoutModule,
materializeReleaseCheckout,
resolveBaselineReleaseRef
} from './release-checkout'
// Why: a cold CI run extracts the baseline checkout before the first pairing.
const SUITE_TIMEOUT_MS = 180_000
const WORKTREE = 'repo-1::\\\\wsl.localhost\\Ubuntu\\home\\me\\repo'
type ParseParams = (
request: RpcRequest,
method: Pick<RpcAnyMethodDeclaration, 'params'>,
meta: unknown
) => { value?: unknown; error?: unknown }
type HostBuild = {
capabilities: readonly string[]
parse: (method: string, params: unknown) => unknown
}
const wire = vi.hoisted(() => {
const state: {
host: null | { capabilities: readonly string[]; parse: (m: string, p: unknown) => unknown }
sent: { method: string; params: unknown; hostSaw: unknown }[]
} = { host: null, sent: [] }
return state
})
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) => {
const actual = await importOriginal<Record<string, unknown>>()
return {
...actual,
runtimeEnvironmentSupportsCapability: async (_environmentId: string, capability: string) =>
wire.host?.capabilities.includes(capability) ?? false,
callRuntimeRpc: async (_target: unknown, method: string, params?: unknown) => {
const hostSaw = wire.host?.parse(method, params)
wire.sent.push({ method, params, hostSaw })
const scoped = typeof hostSaw === 'object' && hostSaw !== null && 'worktreeId' in hostSaw
const agents = scoped ? ['claude', 'codex'] : ['claude']
return method === 'preflight.refreshAgents' ? { agents } : agents
}
}
})
// @ts-expect-error -- minimal window.api stub for the store under test
globalThis.window = { api: {} }
const { createTestStore } =
await import('../../../src/renderer/src/store/slices/store-test-helpers')
function member<T>(module: Record<string, unknown>, name: string): T {
const value = module[name]
if (value === undefined) {
throw new Error(`the baseline release exports no ${name}`)
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a release module is typed unknown; the name is the same export this build calls, and a missing one throws above.
return value as T
}
function hostBuild(
capabilities: readonly string[],
catalog: Record<string, unknown>,
parse: ParseParams
): HostBuild {
return {
capabilities,
parse: (method, params) => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: catalog entries are the Zod schema or null each build's dispatcher reads.
const schema = (catalog[method] ?? null) as RpcAnyMethodDeclaration['params']
const parsed = parse({ id: '1', authToken: 't', method, params }, { params: schema }, {})
if (parsed.error) {
throw new Error(`${method} refused ${JSON.stringify(params)}`)
}
return parsed.value
}
}
}
let oldHost: HostBuild
let newHost: HostBuild
beforeAll(async () => {
const checkout = await materializeReleaseCheckout(resolveBaselineReleaseRef())
const [protocol, catalog, parsing] = await Promise.all([
importReleaseCheckoutModule(checkout, 'src/shared/protocol-version.ts'),
importReleaseCheckoutModule(
checkout,
'src/shared/rpc-contract/rpc-params-catalog.generated.ts'
),
importReleaseCheckoutModule(checkout, 'src/main/runtime/rpc/dispatcher-request-parsing.ts')
])
const oldCatalog = member<Record<string, unknown>>(catalog, 'RPC_PARAMS_BY_METHOD')
oldHost = hostBuild(
member<readonly string[]>(protocol, 'RUNTIME_CAPABILITIES').filter(
(capability) => capability !== PREFLIGHT_WORKSPACE_SCOPED_RUNTIME_CAPABILITY
),
// Why null: once a release ships the schema, the old side must still be the pre-schema host.
{ ...oldCatalog, 'preflight.detectAgents': null, 'preflight.refreshAgents': null },
member<ParseParams>(parsing, 'parseRpcRequestParams')
)
newHost = hostBuild(RUNTIME_CAPABILITIES, RPC_PARAMS_BY_METHOD, (request, method, meta) =>
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the parser only reads `params` from the declaration.
parseRpcRequestParams(request, method as RpcAnyMethodDeclaration, meta as never)
)
}, SUITE_TIMEOUT_MS)
beforeEach(() => {
wire.sent.length = 0
})
describe('workspace-scoped agent detection across versions', () => {
it('a new client asks an old host only for its default list', async () => {
wire.host = oldHost
const store = createTestStore()
const detected = await store.getState().ensureRuntimeDetectedAgents('env-1', WORKTREE)
const refreshed = await store.getState().refreshRuntimeDetectedAgents('env-1', WORKTREE)
expect(wire.sent.map(({ method, params }) => ({ method, params }))).toEqual([
{ method: 'preflight.detectAgents', params: undefined },
{ method: 'preflight.refreshAgents', params: undefined }
])
expect(detected).toEqual(['claude'])
expect(refreshed).toEqual(['claude'])
expect(
store.getState().runtimeDetectedAgentIds[getRuntimeAgentInventoryKey('env-1', WORKTREE)]
).toEqual(['claude'])
})
it('an old host discards the field from an ungated client instead of refusing it', () => {
expect(oldHost.parse('preflight.detectAgents', { worktreeId: WORKTREE })).toBeUndefined()
expect(oldHost.parse('preflight.refreshAgents', { worktreeId: WORKTREE })).toBeUndefined()
})
it('a new client names the workspace to a new host', async () => {
wire.host = newHost
const store = createTestStore()
expect(await store.getState().ensureRuntimeDetectedAgents('env-1', WORKTREE)).toEqual([
'claude',
'codex'
])
expect(wire.sent).toEqual([
{
method: 'preflight.detectAgents',
params: { worktreeId: WORKTREE },
hostSaw: { worktreeId: WORKTREE }
}
])
})
it('an old client gets the host default from a new host', async () => {
wire.host = newHost
for (const params of [undefined, null]) {
expect(newHost.parse('preflight.detectAgents', params)).toEqual({})
expect(newHost.parse('preflight.refreshAgents', params)).toEqual({})
}
// The host-default list keeps its environment-only key, which existing readers index by.
const store = createTestStore()
expect(await store.getState().ensureRuntimeDetectedAgents('env-1')).toEqual(['claude'])
expect(wire.sent[0]?.params).toBeUndefined()
expect(store.getState().runtimeDetectedAgentIds['env-1']).toEqual(['claude'])
})
})