conductor-remote 1.73.2 → 1.75.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-
|
|
16
|
+
<script type="module" crossorigin src="/assets/index-CF1hSnUi.js"></script>
|
|
17
17
|
<link rel="stylesheet" crossorigin href="/assets/index-CXd-zI4t.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
|
|
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:"e7ef44deca46c0539e6ff7bba5eb815e"},{url:"index.html",revision:"3db899836d1b08a3d8216c2a1a06e803"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-CXd-zI4t.css",revision:null},{url:"assets/index-CF1hSnUi.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/notify.js
CHANGED
|
@@ -43,6 +43,14 @@ const TTL_SECONDS = 3600;
|
|
|
43
43
|
const BODY_CHARS = 180;
|
|
44
44
|
/** Consecutive failures before a device is dropped. A `gone` response drops it immediately, regardless. */
|
|
45
45
|
const MAX_FAILURES = 20;
|
|
46
|
+
/**
|
|
47
|
+
* How long a "this device is reading that chat" stamp counts for. The stamp is refreshed
|
|
48
|
+
* by the transcript poll, which runs once a second, so anything past a few seconds means
|
|
49
|
+
* the phone stopped polling: the app was closed, the screen went away, iOS suspended it.
|
|
50
|
+
* Ten seconds is short enough that a phone put down mid-turn gets its notification, and
|
|
51
|
+
* long enough to survive a slow tunnel dropping a handful of ticks.
|
|
52
|
+
*/
|
|
53
|
+
const VIEWING_FRESH_MS = 10_000;
|
|
46
54
|
const clip = (s, n) => (s.length > n ? `${s.slice(0, n).trimEnd()}…` : s);
|
|
47
55
|
/** Alongside the token and the first-prompt queue — one dir holding everything this relay persists. */
|
|
48
56
|
function storePath() {
|
|
@@ -97,6 +105,36 @@ function save() {
|
|
|
97
105
|
function deviceId(endpoint) {
|
|
98
106
|
return crypto.createHash('sha256').update(endpoint).digest('hex').slice(0, 16);
|
|
99
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* The chat each device last had on screen. Deliberately *not* part of `Store`: it is
|
|
110
|
+
* stamped by the transcript poll once a second, so persisting it would rewrite
|
|
111
|
+
* `push.json` at that rate, and a stamp is worthless the moment the relay restarts.
|
|
112
|
+
*/
|
|
113
|
+
const viewing = new Map();
|
|
114
|
+
/**
|
|
115
|
+
* Record that a device is reading a chat right now. The phone sends this along its
|
|
116
|
+
* transcript poll only while the page is visible — the same test that moves its read
|
|
117
|
+
* mark — so "reading" here means on screen, not merely left open in the background.
|
|
118
|
+
*/
|
|
119
|
+
export function noteViewing(id, sessionId) {
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
// The id is whatever the header carried, so nothing here bounds the key space but the
|
|
122
|
+
// token on the request. A device that stopped polling is already ignored; drop it too,
|
|
123
|
+
// rather than keep a row per id anybody ever sent.
|
|
124
|
+
if (viewing.size > 16)
|
|
125
|
+
for (const [key, seen] of viewing)
|
|
126
|
+
if (now - seen.at >= VIEWING_FRESH_MS)
|
|
127
|
+
viewing.delete(key);
|
|
128
|
+
viewing.set(id, { sessionId, at: now });
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Is this device looking at this chat? Suppression is per device on purpose: the phone
|
|
132
|
+
* in a pocket still buzzes for a chat the tablet happens to be showing.
|
|
133
|
+
*/
|
|
134
|
+
export function isReading(id, sessionId, now = Date.now()) {
|
|
135
|
+
const seen = viewing.get(id);
|
|
136
|
+
return !!seen && seen.sessionId === sessionId && now - seen.at < VIEWING_FRESH_MS;
|
|
137
|
+
}
|
|
100
138
|
function info(d) {
|
|
101
139
|
return {
|
|
102
140
|
id: deviceId(d.endpoint),
|
|
@@ -155,6 +193,7 @@ export function unsubscribeDevice(endpoint) {
|
|
|
155
193
|
s.devices = s.devices.filter(d => d.endpoint !== endpoint);
|
|
156
194
|
if (s.devices.length === before)
|
|
157
195
|
return false;
|
|
196
|
+
viewing.delete(deviceId(endpoint));
|
|
158
197
|
console.info(`[push] device unsubscribed — ${s.devices.length} left`);
|
|
159
198
|
save();
|
|
160
199
|
return true;
|
|
@@ -162,6 +201,7 @@ export function unsubscribeDevice(endpoint) {
|
|
|
162
201
|
function drop(endpoint, why) {
|
|
163
202
|
const s = load();
|
|
164
203
|
s.devices = s.devices.filter(d => d.endpoint !== endpoint);
|
|
204
|
+
viewing.delete(deviceId(endpoint));
|
|
165
205
|
console.info(`[push] dropped a dead subscription (${why}) — ${s.devices.length} left`);
|
|
166
206
|
save();
|
|
167
207
|
}
|
|
@@ -197,13 +237,29 @@ async function deliver(device, message) {
|
|
|
197
237
|
save();
|
|
198
238
|
return false;
|
|
199
239
|
}
|
|
200
|
-
/**
|
|
201
|
-
|
|
240
|
+
/**
|
|
241
|
+
* Fan a message out to every subscribed device. Returns how many took it.
|
|
242
|
+
*
|
|
243
|
+
* `unlessReading` names a chat: a device with that chat on screen is skipped, because
|
|
244
|
+
* buzzing about a message already in front of someone is noise. The drop has to happen
|
|
245
|
+
* here rather than in the service worker — Safari treats a push that resolves without a
|
|
246
|
+
* notification as abuse and can revoke the subscription (see public/push-sw.js), so the
|
|
247
|
+
* phone's only way to stay quiet is for nothing to be sent.
|
|
248
|
+
*/
|
|
249
|
+
export async function notifyAll(message, unlessReading) {
|
|
202
250
|
const s = load();
|
|
203
251
|
if (!s.devices.length)
|
|
204
252
|
return 0;
|
|
205
253
|
// Snapshot: `deliver` can prune the live array mid-flight.
|
|
206
|
-
const
|
|
254
|
+
const targets = s.devices.slice().filter(d => !(unlessReading && isReading(deviceId(d.endpoint), unlessReading)));
|
|
255
|
+
const held = s.devices.length - targets.length;
|
|
256
|
+
// "Nothing arrived" and "nothing was sent, on purpose" are the same silence on a
|
|
257
|
+
// phone, and only one of them is a fault. /api/logs is where that question is asked.
|
|
258
|
+
if (held)
|
|
259
|
+
console.info(`[push] held back from ${held} device${held === 1 ? '' : 's'} reading that chat`);
|
|
260
|
+
if (!targets.length)
|
|
261
|
+
return 0;
|
|
262
|
+
const results = await Promise.all(targets.map(d => deliver(d, message)));
|
|
207
263
|
return results.filter(Boolean).length;
|
|
208
264
|
}
|
|
209
265
|
/** Send to one device — the Connect sheet's "Send test", which proves the whole path end to end. */
|
|
@@ -386,7 +442,7 @@ async function fire(reads, sessionId, kind, state) {
|
|
|
386
442
|
url: chatRoute(state.workspaceId, sessionId),
|
|
387
443
|
kind,
|
|
388
444
|
ts: Date.now()
|
|
389
|
-
});
|
|
445
|
+
}, sessionId);
|
|
390
446
|
if (sent)
|
|
391
447
|
console.info(`[push] ${kind} in ${where} → ${sent} device${sent === 1 ? '' : 's'}`);
|
|
392
448
|
}
|
package/dist-node/src/server.js
CHANGED
|
@@ -18,7 +18,7 @@ import { createTools, handleRpc, READ_TIMEOUT_MS } from "./mcp-tools.js";
|
|
|
18
18
|
import { mergePr } from "./merge.js";
|
|
19
19
|
import { ModelCache } from "./model-cache.js";
|
|
20
20
|
import { armNoSleep, disarmNoSleep, MAX_SECONDS as NOSLEEP_MAX_SECONDS, nosleepState, watchNoSleepExpiry } from "./nosleep.js";
|
|
21
|
-
import { chatRoute, notifyAll, notifyDevice, pushConfig, startNotifier, subscribeDevice, unsubscribeDevice } from "./notify.js";
|
|
21
|
+
import { chatRoute, noteViewing, notifyAll, notifyDevice, pushConfig, startNotifier, subscribeDevice, unsubscribeDevice } from "./notify.js";
|
|
22
22
|
import { ParkedPromptQueue } from "./parked.js";
|
|
23
23
|
import { attachPrStatus } from "./pr.js";
|
|
24
24
|
import { readPrefs, writePrefs } from "./prefs.js";
|
|
@@ -27,7 +27,7 @@ import { isRoute, routeParam, routes } from "./routes.js";
|
|
|
27
27
|
import { foldHits, queryTokens, SearchIndex } from "./search.js";
|
|
28
28
|
import { SendOnce } from "./sendonce.js";
|
|
29
29
|
import { readSettings, writeSettings } from "./settings.js";
|
|
30
|
-
import { withoutWindowEvidence } from "./shared.js";
|
|
30
|
+
import { VIEWING_HEADER, withoutWindowEvidence } from "./shared.js";
|
|
31
31
|
import { discardStagedAttachment, materializeStagedAttachments, stageAttachment, stagedAttachments } from "./staged-attachments.js";
|
|
32
32
|
import { driftWarningLines, readExposeMode, tailscaleBin } from "./tailscale.js";
|
|
33
33
|
import { renderTranscript, transcriptThrough } from "./transcript.js";
|
|
@@ -1333,6 +1333,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
1333
1333
|
// GET /api/sessions/:id/messages?after=<rowid>
|
|
1334
1334
|
const messagesOf = routeParam(routes.messages, req.method, pathname);
|
|
1335
1335
|
if (messagesOf) {
|
|
1336
|
+
// The phone's 1s transcript poll doubles as its "I am reading this chat" heartbeat,
|
|
1337
|
+
// which is what keeps a turn ending on screen from also buzzing the lock screen
|
|
1338
|
+
// (src/notify.ts). Only this route is a claim: it is the one read that runs for the
|
|
1339
|
+
// chat on screen and for no other.
|
|
1340
|
+
const device = req.headers[VIEWING_HEADER];
|
|
1341
|
+
if (typeof device === 'string' && device)
|
|
1342
|
+
noteViewing(device, messagesOf);
|
|
1336
1343
|
const after = Number(url.searchParams.get('after') ?? 0);
|
|
1337
1344
|
return json(req, res, 200, reads.getMessages(messagesOf, Number.isFinite(after) ? after : 0));
|
|
1338
1345
|
}
|
package/dist-node/src/shared.js
CHANGED
|
@@ -247,3 +247,14 @@ export function isLockedError(error) {
|
|
|
247
247
|
export function withoutWindowEvidence(error) {
|
|
248
248
|
return error.replace(/\s*\[window server:.*$/s, '').trim();
|
|
249
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Header naming the push device that sent a request, so the relay can tell which chat
|
|
252
|
+
* that device has on screen and skip notifying it about that one chat (src/notify.ts).
|
|
253
|
+
*
|
|
254
|
+
* It rides the transcript poll, which is already a per-second heartbeat for exactly the
|
|
255
|
+
* chat being read and for no other, so this costs no request and no timer. Declared here
|
|
256
|
+
* rather than spelled twice because a typo would be silent in both directions: the relay
|
|
257
|
+
* would simply never learn what is on screen, and every notification would keep arriving
|
|
258
|
+
* as it does today.
|
|
259
|
+
*/
|
|
260
|
+
export const VIEWING_HEADER = 'x-relay-device';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "conductor-remote",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.75.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.",
|