conductor-remote 1.79.0 → 1.80.0

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-CV2YpJf7.js"></script>
16
+ <script type="module" crossorigin src="/assets/index-ILQ9q-V7.js"></script>
17
17
  <link rel="stylesheet" crossorigin href="/assets/index-DaafT1eA.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 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:"e7ef44deca46c0539e6ff7bba5eb815e"},{url:"index.html",revision:"aa274c71195357e8140bfef5bf26f868"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-DaafT1eA.css",revision:null},{url:"assets/index-CV2YpJf7.js",revision:null},{url:"apple-touch-icon.png",revision:"1127bb396b4648add53dce3f22c92aee"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a9b0d962686287452216492cd2247499"},{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:"e7ef44deca46c0539e6ff7bba5eb815e"},{url:"index.html",revision:"4fa6b9e7cf25227330786aa264ae4ce6"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-ILQ9q-V7.js",revision:null},{url:"assets/index-DaafT1eA.css",revision:null},{url:"apple-touch-icon.png",revision:"1127bb396b4648add53dce3f22c92aee"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a9b0d962686287452216492cd2247499"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
@@ -293,6 +293,37 @@ on restartConductor()
293
293
  return ""
294
294
  end restartConductor
295
295
 
296
+ on restartApp()
297
+ -- The restart the phone asks for (POST /api/conductor/restart), as opposed to
298
+ -- the one activateConductor falls back to on its own. Same lever, different
299
+ -- entry, and the second entry is the whole point: that fallback fires only when
300
+ -- a *running* Conductor draws no window, so it is unreachable for the failure
301
+ -- this exists for — a Conductor whose window, sidebar and composer are all fine
302
+ -- while its agent runtime has stopped producing output. Measured live
303
+ -- (2026-09-02): prompts kept landing as `session_messages` rows for hours after
304
+ -- the last agent frame, so every window probe here would have said "healthy".
305
+ --
306
+ -- RELAY_ALLOW_RESTART is deliberately NOT consulted. That flag exists so the
307
+ -- fallback never destroys work the caller didn't ask about; here the caller is
308
+ -- a person who pressed a button, and server.ts has already counted the working
309
+ -- chats from the DB and refused without `stopAgents`.
310
+ set refusal to my refusalReason(my windowProbe())
311
+ if refusal is not "" then error refusal
312
+ -- Behind the lock screen a relaunch comes up windowless, and once came up
313
+ -- wedged (answering deep links, hanging every AX read) — the same reason the
314
+ -- fallback refuses there. The sentence starts with the phrase both sides match
315
+ -- on (MAC_LOCKED in src/shared.ts) so the phone draws its unlock link.
316
+ if my screenLocked() is "locked" then error "The Mac is locked - relaunching Conductor behind the lock screen is what leaves it wedged, so the relay won't. Unlock the Mac and try again." & my windowEvidence()
317
+ set restartError to my restartConductor()
318
+ if restartError is not "" then error restartError & my windowEvidence()
319
+ -- The fallback returns the moment the relaunch starts, because it is spending a
320
+ -- *send's* budget and a cold launch doesn't fit beside one. This run has nothing
321
+ -- else to do with its budget, so it waits for the window and the phone hears
322
+ -- what actually happened instead of "try again": ~5-10s for a cold launch here.
323
+ if not my waitForWindow(60) then error "Conductor was relaunched but hasn't drawn a window yet. Give it a few seconds, then try again." & my windowEvidence()
324
+ return "ok"
325
+ end restartApp
326
+
296
327
  on waitForWindow(attempts)
297
328
  -- "reopen" is the dock-click Apple event, and for most apps that is what
298
329
  -- recreates a closed window ("activate" on its own does not). Nudge a few times
@@ -62,6 +62,12 @@ export const routes = {
62
62
  /** Drop a staged file the user removed before creating its workspace. */
63
63
  discardStagedAttachment: param('DELETE', '/api/attachments/:attachmentId'),
64
64
  logs: flat('GET', '/api/logs'),
65
+ /**
66
+ * Quit Conductor and start it again. Not a workspace route: it is about the app,
67
+ * and its whole reason to exist is a Conductor that looks healthy from every other
68
+ * route while nothing behind it runs (src/writes.ts ▸ restartConductorApp).
69
+ */
70
+ restartConductor: flat('POST', '/api/conductor/restart'),
65
71
  settings: flat('GET', '/api/settings'),
66
72
  updateSettings: flat('PATCH', '/api/settings'),
67
73
  /** Durable, device-independent PWA state. localStorage remains its offline-first mirror. */
@@ -32,7 +32,7 @@ import { discardStagedAttachment, materializeStagedAttachments, stageAttachment,
32
32
  import { driftWarningLines, readExposeMode, tailscaleBin } from "./tailscale.js";
33
33
  import { renderTranscript, transcriptThrough } from "./transcript.js";
34
34
  import { autoJoinHotspotMode, currentSsid, looksLikeHotspot, preferredNetworks } from "./wifi.js";
35
- import { archiveWorkspace, createWorkspace, describeActuator, EFFORT_LABELS, listAgentModels, lockBlocked, newChat, pickActuator, retryWontHelp, screenLocked, sendNeverStarted, setAgentOptions, setDefaultModel, setRestartGuard, setWorkspaceStatus, stopTurn, UiBusyError, uiQueueDepth, WORKSPACE_STATUS_LABELS, withUiPriority } from "./writes.js";
35
+ import { archiveWorkspace, createWorkspace, describeActuator, EFFORT_LABELS, listAgentModels, lockBlocked, newChat, pickActuator, restartConductorApp, retryWontHelp, screenLocked, sendNeverStarted, setAgentOptions, setDefaultModel, setRestartGuard, setWorkspaceStatus, stopTurn, UiBusyError, uiQueueDepth, WORKSPACE_STATUS_LABELS, withUiPriority } from "./writes.js";
36
36
  // Before anything that logs: from here on every console line is also kept in memory for
37
37
  // `GET /api/logs`, so the phone can read why a send failed without ssh-ing into the Mac.
38
38
  installLogCapture();
@@ -957,6 +957,36 @@ const server = http.createServer(async (req, res) => {
957
957
  const result = await disarmNoSleep();
958
958
  return json(req, res, result.ok ? 200 : result.state.available ? 502 : 409, result);
959
959
  }
960
+ // POST /api/conductor/restart { stopAgents? } — quit Conductor and start it again.
961
+ //
962
+ // The lever exists in the actuator already, but only as activateConductor's last
963
+ // resort, which fires exclusively for a *windowless* Conductor. This is for the
964
+ // other shape: window up, prompts landing as rows, and no agent output behind any
965
+ // of it (measured 2026-09-02 — 2h35m of user rows after the last agent frame).
966
+ // The running agents are counted from the DB before the UI is touched and refused
967
+ // unless the caller meant it, the same way archiving is: quitting ends every turn
968
+ // in flight. The lock screen is the actuator's own gate, since only it can ask.
969
+ if (isRoute(routes.restartConductor, req.method, pathname)) {
970
+ const body = JSON.parse((await readBody(req)) || '{}');
971
+ const working = reads.listSessionStates().filter(state => state.status === 'working').length;
972
+ if (working > 0 && body.stopAgents !== true) {
973
+ return json(req, res, 409, {
974
+ ok: false,
975
+ agentsRunning: true,
976
+ working,
977
+ error: `${working} chat${working === 1 ? ' is' : 's are'} mid-turn. Restarting Conductor ends ${working === 1 ? 'it' : 'them'}.`
978
+ });
979
+ }
980
+ const startedAt = Date.now();
981
+ const result = await restartConductorApp();
982
+ const ms = Date.now() - startedAt;
983
+ if (!result.ok) {
984
+ console.warn(`[restart] Conductor restart failed after ${(ms / 1000).toFixed(1)}s: ${result.error}`);
985
+ return json(req, res, 502, { ok: false, ms, error: result.error });
986
+ }
987
+ console.log(`[restart] quit Conductor and relaunched it in ${(ms / 1000).toFixed(1)}s`);
988
+ return json(req, res, 200, { ok: true, ms });
989
+ }
960
990
  // GET /api/logs?file=&limit= — the relay's own log, so a phone can diagnose a failed send
961
991
  // without reaching the Mac. Default is this process's captured console (ordered, timestamped);
962
992
  // `file` tails the daemon's stdout/stderr on disk, which is the only place the *previous*
@@ -103,6 +103,13 @@ export function uiTurn(op) {
103
103
  * a doomed one waits it out, and the caller's own deadline is what bounds that.
104
104
  */
105
105
  export const SEND_ATTEMPT_MS = 28_000;
106
+ /**
107
+ * A restart's own ceiling. It is longer than a send's because it is mostly *waiting*:
108
+ * up to 4s for the quit to be honoured, then a cold launch, then `waitForWindow(60)`'s
109
+ * 15s for the first window — none of which can be hurried, and all of which the caller
110
+ * would rather wait through than be told "try again" about.
111
+ */
112
+ export const RESTART_ATTEMPT_MS = 45_000;
106
113
  /**
107
114
  * A run's own ceiling, taken off the caller's deadline at the moment it actually
108
115
  * starts.
@@ -649,6 +656,42 @@ return "ok"`.trim();
649
656
  return { ok: false, strategy: 'applescript', error: osaError(err) };
650
657
  }
651
658
  }
659
+ /**
660
+ * Quit Conductor and start it again because the phone asked — the one write here
661
+ * whose subject is the app rather than anything inside it.
662
+ *
663
+ * The lever already existed and was unreachable. `activateConductor` restarts as its
664
+ * last resort, but only after a *running* Conductor has drawn no window through
665
+ * `reopen` and a Dock click, so it answers exactly one failure: a windowless app. The
666
+ * failure this is for looks healthy from every probe on that ladder — window up,
667
+ * sidebar drawing, composer taking prompts — while the agent runtime behind it has
668
+ * stopped producing anything. Measured on this Mac (2026-09-02): the last agent frame
669
+ * in `session_messages` was 20:47:44 and prompts kept landing as user rows for the
670
+ * next two and a half hours, each turn flipping `working → idle` having written
671
+ * nothing. Nothing on the read side can fix that, and "quit it on your Mac" is not
672
+ * advice a phone can act on.
673
+ *
674
+ * Two gates, and neither lives here. The **working chats** are counted from the DB by
675
+ * server.ts, which refuses without `stopAgents` — quitting takes every agent mid-turn
676
+ * down with it, so that has to be said out loud, exactly as it is for archiving. The
677
+ * **lock screen** is asked by `restartApp` itself, because a relaunch fired behind it
678
+ * comes up windowless (and once, wedged). What is left for this function is the UI
679
+ * lock: a restart is not a read, and letting one land while a send is mid-flight would
680
+ * quit the app between the composer write and the Enter.
681
+ */
682
+ export async function restartConductorApp() {
683
+ const script = `
684
+ ${CONDUCTOR_HANDLERS}
685
+
686
+ return my restartApp()`.trim();
687
+ try {
688
+ await uiTurn(() => exec('osascript', ['-e', script], { env: { ...process.env }, timeout: RESTART_ATTEMPT_MS }));
689
+ return { ok: true, strategy: 'applescript' };
690
+ }
691
+ catch (err) {
692
+ return { ok: false, strategy: 'applescript', error: osaError(err, 'Conductor didn’t come back in time') };
693
+ }
694
+ }
652
695
  /**
653
696
  * The workspace statuses Conductor's sidebar groups by, mapped from the value it
654
697
  * stores in `workspaces.manual_status` to the label on its own menu. `canceled`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-remote",
3
- "version": "1.79.0",
3
+ "version": "1.80.0",
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.",