seshmux 0.1.3 → 0.1.5
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-build-manifest.json +1 -1
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/page.js +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/app/page-b9c3810608a2c3d4.js +1 -0
- package/.next/standalone/package.json +2 -2
- package/.next/standalone/seshmux-server.js +160 -124
- package/bin/seshmux.js +232 -4
- package/daemon/ensure.js +55 -0
- package/daemon/holder.js +248 -0
- package/daemon/index.js +4 -1
- package/daemon/pty-manager.js +341 -29
- package/package.json +2 -2
- package/.next/standalone/.next/static/chunks/app/page-7104066577bb8e5f.js +0 -1
- /package/.next/standalone/.next/static/{kPJ9M1iZAJK4qUIbW1h9b → INANHMG7kWM6Af6v_Vj9d}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{kPJ9M1iZAJK4qUIbW1h9b → INANHMG7kWM6Af6v_Vj9d}/_ssgManifest.js +0 -0
package/daemon/pty-manager.js
CHANGED
|
@@ -5,21 +5,221 @@
|
|
|
5
5
|
* Provider-agnostic: spawns whatever argv it is handed (args come from the
|
|
6
6
|
* server's provider.commands). NO agent binary names appear here.
|
|
7
7
|
*
|
|
8
|
-
* Two persistence tiers:
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
8
|
+
* Two persistence tiers, BOTH of which survive the daemon's death:
|
|
9
|
+
* - holder tier (default): a detached `daemon/holder.js` process owns the
|
|
10
|
+
* PTY and speaks NDJSON over `<configDir>/holders/<ptyId>.sock`. The daemon
|
|
11
|
+
* is just a client. Kill the daemon and the agent never notices; the next
|
|
12
|
+
* daemon re-adopts the holder under its ORIGINAL ptyId (rehydrateHolders).
|
|
11
13
|
* - tmux tier (tmuxName present): `tmux new-session -A -s seshmux-<name>`,
|
|
12
14
|
* so the session survives a daemon restart and can be re-hydrated from
|
|
13
|
-
* `tmux ls` on startup.
|
|
15
|
+
* `tmux ls` on startup. Unchanged.
|
|
16
|
+
*
|
|
17
|
+
* Externally (spawn/write/resize/kill/list/history RPCs, data/exit events, the
|
|
18
|
+
* ring buffer served on attach) nothing about this changed — the holder tier is
|
|
19
|
+
* internal re-plumbing. Daemon<->server protocol stays FROZEN at 1.
|
|
14
20
|
*
|
|
15
21
|
* Only dependency: @homebridge/node-pty-prebuilt-multiarch.
|
|
16
22
|
*/
|
|
17
23
|
|
|
18
24
|
const pty = require('@homebridge/node-pty-prebuilt-multiarch');
|
|
19
|
-
const { execFile } = require('node:child_process');
|
|
25
|
+
const { execFile, spawn: spawnProcess } = require('node:child_process');
|
|
26
|
+
const net = require('node:net');
|
|
27
|
+
const fs = require('node:fs');
|
|
28
|
+
const crypto = require('node:crypto');
|
|
20
29
|
const path = require('node:path');
|
|
21
30
|
const os = require('node:os');
|
|
22
|
-
const {
|
|
31
|
+
const {
|
|
32
|
+
TMUX_PREFIX,
|
|
33
|
+
RING_BUFFER_LINES,
|
|
34
|
+
RING_BUFFER_BYTES,
|
|
35
|
+
encode,
|
|
36
|
+
createDecoder,
|
|
37
|
+
} = require('./protocol');
|
|
38
|
+
|
|
39
|
+
const HOLDER_ENTRY = path.join(__dirname, 'holder.js');
|
|
40
|
+
// Connect retries while a freshly-spawned holder boots node + binds its socket.
|
|
41
|
+
const HOLDER_CONNECT_TRIES = 100;
|
|
42
|
+
const HOLDER_CONNECT_DELAY_MS = 100;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Path of a holder's unix socket. macOS caps sun_path at ~104 bytes, and a
|
|
46
|
+
* config dir can be arbitrarily deep (tests use mkdtemp under /var/folders/...),
|
|
47
|
+
* so fall back to a short /tmp name keyed by a hash of the holder dir when the
|
|
48
|
+
* natural path would overflow. The holder records the path it actually bound in
|
|
49
|
+
* its .json, so adoption never has to re-derive it.
|
|
50
|
+
*/
|
|
51
|
+
function holderSockPath(holderDir, ptyId) {
|
|
52
|
+
const natural = path.join(holderDir, ptyId + '.sock');
|
|
53
|
+
if (Buffer.byteLength(natural) <= 100) return natural;
|
|
54
|
+
const base = process.platform === 'win32' ? os.tmpdir() : '/tmp';
|
|
55
|
+
const h = crypto.createHash('sha1').update(holderDir).digest('hex').slice(0, 8);
|
|
56
|
+
return path.join(base, `smx-${h}-${ptyId}.sock`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function pidAlive(pid) {
|
|
60
|
+
try {
|
|
61
|
+
process.kill(pid, 0);
|
|
62
|
+
return true;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
return err && err.code === 'EPERM'; // alive, just not ours to signal
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function unlinkQuiet(p) {
|
|
69
|
+
try {
|
|
70
|
+
fs.unlinkSync(p);
|
|
71
|
+
} catch {
|
|
72
|
+
// ignore
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Client side of the holder link, shaped like a node-pty process (onData /
|
|
78
|
+
* onExit / write / resize / kill) so PtyManager entries and _wireProc don't
|
|
79
|
+
* care which tier they're on.
|
|
80
|
+
*
|
|
81
|
+
* Connects with retries (a just-spawned holder needs a moment to bind), queues
|
|
82
|
+
* writes until connected, and buffers inbound frames until onData/onExit are
|
|
83
|
+
* registered (adoption registers them AFTER awaiting ready()).
|
|
84
|
+
*/
|
|
85
|
+
class HolderClient {
|
|
86
|
+
constructor(sockPath, opts = {}) {
|
|
87
|
+
this._sockPath = sockPath;
|
|
88
|
+
this._tries = opts.tries || HOLDER_CONNECT_TRIES;
|
|
89
|
+
this._sock = null;
|
|
90
|
+
this._queue = [];
|
|
91
|
+
this._pendingData = [];
|
|
92
|
+
this._pendingExit = null;
|
|
93
|
+
this._onData = null;
|
|
94
|
+
this._onExit = null;
|
|
95
|
+
this._done = false; // exit delivered/queued — stop reconnecting
|
|
96
|
+
this._detached = false;
|
|
97
|
+
this._ready = false;
|
|
98
|
+
this._readyResolve = null;
|
|
99
|
+
this._readyPromise = new Promise((r) => {
|
|
100
|
+
this._readyResolve = r;
|
|
101
|
+
});
|
|
102
|
+
this._connect(0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** @returns {Promise<boolean>} true once the holder accepted us as ITS client. */
|
|
106
|
+
ready() {
|
|
107
|
+
return this._readyPromise;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_settleReady(ok) {
|
|
111
|
+
if (this._readyResolve) {
|
|
112
|
+
const r = this._readyResolve;
|
|
113
|
+
this._readyResolve = null;
|
|
114
|
+
r(ok);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_connect(attempt) {
|
|
119
|
+
const s = net.connect(this._sockPath);
|
|
120
|
+
const decoder = createDecoder();
|
|
121
|
+
s.on('connect', () => {
|
|
122
|
+
this._sock = s;
|
|
123
|
+
for (const frame of this._queue) s.write(frame);
|
|
124
|
+
this._queue = [];
|
|
125
|
+
});
|
|
126
|
+
s.on('data', (chunk) => {
|
|
127
|
+
for (const m of decoder.push(chunk.toString('utf8'))) this._handle(m);
|
|
128
|
+
});
|
|
129
|
+
s.on('error', () => {});
|
|
130
|
+
s.on('close', () => {
|
|
131
|
+
if (this._sock === s) this._sock = null;
|
|
132
|
+
if (this._done || this._detached) return;
|
|
133
|
+
// Attached-then-dropped means the holder itself died — its PTY died with
|
|
134
|
+
// it (master fd closed). Anything else is a not-yet-listening socket.
|
|
135
|
+
if (this._ready || attempt >= this._tries) {
|
|
136
|
+
this._settleReady(false);
|
|
137
|
+
this._fail(1);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
setTimeout(() => this._connect(attempt + 1), HOLDER_CONNECT_DELAY_MS);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_handle(msg) {
|
|
145
|
+
switch (msg && msg.event) {
|
|
146
|
+
case 'ready':
|
|
147
|
+
this._ready = true;
|
|
148
|
+
this._settleReady(true);
|
|
149
|
+
return;
|
|
150
|
+
case 'busy':
|
|
151
|
+
// Another daemon owns this holder. Never double-attach: give up on it.
|
|
152
|
+
// (Adoption checks ready() and drops the client before it ever becomes
|
|
153
|
+
// an entry; a spawn that somehow lost the race goes dead rather than
|
|
154
|
+
// silently mute.)
|
|
155
|
+
this._settleReady(false);
|
|
156
|
+
this._fail(1);
|
|
157
|
+
return;
|
|
158
|
+
case 'data':
|
|
159
|
+
if (this._onData) this._onData(msg.data);
|
|
160
|
+
else this._pendingData.push(msg.data);
|
|
161
|
+
return;
|
|
162
|
+
case 'exit':
|
|
163
|
+
this._fail(msg.code);
|
|
164
|
+
return;
|
|
165
|
+
default:
|
|
166
|
+
// ignore
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_fail(code) {
|
|
171
|
+
if (this._done) return;
|
|
172
|
+
this._done = true;
|
|
173
|
+
if (this._onExit) this._onExit({ exitCode: code });
|
|
174
|
+
else this._pendingExit = { exitCode: code };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_send(msg) {
|
|
178
|
+
const frame = encode(msg);
|
|
179
|
+
if (this._sock && !this._sock.destroyed) this._sock.write(frame);
|
|
180
|
+
else if (!this._done) this._queue.push(frame);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
onData(fn) {
|
|
184
|
+
this._onData = fn;
|
|
185
|
+
const pending = this._pendingData;
|
|
186
|
+
this._pendingData = [];
|
|
187
|
+
for (const d of pending) fn(d);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
onExit(fn) {
|
|
191
|
+
this._onExit = fn;
|
|
192
|
+
if (this._pendingExit) {
|
|
193
|
+
const e = this._pendingExit;
|
|
194
|
+
this._pendingExit = null;
|
|
195
|
+
fn(e);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
write(data) {
|
|
200
|
+
this._send({ method: 'write', data });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
resize(cols, rows) {
|
|
204
|
+
this._send({ method: 'resize', cols, rows });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
kill() {
|
|
208
|
+
this._send({ method: 'kill' });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Let go of the holder without touching its PTY (daemon shutting down). */
|
|
212
|
+
detach() {
|
|
213
|
+
this._detached = true;
|
|
214
|
+
if (this._sock) {
|
|
215
|
+
try {
|
|
216
|
+
this._sock.end(); // end(), not destroy(): flush a queued kill first
|
|
217
|
+
} catch {
|
|
218
|
+
// ignore
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
23
223
|
|
|
24
224
|
// Dead PTY entries (alive=false) linger so a recently-exited session can still
|
|
25
225
|
// be re-attached / rehydrated, then get swept once past this grace window —
|
|
@@ -141,7 +341,14 @@ class PtyManager {
|
|
|
141
341
|
this._nextId = 1;
|
|
142
342
|
/** listeners: (event) => void, where event is a {event,...} object */
|
|
143
343
|
this._onEvent = null;
|
|
144
|
-
this.
|
|
344
|
+
this._configDir = opts.configDir || defaultConfigDirTag();
|
|
345
|
+
this._configTag = this._configDir;
|
|
346
|
+
this._holderDir = path.join(this._configDir, 'holders');
|
|
347
|
+
// ID stability: surviving holders keep their ORIGINAL ptyId when adopted
|
|
348
|
+
// (the server and browser hold ptyIds). Reserve those ids synchronously
|
|
349
|
+
// HERE — rehydrateHolders() is async and the daemon's socket is already
|
|
350
|
+
// listening by then, so a spawn racing it must not hand out a colliding id.
|
|
351
|
+
this._reserveHolderIds();
|
|
145
352
|
// Unref'd so it never keeps the daemon process alive on its own.
|
|
146
353
|
this._sweepTimer = setInterval(() => void this._sweepDead(), SWEEP_INTERVAL_MS);
|
|
147
354
|
this._sweepTimer.unref();
|
|
@@ -166,9 +373,27 @@ class PtyManager {
|
|
|
166
373
|
}
|
|
167
374
|
}
|
|
168
375
|
|
|
169
|
-
/**
|
|
376
|
+
/** Bump _nextId past every ptyId already claimed by a holder on disk. */
|
|
377
|
+
_reserveHolderIds() {
|
|
378
|
+
let files = [];
|
|
379
|
+
try {
|
|
380
|
+
files = fs.readdirSync(this._holderDir);
|
|
381
|
+
} catch {
|
|
382
|
+
return; // no holders dir yet
|
|
383
|
+
}
|
|
384
|
+
for (const f of files) {
|
|
385
|
+
const m = /^pty-(\d+)\.json$/.exec(f);
|
|
386
|
+
if (m && Number(m[1]) >= this._nextId) this._nextId = Number(m[1]) + 1;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Stop the background sweep, and let go of holders WITHOUT killing them —
|
|
391
|
+
* their PTYs are the whole point: they outlive this daemon. */
|
|
170
392
|
close() {
|
|
171
393
|
clearInterval(this._sweepTimer);
|
|
394
|
+
for (const e of this._ptys.values()) {
|
|
395
|
+
if (e.proc && typeof e.proc.detach === 'function') e.proc.detach();
|
|
396
|
+
}
|
|
172
397
|
}
|
|
173
398
|
|
|
174
399
|
/** Register the sink that receives {event:'data'|'exit', ...} objects. */
|
|
@@ -285,6 +510,34 @@ class PtyManager {
|
|
|
285
510
|
return true;
|
|
286
511
|
}
|
|
287
512
|
|
|
513
|
+
/**
|
|
514
|
+
* Launch a detached holder for this PTY and return a node-pty-shaped client
|
|
515
|
+
* for it. detached + stdio:'ignore' + unref() + the holder's SIGHUP handler
|
|
516
|
+
* are what make `kill -9 <daemon>` a non-event for the agent.
|
|
517
|
+
*/
|
|
518
|
+
_spawnHolder({ ptyId, cwd, args, cols, rows }) {
|
|
519
|
+
fs.mkdirSync(this._holderDir, { recursive: true, mode: 0o700 });
|
|
520
|
+
const sock = holderSockPath(this._holderDir, ptyId);
|
|
521
|
+
const spec = {
|
|
522
|
+
holderDir: this._holderDir,
|
|
523
|
+
ptyId,
|
|
524
|
+
sock,
|
|
525
|
+
cwd,
|
|
526
|
+
args,
|
|
527
|
+
cols,
|
|
528
|
+
rows,
|
|
529
|
+
env: { SESHMUX_PTY_ID: ptyId },
|
|
530
|
+
};
|
|
531
|
+
const child = spawnProcess(process.execPath, [HOLDER_ENTRY, JSON.stringify(spec)], {
|
|
532
|
+
detached: true,
|
|
533
|
+
stdio: 'ignore',
|
|
534
|
+
cwd,
|
|
535
|
+
env: process.env,
|
|
536
|
+
});
|
|
537
|
+
child.unref();
|
|
538
|
+
return new HolderClient(sock);
|
|
539
|
+
}
|
|
540
|
+
|
|
288
541
|
/**
|
|
289
542
|
* Spawn a PTY running the given argv.
|
|
290
543
|
* @param {{cwd?:string, args:string[], cols?:number, rows?:number, tmuxName?:string}} params
|
|
@@ -304,8 +557,7 @@ class PtyManager {
|
|
|
304
557
|
// mapping. Additive env var, not a wire-protocol change.
|
|
305
558
|
const ptyId = this._nextPtyId();
|
|
306
559
|
|
|
307
|
-
let
|
|
308
|
-
let argv;
|
|
560
|
+
let proc;
|
|
309
561
|
let fullTmuxName = null;
|
|
310
562
|
if (tmuxName) {
|
|
311
563
|
// tmux tier: attach-or-create a named session running the argv.
|
|
@@ -324,34 +576,28 @@ class PtyManager {
|
|
|
324
576
|
// so its hook writes to a now-stale status file and that session
|
|
325
577
|
// degrades to heuristics until it's respawned — graceful, not fatal).
|
|
326
578
|
fullTmuxName = TMUX_PREFIX + tmuxName;
|
|
327
|
-
file = 'tmux';
|
|
328
579
|
const envFlags = ['-e', `SESHMUX_PTY_ID=${ptyId}`];
|
|
329
580
|
if (process.env.SESHMUX_CONFIG_DIR) {
|
|
330
581
|
envFlags.push('-e', `SESHMUX_CONFIG_DIR=${process.env.SESHMUX_CONFIG_DIR}`);
|
|
331
582
|
}
|
|
332
|
-
argv = ['new-session', '-A', '-s', fullTmuxName, ...envFlags, '--', ...args];
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
name: 'xterm-256color',
|
|
343
|
-
cols: columns,
|
|
344
|
-
rows: lines,
|
|
345
|
-
cwd: cwdResolved,
|
|
346
|
-
env: baseEnv,
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
if (fullTmuxName) {
|
|
583
|
+
const argv = ['new-session', '-A', '-s', fullTmuxName, ...envFlags, '--', ...args];
|
|
584
|
+
const baseEnv = tmuxEnv();
|
|
585
|
+
baseEnv.SESHMUX_PTY_ID = ptyId;
|
|
586
|
+
proc = pty.spawn('tmux', argv, {
|
|
587
|
+
name: 'xterm-256color',
|
|
588
|
+
cols: columns,
|
|
589
|
+
rows: lines,
|
|
590
|
+
cwd: cwdResolved,
|
|
591
|
+
env: baseEnv,
|
|
592
|
+
});
|
|
350
593
|
// Hide tmux's own status bar for THIS session only — seshmux draws its
|
|
351
594
|
// own statusbar, so the blue tmux chrome is redundant noise. Scoped to
|
|
352
595
|
// the session (not -g) because we share the user's default tmux server.
|
|
353
596
|
hideTmuxStatus(fullTmuxName);
|
|
354
597
|
markTmuxConfig(fullTmuxName, this._configTag);
|
|
598
|
+
} else {
|
|
599
|
+
// Holder tier: a detached process owns the PTY, we're only its client.
|
|
600
|
+
proc = this._spawnHolder({ ptyId, cwd: cwdResolved, args, cols: columns, rows: lines });
|
|
355
601
|
}
|
|
356
602
|
|
|
357
603
|
const entry = {
|
|
@@ -491,6 +737,72 @@ class PtyManager {
|
|
|
491
737
|
}
|
|
492
738
|
}
|
|
493
739
|
|
|
740
|
+
/**
|
|
741
|
+
* On daemon startup, adopt every surviving holder under its ORIGINAL ptyId.
|
|
742
|
+
*
|
|
743
|
+
* For each `<holderDir>/<ptyId>.json`:
|
|
744
|
+
* - pid dead -> crash leftovers; delete json + socket, no entry
|
|
745
|
+
* - socket refuses -> holder wedged; leave the files, no entry
|
|
746
|
+
* - {event:'busy'} -> another daemon already holds it; skip (no double-attach)
|
|
747
|
+
* - ready -> adopt: the holder replays its ring buffer, so bytes
|
|
748
|
+
* produced while NO daemon was attached still reach
|
|
749
|
+
* the client on reattach.
|
|
750
|
+
*/
|
|
751
|
+
async rehydrateHolders() {
|
|
752
|
+
let files = [];
|
|
753
|
+
try {
|
|
754
|
+
files = fs.readdirSync(this._holderDir);
|
|
755
|
+
} catch {
|
|
756
|
+
return this.count();
|
|
757
|
+
}
|
|
758
|
+
for (const f of files) {
|
|
759
|
+
if (!f.endsWith('.json')) continue;
|
|
760
|
+
const jsonPath = path.join(this._holderDir, f);
|
|
761
|
+
let meta;
|
|
762
|
+
try {
|
|
763
|
+
meta = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
764
|
+
} catch {
|
|
765
|
+
unlinkQuiet(jsonPath);
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (!meta || !meta.ptyId || !meta.pid || !meta.sock) {
|
|
769
|
+
unlinkQuiet(jsonPath);
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (this._ptys.has(meta.ptyId)) continue; // already ours
|
|
773
|
+
if (!pidAlive(meta.pid)) {
|
|
774
|
+
unlinkQuiet(jsonPath);
|
|
775
|
+
unlinkQuiet(meta.sock);
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
// Short retry budget: an existing holder is either listening now or isn't.
|
|
779
|
+
const client = new HolderClient(meta.sock, { tries: 3 });
|
|
780
|
+
if (!(await client.ready())) {
|
|
781
|
+
client.detach();
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
const entry = {
|
|
785
|
+
ptyId: meta.ptyId,
|
|
786
|
+
proc: client,
|
|
787
|
+
cwd: meta.cwd,
|
|
788
|
+
args: meta.args,
|
|
789
|
+
tmuxName: null,
|
|
790
|
+
cols: meta.cols || 80,
|
|
791
|
+
rows: meta.rows || 24,
|
|
792
|
+
alive: true,
|
|
793
|
+
deadAt: null,
|
|
794
|
+
ring: [],
|
|
795
|
+
ringLines: 0,
|
|
796
|
+
ringBytes: 0,
|
|
797
|
+
};
|
|
798
|
+
this._ptys.set(meta.ptyId, entry);
|
|
799
|
+
this._wireProc(entry); // registers onData -> flushes the replayed ring
|
|
800
|
+
const n = Number(String(meta.ptyId).replace('pty-', ''));
|
|
801
|
+
if (Number.isFinite(n) && n >= this._nextId) this._nextId = n + 1;
|
|
802
|
+
}
|
|
803
|
+
return this.count();
|
|
804
|
+
}
|
|
805
|
+
|
|
494
806
|
/**
|
|
495
807
|
* On daemon startup, re-hydrate tmux-tier sessions by attaching a fresh PTY
|
|
496
808
|
* to each existing `seshmux-` tmux session. Ring buffers start empty (tmux
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "seshmux",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Local-first mission control for AI coding agents (Claude Code + Codex)",
|
|
5
5
|
"bin": {
|
|
6
6
|
"seshmux": "bin/seshmux.js"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"predev": "node -e \"require('./daemon/ensure').ensureDaemon()\"",
|
|
13
|
-
"dev": "tsx server/index.ts",
|
|
13
|
+
"dev": "PORT=${PORT:-4800} tsx server/index.ts",
|
|
14
14
|
"build": "bash scripts/build-standalone.sh",
|
|
15
15
|
"start": "node bin/seshmux.js",
|
|
16
16
|
"lint:styles": "bash scripts/lint-styles.sh",
|