conductor-remote 1.86.0 → 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/assets/{index-Cvfvb9B8.js → index-BzjkWwgV.js} +35 -35
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/dist-node/src/reads.js +87 -3
- package/dist-node/src/server.js +17 -17
- package/dist-node/src/transcript.js +26 -0
- package/package.json +1 -1
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-
|
|
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:"
|
|
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\//]}))});
|
package/dist-node/src/reads.js
CHANGED
|
@@ -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
|
-
/**
|
|
467
|
-
|
|
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
|
}
|
package/dist-node/src/server.js
CHANGED
|
@@ -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
|
|
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
|
|
101
|
-
*
|
|
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,
|
|
104
|
-
|
|
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
|
|
111
|
-
* a real send is confirmed in a tick and only the failure path
|
|
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,
|
|
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,
|
|
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
|
|
250
|
-
// we started", so a retry can't be fooled by an older
|
|
251
|
-
|
|
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,
|
|
272
|
-
: await confirmDelivery(sessionId, text,
|
|
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.
|
|
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.",
|