conductor-remote 1.131.4 → 1.131.6

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
@@ -25,7 +25,7 @@
25
25
  <title>Conductor Remote</title>
26
26
  <!-- Runs before the module bundle so it can catch a stale shell that fails to boot. -->
27
27
  <script src="/self-heal.js"></script>
28
- <script type="module" crossorigin src="/assets/index-BAEVNqQy.js"></script>
28
+ <script type="module" crossorigin src="/assets/index-BY3vTp3J.js"></script>
29
29
  <link rel="stylesheet" crossorigin href="/assets/index-Dbu5DOed.css">
30
30
  <link rel="manifest" href="/manifest.webmanifest"></head>
31
31
  <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 l=e||("document"in self?document.currentScript.src:"")||location.href;if(s[l])return;let o={};const t=e=>i(e,l),a={module:{uri:l},exports:o,require:t};s[l]=Promise.all(n.map(e=>a[e]||t(e))).then(e=>(r(...e),o))}}define(["./workbox-dcde9eb3"],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:"4e0b714c0ea53e85c167e6d8f5b767aa"},{url:"index.html",revision:"e644d471ce76c93e68869452f62ead76"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/pierre-shiki-wasm-C8ETeZem.js",revision:null},{url:"assets/pierre-light-480U9XYS.js",revision:null},{url:"assets/pierre-dark-CyvmCCZW.js",revision:null},{url:"assets/index-Dbu5DOed.css",revision:null},{url:"assets/index-BAEVNqQy.js",revision:null},{url:"assets/PierrePatch-BmY2b7Ei.js",revision:null},{url:"assets/AutoModelSettings-SBdRr_mv.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\//]})),e.registerRoute(({url:e})=>e.pathname.startsWith("/assets/diff-syntax/"),new e.CacheFirst({cacheName:"diff-syntax",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:31536e3})]}),"GET")});
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 l=e||("document"in self?document.currentScript.src:"")||location.href;if(s[l])return;let o={};const t=e=>i(e,l),a={module:{uri:l},exports:o,require:t};s[l]=Promise.all(n.map(e=>a[e]||t(e))).then(e=>(r(...e),o))}}define(["./workbox-dcde9eb3"],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:"4e0b714c0ea53e85c167e6d8f5b767aa"},{url:"index.html",revision:"ae7f5f4a576844959923e8df23dafac7"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/pierre-shiki-wasm-C8ETeZem.js",revision:null},{url:"assets/pierre-light-480U9XYS.js",revision:null},{url:"assets/pierre-dark-CyvmCCZW.js",revision:null},{url:"assets/index-Dbu5DOed.css",revision:null},{url:"assets/index-BY3vTp3J.js",revision:null},{url:"assets/PierrePatch-Dfh5TW8u.js",revision:null},{url:"assets/AutoModelSettings-BV17IdxP.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\//]})),e.registerRoute(({url:e})=>e.pathname.startsWith("/assets/diff-syntax/"),new e.CacheFirst({cacheName:"diff-syntax",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:31536e3})]}),"GET")});
@@ -2,11 +2,11 @@ import { config } from "./configuration.js";
2
2
  import { FLAG_ENV } from "./flags.js";
3
3
  import { install, restart, uninstall } from "./installation.js";
4
4
  import { logs, status } from "./presentation.js";
5
- export function runServiceCommand() {
5
+ export async function runServiceCommand() {
6
6
  const cmd = process.argv[2] ?? 'status';
7
7
  switch (cmd) {
8
8
  case 'install':
9
- install();
9
+ await install();
10
10
  break;
11
11
  case 'uninstall':
12
12
  uninstall();
@@ -1,3 +1,4 @@
1
+ import { parsePortValue } from "../../src/host/ports.js";
1
2
  /**
2
3
  * Install-time knobs are accepted as documented CLI flags OR the matching env var — a flag wins over the
3
4
  * ambient env. Parsed flags are folded back into process.env so everything downstream (and the plist we
@@ -40,6 +41,13 @@ export function applyFlags(argv) {
40
41
  console.error(`flag ${name} must be on or off`);
41
42
  process.exit(1);
42
43
  }
44
+ // Named here as well as in the install preflight because a typo is worth catching against the
45
+ // argument that carries it. The preflight still runs: an ambient RELAY_PORT reaches install
46
+ // without passing through any flag, and only the preflight sees both ports at once.
47
+ if ((name === '--port' || name === '--voice-port') && parsePortValue(value) === null) {
48
+ console.error(`flag ${name} must be a whole number from 1 to 65535, not "${value}"`);
49
+ process.exit(1);
50
+ }
43
51
  process.env[envKey] = value;
44
52
  }
45
53
  }
@@ -1,9 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { bindFailureLines, listenersOn, parsePortValue, probeListen } from "../../src/host/ports.js";
3
4
  import { exposeStorePath } from "../../src/host/tailscale.js";
4
5
  import { readVoiceConfig } from "../../src/voice/config.js";
5
- import { distBuilt, domain, LABEL, logDir, persistPinnedToken, plistPath, projectDir, readPlistEnv } from "./environment.js";
6
- import { buildPlist, launchctl, reloadAgent } from "./launch-agent.js";
6
+ import { distBuilt, domain, LABEL, logDir, persistPinnedToken, plistPath, projectDir, RELAY_PORT, readPlistEnv, VOICE_PORT } from "./environment.js";
7
+ import { agentPid, buildPlist, launchctl, reloadAgent } from "./launch-agent.js";
7
8
  import { ensureTailscale, resolveExposeMode } from "./network.js";
8
9
  import { printUrl, printVoiceRoute } from "./presentation.js";
9
10
  import { ensureVoiceFunnel } from "./voice.js";
@@ -11,7 +12,55 @@ import { ensureVoiceFunnel } from "./voice.js";
11
12
  function isEphemeralInstall(dir) {
12
13
  return /[\\/]_npx[\\/]|[\\/]\.npm[\\/]_npx[\\/]/.test(dir);
13
14
  }
14
- export function install() {
15
+ /**
16
+ * Refuse a port the relay cannot have, before the plist is written and launchd is reloaded.
17
+ *
18
+ * The alternative is the daemon's own crash: `server.listen` reaches launchd as an unhandled error
19
+ * event, KeepAlive respawns it, and the loop's only trace is a stack trace in relay.err.log — while
20
+ * install itself prints a success line, a phone URL and a QR code for a relay that is not running.
21
+ * A port our own daemon holds is not a conflict; a re-install boots it out moments later.
22
+ */
23
+ async function assertPortFree(what, host, port, setting) {
24
+ const code = await probeListen(host, port);
25
+ if (code === null)
26
+ return;
27
+ if (code === 'EADDRINUSE') {
28
+ const ours = agentPid();
29
+ const holders = listenersOn(port);
30
+ if (ours !== null && holders.length > 0 && holders.every(holder => holder.pid === ours))
31
+ return;
32
+ }
33
+ for (const line of bindFailureLines(what, host, port, code, setting))
34
+ console.error(line);
35
+ process.exit(1);
36
+ }
37
+ /** Both ports, resolved from flags or the ambient environment, checked together and against the machine. */
38
+ async function assertPortsUsable() {
39
+ const relay = parsePortValue(RELAY_PORT);
40
+ if (relay === null) {
41
+ console.error(`✗ the relay port must be a whole number from 1 to 65535, not "${RELAY_PORT}".`);
42
+ console.error(' Set it with `--port <n>` (or `conductor-remote config set port <n>`).');
43
+ process.exit(1);
44
+ }
45
+ const voice = parsePortValue(VOICE_PORT);
46
+ if (voice === null) {
47
+ console.error(`✗ the voice port must be a whole number from 1 to 65535, not "${VOICE_PORT}".`);
48
+ console.error(' Set it with `--voice-port <n>` (or `conductor-remote config set voice-port <n>`).');
49
+ process.exit(1);
50
+ }
51
+ // The two servers live in one process, so equal ports are a collision the relay finds at boot rather
52
+ // than a configuration the machine can report as busy. 8788 is the voice default and a habitual
53
+ // second relay port, which is how this pair meets by accident.
54
+ if (relay === voice) {
55
+ console.error(`✗ the relay and the voice listener would both bind :${relay}.`);
56
+ console.error(' They are two servers in one process; give them different ports.');
57
+ process.exit(1);
58
+ }
59
+ await assertPortFree('the relay', process.env.RELAY_HOST ?? '127.0.0.1', relay, 'port');
60
+ // The voice listener is loopback-only by construction (src/server.ts); Funnel is what publishes it.
61
+ await assertPortFree('the voice listener', '127.0.0.1', voice, 'voice-port');
62
+ }
63
+ export async function install() {
15
64
  if (isEphemeralInstall(projectDir)) {
16
65
  console.error(`✗ refusing to install from an npx cache path:\n ${projectDir}\n` +
17
66
  ' That directory is temporary and gets purged, which would break the LaunchAgent.\n' +
@@ -22,6 +71,7 @@ export function install() {
22
71
  console.error('✗ dist/ not built. Run `yarn build` first (or use `yarn deploy`, which builds).');
23
72
  process.exit(1);
24
73
  }
74
+ await assertPortsUsable();
25
75
  persistPinnedToken();
26
76
  // Resolve (and persist) the expose posture *before* the plist is written and launchd is reloaded, so
27
77
  // the daemon starts with an explicit EXPOSE in its own environment rather than racing the file
@@ -27,6 +27,20 @@ function serviceLoaded() {
27
27
  return false;
28
28
  }
29
29
  }
30
+ /**
31
+ * The pid of the running LaunchAgent, or null when it is not loaded or not running. Used by the install
32
+ * preflight to tell "the port is taken" from "the port is taken by the very relay we are about to replace".
33
+ */
34
+ export function agentPid() {
35
+ try {
36
+ const out = execFileSync('launchctl', ['print', `${domain}/${LABEL}`], { encoding: 'utf8', stdio: 'pipe' });
37
+ const pid = Number(out.match(/pid = (\d+)/)?.[1]);
38
+ return Number.isInteger(pid) ? pid : null;
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
30
44
  /**
31
45
  * Reload the agent from the freshly written plist. `bootout` of a *running* instance is asynchronous,
32
46
  * so we wait for it to fully unload before `bootstrap` — otherwise bootstrap races the teardown and
@@ -14,4 +14,4 @@ if ((process.argv[2] ?? 'status') !== 'logs')
14
14
  applyFlags(process.argv.slice(3));
15
15
  // Import only after parsing flags: environment.ts freezes the relay/voice ports.
16
16
  const { runServiceCommand } = await import("./service/commands.js");
17
- runServiceCommand();
17
+ await runServiceCommand();
@@ -0,0 +1,82 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import net from 'node:net';
3
+ /**
4
+ * Port knobs arrive as free text — a `--port` flag, a plist entry, an ambient shell variable — and every
5
+ * one of them is handed straight to `server.listen`, which rejects a non-number with a bare
6
+ * ERR_SOCKET_BAD_PORT inside the daemon. Reject it here instead, where a person is reading the output.
7
+ */
8
+ export function parsePortValue(raw) {
9
+ if (raw === undefined || raw === null)
10
+ return null;
11
+ const text = raw.trim();
12
+ if (!/^\d+$/.test(text))
13
+ return null;
14
+ const port = Number(text);
15
+ return port >= 1 && port <= 65535 ? port : null;
16
+ }
17
+ /**
18
+ * Parse `lsof -Fpcn` field output into one entry per listening process. The field form is used rather
19
+ * than the default table because lsof's COMMAND column carries spaces ("Google Chrome H"), so a column
20
+ * split names the wrong holder. Process fields are printed once per process, ahead of its file blocks,
21
+ * which is what keeps a process holding several matching descriptors from being reported several times.
22
+ */
23
+ export function parseLsofListeners(out) {
24
+ const holders = [];
25
+ let pid = null;
26
+ for (const line of out.split('\n')) {
27
+ if (line.startsWith('p')) {
28
+ const parsed = Number(line.slice(1));
29
+ pid = Number.isInteger(parsed) ? parsed : null;
30
+ }
31
+ else if (line.startsWith('c') && pid !== null) {
32
+ holders.push({ command: line.slice(1), pid });
33
+ pid = null;
34
+ }
35
+ }
36
+ return holders;
37
+ }
38
+ /** Who is listening on `port`, best-effort — this only ever decorates a message that has already failed. */
39
+ export function listenersOn(port) {
40
+ try {
41
+ const out = execFileSync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-Fpcn'], {
42
+ encoding: 'utf8',
43
+ stdio: 'pipe'
44
+ });
45
+ return parseLsofListeners(out);
46
+ }
47
+ catch {
48
+ // lsof exits non-zero when nothing matches, and it may not be installed at all.
49
+ return [];
50
+ }
51
+ }
52
+ /**
53
+ * Can this process bind `host:port`? Answered by binding it, because that is the only test that agrees
54
+ * with the real listen in every case that matters: a wildcard listener blocks a loopback bind, a
55
+ * privileged port is refused rather than busy, and a host the machine does not hold fails on its own.
56
+ * Resolves the error code, or null when the port is free. The socket never accepts, so it leaves no
57
+ * TIME_WAIT behind and the port is free again the moment this resolves.
58
+ */
59
+ export function probeListen(host, port) {
60
+ return new Promise(resolve => {
61
+ const probe = net.createServer();
62
+ probe.once('error', (err) => resolve(err.code ?? 'EUNKNOWN'));
63
+ probe.listen(port, host, () => probe.close(() => resolve(null)));
64
+ });
65
+ }
66
+ export function describeHolders(holders) {
67
+ if (!holders.length)
68
+ return 'an unidentified process';
69
+ return holders.map(holder => `${holder.command} (pid ${holder.pid})`).join(', ');
70
+ }
71
+ /**
72
+ * The two lines a failed bind prints, shared by the install preflight and the running relay so both name
73
+ * the same port and the same knob. `config set` is the hint in both because it is the one lever that
74
+ * works against an installed daemon, which is where this failure is otherwise invisible.
75
+ */
76
+ export function bindFailureLines(what, host, port, code, setting) {
77
+ const cause = code === 'EADDRINUSE' ? `held by ${describeHolders(listenersOn(port))}` : (code ?? 'unknown error');
78
+ return [
79
+ `✗ ${what} cannot bind ${host}:${port} — ${cause}.`,
80
+ ` Free that port, or move it: conductor-remote config set ${setting} <port>`
81
+ ];
82
+ }
@@ -1,6 +1,7 @@
1
1
  import { startAutoUpdate } from "./host/autoupdate.js";
2
2
  import { startFunnelWatchdog } from "./host/funnel-watchdog.js";
3
3
  import { watchNoSleepExpiry } from "./host/nosleep.js";
4
+ import { bindFailureLines } from "./host/ports.js";
4
5
  import { driftWarningLines, tailscaleBin } from "./host/tailscale.js";
5
6
  import { createRelayServer } from "./http/router.js";
6
7
  import { createRelayServices } from "./http/services.js";
@@ -9,6 +10,21 @@ import { voicePort } from "./voice/config.js";
9
10
  const services = createRelayServices();
10
11
  const { cfg, voiceServer, voiceBroker, voiceHistory, orchestration, relayIdentity, recoverUiLease, wakeWorkflows, actuator, firstPrompts, autoModels, sweepStagedAttachments, STAGED_ATTACHMENT_SWEEP_MS, parkedPrompts, delegationQueue, liveDelegationStores, devServers, reads, sessionPoller } = services;
11
12
  const server = createRelayServer(services);
13
+ // A `listen` failure arrives as an 'error' event, and with nobody listening for it the process throws.
14
+ // Under launchd that is a KeepAlive restart loop whose only trace is a stack trace in relay.err.log with
15
+ // the port nowhere in it, while `service status` says only "loaded but not running". Name the port.
16
+ server.on('error', (err) => {
17
+ for (const line of bindFailureLines('the relay', cfg.host, cfg.port, err.code ?? null, 'port'))
18
+ console.error(line);
19
+ process.exit(1);
20
+ });
21
+ // The voice listener is opt-in and the control panel is the product, so its own bind failure is loud
22
+ // rather than fatal: taking the phone's relay down over an unconfigured second server is the worse trade.
23
+ // `service status` reports the voice route separately, so a listener that never came up still shows there.
24
+ voiceServer.on('error', (err) => {
25
+ for (const line of bindFailureLines('the voice listener', '127.0.0.1', voicePort(), err.code ?? null, 'voice-port'))
26
+ console.error(line);
27
+ });
12
28
  server.listen(cfg.port, cfg.host, () => {
13
29
  voiceServer.listen(voicePort(), '127.0.0.1', () => {
14
30
  console.info(` voice: 127.0.0.1:${voicePort()}${voiceBroker ? '' : ' (waiting for OpenAI config)'}`);
@@ -7,14 +7,16 @@ import { exec, uiTurn } from "./ui-lock.js";
7
7
  * `sessionId` over Conductor's own dispatch socket (see src/writes/sidecar.ts), so it needs
8
8
  * no window focus and the app UI reflects the turn correctly.
9
9
  *
10
- * Opt-in (WRITE_STRATEGY=sidecar) because it speaks a private, versioned IPC and
11
- * hasn't been validated by an automated live send (that would inject a prompt
12
- * into a running agent). It is the intended default once you've confirmed it on
13
- * your setup.
10
+ * Opt-in (WRITE_STRATEGY=sidecar) because it speaks a private, versioned IPC.
11
+ * BROKEN against current Conductor builds the send payload is rejected
12
+ * upstream (schema drift, see src/writes/sidecar.ts and issue #97). Do not
13
+ * select it expecting sends to land; AppleScript is the working path. There is
14
+ * no fallback to AppleScript on send failure: a failed sidecar send reports
15
+ * failure. Fixes welcome.
14
16
  */
15
17
  export class SidecarActuator {
16
18
  name = 'sidecar';
17
- caveat = 'Delivered straight to the target session over Conductor’s dispatch socket precise per-workspace targeting.';
19
+ caveat = 'Broken against current Conductor builds (see issue #97) AppleScript is the working path.';
18
20
  precise = true;
19
21
  available() {
20
22
  return sidecarAvailable();
@@ -25,11 +27,20 @@ export class SidecarActuator {
25
27
  if (!sessionId)
26
28
  return { ok: false, strategy: this.name, error: 'no session id to target' };
27
29
  try {
28
- await sidecarSendUserMessage(sessionId, text, options.queue ? 'queue' : 'default');
30
+ // `options.queue` has no sidecar equivalent: the live schema accepts
31
+ // only 'default' | 'steering' and rejects the old 'queue' value, so
32
+ // always send 'default' rather than a known-invalid enum.
33
+ void options.queue;
34
+ await sidecarSendUserMessage(sessionId, text, 'default');
29
35
  return { ok: true, strategy: this.name };
30
36
  }
31
37
  catch (err) {
32
- return { ok: false, strategy: this.name, error: err instanceof Error ? err.message : String(err) };
38
+ const raw = err instanceof Error ? err.message : String(err);
39
+ return {
40
+ ok: false,
41
+ strategy: this.name,
42
+ error: `${raw} (sidecar sends are broken against current Conductor builds — see issue #97; AppleScript is the working path)`
43
+ };
33
44
  }
34
45
  }
35
46
  }
@@ -13,14 +13,20 @@ import path from 'node:path';
13
13
  * `sessionId`, so there is no window focus or AppleScript involved and the app's
14
14
  * UI updates correctly because it's the real dispatch path. It is also the most
15
15
  * update-fragile surface (a private, versioned IPC), which is why it lives
16
- * behind the Actuator interface and falls back to AppleScript when the socket
17
- * can't be reached (see src/writes/actuator.ts).
16
+ * behind the Actuator interface as an opt-in (`WRITE_STRATEGY=sidecar`).
17
+ *
18
+ * BROKEN against current Conductor builds: the send payload below is rejected
19
+ * by the live sidecar (`ok:false`, schema drift since the 0.76 recon) and the
20
+ * `type:"query"` shape validates then silently drops the prompt for sessions
21
+ * that aren't live in the sidecar's store. AppleScript is the working send
22
+ * path. Fixes welcome — see issue #97 for the probe handover.
18
23
  *
19
24
  * Reverse-engineered from conductor-runtime (Conductor 0.76):
20
25
  * - socket: `$TMPDIR/conductor-sidecar-v2-<sidecarPid>.sock`
21
26
  * - transport: newline-delimited JSON-RPC 2.0 (`{jsonrpc,id,method,params}`)
22
27
  * - local auth: the literal `{ userId: 'local', auth: 'local' }`
23
- * - send prompt: method `query`, params `{ type: 'sendUserMessageRequest', }`
28
+ * - send prompt (STALE rejected by current builds, see above):
29
+ * method `query`, params `{ type: 'sendUserMessageRequest', … }`
24
30
  * - safe read: method `contextUsage`, params `{ sessionId, …auth }`
25
31
  *
26
32
  * Stale socket files from exited sidecars linger in `$TMPDIR`, so discovery is
@@ -129,7 +135,9 @@ async function rpc(method, params, timeoutMs = 8000) {
129
135
  }
130
136
  throw lastErr instanceof Error ? lastErr : new Error('sidecar unreachable');
131
137
  }
132
- /** Resolve a connectable sidecar socket, or null. Used to decide write strategy. */
138
+ /** True when a sidecar socket accepts connections. Note: a reachable socket
139
+ * does NOT mean sends work — the send payload is currently rejected upstream
140
+ * (see header). Used to decide write strategy. */
133
141
  export function sidecarSocket(timeoutMs = 800) {
134
142
  const candidates = listSidecarSockets();
135
143
  return (async () => {
@@ -165,6 +173,9 @@ export async function sidecarAvailable() {
165
173
  /**
166
174
  * Deliver a prompt to a specific session — the real send path, precisely
167
175
  * targeted. Resolves once the sidecar has accepted (queued/sent) the message.
176
+ *
177
+ * Currently BROKEN upstream (see header): expect rejection against current
178
+ * Conductor builds. Failures must surface, never report success.
168
179
  */
169
180
  export async function sidecarSendUserMessage(sessionId, text, deliveryMode = 'default') {
170
181
  await rpc('query', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-remote",
3
- "version": "1.131.4",
3
+ "version": "1.131.6",
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.",