@wenrwa/marketplace-cli 0.7.3 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/bridge-launch-supervisor.test.d.ts +1 -0
- package/dist/__tests__/bridge-launch-supervisor.test.js +186 -0
- package/dist/bridge/launchSupervisor.d.ts +61 -0
- package/dist/bridge/launchSupervisor.js +157 -0
- package/dist/bridge/sessionManager.d.ts +6 -0
- package/dist/bridge/sessionManager.js +9 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const launchSupervisor_1 = require("../bridge/launchSupervisor");
|
|
4
|
+
const sessionManager_1 = require("../bridge/sessionManager");
|
|
5
|
+
const ID = '445be432-0000-4000-8000-000000000001';
|
|
6
|
+
// Verbatim shapes from Claude Code 2.1.289 (stderr is red-wrapped; the resume
|
|
7
|
+
// miss is drawn by the TUI with a truecolor escape).
|
|
8
|
+
const IN_USE = `\x1b[31mError: Session ID ${ID} is already in use.\x1b[39m\r\n`;
|
|
9
|
+
const NO_CONVO = `\x1b[?2026h\x1b[38;2;255;107;128mNo conversation found with session ID: ${ID}\x1b[39m\r\r\n`;
|
|
10
|
+
class FakePty {
|
|
11
|
+
args;
|
|
12
|
+
written = [];
|
|
13
|
+
killed = false;
|
|
14
|
+
dataCb = null;
|
|
15
|
+
exitCb = null;
|
|
16
|
+
constructor(args) {
|
|
17
|
+
this.args = args;
|
|
18
|
+
}
|
|
19
|
+
write(d) { this.written.push(d); }
|
|
20
|
+
resize() { }
|
|
21
|
+
kill() { this.killed = true; }
|
|
22
|
+
onData(cb) { this.dataCb = cb; }
|
|
23
|
+
onExit(cb) { this.exitCb = cb; }
|
|
24
|
+
emit(d) { this.dataCb?.(d); }
|
|
25
|
+
exit(code) { this.exitCb?.(code); }
|
|
26
|
+
}
|
|
27
|
+
function harness(args) {
|
|
28
|
+
const spawned = [];
|
|
29
|
+
const factory = {
|
|
30
|
+
spawn: (o) => {
|
|
31
|
+
const p = new FakePty(o.args ?? []);
|
|
32
|
+
spawned.push(p);
|
|
33
|
+
return p;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
let t = 0;
|
|
37
|
+
const logs = [];
|
|
38
|
+
const pty = (0, launchSupervisor_1.superviseLaunch)(factory, { cols: 80, rows: 24, command: 'claude', args }, { now: () => t, log: (m) => logs.push(m), settle: (fn) => fn(), windowMs: 10_000 });
|
|
39
|
+
const out = [];
|
|
40
|
+
const exits = [];
|
|
41
|
+
pty.onData((d) => out.push(d));
|
|
42
|
+
pty.onExit((c) => exits.push(c));
|
|
43
|
+
return { pty, spawned, out, exits, logs, advance: (ms) => { t += ms; } };
|
|
44
|
+
}
|
|
45
|
+
describe('launchSupervisor — pure helpers', () => {
|
|
46
|
+
it('session-in-use on --session-id → swap to --resume, keeping every other arg', () => {
|
|
47
|
+
expect((0, launchSupervisor_1.retryArgsFor)(['--dangerously-skip-permissions', '--settings', '/s.json', '--session-id', ID], IN_USE))
|
|
48
|
+
.toEqual(['--dangerously-skip-permissions', '--settings', '/s.json', '--resume', ID]);
|
|
49
|
+
});
|
|
50
|
+
it('no-conversation on --resume → swap to --session-id', () => {
|
|
51
|
+
expect((0, launchSupervisor_1.retryArgsFor)(['--resume', ID], NO_CONVO)).toEqual(['--session-id', ID]);
|
|
52
|
+
});
|
|
53
|
+
it('no session flag, or an unrelated error → no retry', () => {
|
|
54
|
+
expect((0, launchSupervisor_1.retryArgsFor)([], IN_USE)).toBeNull();
|
|
55
|
+
expect((0, launchSupervisor_1.retryArgsFor)(['--session-id', ID], 'Error: Invalid session ID. Must be a valid UUID.')).toBeNull();
|
|
56
|
+
expect((0, launchSupervisor_1.retryArgsFor)(['--resume', ID], IN_USE)).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
it('firstMeaningfulLine strips ANSI, "Error:" and stack frames', () => {
|
|
59
|
+
expect((0, launchSupervisor_1.firstMeaningfulLine)(IN_USE)).toBe(`Session ID ${ID} is already in use`);
|
|
60
|
+
expect((0, launchSupervisor_1.firstMeaningfulLine)('\r\n\x1b[1m\x1b[0m\r\nError: boom.\n at foo (x.js:1:1)\n')).toBe('boom');
|
|
61
|
+
expect((0, launchSupervisor_1.firstMeaningfulLine)('\x1b[?25h\r\n')).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
it('friendly line is one short sentence with no stack trace', () => {
|
|
64
|
+
const line = (0, launchSupervisor_1.friendlyStartFailure)('Error: kaput\n at a (b.js:1:1)\n at c (d.js:2:2)', 1);
|
|
65
|
+
expect(line).toContain("Claude couldn't start: kaput. Reopen the pane to try again.");
|
|
66
|
+
expect(line).not.toContain('at a (');
|
|
67
|
+
expect((0, launchSupervisor_1.friendlyStartFailure)('', 3)).toContain("Claude couldn't start: it exited with code 3.");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('launchSupervisor — supervised PTY', () => {
|
|
71
|
+
it('session-in-use → exactly one relaunch with --resume; the healthy retry is passed through', () => {
|
|
72
|
+
const h = harness(['--session-id', ID]);
|
|
73
|
+
h.spawned[0].emit(IN_USE);
|
|
74
|
+
h.advance(800);
|
|
75
|
+
h.spawned[0].exit(1);
|
|
76
|
+
expect(h.spawned).toHaveLength(2);
|
|
77
|
+
expect(h.spawned[1].args).toEqual(['--resume', ID]);
|
|
78
|
+
expect(h.exits).toEqual([]); // the pane never sees the first exit
|
|
79
|
+
expect(h.out.join('')).not.toContain("couldn't start");
|
|
80
|
+
h.pty.write('hi');
|
|
81
|
+
expect(h.spawned[1].written).toEqual(['hi']);
|
|
82
|
+
h.spawned[1].emit('welcome back');
|
|
83
|
+
expect(h.out[h.out.length - 1]).toBe('welcome back');
|
|
84
|
+
expect(h.logs.join('\n')).toMatch(/already in use/);
|
|
85
|
+
});
|
|
86
|
+
it('resume-missing → exactly one relaunch with --session-id', () => {
|
|
87
|
+
const h = harness(['--dangerously-skip-permissions', '--resume', ID]);
|
|
88
|
+
h.spawned[0].emit(NO_CONVO);
|
|
89
|
+
h.spawned[0].exit(1);
|
|
90
|
+
expect(h.spawned).toHaveLength(2);
|
|
91
|
+
expect(h.spawned[1].args).toEqual(['--dangerously-skip-permissions', '--session-id', ID]);
|
|
92
|
+
expect(h.exits).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
it('never loops: a retry that fails the same way gets the friendly line and the exit, no third spawn', () => {
|
|
95
|
+
const h = harness(['--session-id', ID]);
|
|
96
|
+
h.spawned[0].emit(IN_USE);
|
|
97
|
+
h.spawned[0].exit(1);
|
|
98
|
+
h.spawned[1].emit(NO_CONVO);
|
|
99
|
+
h.spawned[1].exit(1);
|
|
100
|
+
expect(h.spawned).toHaveLength(2);
|
|
101
|
+
expect(h.exits).toEqual([1]);
|
|
102
|
+
expect(h.out.join('')).toContain(`Claude couldn't start: No conversation found with session ID: ${ID}. Reopen the pane to try again.`);
|
|
103
|
+
});
|
|
104
|
+
it('generic fast non-zero exit → friendly line, then the exit; raw error kept in the log', () => {
|
|
105
|
+
const h = harness(['--session-id', ID]);
|
|
106
|
+
h.spawned[0].emit('\x1b[31mError: Invalid session ID. Must be a valid UUID.\x1b[39m\r\n');
|
|
107
|
+
h.advance(300);
|
|
108
|
+
h.spawned[0].exit(1);
|
|
109
|
+
expect(h.spawned).toHaveLength(1);
|
|
110
|
+
expect(h.exits).toEqual([1]);
|
|
111
|
+
const last = h.out[h.out.length - 1];
|
|
112
|
+
expect(last).toContain("Claude couldn't start: Invalid session ID. Must be a valid UUID. Reopen the pane to try again.");
|
|
113
|
+
expect(h.logs.join('\n')).toContain('Error: Invalid session ID. Must be a valid UUID.');
|
|
114
|
+
});
|
|
115
|
+
it('a long-running session that exits later is untouched — no retry, no friendly line', () => {
|
|
116
|
+
const h = harness(['--session-id', ID]);
|
|
117
|
+
h.spawned[0].emit('working...');
|
|
118
|
+
h.advance(60_000);
|
|
119
|
+
h.spawned[0].emit(IN_USE); // even matching text, outside the window
|
|
120
|
+
h.spawned[0].exit(1);
|
|
121
|
+
expect(h.spawned).toHaveLength(1);
|
|
122
|
+
expect(h.exits).toEqual([1]);
|
|
123
|
+
expect(h.out.join('')).not.toContain("couldn't start");
|
|
124
|
+
expect(h.logs).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
it('a fast clean exit (code 0) passes straight through', () => {
|
|
127
|
+
const h = harness(['--resume', ID]);
|
|
128
|
+
h.spawned[0].exit(0);
|
|
129
|
+
expect(h.spawned).toHaveLength(1);
|
|
130
|
+
expect(h.exits).toEqual([0]);
|
|
131
|
+
expect(h.out).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
it('a bridge kill is never retried or annotated', () => {
|
|
134
|
+
const h = harness(['--session-id', ID]);
|
|
135
|
+
h.spawned[0].emit(IN_USE);
|
|
136
|
+
h.pty.kill();
|
|
137
|
+
expect(h.spawned[0].killed).toBe(true);
|
|
138
|
+
h.spawned[0].exit(1);
|
|
139
|
+
expect(h.spawned).toHaveLength(1);
|
|
140
|
+
expect(h.exits).toEqual([1]);
|
|
141
|
+
expect(h.out.join('')).not.toContain("couldn't start");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('launchSupervisor — wired through SessionManager', () => {
|
|
145
|
+
const fakeSnapshot = () => ({
|
|
146
|
+
write: () => Promise.resolve(),
|
|
147
|
+
resize: () => { },
|
|
148
|
+
serialize: () => Promise.resolve(''),
|
|
149
|
+
dispose: () => { },
|
|
150
|
+
});
|
|
151
|
+
function manager() {
|
|
152
|
+
const spawned = [];
|
|
153
|
+
const factory = { spawn: (o) => { const p = new FakePty(o.args ?? []); spawned.push(p); return p; } };
|
|
154
|
+
const mgr = new sessionManager_1.SessionManager(factory, {
|
|
155
|
+
snapshotFactory: fakeSnapshot,
|
|
156
|
+
launchSupervisor: { now: () => 0, log: () => { }, settle: (fn) => fn() },
|
|
157
|
+
});
|
|
158
|
+
const exits = [];
|
|
159
|
+
mgr.setOnSessionExit((id, code) => exits.push([id, code]));
|
|
160
|
+
return { mgr, spawned, exits };
|
|
161
|
+
}
|
|
162
|
+
it('a claude launch self-heals inside one session: same id, one exit only when the retry dies', async () => {
|
|
163
|
+
const { mgr, spawned, exits } = manager();
|
|
164
|
+
const id = mgr.open({ cols: 80, rows: 24, id: ID, command: 'claude', args: ['--session-id', ID] });
|
|
165
|
+
const seen = [];
|
|
166
|
+
await mgr.attach(id, (f) => { if (f.t === 'data')
|
|
167
|
+
seen.push(f.data); });
|
|
168
|
+
spawned[0].emit(IN_USE);
|
|
169
|
+
spawned[0].exit(1);
|
|
170
|
+
expect(spawned).toHaveLength(2);
|
|
171
|
+
expect(mgr.list()).toEqual([ID]);
|
|
172
|
+
expect(exits).toEqual([]);
|
|
173
|
+
spawned[1].emit('boom\r\n');
|
|
174
|
+
spawned[1].exit(2);
|
|
175
|
+
expect(exits).toEqual([[ID, 2]]);
|
|
176
|
+
expect(seen.join('')).toContain("Claude couldn't start: boom. Reopen the pane to try again.");
|
|
177
|
+
});
|
|
178
|
+
it('a shell launch (no command) is not supervised', () => {
|
|
179
|
+
const { mgr, spawned, exits } = manager();
|
|
180
|
+
mgr.open({ cols: 80, rows: 24 });
|
|
181
|
+
spawned[0].emit('Error: nope\r\n');
|
|
182
|
+
spawned[0].exit(1);
|
|
183
|
+
expect(spawned).toHaveLength(1);
|
|
184
|
+
expect(exits).toHaveLength(1);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Launch supervisor — makes a `claude` that dies at startup either heal
|
|
3
|
+
* itself or say so in plain words, instead of leaving its raw stderr as the
|
|
4
|
+
* last thing in the pane.
|
|
5
|
+
*
|
|
6
|
+
* Wraps ONE agent launch (SessionManager.open with `command: 'claude'`) in a
|
|
7
|
+
* PtyLike that the session manager cannot tell apart from a plain PTY. Inside,
|
|
8
|
+
* it watches the first `windowMs` of the process's life:
|
|
9
|
+
*
|
|
10
|
+
* - `--session-id <id>` exits early with "Session ID <id> is already in use"
|
|
11
|
+
* → the conversation already exists (e.g. it was recorded under a project
|
|
12
|
+
* dir the existence check missed), so relaunch ONCE with `--resume <id>`.
|
|
13
|
+
* - `--resume <id>` exits early with "No conversation found with session ID"
|
|
14
|
+
* → there is nothing to resume, so relaunch ONCE with `--session-id <id>`.
|
|
15
|
+
* - any other early non-zero exit (or a retry that also fails) → write one
|
|
16
|
+
* short line into the stream ("Claude couldn't start: …") and let the exit
|
|
17
|
+
* through. The raw output goes to the bridge log, not into a second copy.
|
|
18
|
+
*
|
|
19
|
+
* At most one retry per open — the retry itself is never retried, so no loop.
|
|
20
|
+
* A process that outlives the window, exits 0, or was killed by the bridge is
|
|
21
|
+
* passed through untouched.
|
|
22
|
+
*
|
|
23
|
+
* Wording checked against Claude Code 2.1.289: the session-in-use error is
|
|
24
|
+
* `Error: Session ID <uuid> is already in use.` (stderr); the resume miss is
|
|
25
|
+
* `No conversation found with session ID: <uuid>` (rendered by the TUI).
|
|
26
|
+
* Both arrive on the PTY, so matching the combined output catches both.
|
|
27
|
+
*
|
|
28
|
+
* Both lanes: the local bridge (loopback + crew relay on a laptop) spawns
|
|
29
|
+
* `claude` through here. The hosted container does NOT — it spawns $SHELL and
|
|
30
|
+
* infra/terminal-image/autolaunch.sh runs `claude`, so the same rules live
|
|
31
|
+
* there too.
|
|
32
|
+
*/
|
|
33
|
+
import { PtyFactory, PtyLike } from './sessionManager';
|
|
34
|
+
/** How long after launch an exit still counts as "couldn't start". */
|
|
35
|
+
export declare const DEFAULT_LAUNCH_WINDOW_MS = 10000;
|
|
36
|
+
export declare const SESSION_IN_USE_RE: RegExp;
|
|
37
|
+
export declare const RESUME_MISSING_RE: RegExp;
|
|
38
|
+
export declare function stripAnsi(s: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* The first line of `output` worth showing a person: ANSI stripped, blank
|
|
41
|
+
* lines and stack frames skipped, a leading "Error:" dropped, trailing period
|
|
42
|
+
* dropped (the caller adds its own punctuation), clipped to ~160 chars.
|
|
43
|
+
*/
|
|
44
|
+
export declare function firstMeaningfulLine(output: string): string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Pure decision for an EARLY exit (caller has already checked the window and
|
|
47
|
+
* the retry budget): the argv to relaunch with, or null for "don't retry".
|
|
48
|
+
*/
|
|
49
|
+
export declare function retryArgsFor(args: string[], output: string): string[] | null;
|
|
50
|
+
export declare function friendlyStartFailure(output: string, code: number): string;
|
|
51
|
+
export interface LaunchSupervisorOptions {
|
|
52
|
+
windowMs?: number;
|
|
53
|
+
now?: () => number;
|
|
54
|
+
log?: (msg: string) => void;
|
|
55
|
+
/** node-pty can report exit before the last output chunk is read; an early
|
|
56
|
+
* failure is judged after this short settle. Tests pass a synchronous one. */
|
|
57
|
+
settle?: (fn: () => void) => void;
|
|
58
|
+
}
|
|
59
|
+
type SpawnOpts = Parameters<PtyFactory['spawn']>[0];
|
|
60
|
+
export declare function superviseLaunch(factory: PtyFactory, spawnOpts: SpawnOpts, opts?: LaunchSupervisorOptions): PtyLike;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RESUME_MISSING_RE = exports.SESSION_IN_USE_RE = exports.DEFAULT_LAUNCH_WINDOW_MS = void 0;
|
|
4
|
+
exports.stripAnsi = stripAnsi;
|
|
5
|
+
exports.firstMeaningfulLine = firstMeaningfulLine;
|
|
6
|
+
exports.retryArgsFor = retryArgsFor;
|
|
7
|
+
exports.friendlyStartFailure = friendlyStartFailure;
|
|
8
|
+
exports.superviseLaunch = superviseLaunch;
|
|
9
|
+
/** How long after launch an exit still counts as "couldn't start". */
|
|
10
|
+
exports.DEFAULT_LAUNCH_WINDOW_MS = 10_000;
|
|
11
|
+
/** Cap on how much early output is kept for matching/logging. */
|
|
12
|
+
const MAX_CAPTURE = 16 * 1024;
|
|
13
|
+
exports.SESSION_IN_USE_RE = /Session ID \S+ is already in use/i;
|
|
14
|
+
exports.RESUME_MISSING_RE = /No conversation found/i;
|
|
15
|
+
// CSI, OSC (BEL- or ST-terminated), and lone two-char escapes.
|
|
16
|
+
// eslint-disable-next-line no-control-regex
|
|
17
|
+
const ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-Z\\-_()#%][0-9A-Za-z]?/g;
|
|
18
|
+
function stripAnsi(s) {
|
|
19
|
+
// eslint-disable-next-line no-control-regex
|
|
20
|
+
return s.replace(ANSI_RE, '').replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The first line of `output` worth showing a person: ANSI stripped, blank
|
|
24
|
+
* lines and stack frames skipped, a leading "Error:" dropped, trailing period
|
|
25
|
+
* dropped (the caller adds its own punctuation), clipped to ~160 chars.
|
|
26
|
+
*/
|
|
27
|
+
function firstMeaningfulLine(output) {
|
|
28
|
+
const lines = stripAnsi(output).split(/\r?\n|\r/);
|
|
29
|
+
for (const raw of lines) {
|
|
30
|
+
const line = raw.trim();
|
|
31
|
+
if (!line)
|
|
32
|
+
continue;
|
|
33
|
+
if (/^at\s+\S/.test(line))
|
|
34
|
+
continue;
|
|
35
|
+
if (/^[-─━═_=*·•\s]+$/.test(line))
|
|
36
|
+
continue;
|
|
37
|
+
let msg = line.replace(/^(?:error|fatal)\s*:\s*/i, '').replace(/[.\s]+$/, '');
|
|
38
|
+
if (!msg)
|
|
39
|
+
continue;
|
|
40
|
+
if (msg.length > 160)
|
|
41
|
+
msg = `${msg.slice(0, 157)}...`;
|
|
42
|
+
return msg;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/** Where `--resume`/`--session-id` sits in argv, or null. */
|
|
47
|
+
function sessionFlagIndex(args) {
|
|
48
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
49
|
+
if (args[i] === '--resume' || args[i] === '--session-id')
|
|
50
|
+
return i;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Pure decision for an EARLY exit (caller has already checked the window and
|
|
56
|
+
* the retry budget): the argv to relaunch with, or null for "don't retry".
|
|
57
|
+
*/
|
|
58
|
+
function retryArgsFor(args, output) {
|
|
59
|
+
const i = sessionFlagIndex(args);
|
|
60
|
+
if (i === null)
|
|
61
|
+
return null;
|
|
62
|
+
const text = stripAnsi(output);
|
|
63
|
+
const flag = args[i];
|
|
64
|
+
if (flag === '--session-id' && exports.SESSION_IN_USE_RE.test(text)) {
|
|
65
|
+
const next = [...args];
|
|
66
|
+
next[i] = '--resume';
|
|
67
|
+
return next;
|
|
68
|
+
}
|
|
69
|
+
if (flag === '--resume' && exports.RESUME_MISSING_RE.test(text)) {
|
|
70
|
+
const next = [...args];
|
|
71
|
+
next[i] = '--session-id';
|
|
72
|
+
return next;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
function friendlyStartFailure(output, code) {
|
|
77
|
+
const reason = firstMeaningfulLine(output) ?? `it exited with code ${code}`;
|
|
78
|
+
return `\r\n\x1b[33mClaude couldn't start: ${reason}. Reopen the pane to try again.\x1b[0m\r\n`;
|
|
79
|
+
}
|
|
80
|
+
/** Clears the pane before the retry so the raw error from the first attempt
|
|
81
|
+
* doesn't sit above the healthy session. */
|
|
82
|
+
const RETRY_CLEAR = '\x1b[2J\x1b[3J\x1b[H';
|
|
83
|
+
function superviseLaunch(factory, spawnOpts, opts = {}) {
|
|
84
|
+
const windowMs = opts.windowMs ?? exports.DEFAULT_LAUNCH_WINDOW_MS;
|
|
85
|
+
const now = opts.now ?? Date.now;
|
|
86
|
+
const log = opts.log ?? ((m) => console.error(m));
|
|
87
|
+
const settle = opts.settle ?? ((fn) => { setTimeout(fn, 100); });
|
|
88
|
+
const dataListeners = [];
|
|
89
|
+
const exitListeners = [];
|
|
90
|
+
const emitData = (d) => { for (const cb of dataListeners)
|
|
91
|
+
cb(d); };
|
|
92
|
+
const emitExit = (code) => { for (const cb of exitListeners)
|
|
93
|
+
cb(code); };
|
|
94
|
+
let cols = spawnOpts.cols;
|
|
95
|
+
let rows = spawnOpts.rows;
|
|
96
|
+
let args = spawnOpts.args ?? [];
|
|
97
|
+
let retried = false;
|
|
98
|
+
let killed = false;
|
|
99
|
+
let current;
|
|
100
|
+
const start = () => {
|
|
101
|
+
const startedAt = now();
|
|
102
|
+
let captured = '';
|
|
103
|
+
const pty = factory.spawn({ ...spawnOpts, cols, rows, args });
|
|
104
|
+
current = pty;
|
|
105
|
+
pty.onData((d) => {
|
|
106
|
+
if (pty !== current)
|
|
107
|
+
return;
|
|
108
|
+
if (captured.length < MAX_CAPTURE && now() - startedAt < windowMs)
|
|
109
|
+
captured += d;
|
|
110
|
+
emitData(d);
|
|
111
|
+
});
|
|
112
|
+
pty.onExit((code) => {
|
|
113
|
+
if (pty !== current)
|
|
114
|
+
return;
|
|
115
|
+
const early = now() - startedAt < windowMs;
|
|
116
|
+
if (killed || !early || code === 0) {
|
|
117
|
+
emitExit(code);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
settle(() => onEarlyFailure(code));
|
|
121
|
+
});
|
|
122
|
+
const onEarlyFailure = (code) => {
|
|
123
|
+
if (pty !== current)
|
|
124
|
+
return;
|
|
125
|
+
if (killed) {
|
|
126
|
+
emitExit(code);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const raw = stripAnsi(captured).trim();
|
|
130
|
+
log(`[bridge] claude exited at startup (code ${code}, args: ${args.join(' ')}): ${raw || '(no output)'}`);
|
|
131
|
+
const nextArgs = retried ? null : retryArgsFor(args, captured);
|
|
132
|
+
if (nextArgs) {
|
|
133
|
+
retried = true;
|
|
134
|
+
log(`[bridge] relaunching claude once with: ${nextArgs.join(' ')}`);
|
|
135
|
+
args = nextArgs;
|
|
136
|
+
emitData(RETRY_CLEAR);
|
|
137
|
+
try {
|
|
138
|
+
start();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
log(`[bridge] claude relaunch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
emitData(friendlyStartFailure(captured, code));
|
|
146
|
+
emitExit(code);
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
start();
|
|
150
|
+
return {
|
|
151
|
+
write: (d) => current.write(d),
|
|
152
|
+
resize: (c, r) => { cols = c; rows = r; current.resize(c, r); },
|
|
153
|
+
kill: () => { killed = true; current.kill(); },
|
|
154
|
+
onData: (cb) => { dataListeners.push(cb); },
|
|
155
|
+
onExit: (cb) => { exitListeners.push(cb); },
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TermSnapshot } from './termSnapshot';
|
|
2
|
+
import { LaunchSupervisorOptions } from './launchSupervisor';
|
|
2
3
|
export interface PtyLike {
|
|
3
4
|
write(data: string): void;
|
|
4
5
|
resize(cols: number, rows: number): void;
|
|
@@ -83,6 +84,10 @@ export interface SessionManagerOptions {
|
|
|
83
84
|
snapshotFactory?: (cols: number, rows: number, opts?: {
|
|
84
85
|
scrollback?: number;
|
|
85
86
|
}) => TermSnapshot;
|
|
87
|
+
/** Startup supervision for `claude` launches (launchSupervisor.ts): one
|
|
88
|
+
* self-healing retry for a session-id/resume mismatch, else a plain
|
|
89
|
+
* "Claude couldn't start" line. Injectable clock/settle for tests. */
|
|
90
|
+
launchSupervisor?: LaunchSupervisorOptions;
|
|
86
91
|
}
|
|
87
92
|
/**
|
|
88
93
|
* lws-07 (ADR-0033 port): validates a client-supplied size pair for the
|
|
@@ -106,6 +111,7 @@ export declare class SessionManager {
|
|
|
106
111
|
private scheduleTimeout;
|
|
107
112
|
private cancelTimeout;
|
|
108
113
|
private snapshotFactory;
|
|
114
|
+
private launchSupervisor;
|
|
109
115
|
/** twr-16: cumulative count of attach-pipeline wedges (any stage timeout,
|
|
110
116
|
* or a serialize-wedge whose recovery retry ALSO timed out) since this
|
|
111
117
|
* SessionManager was created. Surfaced via attachFailures() into the
|
|
@@ -21,6 +21,7 @@ exports.validSize = validSize;
|
|
|
21
21
|
*/
|
|
22
22
|
const crypto_1 = __importDefault(require("crypto"));
|
|
23
23
|
const termSnapshot_1 = require("./termSnapshot");
|
|
24
|
+
const launchSupervisor_1 = require("./launchSupervisor");
|
|
24
25
|
/** Default settle window (lws-07): after a resize-on-attach, how long we give
|
|
25
26
|
* the PTY's SIGWINCH-triggered repaint to land in the snapshot before
|
|
26
27
|
* serializing. Small — this only needs to cover one render tick, not a
|
|
@@ -73,6 +74,7 @@ class SessionManager {
|
|
|
73
74
|
scheduleTimeout;
|
|
74
75
|
cancelTimeout;
|
|
75
76
|
snapshotFactory;
|
|
77
|
+
launchSupervisor;
|
|
76
78
|
/** twr-16: cumulative count of attach-pipeline wedges (any stage timeout,
|
|
77
79
|
* or a serialize-wedge whose recovery retry ALSO timed out) since this
|
|
78
80
|
* SessionManager was created. Surfaced via attachFailures() into the
|
|
@@ -89,6 +91,7 @@ class SessionManager {
|
|
|
89
91
|
this.scheduleTimeout = opts.scheduleTimeout ?? ((cb, ms) => setTimeout(cb, ms));
|
|
90
92
|
this.cancelTimeout = opts.cancelTimeout ?? ((h) => clearTimeout(h));
|
|
91
93
|
this.snapshotFactory = opts.snapshotFactory ?? termSnapshot_1.createTermSnapshot;
|
|
94
|
+
this.launchSupervisor = opts.launchSupervisor ?? {};
|
|
92
95
|
}
|
|
93
96
|
/** twr-16: see attachFailureCount. */
|
|
94
97
|
attachFailures() {
|
|
@@ -123,7 +126,12 @@ class SessionManager {
|
|
|
123
126
|
prior.snapshot.dispose();
|
|
124
127
|
this.sessions.delete(id);
|
|
125
128
|
}
|
|
126
|
-
|
|
129
|
+
// A `claude` launch (agent panes, crew members) is supervised through its
|
|
130
|
+
// startup window so a fast failure heals or explains itself; a shell is
|
|
131
|
+
// spawned exactly as before.
|
|
132
|
+
const pty = opts.command === 'claude'
|
|
133
|
+
? (0, launchSupervisor_1.superviseLaunch)(this.factory, opts, this.launchSupervisor)
|
|
134
|
+
: this.factory.spawn(opts);
|
|
127
135
|
const snapshot = this.snapshotFactory(opts.cols, opts.rows, { scrollback: this.snapshotScrollback });
|
|
128
136
|
const session = {
|
|
129
137
|
pty,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenrwa/marketplace-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "CLI tool for the Wenrwa Agent Marketplace \u2014 monitor bounties, agents, workspaces, and treasury from your terminal",
|
|
5
5
|
"author": "Wenrwa <dev@wenrwa.com>",
|
|
6
6
|
"license": "MIT",
|