conductor-remote 1.86.1 → 1.86.2

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-D_QsGlJg.js"></script>
16
+ <script type="module" crossorigin src="/assets/index-BzjkWwgV.js"></script>
17
17
  <link rel="stylesheet" crossorigin href="/assets/index-YApUyC0Y.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:"0ec954ffb2c5de85c62b8911a4238feb"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-YApUyC0Y.css",revision:null},{url:"assets/index-D_QsGlJg.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:"7f3a48e0f885bb8ad356ddaa772fc22d"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-YApUyC0Y.css",revision:null},{url:"assets/index-BzjkWwgV.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\//]}))});
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { agentProcessStarts, openBackgroundTasks, TASK_FRAME_FILTER } from "./background-tasks.js";
5
5
  import { describeRepoIcon, resolveRepoIcon } from "./icons.js";
6
6
  import { workspaceTitle } from "./shared.js";
7
- import { parseMessage, toolImageAt } from "./transcript.js";
7
+ import { parseMessage, parseOutboxMessage, toolImageAt } from "./transcript.js";
8
8
  /** Keep the stable wire field in sync with whichever provider owns the chat. */
9
9
  function toSessionRow(row, background_tasks) {
10
10
  const { codex_thinking_level, ...session } = row;
@@ -121,6 +121,8 @@ export class Reads {
121
121
  workspacesRoot;
122
122
  /** Chats with a live agent process, and its start — the gate on `background_tasks`. */
123
123
  liveAgents;
124
+ messageOutboxAvailable = null;
125
+ messageOutboxCheckedAt = 0;
124
126
  constructor(db, workspacesRoot, liveAgents = agentProcessStarts) {
125
127
  this.db = db;
126
128
  this.workspacesRoot = workspacesRoot;
@@ -463,8 +465,86 @@ export class Reads {
463
465
  this.worktreeBySession.set(sessionId, worktree);
464
466
  return worktree;
465
467
  }
466
- /** Incremental transcript fetch. `afterRowid` is the cursor from a prior call. */
467
- getMessages(sessionId, afterRowid = 0) {
468
+ /**
469
+ * The outbox arrived in Conductor migration 123. Keep older desktop builds usable,
470
+ * and re-probe occasionally so a running relay notices an in-place app migration.
471
+ */
472
+ hasMessageOutbox() {
473
+ if (this.messageOutboxAvailable === true)
474
+ return true;
475
+ const now = Date.now();
476
+ if (this.messageOutboxAvailable === false && now - this.messageOutboxCheckedAt < 60_000)
477
+ return false;
478
+ const rows = this.db.query("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'session_messages_outbox' LIMIT 1");
479
+ this.messageOutboxAvailable = rows.length > 0;
480
+ this.messageOutboxCheckedAt = now;
481
+ return this.messageOutboxAvailable;
482
+ }
483
+ /** Current outbox contents. `queueOnly` is the snapshot the transcript renders. */
484
+ outboxMessages(sessionId, queueOnly) {
485
+ if (!this.hasMessageOutbox())
486
+ return [];
487
+ try {
488
+ const rows = this.db.query(`SELECT message_id, delivery_payload, created_at
489
+ FROM session_messages_outbox
490
+ WHERE session_id = ?${queueOnly ? " AND mode = 'queue'" : ''}
491
+ ORDER BY COALESCE(queue_order, 2147483647), created_at ASC`, [sessionId]);
492
+ return rows.flatMap(row => {
493
+ const entry = parseOutboxMessage(row);
494
+ return entry ? [entry] : [];
495
+ });
496
+ }
497
+ catch (error) {
498
+ // A Conductor rollback can swap the DB beneath the relay. Treat only the
499
+ // missing-table case as an older schema; every other read failure stays loud.
500
+ if (error instanceof Error && /no such table:\s*session_messages_outbox/i.test(error.message)) {
501
+ this.messageOutboxAvailable = false;
502
+ this.messageOutboxCheckedAt = Date.now();
503
+ return [];
504
+ }
505
+ throw error;
506
+ }
507
+ }
508
+ /** Snapshot the durable transcript cursor and pending message ids in one SQLite read. */
509
+ deliveryCursor(sessionId) {
510
+ if (!this.hasMessageOutbox()) {
511
+ const row = this.db.query('SELECT COALESCE(MAX(rowid), 0) AS rowid FROM session_messages WHERE session_id = ?', [sessionId])[0];
512
+ return { rowid: row?.rowid ?? 0, outboxIds: new Set() };
513
+ }
514
+ try {
515
+ const rows = this.db.query(`WITH transcript_cursor AS (
516
+ SELECT COALESCE(MAX(rowid), 0) AS rowid
517
+ FROM session_messages
518
+ WHERE session_id = ?
519
+ )
520
+ SELECT transcript_cursor.rowid, NULL AS message_id
521
+ FROM transcript_cursor
522
+ UNION ALL
523
+ SELECT transcript_cursor.rowid, outbox.message_id
524
+ FROM transcript_cursor
525
+ JOIN session_messages_outbox outbox ON outbox.session_id = ?`, [sessionId, sessionId]);
526
+ return {
527
+ rowid: rows[0]?.rowid ?? 0,
528
+ outboxIds: new Set(rows.flatMap(row => (row.message_id ? [row.message_id] : [])))
529
+ };
530
+ }
531
+ catch (error) {
532
+ if (!(error instanceof Error && /no such table:\s*session_messages_outbox/i.test(error.message)))
533
+ throw error;
534
+ this.messageOutboxAvailable = false;
535
+ this.messageOutboxCheckedAt = Date.now();
536
+ return this.deliveryCursor(sessionId);
537
+ }
538
+ }
539
+ /** Has this send created either a durable user row or a newly-owned outbox item? */
540
+ promptDeliveredSince(sessionId, text, before) {
541
+ const target = text.trim();
542
+ const durable = this.durableMessages(sessionId, before.rowid).entries;
543
+ const pending = this.outboxMessages(sessionId, false);
544
+ return [...durable, ...pending].some(entry => entry.role === 'user' && entry.text.trim() === target && !before.outboxIds.has(entry.id));
545
+ }
546
+ /** Incremental durable transcript fetch, without the independently replaced outbox snapshot. */
547
+ durableMessages(sessionId, afterRowid) {
468
548
  const rows = this.db.query(`SELECT rowid, id, role, content, full_message, created_at, sent_at, queue_order
469
549
  FROM session_messages
470
550
  WHERE session_id = ? AND rowid > ?
@@ -478,4 +558,8 @@ export class Reads {
478
558
  }
479
559
  return { entries, cursor };
480
560
  }
561
+ /** Incremental transcript rows plus the full current queue snapshot. */
562
+ getMessages(sessionId, afterRowid = 0) {
563
+ return { ...this.durableMessages(sessionId, afterRowid), queued: this.outboxMessages(sessionId, true) };
564
+ }
481
565
  }
@@ -94,21 +94,20 @@ const mcpTools = createTools(async (route, opts = {}) => {
94
94
  setRestartGuard(() => !reads.listWorkspaces().some(w => w.session_status === 'working'));
95
95
  const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
96
96
  /**
97
- * Has the prompt shown up as a user row yet? The receipt everything below is built
98
- * on. The AppleScript actuator reports `ok` on `osascript` exit 0 — which only
97
+ * Has Conductor taken ownership of the prompt yet? The receipt everything below is
98
+ * built on. The AppleScript actuator reports `ok` on `osascript` exit 0 — which only
99
99
  * means the script *ran*, not that Conductor accepted the keystrokes — so without
100
- * this a dropped send (asleep/unfocused Mac) looks delivered. A queued prompt still
101
- * writes a user row, so it counts as delivered.
100
+ * this a dropped send (asleep/unfocused Mac) looks delivered. A prompt accepted into
101
+ * Conductor's durable outbox also counts, before it becomes a transcript row.
102
102
  */
103
- function deliveredSince(sessionId, text, sinceRowid) {
104
- const target = text.trim();
105
- const { entries } = reads.getMessages(sessionId, sinceRowid);
106
- return entries.some(e => e.role === 'user' && e.text.trim() === target);
103
+ function deliveredSince(sessionId, text, since) {
104
+ return reads.promptDeliveredSince(sessionId, text, since);
107
105
  }
108
106
  /**
109
107
  * Watch for that row, ending on a check rather than a sleep, and never past
110
- * `budgetDeadline`. Conductor writes the row right after the send presses Enter, so
111
- * a real send is confirmed in a tick and only the failure path waits the window out.
108
+ * `budgetDeadline`. Conductor records the row or outbox item right after the send
109
+ * presses Enter, so a real send is confirmed in a tick and only the failure path
110
+ * waits the window out.
112
111
  *
113
112
  * The window is *also* what makes a retry safe — it is deliberately longer than the
114
113
  * row takes to appear, because everything past it is allowed to type into the
@@ -117,10 +116,10 @@ function deliveredSince(sessionId, text, sinceRowid) {
117
116
  * a confirm *followed by another attempt* always gets its full window. Only the
118
117
  * last confirm of all can be cut short, and nothing follows it to duplicate a row.
119
118
  */
120
- async function confirmDelivery(sessionId, text, sinceRowid, budgetDeadline) {
119
+ async function confirmDelivery(sessionId, text, since, budgetDeadline) {
121
120
  const stopAt = Math.min(Date.now() + CONFIRM_WINDOW_MS, budgetDeadline);
122
121
  for (;;) {
123
- if (deliveredSince(sessionId, text, sinceRowid))
122
+ if (deliveredSince(sessionId, text, since))
124
123
  return true;
125
124
  if (Date.now() >= stopAt)
126
125
  return false;
@@ -246,9 +245,10 @@ async function deliverPrompt(ws, sessionId, text, budgetMs = SEND_BUDGET_MS, que
246
245
  const located = locateChat(ws, sessionId);
247
246
  if ('error' in located)
248
247
  return { ok: false, strategy: actuator.name, attempts: 0, error: located.error };
249
- // Snapshot the cursor once: every check below asks "did *this* prompt arrive since
250
- // we started", so a retry can't be fooled by an older identical prompt.
251
- const beforeRowid = reads.getMessages(sessionId).cursor;
248
+ // Snapshot the transcript cursor and outbox ids once: every check below asks "did
249
+ // *this* prompt arrive since we started", so a retry can't be fooled by an older
250
+ // identical prompt moving from the outbox into a new transcript row.
251
+ const before = reads.deliveryCursor(sessionId);
252
252
  const label = ws.branch ?? ws.id;
253
253
  const deadline = Date.now() + budgetMs;
254
254
  let attempts = 0;
@@ -268,8 +268,8 @@ async function deliverPrompt(ws, sessionId, text, budgetMs = SEND_BUDGET_MS, que
268
268
  // an *earlier* attempt's row can be arriving, and typing again over that is the
269
269
  // duplicate this whole path exists to avoid.
270
270
  const landed = sendNeverStarted(last.error)
271
- ? deliveredSince(sessionId, text, beforeRowid)
272
- : await confirmDelivery(sessionId, text, beforeRowid, deadline);
271
+ ? deliveredSince(sessionId, text, before)
272
+ : await confirmDelivery(sessionId, text, before, deadline);
273
273
  if (landed) {
274
274
  if (attempts > 1)
275
275
  console.info(`[relay] send to ${label} landed on attempt ${attempts}`);
@@ -11,6 +11,32 @@
11
11
  import { isToolResult } from "./shared.js";
12
12
  const clip = (s, n) => (s.length > n ? `${s.slice(0, n)}…` : s);
13
13
  const str = (v) => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
14
+ /**
15
+ * Turn Conductor's outbox payload into the same user entry its eventual
16
+ * `session_messages` row will become. Outbox rows have no transcript rowid yet;
17
+ * zero keeps them out of the durable cursor namespace while their message id remains
18
+ * stable when Conductor dispatches them.
19
+ */
20
+ export function parseOutboxMessage(row) {
21
+ let payload;
22
+ try {
23
+ payload = JSON.parse(row.delivery_payload ?? '');
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ const text = str(payload.message);
29
+ if (!text)
30
+ return null;
31
+ return {
32
+ id: row.message_id,
33
+ rowid: 0,
34
+ role: 'user',
35
+ text,
36
+ ts: row.created_at,
37
+ queued: true
38
+ };
39
+ }
14
40
  /** Make tool details repo-relative: absolute worktree paths waste the whole line on a phone. */
15
41
  function stripWorktree(s, worktree) {
16
42
  if (!worktree)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-remote",
3
- "version": "1.86.1",
3
+ "version": "1.86.2",
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.",