@zeph-to/cli 1.25.0 → 1.26.1

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/README.md CHANGED
@@ -202,12 +202,22 @@ can never false-flag. Muted projects are never flagged.
202
202
 
203
203
  ### Diagnostics
204
204
 
205
- The auto-spawned listener writes to two files under `~/.zeph/`:
205
+ The auto-spawned listener writes to three files under `~/.zeph/`:
206
206
 
207
207
  - `listener.pid` — the running daemon's PID. `cat ~/.zeph/listener.pid`
208
208
  + `ps -p <pid>` to confirm it's alive.
209
+ - `listener.version` — the CLI version the daemon booted from. This is
210
+ what `zeph cc` compares against the installed package to spot a stale
211
+ daemon (see below).
209
212
  - `listener.log` — stdout + stderr from the daemon. `tail -f` to watch.
210
213
 
214
+ The daemon logs its version on the first line, which is the only
215
+ reliable way to tell which build a long-running process is on:
216
+
217
+ ```
218
+ [xx:xx:xx] zeph listener starting — v1.26.0 — wss://ws.zeph.to
219
+ ```
220
+
211
221
  A healthy listener log shows one line per cycle:
212
222
 
213
223
  ```
@@ -219,12 +229,31 @@ If you see `! server rejected listener.sessions: ...` instead, the
219
229
  message points at the failure (auth, missing device record, etc.) so
220
230
  you can fix the actual problem instead of guessing.
221
231
 
222
- To force a restart — e.g. after upgrading `@zeph-to/cli`:
232
+ To force a restart:
233
+
234
+ ```bash
235
+ zeph listener --restart
236
+ ```
237
+
238
+ **After upgrading `@zeph-to/cli` you normally don't have to.** `npm i -g`
239
+ replaces the package on disk but never the daemon already running from
240
+ the old build — that daemon keeps answering pushes (so agent chat looks
241
+ fine) while silently ignoring every message subtype added since it
242
+ booted. `zeph cc` compares `listener.version` against the installed
243
+ version and restarts the daemon for you when the installed one is newer
244
+ (a daemon *newer* than the `zeph cc` you ran is left alone, so several
245
+ installs on one machine don't fight over it):
246
+
247
+ ```
248
+ zeph: listener 1.25.0 is stale — restarting on 1.26.0
249
+ ```
250
+
251
+ If the PID file is missing (a different account started the daemon, or
252
+ it was removed by hand) the singleton guard can't see it. Find the real
253
+ process instead:
223
254
 
224
255
  ```bash
