conductor-remote 1.32.0 → 1.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -13,7 +13,7 @@
13
13
  <title>Conductor Remote</title>
14
14
  <!-- Runs before the module bundle so it can catch a stale shell that fails to boot. -->
15
15
  <script src="/self-heal.js"></script>
16
- <script type="module" crossorigin src="/assets/index-CqXxJtQ2.js"></script>
16
+ <script type="module" crossorigin src="/assets/index-D883fUSq.js"></script>
17
17
  <link rel="stylesheet" crossorigin href="/assets/index-DwrzkcPG.css">
18
18
  <link rel="manifest" href="/manifest.webmanifest"></head>
19
19
  <body>
package/dist/sw.js CHANGED
@@ -1 +1 @@
1
- if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let l={};const t=e=>i(e,o),c={module:{uri:o},exports:l,require:t};s[o]=Promise.all(n.map(e=>c[e]||t(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"890f6a70e97d144f4d025254e90892a1"},{url:"index.html",revision:"22772614b29105aa581314e931fae671"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-DwrzkcPG.css",revision:null},{url:"assets/index-CqXxJtQ2.js",revision:null},{url:"apple-touch-icon.png",revision:"2b9301416b880d45d4bb655f2600d1f2"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
1
+ if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let l={};const c=e=>i(e,o),t={module:{uri:o},exports:l,require:c};s[o]=Promise.all(n.map(e=>t[e]||c(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"890f6a70e97d144f4d025254e90892a1"},{url:"index.html",revision:"77e0c625cdd4aa58ad272304a943de7b"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-DwrzkcPG.css",revision:null},{url:"assets/index-D883fUSq.js",revision:null},{url:"apple-touch-icon.png",revision:"2b9301416b880d45d4bb655f2600d1f2"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
@@ -15,7 +15,7 @@ import { notifyDevice, pushConfig, startNotifier, subscribeDevice, unsubscribeDe
15
15
  import { attachPrStatus } from "./pr.js";
16
16
  import { Reads } from "./reads.js";
17
17
  import { driftWarningLines, tailscaleBin } from "./tailscale.js";
18
- import { createWorkspace, describeActuator, EFFORT_LABELS, listAgentModels, newChat, pickActuator, setAgentOptions, setRestartGuard } from "./writes.js";
18
+ import { createWorkspace, describeActuator, EFFORT_LABELS, listAgentModels, newChat, pickActuator, retryWontHelp, setAgentOptions, setRestartGuard } from "./writes.js";
19
19
  // Before anything that logs: from here on every console line is also kept in memory for
20
20
  // `GET /api/logs`, so the phone can read why a send failed without ssh-ing into the Mac.
21
21
  installLogCapture();
@@ -31,23 +31,74 @@ const actuator = pickActuator(cfg.writeStrategy);
31
31
  setRestartGuard(() => !reads.listWorkspaces().some(w => w.session_status === 'working'));
32
32
  const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
33
33
  /**
34
- * Confirm a prompt actually reached the session by watching the transcript for a
35
- * new user row matching it. The AppleScript actuator reports `ok` on `osascript`
36
- * exit 0 — which only means the script *ran*, not that Conductor was focused and
37
- * accepted the keystrokes — so without this a dropped send (asleep/unfocused Mac)
38
- * looks delivered. Conductor writes the row right after the send presses Enter, so
39
- * a short poll catches a real send fast and only the failure path waits the full
40
- * window. A queued prompt still writes a user row, so it counts as delivered.
34
+ * Has the prompt shown up as a user row yet? The receipt everything below is built
35
+ * on. The AppleScript actuator reports `ok` on `osascript` exit 0 — which only
36
+ * means the script *ran*, not that Conductor accepted the keystrokes — so without
37
+ * this a dropped send (asleep/unfocused Mac) looks delivered. A queued prompt still
38
+ * writes a user row, so it counts as delivered.
41
39
  */
42
- async function confirmDelivery(sessionId, text, sinceRowid) {
40
+ function deliveredSince(sessionId, text, sinceRowid) {
43
41
  const target = text.trim();
44
- for (let attempt = 0; attempt < 12; attempt++) {
45
- const { entries } = reads.getMessages(sessionId, sinceRowid);
46
- if (entries.some(e => e.role === 'user' && e.text.trim() === target))
42
+ const { entries } = reads.getMessages(sessionId, sinceRowid);
43
+ return entries.some(e => e.role === 'user' && e.text.trim() === target);
44
+ }
45
+ /**
46
+ * Watch for that row, ending on a check rather than a sleep, and never past
47
+ * `budgetDeadline`. Conductor writes the row right after the send presses Enter, so
48
+ * a real send is confirmed in a tick and only the failure path waits the window out.
49
+ *
50
+ * The window is *also* what makes a retry safe — it is deliberately longer than the
51
+ * row takes to appear, because everything past it is allowed to type into the
52
+ * composer again — so note which end of the budget gets clipped when the two
53
+ * compete: a retry only happens with `MIN_ATTEMPT_MS + CONFIRM_WINDOW_MS` left, so
54
+ * a confirm *followed by another attempt* always gets its full window. Only the
55
+ * last confirm of all can be cut short, and nothing follows it to duplicate a row.
56
+ */
57
+ async function confirmDelivery(sessionId, text, sinceRowid, budgetDeadline) {
58
+ const stopAt = Math.min(Date.now() + CONFIRM_WINDOW_MS, budgetDeadline);
59
+ for (;;) {
60
+ if (deliveredSince(sessionId, text, sinceRowid))
47
61
  return true;
48
- await sleep(350);
62
+ if (Date.now() >= stopAt)
63
+ return false;
64
+ await sleep(300);
49
65
  }
50
- return false;
66
+ }
67
+ /** How long we watch the transcript after a run before deciding it didn't land. */
68
+ const CONFIRM_WINDOW_MS = 6_000;
69
+ /** Ceiling on a whole send, retries included — no phone should hold a request open longer. */
70
+ const SEND_BUDGET_MS = 55_000;
71
+ /** Below this there isn't room for a run that could plausibly succeed, so don't start one. */
72
+ const MIN_ATTEMPT_MS = 12_000;
73
+ /**
74
+ * The least a confirm is worth doing at all. Held back from every run so a send that
75
+ * lands can be *seen* to have landed — an unconfirmed send is indistinguishable from
76
+ * a lost one, which is the failure this whole path exists to avoid.
77
+ */
78
+ const MIN_CONFIRM_MS = 2_000;
79
+ /** Leaves the response itself time to get home before the caller's own timer fires. */
80
+ const RESPONSE_MARGIN_MS = 5_000;
81
+ /**
82
+ * Budget for a caller that didn't say how long it would wait — a PWA build from
83
+ * before `x-client-timeout-ms`, which aborted a send at a flat 25s. Sized so that
84
+ * such a phone is no worse off than it was: one run with a ceiling like the old one,
85
+ * and no retry (there was never room for a retry inside 25s).
86
+ */
87
+ const LEGACY_SEND_BUDGET_MS = 20_000;
88
+ /**
89
+ * Never outlast the caller. The relay giving up *after* the phone has is the worst
90
+ * available outcome: the phone shows a failure while the send goes on to land, and
91
+ * the user can't tell that from a send that really didn't. Pairing our budget to
92
+ * the PWA's by hand wouldn't hold — the relay updates itself (autoupdate.ts) while
93
+ * the app sits in a service-worker cache — so the caller states its own deadline
94
+ * and we retry inside it.
95
+ */
96
+ function sendBudget(req) {
97
+ const asked = Number(req.headers['x-client-timeout-ms']);
98
+ if (!Number.isFinite(asked) || asked <= 0)
99
+ return LEGACY_SEND_BUDGET_MS;
100
+ // Floor at one confirmable attempt: a caller in a hurry still gets a real try.
101
+ return Math.max(MIN_ATTEMPT_MS + CONFIRM_WINDOW_MS, Math.min(SEND_BUDGET_MS, asked - RESPONSE_MARGIN_MS));
51
102
  }
52
103
  /**
53
104
  * Where a chat sits in Conductor's tab strip. Both write paths need it: the
@@ -79,32 +130,67 @@ async function confirmAgentOptions(ws, sessionId, opts) {
79
130
  return false;
80
131
  }
81
132
  /**
82
- * Deliver a prompt to one chat and confirm it landed. The single write path: the
83
- * phone's own sends go through it, and so does the first-prompt queue, so both
84
- * get the same targeting, the same read-back and the same errors.
133
+ * Deliver a prompt to one chat and confirm it landed, retrying until the caller's
134
+ * budget runs out. The single write path: the phone's own sends go through it, and so
135
+ * does the first-prompt queue, so both get the same targeting, the same read-back,
136
+ * the same retries and the same errors.
137
+ *
138
+ * Retrying here rather than handing the phone a Retry button is the point: the
139
+ * failures this path hits are overwhelmingly warm-up costs — a cold or busy Conductor
140
+ * makes the first AppleScript run slow enough to be killed, and the second finds an
141
+ * activated app and lands — which is exactly why tapping Retry always worked. Two
142
+ * things make doing it automatically safe rather than a way to send a prompt twice:
143
+ * - **The transcript is the receipt.** Every run is followed by a full
144
+ * `CONFIRM_WINDOW_MS` of watching for the matching user row, *including* runs that
145
+ * reported an error, and the last of those checks is the moment before we type
146
+ * again. A run that actually landed — even one killed just after pressing Enter,
147
+ * or one whose row appeared after we'd stopped looking — is reported as delivered.
148
+ * - **The composer is written, not appended to** (`fillComposer` sets AXValue), so a
149
+ * retry replaces a half-finished attempt's text instead of doubling it.
150
+ *
151
+ * Bounded by a wall clock rather than an attempt count, because someone is holding
152
+ * this request open: runs are bounded by the caller's deadline, and we stop rather
153
+ * than start one the budget could not also confirm. The queue's own 3-sends-over-15-
154
+ * minutes schedule sits *outside* this and is unaffected — it retries a delivery that
155
+ * never got off the ground (worktree still setting up), not one Conductor fumbled.
85
156
  */
86
- async function deliverPrompt(ws, sessionId, text) {
157
+ async function deliverPrompt(ws, sessionId, text, budgetMs = SEND_BUDGET_MS) {
87
158
  const located = locateChat(ws, sessionId);
88
159
  if ('error' in located)
89
- return { ok: false, strategy: actuator.name, error: located.error };
90
- // Snapshot the transcript cursor so we can confirm the prompt actually lands.
160
+ return { ok: false, strategy: actuator.name, attempts: 0, error: located.error };
161
+ // Snapshot the cursor once: every check below asks "did *this* prompt arrive since
162
+ // we started", so a retry can't be fooled by an older identical prompt.
91
163
  const beforeRowid = reads.getMessages(sessionId).cursor;
92
- const result = await actuator.send({ workspace: ws, sessionId, tab: located.tab }, text);
93
- if (!result.ok) {
94
- console.warn(`[relay] send to ${ws.branch ?? ws.id} failed: ${result.error}`);
95
- return result;
96
- }
97
- if (!(await confirmDelivery(sessionId, text, beforeRowid))) {
98
- // The phone only ever sees "try again"; the reason a send goes missing lives
99
- // on this side, so leave a trail in relay.log rather than nothing at all.
100
- console.warn(`[relay] send to ${ws.branch ?? ws.id} drove the UI but never landed in the chat`);
101
- return {
102
- ok: false,
103
- strategy: result.strategy,
104
- error: 'Send didn’t land in the chat — Conductor may have been asleep or unfocused. Try again.'
105
- };
164
+ const label = ws.branch ?? ws.id;
165
+ const deadline = Date.now() + budgetMs;
166
+ let attempts = 0;
167
+ let last = { ok: false, strategy: actuator.name };
168
+ for (;;) {
169
+ attempts++;
170
+ // The run gets the deadline, not a duration: `uiTurn` may hold it behind another
171
+ // write, and only the run knows what was left of the budget when it started. Minus
172
+ // the confirm, so a caller on a tight budget spends it on the run rather than on
173
+ // watching — a 25s-era phone gets one full-length attempt, not two too short to finish.
174
+ last = await actuator.send({ workspace: ws, sessionId, tab: located.tab }, text, deadline - MIN_CONFIRM_MS);
175
+ if (await confirmDelivery(sessionId, text, beforeRowid, deadline)) {
176
+ if (attempts > 1)
177
+ console.info(`[relay] send to ${label} landed on attempt ${attempts}`);
178
+ return { ok: true, strategy: last.strategy, attempts };
179
+ }
180
+ if (retryWontHelp(last.error))
181
+ break;
182
+ if (deadline - Date.now() < MIN_ATTEMPT_MS + CONFIRM_WINDOW_MS)
183
+ break;
184
+ // The phone only ever sees the outcome; why a send goes missing lives on this
185
+ // side, so leave the trail in relay.log rather than nothing at all.
186
+ console.warn(`[relay] send to ${label} attempt ${attempts} didn’t land (${last.error ?? 'no user row appeared'}) — retrying`);
106
187
  }
107
- return result;
188
+ const tried = attempts > 1 ? ` (tried ${attempts}×)` : '';
189
+ const error = last.ok
190
+ ? `Send didn’t land in the chat — Conductor may have been asleep or unfocused${tried}. Try again.`
191
+ : `${last.error}${tried}`;
192
+ console.warn(`[relay] send to ${label} failed after ${attempts} attempt(s): ${error}`);
193
+ return { ok: false, strategy: last.strategy, attempts, error };
108
194
  }
109
195
  /**
110
196
  * Undelivered first prompts, owned by this process rather than by the phone (see
@@ -359,8 +445,9 @@ const server = http.createServer(async (req, res) => {
359
445
  }
360
446
  // Return as soon as the row exists (~2s) — waiting for delivery would block the
361
447
  // request through Conductor's whole setup, measured at 30s+ on a real repo and
362
- // past the phone's own 25s budget. The queue delivers on its own schedule and
363
- // the phone watches it in /api/state; `send:true` opts API callers into waiting.
448
+ // past any budget a phone should hold a request open for. The queue delivers on
449
+ // its own schedule and the phone watches it in /api/state; `send:true` opts API
450
+ // callers into waiting.
364
451
  // Whatever happens, the prompt is already pre-filled in Conductor's composer.
365
452
  const settled = prompt ? firstPrompts.enqueue(created.id, prompt) : null;
366
453
  const failed = settled && body.send === true ? await settled : null;
@@ -500,7 +587,9 @@ const server = http.createServer(async (req, res) => {
500
587
  : (reads.listWorkspaces().find(w => w.active_session_id === sessionId) ?? null);
501
588
  if (!ws)
502
589
  return json(req, res, 404, { error: 'workspace for session not found' });
503
- const result = await deliverPrompt(ws, sessionId, text);
590
+ // Retries live inside deliverPrompt, confirmed against the transcript each time,
591
+ // and inside the deadline this phone told us it would wait.
592
+ const result = await deliverPrompt(ws, sessionId, text, sendBudget(req));
504
593
  // Whatever the queue was still holding for this workspace has now been said by
505
594
  // hand — including a failed entry the user retried from the chat.
506
595
  if (result.ok)
@@ -25,6 +25,55 @@ function uiTurn(op) {
25
25
  uiTail = turn.catch(() => undefined);
26
26
  return turn;
27
27
  }
28
+ /**
29
+ * How long one AppleScript run may take before it's killed.
30
+ *
31
+ * Sized from measurement, not taste. A send that *worked* measured 23.6s end to
32
+ * end on a 30-workspace sidebar — past the 20s ceiling this replaces, which is why
33
+ * ordinary sends were being killed mid-run and reported as "Conductor took too long
34
+ * to respond". The cost is Accessibility round trips, not waiting: activating a
35
+ * backgrounded Conductor and reading the pane cost ~10s cold, and finding the
36
+ * sidebar row to press another ~10s (see `atTargetWorkspace`, which is why a warm
37
+ * send is now ~2s). A ceiling costs nothing when a send is fast — only a doomed one
38
+ * waits it out, and the caller's own deadline is what bounds that.
39
+ */
40
+ export const SEND_ATTEMPT_MS = 28_000;
41
+ /**
42
+ * A run's own ceiling, taken off the caller's deadline at the moment it actually
43
+ * starts.
44
+ *
45
+ * Both halves matter. `uiTurn` above means a run can sit in the queue behind
46
+ * another write, so a duration computed when it was *requested* would let a queued
47
+ * run overshoot a deadline the caller is still holding a phone open on.
48
+ * `SEND_ATTEMPT_MS` then caps it, because a caller with a minute of budget still
49
+ * shouldn't spend all of it on one doomed run when a retry is the thing that works.
50
+ * The floor keeps a squeezed run honest instead of passing `timeout: 0`, which node
51
+ * reads as "no timeout at all".
52
+ */
53
+ function runCeiling(deadline) {
54
+ return Math.max(5_000, Math.min(SEND_ATTEMPT_MS, deadline - Date.now()));
55
+ }
56
+ /**
57
+ * Failures no amount of retrying will fix, so a caller that retries can stop at
58
+ * once instead of spending a whole budget to arrive at the same sentence.
59
+ *
60
+ * Matched on phrases this file writes itself — the first two from `refusalReason`,
61
+ * the rest from the target checks below — never on macOS's own wording, which we
62
+ * quote verbatim precisely because it drifts. The refusals are the ones a node
63
+ * upgrade causes by silently revoking Accessibility: they fail instantly and
64
+ * identically every time, so making the phone sit through a whole retry budget to
65
+ * be told a permission is missing is worse than being told at once. The rest are
66
+ * malformed targets — no session, no branch — which no attempt can turn into one.
67
+ */
68
+ const TERMINAL_ERRORS = [
69
+ 'not trusted for Accessibility',
70
+ 'blocked the relay from controlling the UI',
71
+ 'no session id to target',
72
+ 'workspace has no branch to focus'
73
+ ];
74
+ export function retryWontHelp(error) {
75
+ return error !== undefined && TERMINAL_ERRORS.some(phrase => error.includes(phrase));
76
+ }
28
77
  /**
29
78
  * The sidecar IPC path — the precise, per-session write. Delivers straight to
30
79
  * `sessionId` over Conductor's own dispatch socket (see sidecar.ts), so it needs
@@ -42,7 +91,8 @@ export class SidecarActuator {
42
91
  available() {
43
92
  return sidecarAvailable();
44
93
  }
45
- async send(target, text) {
94
+ /** `deadline` is ignored — the sidecar is one socket write, with no UI to wait on. */
95
+ async send(target, text, _deadline) {
46
96
  const sessionId = target.sessionId ?? target.workspace.active_session_id;
47
97
  if (!sessionId)
48
98
  return { ok: false, strategy: this.name, error: 'no session id to target' };
@@ -382,16 +432,35 @@ on focusViaPalette()
382
432
  end tell
383
433
  end focusViaPalette
384
434
 
435
+ on paneAgrees()
436
+ -- Nothing in the open pane contradicts the target: a chat pane is there and its
437
+ -- header assertion passed (which is a no-op when there's no branch to check it
438
+ -- against). This is what confirms a sidebar press landed.
439
+ try
440
+ set strips to my tabGroups()
441
+ if (count of strips) is 0 then return false
442
+ my assertWorkspace(item 1 of strips)
443
+ return true
444
+ end try
445
+ return false
446
+ end paneAgrees
447
+
448
+ on atTargetWorkspace()
449
+ -- "Are we already there?" — the *strict* form of the question, so it needs a
450
+ -- branch to have been checked and not merely skipped. Worth asking first because
451
+ -- the pane header answers in ~0.5s where reading the sidebar's rows to press one
452
+ -- measured 5-7s, the single largest cost in a send: a phone sends to the same
453
+ -- workspace over and over, and Conductor stays where it was left. Safe as a
454
+ -- shortcut because it *is* the assertion the send makes before typing anyway.
455
+ if (system attribute "RELAY_WS_BRANCH") is "" then return false
456
+ return my paneAgrees()
457
+ end atTargetWorkspace
458
+
385
459
  on focusWorkspace()
386
460
  if (system attribute "RELAY_WS_QUERY") is "" then return
461
+ if my atTargetWorkspace() then return
387
462
  if my focusViaSidebar() then
388
- try
389
- set strips to my tabGroups()
390
- if (count of strips) > 0 then
391
- my assertWorkspace(item 1 of strips)
392
- return
393
- end if
394
- end try
463
+ if my paneAgrees() then return
395
464
  end if
396
465
  my focusViaPalette()
397
466
  end focusWorkspace
@@ -884,7 +953,7 @@ export class AppleScriptActuator {
884
953
  name = 'applescript';
885
954
  caveat = 'Focuses the target workspace (Cmd+K) and its chat tab before sending.';
886
955
  precise = true;
887
- async send(target, text) {
956
+ async send(target, text, deadline = Date.now() + SEND_ATTEMPT_MS) {
888
957
  // Focus the target workspace, select its chat tab, fill the composer, send.
889
958
  // Filling is an Accessibility write (no keystrokes, no clipboard); the
890
959
  // clipboard paste is kept only as a fallback, and stashes/restores around it.
@@ -914,7 +983,7 @@ end tell
914
983
  try {
915
984
  await uiTurn(() => exec('osascript', ['-e', script], {
916
985
  env: { ...process.env, RELAY_PROMPT_FILE: tmp, ...targetEnv(target) },
917
- timeout: 20000
986
+ timeout: runCeiling(deadline)
918
987
  }));
919
988
  return { ok: true, strategy: this.name };
920
989
  }
@@ -968,7 +1037,7 @@ return "ok"`.trim();
968
1037
  RELAY_SET_FAST: opts.toggleFast ? '1' : '',
969
1038
  RELAY_SET_MODEL: opts.model ?? ''
970
1039
  },
971
- timeout: 25000
1040
+ timeout: SEND_ATTEMPT_MS
972
1041
  }));
973
1042
  return { ok: true, strategy: 'applescript' };
974
1043
  }
@@ -988,7 +1057,7 @@ return my listModels()`.trim();
988
1057
  try {
989
1058
  const { stdout } = await uiTurn(() => exec('osascript', ['-e', script], {
990
1059
  env: { ...process.env, ...targetEnv(target) },
991
- timeout: 25000
1060
+ timeout: SEND_ATTEMPT_MS
992
1061
  }));
993
1062
  const models = stdout
994
1063
  .split('\n')
@@ -1060,9 +1129,11 @@ tell application "System Events"
1060
1129
  keystroke "t" using {command down}
1061
1130
  end tell`.trim();
1062
1131
  try {
1132
+ // Shares the focus path with a send, so it needs the same ceiling: 15s was under
1133
+ // the cost of activating a cold Conductor and finding the row on its own.
1063
1134
  await uiTurn(() => exec('osascript', ['-e', script], {
1064
1135
  env: { ...process.env, ...targetEnv({ workspace, sessionId: null }) },
1065
- timeout: 15000
1136
+ timeout: SEND_ATTEMPT_MS
1066
1137
  }));
1067
1138
  return { ok: true, strategy: 'applescript' };
1068
1139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-remote",
3
- "version": "1.32.0",
3
+ "version": "1.32.1",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.15.0",
6
6
  "description": "Phone control panel for local Conductor agents. Reads ride SQLite + git; prompts ride Conductor's own dispatch path.",