@cluesmith/codev 2.0.0-rc.73 → 2.0.0-rc.74
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/dashboard/dist/assets/{index-CH_utkcW.js → index-b38SaXk5.js} +31 -31
- package/dashboard/dist/assets/{index-CH_utkcW.js.map → index-b38SaXk5.js.map} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dist/agent-farm/commands/spawn-worktree.js +1 -1
- package/dist/agent-farm/db/index.d.ts.map +1 -1
- package/dist/agent-farm/db/index.js +56 -2
- package/dist/agent-farm/db/index.js.map +1 -1
- package/dist/agent-farm/db/schema.d.ts +1 -1
- package/dist/agent-farm/db/schema.js +3 -3
- package/dist/agent-farm/servers/tower-instances.d.ts +6 -6
- package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-instances.js +47 -34
- package/dist/agent-farm/servers/tower-instances.js.map +1 -1
- package/dist/agent-farm/servers/tower-routes.d.ts +1 -1
- package/dist/agent-farm/servers/tower-routes.js +34 -34
- package/dist/agent-farm/servers/tower-server.js +17 -17
- package/dist/agent-farm/servers/tower-terminals.d.ts +8 -8
- package/dist/agent-farm/servers/tower-terminals.js +46 -46
- package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
- package/dist/agent-farm/servers/tower-types.d.ts +5 -4
- package/dist/agent-farm/servers/tower-types.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-utils.d.ts +7 -0
- package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-utils.js +21 -0
- package/dist/agent-farm/servers/tower-utils.js.map +1 -1
- package/dist/agent-farm/utils/shell.d.ts +1 -1
- package/dist/agent-farm/utils/shell.js +1 -1
- package/dist/commands/porch/next.js +4 -4
- package/dist/commands/porch/next.js.map +1 -1
- package/dist/terminal/pty-manager.d.ts +1 -1
- package/dist/terminal/pty-manager.js +5 -5
- package/dist/terminal/pty-session.d.ts +20 -20
- package/dist/terminal/pty-session.js +55 -55
- package/dist/terminal/session-manager.d.ts +15 -15
- package/dist/terminal/session-manager.js +34 -34
- package/dist/terminal/{shepherd-client.d.ts → shellper-client.d.ts} +10 -10
- package/dist/terminal/{shepherd-client.d.ts.map → shellper-client.d.ts.map} +1 -1
- package/dist/terminal/{shepherd-client.js → shellper-client.js} +20 -20
- package/dist/terminal/{shepherd-client.js.map → shellper-client.js.map} +1 -1
- package/dist/terminal/{shepherd-main.d.ts → shellper-main.d.ts} +3 -3
- package/dist/terminal/shellper-main.d.ts.map +1 -0
- package/dist/terminal/{shepherd-main.js → shellper-main.js} +17 -17
- package/dist/terminal/{shepherd-main.js.map → shellper-main.js.map} +1 -1
- package/dist/terminal/{shepherd-process.d.ts → shellper-process.d.ts} +8 -8
- package/dist/terminal/{shepherd-process.d.ts.map → shellper-process.d.ts.map} +1 -1
- package/dist/terminal/{shepherd-process.js → shellper-process.js} +11 -11
- package/dist/terminal/{shepherd-process.js.map → shellper-process.js.map} +1 -1
- package/dist/terminal/{shepherd-protocol.d.ts → shellper-protocol.d.ts} +5 -5
- package/dist/terminal/{shepherd-protocol.d.ts.map → shellper-protocol.d.ts.map} +1 -1
- package/dist/terminal/{shepherd-protocol.js → shellper-protocol.js} +5 -5
- package/dist/terminal/{shepherd-protocol.js.map → shellper-protocol.js.map} +1 -1
- package/dist/terminal/{shepherd-replay-buffer.d.ts → shellper-replay-buffer.d.ts} +4 -4
- package/dist/terminal/{shepherd-replay-buffer.d.ts.map → shellper-replay-buffer.d.ts.map} +1 -1
- package/dist/terminal/{shepherd-replay-buffer.js → shellper-replay-buffer.js} +4 -4
- package/dist/terminal/{shepherd-replay-buffer.js.map → shellper-replay-buffer.js.map} +1 -1
- package/package.json +1 -1
- package/skeleton/protocols/bugfix/builder-prompt.md +7 -1
- package/skeleton/protocols/maintain/protocol.md +3 -3
- package/skeleton/protocols/spir/builder-prompt.md +7 -0
- package/skeleton/resources/commands/agent-farm.md +2 -2
- package/skeleton/roles/builder.md +15 -1
- package/dist/terminal/shepherd-main.d.ts.map +0 -1
|
@@ -13,10 +13,10 @@ export class PtySession extends EventEmitter {
|
|
|
13
13
|
createdAt;
|
|
14
14
|
ringBuffer;
|
|
15
15
|
pty = null;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
shellperClient = null;
|
|
17
|
+
_shellperBacked = false;
|
|
18
|
+
_shellperSessionId = null;
|
|
19
|
+
shellperPid = -1;
|
|
20
20
|
cols;
|
|
21
21
|
rows;
|
|
22
22
|
exitCode;
|
|
@@ -68,67 +68,67 @@ export class PtySession extends EventEmitter {
|
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
* Attach a
|
|
72
|
-
* Data flows:
|
|
73
|
-
* User input flows: WebSocket → write() →
|
|
71
|
+
* Attach a shellper client as the I/O backend instead of node-pty.
|
|
72
|
+
* Data flows: shellper → ring buffer → WebSocket clients.
|
|
73
|
+
* User input flows: WebSocket → write() → shellper.
|
|
74
74
|
*/
|
|
75
|
-
|
|
76
|
-
this.
|
|
77
|
-
this.
|
|
78
|
-
this.
|
|
79
|
-
this.
|
|
75
|
+
attachShellper(client, replayData, shellperPid, shellperSessionId) {
|
|
76
|
+
this._shellperBacked = true;
|
|
77
|
+
this.shellperClient = client;
|
|
78
|
+
this.shellperPid = shellperPid;
|
|
79
|
+
this._shellperSessionId = shellperSessionId ?? null;
|
|
80
80
|
// Ensure log directory exists
|
|
81
81
|
if (this.diskLogEnabled) {
|
|
82
82
|
fs.mkdirSync(path.dirname(this.logPath), { recursive: true });
|
|
83
83
|
this.logFd = fs.openSync(this.logPath, 'a');
|
|
84
84
|
}
|
|
85
|
-
// Populate ring buffer with replay data from
|
|
85
|
+
// Populate ring buffer with replay data from shellper
|
|
86
86
|
if (replayData.length > 0) {
|
|
87
87
|
this.ringBuffer.pushData(replayData.toString('utf-8'));
|
|
88
88
|
}
|
|
89
|
-
// Forward
|
|
89
|
+
// Forward shellper data to ring buffer + WebSocket clients
|
|
90
90
|
client.on('data', (buf) => {
|
|
91
91
|
this.onPtyData(buf.toString('utf-8'));
|
|
92
92
|
});
|
|
93
|
-
// Handle
|
|
93
|
+
// Handle shellper exit (process inside shellper exited)
|
|
94
94
|
client.on('exit', (exitInfo) => {
|
|
95
95
|
this.exitCode = exitInfo.code;
|
|
96
96
|
this.emit('exit', exitInfo.code);
|
|
97
|
-
// For
|
|
98
|
-
// but doesn't clear the ring buffer (
|
|
99
|
-
this.
|
|
97
|
+
// For shellper-backed sessions, cleanup closes disk log and clients
|
|
98
|
+
// but doesn't clear the ring buffer (shellper may still have replay data)
|
|
99
|
+
this.cleanupShellper();
|
|
100
100
|
});
|
|
101
|
-
// Handle
|
|
101
|
+
// Handle shellper disconnect (socket closed without EXIT)
|
|
102
102
|
client.on('close', () => {
|
|
103
103
|
if (this.exitCode === undefined) {
|
|
104
|
-
// Unexpected disconnect —
|
|
104
|
+
// Unexpected disconnect — shellper may have crashed
|
|
105
105
|
this.exitCode = -1;
|
|
106
106
|
this.emit('exit', -1);
|
|
107
|
-
this.
|
|
107
|
+
this.cleanupShellper();
|
|
108
108
|
}
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
|
-
/** Whether this session is backed by a
|
|
112
|
-
get
|
|
113
|
-
return this.
|
|
111
|
+
/** Whether this session is backed by a shellper process. */
|
|
112
|
+
get shellperBacked() {
|
|
113
|
+
return this._shellperBacked;
|
|
114
114
|
}
|
|
115
|
-
/** The SessionManager session ID for this
|
|
116
|
-
get
|
|
117
|
-
return this.
|
|
115
|
+
/** The SessionManager session ID for this shellper-backed session, or null. */
|
|
116
|
+
get shellperSessionId() {
|
|
117
|
+
return this._shellperSessionId;
|
|
118
118
|
}
|
|
119
119
|
/**
|
|
120
|
-
* Detach from
|
|
120
|
+
* Detach from shellper client during Tower shutdown.
|
|
121
121
|
* Removes all event listeners so that SessionManager.shutdown() disconnecting
|
|
122
122
|
* the client doesn't cascade into exit events and SQLite row deletion.
|
|
123
123
|
*/
|
|
124
|
-
|
|
125
|
-
if (this.
|
|
126
|
-
this.
|
|
127
|
-
this.
|
|
124
|
+
detachShellper() {
|
|
125
|
+
if (this.shellperClient) {
|
|
126
|
+
this.shellperClient.removeAllListeners();
|
|
127
|
+
this.shellperClient = null;
|
|
128
128
|
}
|
|
129
|
-
this.
|
|
129
|
+
this.cleanupShellper();
|
|
130
130
|
}
|
|
131
|
-
|
|
131
|
+
cleanupShellper() {
|
|
132
132
|
if (this.disconnectTimer) {
|
|
133
133
|
clearTimeout(this.disconnectTimer);
|
|
134
134
|
this.disconnectTimer = null;
|
|
@@ -142,8 +142,8 @@ export class PtySession extends EventEmitter {
|
|
|
142
142
|
catch { /* ignore */ }
|
|
143
143
|
this.logFd = null;
|
|
144
144
|
}
|
|
145
|
-
// Note: ring buffer is NOT cleared —
|
|
146
|
-
// Note:
|
|
145
|
+
// Note: ring buffer is NOT cleared — shellper handles replay
|
|
146
|
+
// Note: shellper client is NOT disconnected — SessionManager owns that lifecycle
|
|
147
147
|
}
|
|
148
148
|
onPtyData(data) {
|
|
149
149
|
// Store in ring buffer
|
|
@@ -193,11 +193,11 @@ export class PtySession extends EventEmitter {
|
|
|
193
193
|
this.logFd = fs.openSync(this.logPath, 'a');
|
|
194
194
|
this.logBytes = 0;
|
|
195
195
|
}
|
|
196
|
-
/** Write user input to the PTY or
|
|
196
|
+
/** Write user input to the PTY or shellper. */
|
|
197
197
|
write(data) {
|
|
198
|
-
if (this.
|
|
199
|
-
if (this.
|
|
200
|
-
this.
|
|
198
|
+
if (this._shellperBacked) {
|
|
199
|
+
if (this.shellperClient && this.status === 'running') {
|
|
200
|
+
this.shellperClient.write(data);
|
|
201
201
|
}
|
|
202
202
|
return;
|
|
203
203
|
}
|
|
@@ -205,13 +205,13 @@ export class PtySession extends EventEmitter {
|
|
|
205
205
|
this.pty.write(data);
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
|
-
/** Resize the PTY or
|
|
208
|
+
/** Resize the PTY or shellper. */
|
|
209
209
|
resize(cols, rows) {
|
|
210
210
|
this.cols = cols;
|
|
211
211
|
this.rows = rows;
|
|
212
|
-
if (this.
|
|
213
|
-
if (this.
|
|
214
|
-
this.
|
|
212
|
+
if (this._shellperBacked) {
|
|
213
|
+
if (this.shellperClient && this.status === 'running') {
|
|
214
|
+
this.shellperClient.resize(cols, rows);
|
|
215
215
|
}
|
|
216
216
|
return;
|
|
217
217
|
}
|
|
@@ -219,13 +219,13 @@ export class PtySession extends EventEmitter {
|
|
|
219
219
|
this.pty.resize(cols, rows);
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
|
-
/** Kill the PTY process or send signal to
|
|
222
|
+
/** Kill the PTY process or send signal to shellper. */
|
|
223
223
|
kill() {
|
|
224
|
-
if (this.
|
|
225
|
-
if (this.
|
|
226
|
-
this.
|
|
224
|
+
if (this._shellperBacked) {
|
|
225
|
+
if (this.shellperClient && this.status === 'running') {
|
|
226
|
+
this.shellperClient.signal(15); // SIGTERM
|
|
227
227
|
}
|
|
228
|
-
this.
|
|
228
|
+
this.cleanupShellper();
|
|
229
229
|
return;
|
|
230
230
|
}
|
|
231
231
|
if (this.pty && this.status === 'running') {
|
|
@@ -263,12 +263,12 @@ export class PtySession extends EventEmitter {
|
|
|
263
263
|
}
|
|
264
264
|
return this.ringBuffer.getSince(sinceSeq);
|
|
265
265
|
}
|
|
266
|
-
/** Detach a WebSocket client. Starts disconnect timer if no clients remain (non-
|
|
266
|
+
/** Detach a WebSocket client. Starts disconnect timer if no clients remain (non-shellper only). */
|
|
267
267
|
detach(client) {
|
|
268
268
|
this.clients.delete(client);
|
|
269
|
-
//
|
|
269
|
+
// Shellper-backed sessions don't need a disconnect timer — the shellper
|
|
270
270
|
// keeps the process alive independently of WebSocket connections.
|
|
271
|
-
if (this.
|
|
271
|
+
if (this._shellperBacked)
|
|
272
272
|
return;
|
|
273
273
|
if (this.clients.size === 0 && this.status === 'running') {
|
|
274
274
|
this.disconnectTimer = setTimeout(() => {
|
|
@@ -285,8 +285,8 @@ export class PtySession extends EventEmitter {
|
|
|
285
285
|
return this.exitCode === undefined ? 'running' : 'exited';
|
|
286
286
|
}
|
|
287
287
|
get pid() {
|
|
288
|
-
if (this.
|
|
289
|
-
return this.
|
|
288
|
+
if (this._shellperBacked)
|
|
289
|
+
return this.shellperPid;
|
|
290
290
|
return this.pty?.pid ?? -1;
|
|
291
291
|
}
|
|
292
292
|
get info() {
|
|
@@ -299,7 +299,7 @@ export class PtySession extends EventEmitter {
|
|
|
299
299
|
status: this.status,
|
|
300
300
|
createdAt: this.createdAt,
|
|
301
301
|
exitCode: this.exitCode,
|
|
302
|
-
persistent: this.
|
|
302
|
+
persistent: this._shellperBacked,
|
|
303
303
|
};
|
|
304
304
|
}
|
|
305
305
|
get clientCount() {
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SessionManager: orchestrates
|
|
2
|
+
* SessionManager: orchestrates shellper process lifecycle.
|
|
3
3
|
*
|
|
4
4
|
* Responsibilities:
|
|
5
|
-
* - Spawn
|
|
6
|
-
* - Connect
|
|
5
|
+
* - Spawn shellper processes as detached children
|
|
6
|
+
* - Connect ShellperClient to each shellper
|
|
7
7
|
* - Kill sessions (SIGTERM → wait → SIGKILL)
|
|
8
8
|
* - Detect and clean up stale sockets
|
|
9
9
|
* - Auto-restart on exit (configurable per session)
|
|
10
|
-
* - Reconnect to existing
|
|
10
|
+
* - Reconnect to existing shellpers after Tower restart
|
|
11
11
|
*
|
|
12
12
|
* Process start time validation prevents PID reuse reconnection.
|
|
13
13
|
*/
|
|
14
14
|
import { EventEmitter } from 'node:events';
|
|
15
|
-
import { type
|
|
15
|
+
import { type IShellperClient } from './shellper-client.js';
|
|
16
16
|
export interface SessionManagerConfig {
|
|
17
17
|
socketDir: string;
|
|
18
|
-
|
|
18
|
+
shellperScript: string;
|
|
19
19
|
nodeExecutable: string;
|
|
20
20
|
}
|
|
21
21
|
export interface CreateSessionOptions {
|
|
@@ -45,16 +45,16 @@ export declare class SessionManager extends EventEmitter {
|
|
|
45
45
|
private sessions;
|
|
46
46
|
constructor(config: SessionManagerConfig);
|
|
47
47
|
/**
|
|
48
|
-
* Spawn a new
|
|
48
|
+
* Spawn a new shellper process and connect to it.
|
|
49
49
|
* Returns the connected client.
|
|
50
50
|
*/
|
|
51
|
-
createSession(opts: CreateSessionOptions): Promise<
|
|
51
|
+
createSession(opts: CreateSessionOptions): Promise<IShellperClient>;
|
|
52
52
|
/**
|
|
53
|
-
* Reconnect to an existing
|
|
53
|
+
* Reconnect to an existing shellper process after Tower restart.
|
|
54
54
|
* Validates PID is alive and start time matches.
|
|
55
|
-
* Returns connected client, or null if
|
|
55
|
+
* Returns connected client, or null if shellper is stale/dead.
|
|
56
56
|
*/
|
|
57
|
-
reconnectSession(sessionId: string, socketPath: string, pid: number, startTime: number, restartOptions?: ReconnectRestartOptions): Promise<
|
|
57
|
+
reconnectSession(sessionId: string, socketPath: string, pid: number, startTime: number, restartOptions?: ReconnectRestartOptions): Promise<IShellperClient | null>;
|
|
58
58
|
/**
|
|
59
59
|
* Kill a session: SIGTERM, wait 5s, SIGKILL if needed.
|
|
60
60
|
* Cleans up socket file and removes from session map.
|
|
@@ -63,7 +63,7 @@ export declare class SessionManager extends EventEmitter {
|
|
|
63
63
|
/**
|
|
64
64
|
* List all active sessions.
|
|
65
65
|
*/
|
|
66
|
-
listSessions(): Map<string,
|
|
66
|
+
listSessions(): Map<string, IShellperClient>;
|
|
67
67
|
/**
|
|
68
68
|
* Get session metadata (pid, startTime, socketPath) for a session.
|
|
69
69
|
*/
|
|
@@ -84,9 +84,9 @@ export declare class SessionManager extends EventEmitter {
|
|
|
84
84
|
*/
|
|
85
85
|
private probeSocket;
|
|
86
86
|
/**
|
|
87
|
-
* Disconnect from all sessions without killing
|
|
87
|
+
* Disconnect from all sessions without killing shellper processes.
|
|
88
88
|
* Per spec: "When Tower intentionally stops, Tower closes its socket
|
|
89
|
-
* connections to
|
|
89
|
+
* connections to shellpers. Shellpers continue running."
|
|
90
90
|
*/
|
|
91
91
|
shutdown(): void;
|
|
92
92
|
/**
|
|
@@ -96,7 +96,7 @@ export declare class SessionManager extends EventEmitter {
|
|
|
96
96
|
private removeDeadSession;
|
|
97
97
|
private getSocketPath;
|
|
98
98
|
private unlinkSocketIfExists;
|
|
99
|
-
private
|
|
99
|
+
private readShellperInfo;
|
|
100
100
|
private waitForSocket;
|
|
101
101
|
private isProcessAlive;
|
|
102
102
|
private waitForProcessExit;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SessionManager: orchestrates
|
|
2
|
+
* SessionManager: orchestrates shellper process lifecycle.
|
|
3
3
|
*
|
|
4
4
|
* Responsibilities:
|
|
5
|
-
* - Spawn
|
|
6
|
-
* - Connect
|
|
5
|
+
* - Spawn shellper processes as detached children
|
|
6
|
+
* - Connect ShellperClient to each shellper
|
|
7
7
|
* - Kill sessions (SIGTERM → wait → SIGKILL)
|
|
8
8
|
* - Detect and clean up stale sockets
|
|
9
9
|
* - Auto-restart on exit (configurable per session)
|
|
10
|
-
* - Reconnect to existing
|
|
10
|
+
* - Reconnect to existing shellpers after Tower restart
|
|
11
11
|
*
|
|
12
12
|
* Process start time validation prevents PID reuse reconnection.
|
|
13
13
|
*/
|
|
@@ -17,7 +17,7 @@ import net from 'node:net';
|
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import { EventEmitter } from 'node:events';
|
|
19
19
|
import { execFile } from 'node:child_process';
|
|
20
|
-
import {
|
|
20
|
+
import { ShellperClient } from './shellper-client.js';
|
|
21
21
|
export class SessionManager extends EventEmitter {
|
|
22
22
|
config;
|
|
23
23
|
sessions = new Map();
|
|
@@ -26,7 +26,7 @@ export class SessionManager extends EventEmitter {
|
|
|
26
26
|
this.config = config;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* Spawn a new
|
|
29
|
+
* Spawn a new shellper process and connect to it.
|
|
30
30
|
* Returns the connected client.
|
|
31
31
|
*/
|
|
32
32
|
async createSession(opts) {
|
|
@@ -35,8 +35,8 @@ export class SessionManager extends EventEmitter {
|
|
|
35
35
|
fs.mkdirSync(this.config.socketDir, { recursive: true, mode: 0o700 });
|
|
36
36
|
// Clean up any stale socket file
|
|
37
37
|
this.unlinkSocketIfExists(socketPath);
|
|
38
|
-
// Build config for
|
|
39
|
-
const
|
|
38
|
+
// Build config for shellper-main.js
|
|
39
|
+
const shellperConfig = JSON.stringify({
|
|
40
40
|
command: opts.command,
|
|
41
41
|
args: opts.args,
|
|
42
42
|
cwd: opts.cwd,
|
|
@@ -45,26 +45,26 @@ export class SessionManager extends EventEmitter {
|
|
|
45
45
|
rows: opts.rows,
|
|
46
46
|
socketPath,
|
|
47
47
|
});
|
|
48
|
-
// Spawn
|
|
49
|
-
const child = cpSpawn(this.config.nodeExecutable, [this.config.
|
|
48
|
+
// Spawn shellper as detached process
|
|
49
|
+
const child = cpSpawn(this.config.nodeExecutable, [this.config.shellperScript, shellperConfig], {
|
|
50
50
|
detached: true,
|
|
51
51
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
52
52
|
});
|
|
53
53
|
// Read PID + startTime from stdout
|
|
54
|
-
const info = await this.
|
|
54
|
+
const info = await this.readShellperInfo(child);
|
|
55
55
|
child.unref();
|
|
56
|
-
// Post-spawn setup with rollback: if anything fails after the
|
|
56
|
+
// Post-spawn setup with rollback: if anything fails after the shellper
|
|
57
57
|
// is spawned, kill the orphaned process and clean up the socket.
|
|
58
58
|
let client;
|
|
59
59
|
try {
|
|
60
60
|
// Wait briefly for socket to be ready
|
|
61
61
|
await this.waitForSocket(socketPath);
|
|
62
62
|
// Connect client
|
|
63
|
-
client = new
|
|
63
|
+
client = new ShellperClient(socketPath);
|
|
64
64
|
await client.connect();
|
|
65
65
|
}
|
|
66
66
|
catch (err) {
|
|
67
|
-
// Rollback: kill the orphaned
|
|
67
|
+
// Rollback: kill the orphaned shellper process
|
|
68
68
|
try {
|
|
69
69
|
process.kill(info.pid, 'SIGKILL');
|
|
70
70
|
}
|
|
@@ -99,13 +99,13 @@ export class SessionManager extends EventEmitter {
|
|
|
99
99
|
client.on('error', (err) => {
|
|
100
100
|
this.emit('session-error', opts.sessionId, err);
|
|
101
101
|
});
|
|
102
|
-
// Handle
|
|
102
|
+
// Handle shellper crash (socket disconnects without EXIT frame)
|
|
103
103
|
client.on('close', () => {
|
|
104
104
|
// If the session is still in the map (wasn't already cleaned up by exit/kill),
|
|
105
|
-
// the
|
|
105
|
+
// the shellper died without sending EXIT. Remove the dead session.
|
|
106
106
|
if (this.sessions.has(opts.sessionId)) {
|
|
107
107
|
this.removeDeadSession(opts.sessionId);
|
|
108
|
-
this.emit('session-error', opts.sessionId, new Error('
|
|
108
|
+
this.emit('session-error', opts.sessionId, new Error('Shellper disconnected unexpectedly'));
|
|
109
109
|
}
|
|
110
110
|
});
|
|
111
111
|
// Start restart reset timer if configured
|
|
@@ -115,9 +115,9 @@ export class SessionManager extends EventEmitter {
|
|
|
115
115
|
return client;
|
|
116
116
|
}
|
|
117
117
|
/**
|
|
118
|
-
* Reconnect to an existing
|
|
118
|
+
* Reconnect to an existing shellper process after Tower restart.
|
|
119
119
|
* Validates PID is alive and start time matches.
|
|
120
|
-
* Returns connected client, or null if
|
|
120
|
+
* Returns connected client, or null if shellper is stale/dead.
|
|
121
121
|
*/
|
|
122
122
|
async reconnectSession(sessionId, socketPath, pid, startTime, restartOptions) {
|
|
123
123
|
// Check if process is alive
|
|
@@ -141,7 +141,7 @@ export class SessionManager extends EventEmitter {
|
|
|
141
141
|
return null;
|
|
142
142
|
}
|
|
143
143
|
// Connect client
|
|
144
|
-
const client = new
|
|
144
|
+
const client = new ShellperClient(socketPath);
|
|
145
145
|
try {
|
|
146
146
|
await client.connect();
|
|
147
147
|
}
|
|
@@ -184,11 +184,11 @@ export class SessionManager extends EventEmitter {
|
|
|
184
184
|
client.on('error', (err) => {
|
|
185
185
|
this.emit('session-error', sessionId, err);
|
|
186
186
|
});
|
|
187
|
-
// Handle
|
|
187
|
+
// Handle shellper crash (socket disconnects without EXIT frame)
|
|
188
188
|
client.on('close', () => {
|
|
189
189
|
if (this.sessions.has(sessionId)) {
|
|
190
190
|
this.removeDeadSession(sessionId);
|
|
191
|
-
this.emit('session-error', sessionId, new Error('
|
|
191
|
+
this.emit('session-error', sessionId, new Error('Shellper disconnected unexpectedly'));
|
|
192
192
|
}
|
|
193
193
|
});
|
|
194
194
|
// Start restart reset timer if auto-restart is enabled
|
|
@@ -273,7 +273,7 @@ export class SessionManager extends EventEmitter {
|
|
|
273
273
|
return 0;
|
|
274
274
|
}
|
|
275
275
|
for (const file of files) {
|
|
276
|
-
if (!file.startsWith('
|
|
276
|
+
if (!file.startsWith('shellper-') || !file.endsWith('.sock'))
|
|
277
277
|
continue;
|
|
278
278
|
const fullPath = path.join(this.config.socketDir, file);
|
|
279
279
|
// Safety: reject symlinks
|
|
@@ -287,14 +287,14 @@ export class SessionManager extends EventEmitter {
|
|
|
287
287
|
catch {
|
|
288
288
|
continue;
|
|
289
289
|
}
|
|
290
|
-
// Extract session ID from filename:
|
|
291
|
-
const sessionId = file.replace('
|
|
290
|
+
// Extract session ID from filename: shellper-{sessionId}.sock
|
|
291
|
+
const sessionId = file.replace('shellper-', '').replace('.sock', '');
|
|
292
292
|
// Skip if we have an active session for this
|
|
293
293
|
if (this.sessions.has(sessionId))
|
|
294
294
|
continue;
|
|
295
|
-
// Probe the socket: try connecting to see if a
|
|
295
|
+
// Probe the socket: try connecting to see if a shellper is alive.
|
|
296
296
|
// If connection is refused, the socket is stale and safe to delete.
|
|
297
|
-
// If connection succeeds, a
|
|
297
|
+
// If connection succeeds, a shellper is still running — leave it alone.
|
|
298
298
|
const isAlive = await this.probeSocket(fullPath);
|
|
299
299
|
if (isAlive)
|
|
300
300
|
continue;
|
|
@@ -333,9 +333,9 @@ export class SessionManager extends EventEmitter {
|
|
|
333
333
|
});
|
|
334
334
|
}
|
|
335
335
|
/**
|
|
336
|
-
* Disconnect from all sessions without killing
|
|
336
|
+
* Disconnect from all sessions without killing shellper processes.
|
|
337
337
|
* Per spec: "When Tower intentionally stops, Tower closes its socket
|
|
338
|
-
* connections to
|
|
338
|
+
* connections to shellpers. Shellpers continue running."
|
|
339
339
|
*/
|
|
340
340
|
shutdown() {
|
|
341
341
|
for (const [id, session] of this.sessions) {
|
|
@@ -364,7 +364,7 @@ export class SessionManager extends EventEmitter {
|
|
|
364
364
|
this.unlinkSocketIfExists(session.socketPath);
|
|
365
365
|
}
|
|
366
366
|
getSocketPath(sessionId) {
|
|
367
|
-
return path.join(this.config.socketDir, `
|
|
367
|
+
return path.join(this.config.socketDir, `shellper-${sessionId}.sock`);
|
|
368
368
|
}
|
|
369
369
|
unlinkSocketIfExists(socketPath) {
|
|
370
370
|
try {
|
|
@@ -377,11 +377,11 @@ export class SessionManager extends EventEmitter {
|
|
|
377
377
|
// Doesn't exist — fine
|
|
378
378
|
}
|
|
379
379
|
}
|
|
380
|
-
|
|
380
|
+
readShellperInfo(child) {
|
|
381
381
|
return new Promise((resolve, reject) => {
|
|
382
382
|
let data = '';
|
|
383
383
|
const timeout = setTimeout(() => {
|
|
384
|
-
reject(new Error('Timed out reading
|
|
384
|
+
reject(new Error('Timed out reading shellper info from stdout'));
|
|
385
385
|
}, 10_000);
|
|
386
386
|
child.stdout.on('data', (chunk) => {
|
|
387
387
|
data += chunk.toString();
|
|
@@ -393,7 +393,7 @@ export class SessionManager extends EventEmitter {
|
|
|
393
393
|
resolve(info);
|
|
394
394
|
}
|
|
395
395
|
catch {
|
|
396
|
-
reject(new Error(`Invalid
|
|
396
|
+
reject(new Error(`Invalid shellper info JSON: ${data}`));
|
|
397
397
|
}
|
|
398
398
|
});
|
|
399
399
|
child.on('error', (err) => {
|
|
@@ -403,7 +403,7 @@ export class SessionManager extends EventEmitter {
|
|
|
403
403
|
child.on('exit', (code) => {
|
|
404
404
|
if (code !== null && code !== 0 && data === '') {
|
|
405
405
|
clearTimeout(timeout);
|
|
406
|
-
reject(new Error(`
|
|
406
|
+
reject(new Error(`Shellper exited with code ${code} before writing info`));
|
|
407
407
|
}
|
|
408
408
|
});
|
|
409
409
|
});
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* ShellperClient: Tower's connection to a single shellper process.
|
|
3
3
|
*
|
|
4
|
-
* Connects to a
|
|
4
|
+
* Connects to a shellper via Unix socket, performs HELLO/WELCOME handshake,
|
|
5
5
|
* and provides a typed API for sending/receiving frames. Emits events for
|
|
6
6
|
* data, exit, replay, and errors.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
|
-
* const client = new
|
|
9
|
+
* const client = new ShellperClient('/path/to/shellper.sock');
|
|
10
10
|
* const welcome = await client.connect();
|
|
11
11
|
* client.on('data', (buf) => { ... });
|
|
12
12
|
* client.write('ls\n');
|
|
13
13
|
* client.disconnect();
|
|
14
14
|
*/
|
|
15
15
|
import { EventEmitter } from 'node:events';
|
|
16
|
-
import { type WelcomeMessage, type SpawnMessage } from './
|
|
17
|
-
export interface
|
|
16
|
+
import { type WelcomeMessage, type SpawnMessage } from './shellper-protocol.js';
|
|
17
|
+
export interface IShellperClient extends EventEmitter {
|
|
18
18
|
connect(): Promise<WelcomeMessage>;
|
|
19
19
|
disconnect(): void;
|
|
20
20
|
write(data: string | Buffer): void;
|
|
@@ -26,7 +26,7 @@ export interface IShepherdClient extends EventEmitter {
|
|
|
26
26
|
waitForReplay(timeoutMs?: number): Promise<Buffer>;
|
|
27
27
|
readonly connected: boolean;
|
|
28
28
|
}
|
|
29
|
-
export declare class
|
|
29
|
+
export declare class ShellperClient extends EventEmitter implements IShellperClient {
|
|
30
30
|
private readonly socketPath;
|
|
31
31
|
private socket;
|
|
32
32
|
private _connected;
|
|
@@ -40,7 +40,7 @@ export declare class ShepherdClient extends EventEmitter implements IShepherdCli
|
|
|
40
40
|
private safeEmitError;
|
|
41
41
|
get connected(): boolean;
|
|
42
42
|
/**
|
|
43
|
-
* Connect to the
|
|
43
|
+
* Connect to the shellper, perform HELLO/WELCOME handshake.
|
|
44
44
|
* Resolves with the WelcomeMessage on success.
|
|
45
45
|
* Rejects on connection error or handshake failure.
|
|
46
46
|
*/
|
|
@@ -57,10 +57,10 @@ export declare class ShepherdClient extends EventEmitter implements IShepherdCli
|
|
|
57
57
|
getReplayData(): Buffer | null;
|
|
58
58
|
/**
|
|
59
59
|
* Wait for the REPLAY frame to arrive after connection.
|
|
60
|
-
* The
|
|
60
|
+
* The shellper sends REPLAY immediately after WELCOME, but they may
|
|
61
61
|
* arrive in separate reads. Returns the replay data, or empty Buffer
|
|
62
|
-
* if no REPLAY arrives within the timeout (
|
|
62
|
+
* if no REPLAY arrives within the timeout (shellper had nothing to replay).
|
|
63
63
|
*/
|
|
64
64
|
waitForReplay(timeoutMs?: number): Promise<Buffer>;
|
|
65
65
|
}
|
|
66
|
-
//# sourceMappingURL=
|
|
66
|
+
//# sourceMappingURL=shellper-client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"
|
|
1
|
+
{"version":3,"file":"shellper-client.d.ts","sourceRoot":"","sources":["../../src/terminal/shellper-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAcL,KAAK,cAAc,EAEnB,KAAK,YAAY,EAClB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACnD,OAAO,IAAI,OAAO,CAAC,cAAc,CAAC,CAAC;IACnC,UAAU,IAAI,IAAI,CAAC;IACnB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACnC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/B,IAAI,IAAI,IAAI,CAAC;IACb,aAAa,IAAI,MAAM,GAAG,IAAI,CAAC;IAC/B,aAAa,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,qBAAa,cAAe,SAAQ,YAAa,YAAW,eAAe;IAK7D,OAAO,CAAC,QAAQ,CAAC,UAAU;IAJvC,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,UAAU,CAAuB;gBAEZ,UAAU,EAAE,MAAM;IAI/C;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAMrB,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED;;;;OAIG;IACH,OAAO,IAAI,OAAO,CAAC,cAAc,CAAC;IAgGlC,OAAO,CAAC,WAAW;IAuCnB,UAAU,IAAI,IAAI;IAIlB,OAAO,CAAC,OAAO;IAQf,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAKlC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAKxC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAKzB,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI;IAK9B,IAAI,IAAI,IAAI;IAKZ,0DAA0D;IAC1D,aAAa,IAAI,MAAM,GAAG,IAAI;IAI9B;;;;;OAKG;IACH,aAAa,CAAC,SAAS,GAAE,MAAY,GAAG,OAAO,CAAC,MAAM,CAAC;CAgBxD"}
|