225
- kill $(cat ~/.zeph/listener.pid)
226
- rm ~/.zeph/listener.pid
227
- zeph cc # autospawns the new build
256
+ ps aux | grep '[c]li.js listener'
228
257
  ```
229
258
 
230
259
  To run it in the foreground (for development of the SDK itself):
@@ -385,6 +414,8 @@ which project + branch finished without writing per-IDE wrappers. Pass
385
414
  | `--ws-url <url>` | WebSocket endpoint (or set `ZEPH_WS_URL` env, or `wsUrl` in `~/.zeph/config.json`) |
386
415
  | `--key <api-key>` | API key (or set `ZEPH_API_KEY` env) |
387
416
  | `--base-url <url>` | REST API base URL (or set `ZEPH_BASE_URL` env, or `baseUrl` in `~/.zeph/config.json`) |
417
+ | `--stop` | Stop the running daemon and clear its PID/version stamps |
418
+ | `--restart` | Stop it and relaunch detached (logs to `~/.zeph/listener.log`) |
388
419
 
389
420
  The listener reconnects with exponential backoff + jitter (1 s → 30 s
390
421
  cap). Heartbeat is ping every 25 s with a 10 s pong timeout. On an
package/dist/cli.js CHANGED
@@ -81,6 +81,10 @@ ${usageAgentLines()}
81
81
  listener Resident daemon — receives 'agent.command' pushes from
82
82
  the phone picker and injects them into the matching
83
83
  tmux session.
84
+ --stop stop the running daemon
85
+ --restart stop it and relaunch in the background
86
+ (needed after an upgrade: 'npm i -g' swaps
87
+ the package but never the live process)
84
88
 
85
89
  Notify options:
86
90
  --title <text> Push title
@@ -0,0 +1,63 @@
1
+ export declare const LISTENER_PID_FILE: string;
2
+ /** CLI version the running daemon booted from. Absent ⇒ pre-stamp build. */
3
+ export declare const LISTENER_VERSION_FILE: string;
4
+ export declare const LISTENER_LOG_FILE: string;
5
+ /**
6
+ * PID of the listener on record, or null when the file is missing or stale.
7
+ * Stale PID files (process gone) read as "no listener" so a crashed daemon
8
+ * never blocks the next autostart.
9
+ */
10
+ export declare const runningListenerPid: () => number | null;
11
+ /**
12
+ * CLI version the running daemon started from, or null when unknown.
13
+ * null is meaningful: every build before the stamp existed predates the
14
+ * live-terminal protocol, so callers treat "unknown" as "stale".
15
+ */
16
+ export declare const runningListenerVersion: () => string | null;
17
+ /** Claim the singleton slot for THIS process and stamp the version with it. */
18
+ export declare const writeListenerRuntime: (version: string) => void;
19
+ /**
20
+ * Drop the PID/version stamps, but only when they still name `pid` — a
21
+ * successor that already claimed the slot must not be trampled by a
22
+ * predecessor's exit handler.
23
+ */
24
+ export declare const clearListenerRuntime: (pid: number) => void;
25
+ /**
26
+ * Drop stamps left behind by a daemon that died without running its exit
27
+ * handler (crash, SIGKILL, reboot-era leftovers). No-op while one is alive.
28
+ */
29
+ export declare const clearStaleListenerRuntime: () => void;
30
+ /**
31
+ * Stop the daemon and wait for the process to actually go away. SIGTERM
32
+ * first so its exit handler clears the stamps, SIGKILL once the grace window
33
+ * expires. Resolves true when the process is confirmed gone.
34
+ *
35
+ * Waiting matters: the caller's next move is spawning a replacement, and two
36
+ * live daemons on one machine both answer the same pushes.
37
+ */
38
+ export declare const stopListener: (pid: number, timeoutMs?: number) => Promise<boolean>;
39
+ /**
40
+ * Path to the running cli.js entry. wrapper.js/listener.js sit next to cli.js
41
+ * in dist/, so __dirname resolves it directly — independent of how the user
42
+ * invoked us.
43
+ *
44
+ * `process.argv[1]` is unreliable here: when `zeph` runs via the npm-installed
45
+ * bin shim (`/usr/local/bin/zeph` → cli.js via a wrapper script), argv[1] is
46
+ * the shim path (`.../bin/zeph`), NOT `cli.js`. That made the original
47
+ * `/cli\.(js|ts|mjs|cjs)$/` check silently reject the entry and the autospawn
48
+ * never fired — the bug only surfaced once the user switched to the global
49
+ * npm install.
50
+ *
51
+ * Fall back to argv[1] only when the __dirname-relative file doesn't exist
52
+ * (some packaging where dist layout differs).
53
+ */
54
+ export declare const resolveCliPath: () => string | null;
55
+ export declare const rotateListenerLogIfLarge: () => void;
56
+ /**
57
+ * Launch `zeph listener` detached, with output appended to the listener log
58
+ * so nothing is lost once we let go of it. Returns false when the cli entry
59
+ * couldn't be resolved or the spawn failed — the caller decides how loud that
60
+ * is (autostart is best-effort, an explicit `--restart` is not).
61
+ */
62
+ export declare const spawnListenerDetached: () => boolean;
63
+ //# sourceMappingURL=listener-process.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listener-process.d.ts","sourceRoot":"","sources":["../src/listener-process.ts"],"names":[],"mappings":"AAwBA,eAAO,MAAM,iBAAiB,QAAiC,CAAC;AAChE,4EAA4E;AAC5E,eAAO,MAAM,qBAAqB,QAAqC,CAAC;AACxE,eAAO,MAAM,iBAAiB,QAAiC,CAAC;AAIhE;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,QAAO,MAAM,GAAG,IAS9C,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAO,MAAM,GAAG,IAMlD,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,GAAI,SAAS,MAAM,KAAG,IAItD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,MAAM,KAAG,IASlD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,yBAAyB,QAAO,IAQ5C,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,GAAU,KAAK,MAAM,EAAE,kBAAiB,KAAG,OAAO,CAAC,OAAO,CAqBlF,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,QAAO,MAAM,GAAG,IAM1C,CAAC;AASF,eAAO,MAAM,wBAAwB,QAAO,IAM3C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAO,OAiBxC,CAAC"}
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.spawnListenerDetached = exports.rotateListenerLogIfLarge = exports.resolveCliPath = exports.stopListener = exports.clearStaleListenerRuntime = exports.clearListenerRuntime = exports.writeListenerRuntime = exports.runningListenerVersion = exports.runningListenerPid = exports.LISTENER_LOG_FILE = exports.LISTENER_VERSION_FILE = exports.LISTENER_PID_FILE = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_os_1 = require("node:os");
7
+ const node_path_1 = require("node:path");
8
+ /**
9
+ * Process-level facts about the resident `zeph listener` daemon: where its
10
+ * PID/version stamps live, whether one is alive, how to stop it and how to
11
+ * respawn it detached.
12
+ *
13
+ * This lives OUTSIDE listener.ts on purpose. `wrapper.ts` (the `zeph cc` hot
14
+ * path) needs all of it, and importing listener.ts there would drag `ws` +
15
+ * the crypto stack into every agent launch. Same split as
16
+ * listener-device-id.ts — one small module both sides can read.
17
+ *
18
+ * Why a version stamp at all: `npm i -g @zeph-to/cli` replaces the package on
19
+ * disk but NOT the daemon already running from the old dist. That daemon
20
+ * keeps answering pushes (so chat looks fine) while silently ignoring every
21
+ * subtype added since it started — which is exactly how a pre-1.24 listener
22
+ * left the phone's live terminal spinning until the machine was rebooted.
23
+ * Stamping the version next to the PID makes that drift detectable.
24
+ */
25
+ const ZEPH_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), '.zeph');
26
+ exports.LISTENER_PID_FILE = (0, node_path_1.join)(ZEPH_DIR, 'listener.pid');
27
+ /** CLI version the running daemon booted from. Absent ⇒ pre-stamp build. */
28
+ exports.LISTENER_VERSION_FILE = (0, node_path_1.join)(ZEPH_DIR, 'listener.version');
29
+ exports.LISTENER_LOG_FILE = (0, node_path_1.join)(ZEPH_DIR, 'listener.log');
30
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
31
+ /**
32
+ * PID of the listener on record, or null when the file is missing or stale.
33
+ * Stale PID files (process gone) read as "no listener" so a crashed daemon
34
+ * never blocks the next autostart.
35
+ */
36
+ const runningListenerPid = () => {
37
+ try {
38
+ const pid = Number((0, node_fs_1.readFileSync)(exports.LISTENER_PID_FILE, 'utf-8').trim());
39
+ if (!Number.isFinite(pid) || pid <= 0)
40
+ return null;
41
+ process.kill(pid, 0); // signal 0 = existence check, throws when gone
42
+ return pid;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ };
48
+ exports.runningListenerPid = runningListenerPid;
49
+ /**
50
+ * CLI version the running daemon started from, or null when unknown.
51
+ * null is meaningful: every build before the stamp existed predates the
52
+ * live-terminal protocol, so callers treat "unknown" as "stale".
53
+ */
54
+ const runningListenerVersion = () => {
55
+ try {
56
+ return (0, node_fs_1.readFileSync)(exports.LISTENER_VERSION_FILE, 'utf-8').trim() || null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ };
62
+ exports.runningListenerVersion = runningListenerVersion;
63
+ /** Claim the singleton slot for THIS process and stamp the version with it. */
64
+ const writeListenerRuntime = (version) => {
65
+ (0, node_fs_1.mkdirSync)(ZEPH_DIR, { recursive: true });
66
+ (0, node_fs_1.writeFileSync)(exports.LISTENER_PID_FILE, String(process.pid));
67
+ (0, node_fs_1.writeFileSync)(exports.LISTENER_VERSION_FILE, version);
68
+ };
69
+ exports.writeListenerRuntime = writeListenerRuntime;
70
+ /**
71
+ * Drop the PID/version stamps, but only when they still name `pid` — a
72
+ * successor that already claimed the slot must not be trampled by a
73
+ * predecessor's exit handler.
74
+ */
75
+ const clearListenerRuntime = (pid) => {
76
+ try {
77
+ if (!(0, node_fs_1.existsSync)(exports.LISTENER_PID_FILE))
78
+ return;
79
+ if (Number((0, node_fs_1.readFileSync)(exports.LISTENER_PID_FILE, 'utf-8').trim()) !== pid)
80
+ return;
81
+ (0, node_fs_1.unlinkSync)(exports.LISTENER_PID_FILE);
82
+ try {
83
+ (0, node_fs_1.unlinkSync)(exports.LISTENER_VERSION_FILE);
84
+ }
85
+ catch { /* best-effort — a missing stamp is already the desired state */ }
86
+ }
87
+ catch { /* best-effort */ }
88
+ };
89
+ exports.clearListenerRuntime = clearListenerRuntime;
90
+ /**
91
+ * Drop stamps left behind by a daemon that died without running its exit
92
+ * handler (crash, SIGKILL, reboot-era leftovers). No-op while one is alive.
93
+ */
94
+ const clearStaleListenerRuntime = () => {
95
+ if ((0, exports.runningListenerPid)() !== null)
96
+ return;
97
+ try {
98
+ (0, node_fs_1.unlinkSync)(exports.LISTENER_PID_FILE);
99
+ }
100
+ catch { /* nothing to clear */ }
101
+ try {
102
+ (0, node_fs_1.unlinkSync)(exports.LISTENER_VERSION_FILE);
103
+ }
104
+ catch { /* nothing to clear */ }
105
+ };
106
+ exports.clearStaleListenerRuntime = clearStaleListenerRuntime;
107
+ /**
108
+ * Stop the daemon and wait for the process to actually go away. SIGTERM
109
+ * first so its exit handler clears the stamps, SIGKILL once the grace window
110
+ * expires. Resolves true when the process is confirmed gone.
111
+ *
112
+ * Waiting matters: the caller's next move is spawning a replacement, and two
113
+ * live daemons on one machine both answer the same pushes.
114
+ */
115
+ const stopListener = async (pid, timeoutMs = 3_000) => {
116
+ try {
117
+ process.kill(pid, 'SIGTERM');
118
+ }
119
+ catch {
120
+ return true; // already gone
121
+ }
122
+ const deadline = Date.now() + timeoutMs;
123
+ while (Date.now() < deadline) {
124
+ await sleep(50);
125
+ try {
126
+ process.kill(pid, 0);
127
+ }
128
+ catch {
129
+ (0, exports.clearListenerRuntime)(pid);
130
+ return true;
131
+ }
132
+ }
133
+ try {
134
+ process.kill(pid, 'SIGKILL');
135
+ }
136
+ catch { /* raced us to exit */ }
137
+ (0, exports.clearListenerRuntime)(pid);
138
+ return false;
139
+ };
140
+ exports.stopListener = stopListener;
141
+ /**
142
+ * Path to the running cli.js entry. wrapper.js/listener.js sit next to cli.js
143
+ * in dist/, so __dirname resolves it directly — independent of how the user
144
+ * invoked us.
145
+ *
146
+ * `process.argv[1]` is unreliable here: when `zeph` runs via the npm-installed
147
+ * bin shim (`/usr/local/bin/zeph` → cli.js via a wrapper script), argv[1] is
148
+ * the shim path (`.../bin/zeph`), NOT `cli.js`. That made the original
149
+ * `/cli\.(js|ts|mjs|cjs)$/` check silently reject the entry and the autospawn
150
+ * never fired — the bug only surfaced once the user switched to the global
151
+ * npm install.
152
+ *
153
+ * Fall back to argv[1] only when the __dirname-relative file doesn't exist
154
+ * (some packaging where dist layout differs).
155
+ */
156
+ const resolveCliPath = () => {
157
+ const local = (0, node_path_1.join)(__dirname, 'cli.js');
158
+ if ((0, node_fs_1.existsSync)(local))
159
+ return local;
160
+ const entry = process.argv[1];
161
+ if (entry && /cli\.(js|ts|mjs|cjs)$/.test(entry))
162
+ return entry;
163
+ return null;
164
+ };
165
+ exports.resolveCliPath = resolveCliPath;
166
+ /**
167
+ * Rotate the listener log once it grows past 5 MB. The daemon runs for days
168
+ * and writes 2-3 lines per 5-s cycle, so without rotation the file climbs
169
+ * into the tens of megabytes. Keep the previous run's tail under `.old`.
170
+ */
171
+ const LISTENER_LOG_MAX_BYTES = 5 * 1024 * 1024;
172
+ const rotateListenerLogIfLarge = () => {
173
+ try {
174
+ if (!(0, node_fs_1.existsSync)(exports.LISTENER_LOG_FILE))
175
+ return;
176
+ if ((0, node_fs_1.statSync)(exports.LISTENER_LOG_FILE).size <= LISTENER_LOG_MAX_BYTES)
177
+ return;
178
+ (0, node_fs_1.renameSync)(exports.LISTENER_LOG_FILE, exports.LISTENER_LOG_FILE + '.old');
179
+ }
180
+ catch { /* best-effort */ }
181
+ };
182
+ exports.rotateListenerLogIfLarge = rotateListenerLogIfLarge;
183
+ /**
184
+ * Launch `zeph listener` detached, with output appended to the listener log
185
+ * so nothing is lost once we let go of it. Returns false when the cli entry
186
+ * couldn't be resolved or the spawn failed — the caller decides how loud that
187
+ * is (autostart is best-effort, an explicit `--restart` is not).
188
+ */
189
+ const spawnListenerDetached = () => {
190
+ const cliPath = (0, exports.resolveCliPath)();
191
+ if (!cliPath)
192
+ return false;
193
+ try {
194
+ (0, node_fs_1.mkdirSync)(ZEPH_DIR, { recursive: true });
195
+ (0, exports.rotateListenerLogIfLarge)();
196
+ const out = (0, node_fs_1.openSync)(exports.LISTENER_LOG_FILE, 'a');
197
+ const child = (0, node_child_process_1.spawn)(process.execPath, [cliPath, 'listener'], {
198
+ detached: true,
199
+ stdio: ['ignore', out, out],
200
+ env: { ...process.env, ZEPH_LISTENER_AUTOSTART: '1' },
201
+ });
202
+ child.unref();
203
+ return true;
204
+ }
205
+ catch {
206
+ return false;
207
+ }
208
+ };
209
+ exports.spawnListenerDetached = spawnListenerDetached;
@@ -174,7 +174,14 @@ export interface StreamControl {
174
174
  sessionName?: string;
175
175
  /** Subscriber's device public key (Base64 SPKI) — presence turns on E2EE. */
176
176
  subscriberPublicKey?: string;
177
+ /**
178
+ * Subscriber promises to send `agent.stream.renew` while it's watching.
179
+ * Only such a subscriber gets the short lease; clients that predate the
180
+ * renew protocol keep the 5-minute orphan guard (see STREAM_LEASE_MS).
181
+ */
182
+ renew?: boolean;
177
183
  }
184
+ export declare const STREAM_LEASE_MS = 15000;
178
185
  export declare const MAX_CONCURRENT_STREAMS = 3;
179
186
  export declare const stopStream: (sessionName: string) => void;
180
187
  export declare const stopAllStreams: () => void;
@@ -198,7 +205,7 @@ export type StreamFramePayload = {
198
205
  export type StreamErrorFrame = {
199
206
  subtype: 'agent.stream.frame';
200
207
  sessionName: string;
201
- error: 'unknown_session' | 'e2ee_unavailable' | 'encrypt_failed' | 'stream_limit';
208
+ error: 'unknown_session' | 'e2ee_unavailable' | 'encrypt_failed' | 'stream_limit' | 'stream_gone';
202
209
  };
203
210
  /**
204
211
  * Per-listener concurrency guard, as a pure predicate so the boundary is
@@ -1 +1 @@
1
- {"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"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"}
package/dist/listener.js CHANGED
@@ -25,7 +25,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
25
25
  return (mod && mod.__esModule) ? mod : { "default": mod };
26
26
  };
27
27
  Object.defineProperty(exports, "__esModule", { value: true });
28
- exports.handleListener = exports.resolveWsUrl = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.setAttachmentContext = exports.writeRemoteMarker = exports.collectSessions = exports.collectSessionsVerbose = exports.handleStreamControl = exports.buildStreamFrame = exports.isStreamCapReached = exports.stopAllStreams = exports.stopStream = exports.MAX_CONCURRENT_STREAMS = exports.scheduleInjectSnapshots = exports.handleScreenRequest = exports.collectWatchHits = exports.resetPatternWatches = exports.setPatternWatches = exports.deriveSessionState = exports.resetSessionStates = exports.detectRemoteAgent = exports.parseSessionName = exports.invalidateTmuxSocketCache = exports.resolveKeys = exports.paneCurrentCommand = exports.checkRateLimit = exports.AUTH_FAILURE_CODES = exports.sessionsReportDue = exports.sessionsFingerprint = exports.SESSION_REPORT_HEARTBEAT_MS = void 0;
28
+ exports.handleListener = exports.resolveWsUrl = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.setAttachmentContext = exports.writeRemoteMarker = exports.collectSessions = exports.collectSessionsVerbose = exports.handleStreamControl = exports.buildStreamFrame = exports.isStreamCapReached = exports.stopAllStreams = exports.stopStream = exports.MAX_CONCURRENT_STREAMS = exports.STREAM_LEASE_MS = exports.scheduleInjectSnapshots = exports.handleScreenRequest = exports.collectWatchHits = exports.resetPatternWatches = exports.setPatternWatches = exports.deriveSessionState = exports.resetSessionStates = exports.detectRemoteAgent = exports.parseSessionName = exports.invalidateTmuxSocketCache = exports.resolveKeys = exports.paneCurrentCommand = exports.checkRateLimit = exports.AUTH_FAILURE_CODES = exports.sessionsReportDue = exports.sessionsFingerprint = exports.SESSION_REPORT_HEARTBEAT_MS = void 0;
29
29
  const child_process_1 = require("child_process");
30
30
  const crypto_1 = require("crypto");
31
31
  const fs_1 = require("fs");
@@ -33,6 +33,7 @@ const os_1 = require("os");
33
33
  const path_1 = require("path");
34
34
  const ws_1 = __importDefault(require("ws"));
35
35
  const config_js_1 = require("./config.js");
36
+ const listener_process_js_1 = require("./listener-process.js");
36
37
  const gate_js_1 = require("./gate.js");
37
38
  const remote_agents_js_1 = require("./remote-agents.js");
38
39
  const agent_state_js_1 = require("./agent-state.js");
@@ -765,10 +766,27 @@ exports.scheduleInjectSnapshots = scheduleInjectSnapshots;
765
766
  // still caps the actual payload, so very wide panes get top-truncated.
766
767
  const STREAM_CAPTURE_LINES = 200;
767
768
  const STREAM_INTERVAL_MS = 400;
768
- // Orphan guard: a phone that dies without sending agent.stream.stop must not
769
- // leak an interval forever. Auto-stop after this long; the phone re-subscribes
770
- // on reopen.
769
+ // Orphan guard for subscribers that can't renew (clients older than the renew
770
+ // protocol): a phone that dies without sending agent.stream.stop must not leak
771
+ // an interval forever. Auto-stop after this long; the phone re-subscribes on
772
+ // reopen.
771
773
  const STREAM_MAX_MS = 5 * 60_000;
774
+ // Lease for a renewal-capable subscriber (`start` carried `renew: true`).
775
+ // stream.stop is NOT a reliable slot-release signal: the native terminal runs
776
+ // in its own WebView that the OS destroys on swipe-back, killing the page mid-
777
+ // flight — no unmount, no stop, and the relay is pass-through so we never see
778
+ // the phone's socket drop either. Three such opens used to wedge
779
+ // MAX_CONCURRENT_STREAMS for a full STREAM_MAX_MS with every retry answering
780
+ // stream_limit. A lease inverts the burden: the viewer must keep proving it's
781
+ // there.
782
+ //
783
+ // This is also the worst-case wait before a vanished viewer's slot comes back,
784
+ // so it wants to be short — but a lease can only be as tight as the proof is
785
+ // frequent. At the web's 5s renew cadence this tolerates 3 consecutive dropped
786
+ // renews, the same margin as before, while cutting the wedge from 5 minutes to
787
+ // 15 seconds. A viewer that loses the race gets its stream reaped and re-
788
+ // subscribes on the next renew (see the stream_gone reply below).
789
+ exports.STREAM_LEASE_MS = 15_000;
772
790
  // Per-listener concurrency guard: the API Gateway WS stage throttle (50 rps)
773
791
  // is SHARED across every user, so a runaway machine (many sessions, or a
774
792
  // client re-subscribe bug) streaming at 2.5 fps each can starve push delivery
@@ -784,6 +802,8 @@ const STREAM_MAX_ENCRYPT_FAILURES = 3;
784
802
  // the log every STREAM_LOG_INTERVAL_MS and summarized on stop — the data that
785
803
  // decides cloud-throttled (B-lite) vs direct/Tailscale (B-full).
786
804
  const STREAM_LOG_INTERVAL_MS = 5_000;
805
+ /** How far a start (and each renew) pushes the deadline out. */
806
+ const leaseFor = (renewing) => (renewing ? exports.STREAM_LEASE_MS : STREAM_MAX_MS);
787
807
  const activeStreams = new Map();
788
808
  const maybeLogStreamStats = (sessionName, stats) => {
789
809
  const elapsed = Date.now() - stats.lastLogAt;
@@ -822,6 +842,28 @@ exports.stopAllStreams = stopAllStreams;
822
842
  */
823
843
  const isStreamCapReached = (activeCount) => activeCount >= exports.MAX_CONCURRENT_STREAMS;
824
844
  exports.isStreamCapReached = isStreamCapReached;
845
+ /**
846
+ * At the cap, hand the slot to the new subscriber when the stalest holder
847
+ * can't prove it's still watching — an expired lease, or a client too old to
848
+ * renew at all (those hold a slot for STREAM_MAX_MS on nothing but hope, and
849
+ * a cached web build can keep producing them long after a deploy). Three
850
+ * actively-renewing viewers are all real, so that case still refuses.
851
+ * Returns the evicted session name, or null when every holder is live.
852
+ */
853
+ const evictStalestStream = (now) => {
854
+ let victim = null;
855
+ for (const [name, entry] of activeStreams) {
856
+ if (entry.renewing && entry.expiresAt > now)
857
+ continue; // provably watched
858
+ if (!victim || entry.expiresAt < victim.expiresAt)
859
+ victim = { name, expiresAt: entry.expiresAt };
860
+ }
861
+ if (!victim)
862
+ return null;
863
+ log(`⧉ stream ${victim.name}: evicted — slot handed to a newer subscriber (no live lease)`);
864
+ (0, exports.stopStream)(victim.name);
865
+ return victim.name;
866
+ };
825
867
  const streamErrorFrame = (sessionName, error) => ({
826
868
  subtype: 'agent.stream.frame',
827
869
  sessionName,
@@ -858,16 +900,40 @@ exports.buildStreamFrame = buildStreamFrame;
858
900
  * path). start is idempotent — a repeat restarts the loop.
859
901
  */
860
902
  const handleStreamControl = (req, send) => {
903
+ if (req.subtype !== 'agent.stream.start' && req.subtype !== 'agent.stream.stop' && req.subtype !== 'agent.stream.renew') {
904
+ return false;
905
+ }
906
+ // Every stream-control message names one machine, and the relay fans them
907
+ // out to all of this user's connections — so decide addressing once, here.
908
+ // Two machines can run the same tmux session name, and stop/renew carry no
909
+ // other identity: without this gate a stop meant for one listener would
910
+ // reach into the other's stream of that name (and a renew would refresh it).
911
+ if (req.targetDeviceId !== (0, exports.computeListenerDeviceId)())
912
+ return false;
861
913
  if (req.subtype === 'agent.stream.stop') {
862
914
  if (req.sessionName)
863
915
  (0, exports.stopStream)(req.sessionName);
864
916
  return true;
865
917
  }
866
- if (req.subtype !== 'agent.stream.start')
867
- return false;
868
- // Not addressed to this machine — let other listeners answer.
869
- if (req.targetDeviceId !== (0, exports.computeListenerDeviceId)())
870
- return false;
918
+ // Lease renewal — the subscriber is still watching.
919
+ if (req.subtype === 'agent.stream.renew') {
920
+ if (!req.sessionName)
921
+ return true;
922
+ const entry = activeStreams.get(req.sessionName);
923
+ if (entry) {
924
+ entry.expiresAt = Date.now() + leaseFor(entry.renewing);
925
+ return true;
926
+ }
927
+ // Addressed to us but we hold no such stream: it was reaped (lost
928
+ // renews), evicted, or dropped when our socket last reconnected. The
929
+ // viewer has no other way to learn that — it just keeps painting a
930
+ // frozen pane under a LIVE badge — so tell it to re-subscribe. Renew
931
+ // carries no subscriber key, so restarting it here would silently
932
+ // downgrade an E2EE stream to plaintext; only the client can redo the
933
+ // handshake.
934
+ send(streamErrorFrame(req.sessionName, 'stream_gone'));
935
+ return true;
936
+ }
871
937
  const sessionName = req.sessionName;
872
938
  if (!sessionName)
873
939
  return true;
@@ -879,7 +945,7 @@ const handleStreamControl = (req, send) => {
879
945
  // Concurrency guard AFTER the restart-stop: re-subscribing to an already
880
946
  // active session doesn't count against the cap, only a genuinely new one
881
947
  // does. Refuse the new stream instead of adding to the shared-throttle load.
882
- if ((0, exports.isStreamCapReached)(activeStreams.size)) {
948
+ if ((0, exports.isStreamCapReached)(activeStreams.size) && !evictStalestStream(Date.now())) {
883
949
  log(`⧉ stream ${sessionName}: refused — ${activeStreams.size}/${exports.MAX_CONCURRENT_STREAMS} streams already active on this listener`);
884
950
  send(streamErrorFrame(sessionName, 'stream_limit'));
885
951
  return true;
@@ -914,6 +980,15 @@ const handleStreamControl = (req, send) => {
914
980
  let wireSeq = 0;
915
981
  let encryptFailures = 0;
916
982
  const timer = setInterval(() => {
983
+ // Lease check rides the capture tick: one deadline field, no second
984
+ // timer to leak. (The previous per-start expiry setTimeout was never
985
+ // cleared on stop, so a restart left the old one armed to kill the new
986
+ // incarnation.) Reaping here is what frees the slot for everyone whose
987
+ // stop never arrived.
988
+ if (Date.now() >= (activeStreams.get(sessionName)?.expiresAt ?? 0)) {
989
+ (0, exports.stopStream)(sessionName);
990
+ return;
991
+ }
917
992
  const captured = capturePane(sessionName, true, STREAM_CAPTURE_LINES);
918
993
  if (!captured || captured.content === lastContent) {
919
994
  stats.skipped++;
@@ -966,9 +1041,15 @@ const handleStreamControl = (req, send) => {
966
1041
  });
967
1042
  }, STREAM_INTERVAL_MS);
968
1043
  timer.unref?.();
969
- activeStreams.set(sessionName, { timer, stats });
970
- const expiry = setTimeout(() => (0, exports.stopStream)(sessionName), STREAM_MAX_MS);
971
- expiry.unref?.();
1044
+ // A renewing subscriber gets the short lease; anything older keeps the
1045
+ // 5-minute orphan guard so a version-skewed client isn't cut off mid-view.
1046
+ const renewing = !!req.renew;
1047
+ activeStreams.set(sessionName, {
1048
+ timer,
1049
+ stats,
1050
+ expiresAt: Date.now() + leaseFor(renewing),
1051
+ renewing,
1052
+ });
972
1053
  return true;
973
1054
  };
974
1055
  exports.handleStreamControl = handleStreamControl;
@@ -1644,8 +1725,6 @@ const resolveWsUrl = (args, config, baseUrl) => {
1644
1725
  };
1645
1726
  exports.resolveWsUrl = resolveWsUrl;
1646
1727
  // ── Singleton guard (PID file) ──────────────────────────────────────
1647
- const ZEPH_DIR = (0, path_1.join)((0, os_1.homedir)(), '.zeph');
1648
- const LISTENER_PID_FILE = (0, path_1.join)(ZEPH_DIR, 'listener.pid');
1649
1728
  /**
1650
1729
  * Whether another `zeph listener` is already running on this machine.
1651
1730
  * The wrapper's autostart and a user typing `zeph listener` by hand can
@@ -1656,38 +1735,54 @@ const LISTENER_PID_FILE = (0, path_1.join)(ZEPH_DIR, 'listener.pid');
1656
1735
  * wrapper can recover from crashes without manual cleanup.
1657
1736
  */
1658
1737
  const otherListenerAlive = () => {
1659
- try {
1660
- const pid = Number((0, fs_1.readFileSync)(LISTENER_PID_FILE, 'utf-8').trim());
1661
- if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid)
1662
- return null;
1663
- process.kill(pid, 0); // existence check, throws if dead
1664
- return pid;
1665
- }
1666
- catch {
1667
- return null;
1668
- }
1738
+ const pid = (0, listener_process_js_1.runningListenerPid)();
1739
+ return pid === null || pid === process.pid ? null : pid;
1669
1740
  };
1670
1741
  const writeListenerPid = () => {
1671
1742
  try {
1672
- (0, fs_1.mkdirSync)(ZEPH_DIR, { recursive: true });
1673
- (0, fs_1.writeFileSync)(LISTENER_PID_FILE, String(process.pid));
1743
+ // Stamp the version alongside the pid: `npm i -g` swaps the package
1744
+ // on disk without touching this already-running process, and the
1745
+ // stamp is how `zeph cc` notices the drift and restarts us.
1746
+ (0, listener_process_js_1.writeListenerRuntime)(config_js_1.VERSION);
1674
1747
  }
1675
1748
  catch (err) {
1676
- log(`! could not write ${LISTENER_PID_FILE}: ${err.message}`);
1749
+ log(`! could not write ${listener_process_js_1.LISTENER_PID_FILE}: ${err.message}`);
1677
1750
  }
1678
1751
  };
1679
- const removeListenerPid = () => {
1680
- try {
1681
- if (!(0, fs_1.existsSync)(LISTENER_PID_FILE))
1682
- return;
1683
- // Only remove our own pid file — don't trample a successor's.
1684
- const pid = Number((0, fs_1.readFileSync)(LISTENER_PID_FILE, 'utf-8').trim());
1685
- if (pid === process.pid)
1686
- (0, fs_1.unlinkSync)(LISTENER_PID_FILE);
1752
+ const removeListenerPid = () => (0, listener_process_js_1.clearListenerRuntime)(process.pid);
1753
+ /**
1754
+ * `--stop` / `--restart`: the daemon is a background service, so the only
1755
+ * way to replace a stale one used to be hunting its pid by hand. `--restart`
1756
+ * relaunches detached (not in the caller's foreground) because restarting is
1757
+ * an ops action — the user wants their shell back, not a new daemon pinned
1758
+ * to it.
1759
+ */
1760
+ const handleListenerLifecycle = async (restart) => {
1761
+ const pid = otherListenerAlive();
1762
+ if (pid) {
1763
+ const stopped = await (0, listener_process_js_1.stopListener)(pid);
1764
+ console.log(`zeph listener: stopped pid ${pid}${stopped ? '' : ' (forced)'}`);
1765
+ }
1766
+ else {
1767
+ console.log('zeph listener: no listener running');
1768
+ // A pid file left behind by a crashed daemon would otherwise keep
1769
+ // tripping the singleton guard on the next start.
1770
+ (0, listener_process_js_1.clearStaleListenerRuntime)();
1687
1771
  }
1688
- catch { /* best-effort */ }
1772
+ if (!restart)
1773
+ return 0;
1774
+ if (!(0, listener_process_js_1.spawnListenerDetached)()) {
1775
+ console.error('zeph listener: could not resolve the cli entry to respawn — run `zeph listener` manually.');
1776
+ return 1;
1777
+ }
1778
+ console.log(`zeph listener: restarted in background (log: ${listener_process_js_1.LISTENER_LOG_FILE})`);
1779
+ return 0;
1689
1780
  };
1690
1781
  const handleListener = async (args) => {
1782
+ // Lifecycle flags run before verifyTmux — stopping a daemon shouldn't
1783
+ // require a healthy tmux.
1784
+ if (args.stop === true || args.restart === true)
1785
+ return handleListenerLifecycle(args.restart === true);
1691
1786
  verifyTmux();
1692
1787
  // Refuse to start when another listener is already running. The
1693
1788
  // wrapper's autostart calls us blindly on every `zeph cc`; the user
@@ -1723,7 +1818,10 @@ const handleListener = async (args) => {
1723
1818
  }
1724
1819
  writeListenerPid();
1725
1820
  process.on('exit', removeListenerPid);
1726
- log(`zeph listener starting ${wsUrl}`);
1821
+ // Version on the first line: the only way to tell which build a
1822
+ // long-lived daemon is actually running (an `npm i -g` days ago says
1823
+ // nothing about the process that's been up since before it).
1824
+ log(`zeph listener starting — v${config_js_1.VERSION} — ${wsUrl}`);
1727
1825
  log(`device=${(0, exports.computeListenerDeviceId)()} host=${(0, os_1.hostname)()} pid=${process.pid}`);
1728
1826
  log("Waiting for 'agent.command' pushes from the phone picker. Ctrl-C to stop.");
1729
1827
  // Heartbeat memory log — once an hour. Lets the user (and us) spot
package/dist/wrapper.d.ts CHANGED
@@ -17,6 +17,31 @@ export declare const tmuxSessionName: (project: string) => string;
17
17
  * dependency-free against the user's running tmux server.
18
18
  */
19
19
  export declare const findAvailableSession: (base: string) => string;
20
+ /**
21
+ * Whether the daemon on record is running a different build than the one
22
+ * we're launching from. `npm i -g @zeph-to/cli` swaps dist/ but leaves the
23
+ * live process untouched, so a daemon can stay days behind the installed
24
+ * package — answering pushes (chat looks fine) while silently ignoring every
25
+ * message subtype added since it booted. That is exactly how a pre-1.24
26
+ * listener left the phone's live terminal spinning until a reboot.
27
+ *
28
+ * A missing stamp means the daemon predates version stamping, which puts it
29
+ * behind by definition — treat unknown as drifted.
30
+ *
31
+ * Strictly "installed is newer", never a plain inequality: one account can
32
+ * have several installs at different versions (nvm node versions, a repo
33
+ * build alongside the global one), and `!==` would make each `zeph cc` kill
34
+ * and respawn the other's daemon forever. An older `zeph cc` leaves a newer
35
+ * daemon alone — downgrading it would reintroduce the very gap this closes.
36
+ */
37
+ export declare const listenerVersionDrifted: (running: string | null, installed: string) => boolean;
38
+ /**
39
+ * Make sure the phone-bridge daemon is running AND is the build we just
40
+ * launched from. `zeph cc` is the right moment to replace a drifted one: the
41
+ * user is starting fresh work rather than mid-task, so the ~1s restart window
42
+ * (in which a push would be dropped — WS fan-out has no queue) costs nothing.
43
+ */
44
+ export declare const ensureListenerRunning: () => Promise<void>;
20
45
  /**
21
46
  * Launch the agent in a named tmux session (or directly if nested) and
22
47
  * forward its exit code. `extra` is appended to the agent invocation, so
@@ -1 +1 @@
1
- {"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../src/wrapper.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAOtD,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,QAAO,MAapC,CAAC;AAEF,+DAA+D;AAC/D,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,KAAG,MAA2B,CAAC;AAI9E;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,MAenD,CAAC;AA6HF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,WAAW,EAAE,QAAO,MAAM,EAAO,KAAG,OAAO,CAAC,MAAM,CAmC3F,CAAC"}
1
+ {"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../src/wrapper.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAOtD,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,QAAO,MAapC,CAAC;AAEF,+DAA+D;AAC/D,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,KAAG,MAA2B,CAAC;AAI9E;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,MAenD,CAAC;AAmCF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,sBAAsB,GAAI,SAAS,MAAM,GAAG,IAAI,EAAE,WAAW,MAAM,KAAG,OAChC,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAa,OAAO,CAAC,IAAI,CAa1D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAU,OAAO,WAAW,EAAE,QAAO,MAAM,EAAO,KAAG,OAAO,CAAC,MAAM,CAmCjG,CAAC"}
package/dist/wrapper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.handleAgentSession = exports.findAvailableSession = exports.tmuxSessionName = exports.detectProjectName = void 0;
3
+ exports.handleAgentSession = exports.ensureListenerRunning = exports.listenerVersionDrifted = exports.findAvailableSession = exports.tmuxSessionName = exports.detectProjectName = void 0;
4
4
  /**
5
5
  * `zeph cc` / `zeph codex` / `zeph gemini` — spawn an agent inside a named
6
6
  * tmux session so the resident listener (`zeph listener`) can address it
@@ -13,10 +13,10 @@ exports.handleAgentSession = exports.findAvailableSession = exports.tmuxSessionN
13
13
  * agent directly — letting power users keep their own multiplexer setup.
14
14
  */
15
15
  const child_process_1 = require("child_process");
16
- const fs_1 = require("fs");
17
- const os_1 = require("os");
18
16
  const path_1 = require("path");
17
+ const check_update_js_1 = require("./check-update.js");
19
18
  const config_js_1 = require("./config.js");
19
+ const listener_process_js_1 = require("./listener-process.js");
20
20
  const FALLBACK_NAME = 'project';
21
21
  /** basename(), with a stable fallback for edge paths like `/`. */
22
22
  const safeBasename = (path) => (0, path_1.basename)(path) || FALLBACK_NAME;
@@ -99,111 +99,60 @@ const targetForAgent = (agent, extra) => {
99
99
  return { cmd: 'tmux', args: ['new', '-A', '-s', session, shellCmd] };
100
100
  };
101
101
  // ── Background listener auto-start ────────────────────────────────────
102
- const ZEPH_DIR = (0, path_1.join)((0, os_1.homedir)(), '.zeph');
103
- const LISTENER_PID_FILE = (0, path_1.join)(ZEPH_DIR, 'listener.pid');
104
- const LISTENER_LOG_FILE = (0, path_1.join)(ZEPH_DIR, 'listener.log');
105
- /** True when the PID file points at a still-alive process. */
106
- const listenerAlive = () => {
107
- try {
108
- const pid = Number((0, fs_1.readFileSync)(LISTENER_PID_FILE, 'utf-8').trim());
109
- if (!Number.isFinite(pid) || pid <= 0)
110
- return false;
111
- // Signal 0 = existence check; throws when the process is gone.
112
- process.kill(pid, 0);
113
- return true;
114
- }
115
- catch {
116
- return false;
117
- }
118
- };
119
102
  /**
120
- * Path to the running cli.js entry. wrapper.js sits next to cli.js in
121
- * dist/, so __dirname resolves it directly independent of how the
122
- * user invoked us.
103
+ * Whether the daemon on record is running a different build than the one
104
+ * we're launching from. `npm i -g @zeph-to/cli` swaps dist/ but leaves the
105
+ * live process untouched, so a daemon can stay days behind the installed
106
+ * package — answering pushes (chat looks fine) while silently ignoring every
107
+ * message subtype added since it booted. That is exactly how a pre-1.24
108
+ * listener left the phone's live terminal spinning until a reboot.
123
109
  *
124
- * `process.argv[1]` is unreliable here: when `zeph` runs via the
125
- * npm-installed bin shim (`/usr/local/bin/zeph` cli.js via a wrapper
126
- * script), argv[1] is the shim path (`.../bin/zeph`), NOT `cli.js`.
127
- * That made the original `/cli\\.(js|ts|mjs|cjs)$/` check silently
128
- * reject the entry and the autospawn never fired — exactly the bug
129
- * the user hit ('원래 싱글톤으로 됐었잖아' — yes, on the local alias
130
- * path where argv[1] IS cli.js; the bug only surfaced once the user
131
- * switched to the global npm install).
110
+ * A missing stamp means the daemon predates version stamping, which puts it
111
+ * behind by definition treat unknown as drifted.
132
112
  *
133
- * Fall back to argv[1] only when the __dirname-relative file doesn't
134
- * exist (some packaging where dist layout differs).
113
+ * Strictly "installed is newer", never a plain inequality: one account can
114
+ * have several installs at different versions (nvm node versions, a repo
115
+ * build alongside the global one), and `!==` would make each `zeph cc` kill
116
+ * and respawn the other's daemon forever. An older `zeph cc` leaves a newer
117
+ * daemon alone — downgrading it would reintroduce the very gap this closes.
135
118
  */
136
- const resolveCliPath = () => {
137
- const local = (0, path_1.join)(__dirname, 'cli.js');
138
- if ((0, fs_1.existsSync)(local))
139
- return local;
140
- const entry = process.argv[1];
141
- if (entry && /cli\.(js|ts|mjs|cjs)$/.test(entry))
142
- return entry;
143
- return null;
144
- };
119
+ const listenerVersionDrifted = (running, installed) => running === null || (0, check_update_js_1.isNewer)(installed, running);
120
+ exports.listenerVersionDrifted = listenerVersionDrifted;
145
121
  /**
146
- * Spawn `zeph listener` in the background if it isn't already running on
147
- * this machine. The intent is that the user only ever has to know about
148
- * `zeph cc` the phone-to-tmux bridge tags along automatically. Output
149
- * goes to `~/.zeph/listener.log` so it isn't lost on detach; the listener
150
- * itself writes its own PID to `~/.zeph/listener.pid` on startup and
151
- * removes it on graceful exit, so subsequent `zeph cc` invocations skip
152
- * the spawn when a listener is already up.
153
- *
154
- * Failure here is non-fatal — `zeph cc` still launches the agent. The
155
- * user just loses the phone-bridge feature until they restart.
122
+ * Make sure the phone-bridge daemon is running AND is the build we just
123
+ * launched from. `zeph cc` is the right moment to replace a drifted one: the
124
+ * user is starting fresh work rather than mid-task, so the ~1s restart window
125
+ * (in which a push would be dropped WS fan-out has no queue) costs nothing.
156
126
  */
157
- /**
158
- * Rotate the listener log once it grows past 5 MB. The daemon runs for
159
- * days and writes 2-3 lines per 5-s cycle, so without rotation the file
160
- * climbs into the tens of megabytes range pretty quickly. We keep the
161
- * previous run's tail under `.old` for post-mortem and start fresh.
162
- */
163
- const LISTENER_LOG_MAX_BYTES = 5 * 1024 * 1024;
164
- const rotateListenerLogIfLarge = () => {
165
- try {
166
- if (!(0, fs_1.existsSync)(LISTENER_LOG_FILE))
167
- return;
168
- if ((0, fs_1.statSync)(LISTENER_LOG_FILE).size <= LISTENER_LOG_MAX_BYTES)
127
+ const ensureListenerRunning = async () => {
128
+ const pid = (0, listener_process_js_1.runningListenerPid)();
129
+ if (pid !== null) {
130
+ const running = (0, listener_process_js_1.runningListenerVersion)();
131
+ if (!(0, exports.listenerVersionDrifted)(running, config_js_1.VERSION))
169
132
  return;
170
- (0, fs_1.renameSync)(LISTENER_LOG_FILE, LISTENER_LOG_FILE + '.old');
133
+ console.log(`zeph: listener ${running ?? '(pre-1.26)'} is stale — restarting on ${config_js_1.VERSION}`);
134
+ await (0, listener_process_js_1.stopListener)(pid);
171
135
  }
172
- catch { /* best-effort */ }
173
- };
174
- const ensureListenerRunning = () => {
175
- if (listenerAlive())
176
- return;
177
- const cliPath = resolveCliPath();
178
- if (!cliPath || !(0, fs_1.existsSync)(cliPath))
179
- return;
180
- try {
181
- (0, fs_1.mkdirSync)(ZEPH_DIR, { recursive: true });
182
- rotateListenerLogIfLarge();
183
- const out = (0, fs_1.openSync)(LISTENER_LOG_FILE, 'a');
184
- const child = (0, child_process_1.spawn)(process.execPath, [cliPath, 'listener'], {
185
- detached: true,
186
- stdio: ['ignore', out, out],
187
- env: { ...process.env, ZEPH_LISTENER_AUTOSTART: '1' },
188
- });
189
- child.unref();
190
- console.log(`zeph: listener autostarted in background (log: ${LISTENER_LOG_FILE})`);
136
+ if ((0, listener_process_js_1.spawnListenerDetached)()) {
137
+ if (pid === null)
138
+ console.log(`zeph: listener autostarted in background (log: ${listener_process_js_1.LISTENER_LOG_FILE})`);
191
139
  }
192
- catch (err) {
193
- console.error(`zeph: listener autostart failed: ${err.message}`);
140
+ else {
141
+ console.error('zeph: listener autostart failed — run `zeph listener` manually.');
194
142
  }
195
143
  };
144
+ exports.ensureListenerRunning = ensureListenerRunning;
196
145
  /**
197
146
  * Launch the agent in a named tmux session (or directly if nested) and
198
147
  * forward its exit code. `extra` is appended to the agent invocation, so
199
148
  * `zeph cc --resume foo` runs `claude --resume foo` inside the session.
200
149
  * Returns when the agent exits.
201
150
  */
202
- const handleAgentSession = (agent, extra = []) => {
203
- // Best-effort: make sure the phone-bridge daemon is running before we
204
- // launch the agent. The user shouldn't need to remember a second
205
- // command for the picker on their phone to work.
206
- ensureListenerRunning();
151
+ const handleAgentSession = async (agent, extra = []) => {
152
+ // Best-effort: make sure the phone-bridge daemon is running, and running
153
+ // the build we were launched from. The user shouldn't need to remember a
154
+ // second command for the picker on their phone to work.
155
+ await (0, exports.ensureListenerRunning)();
207
156
  return new Promise((resolve) => {
208
157
  const { cmd, args } = targetForAgent(agent.binary, extra);
209
158
  const start = Date.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/cli",
3
- "version": "1.25.0",
3
+ "version": "1.26.1",
4
4
  "description": "Zeph CLI + push notification SDK for AI agents",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",