@volter/supercode-terminal 0.2.60 → 0.2.62
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/index.mjs +29 -9
- package/package.json +1 -1
- package/substrate/pane-server.mjs +21 -5
- package/substrate/tmux.js +14 -3
package/index.mjs
CHANGED
|
@@ -1069,12 +1069,20 @@ export class PaneServerHost {
|
|
|
1069
1069
|
}
|
|
1070
1070
|
|
|
1071
1071
|
async #request(op, args, extra = {}) {
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1072
|
+
// A server that is leaving refuses a request unrun (`pane_server_restarting`): it is asked again of the next one.
|
|
1073
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
1074
|
+
const connection = await this.#connect();
|
|
1075
|
+
const id = this.#next++;
|
|
1076
|
+
try {
|
|
1077
|
+
return await new Promise((resolve, reject) => {
|
|
1078
|
+
this.#pending.set(id, { resolve, reject, connection });
|
|
1079
|
+
connection.write(`${JSON.stringify({ id, op, args, ...extra })}\n`);
|
|
1080
|
+
});
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
if (error?.code !== 'pane_server_restarting' || attempt >= 20) throw error;
|
|
1083
|
+
await sleep(250);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1078
1086
|
}
|
|
1079
1087
|
|
|
1080
1088
|
#connect() {
|
|
@@ -1124,15 +1132,27 @@ export class PaneServerHost {
|
|
|
1124
1132
|
const waiter = this.#pending.get(message.id);
|
|
1125
1133
|
if (!waiter) continue;
|
|
1126
1134
|
this.#pending.delete(message.id);
|
|
1127
|
-
if (message.error)
|
|
1135
|
+
if (message.error) {
|
|
1136
|
+
// The server ends after a runtime that failed to load: the next request opens a new connection.
|
|
1137
|
+
// The server is leaving: this client lets go of it, and its next request opens (or starts) the next one.
|
|
1138
|
+
if (message.error.code === 'pane_runtime_missing' || message.error.code === 'pane_server_restarting') {
|
|
1139
|
+
if (this.#connection === connection) this.#connection = null;
|
|
1140
|
+
connection.end();
|
|
1141
|
+
}
|
|
1142
|
+
waiter.reject(new TerminalHostError(message.error.code, message.error.message));
|
|
1143
|
+
}
|
|
1128
1144
|
else waiter.resolve(message.result);
|
|
1129
1145
|
}
|
|
1130
1146
|
});
|
|
1131
1147
|
// The server went (or this connection did): waiting requests fail, and viewers are told; the next call reconnects.
|
|
1132
1148
|
connection.on('close', () => {
|
|
1133
1149
|
if (this.#connection === connection) this.#connection = null;
|
|
1134
|
-
|
|
1135
|
-
this.#pending
|
|
1150
|
+
// Only the requests sent on this connection fail with it; one already replaced keeps the next one's requests.
|
|
1151
|
+
for (const [id, waiter] of this.#pending) {
|
|
1152
|
+
if (waiter.connection && waiter.connection !== connection) continue;
|
|
1153
|
+
this.#pending.delete(id);
|
|
1154
|
+
waiter.reject(new TerminalHostError('pane_server_unreachable', 'The pane server connection closed.'));
|
|
1155
|
+
}
|
|
1136
1156
|
for (const relay of [...this.#streams.values()]) { relay.disposed = true; relay.socket?.close?.(1011, 'pane server connection closed'); }
|
|
1137
1157
|
this.#streams.clear();
|
|
1138
1158
|
});
|
package/package.json
CHANGED
|
@@ -20,6 +20,18 @@ const clients = new Set();
|
|
|
20
20
|
const IDLE_EXIT_MS = 60_000;
|
|
21
21
|
let idleTimer = null;
|
|
22
22
|
|
|
23
|
+
let leaving = false;
|
|
24
|
+
/**
|
|
25
|
+
* Leave once no client holds a connection: each client closes its own on the answer that says the runtime is missing
|
|
26
|
+
* (or on a refusal while this server is leaving), so nothing it sends is lost between servers. A client that never
|
|
27
|
+
* closes (an older daemon) is ended after a few seconds.
|
|
28
|
+
*/
|
|
29
|
+
function leave() {
|
|
30
|
+
const done = () => { host.dispose(); process.exit(0); };
|
|
31
|
+
if (!clients.size) done();
|
|
32
|
+
setTimeout(() => { server.close(); for (const connection of clients) connection.end(); setTimeout(done, 500); }, 3_000).unref?.();
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
const OPS = new Set(['listSessions', 'createSession', 'capture', 'sendInput', 'closeSession', 'renameWindow', 'issueAttachment', 'bindContext']);
|
|
24
36
|
|
|
25
37
|
function scheduleIdleExit() {
|
|
@@ -86,19 +98,23 @@ const server = createServer((connection) => {
|
|
|
86
98
|
const { id, op, args = [] } = message;
|
|
87
99
|
if (op === 'attach') { attach(id, args, message.stream); continue; }
|
|
88
100
|
if (!OPS.has(op)) { refuse(id, { code: 'unsupported', message: `pane server: no operation ${op}` }); continue; }
|
|
101
|
+
// A server that is leaving runs nothing more: the request is refused unrun, and its client asks the next server.
|
|
102
|
+
if (leaving) { refuse(id, { code: 'pane_server_restarting', message: 'The pane server is restarting; ask again.' }); continue; }
|
|
89
103
|
Promise.resolve().then(() => host[op](...args)).then((result) => answer(id, result), async (error) => {
|
|
90
|
-
|
|
91
|
-
// The pane runtime failed to load in this process: with no pane to keep, it ends once the answer is out, so
|
|
104
|
+
// The pane runtime failed to load in this process: with no pane to keep, it ends once its answers are out, so
|
|
92
105
|
// the next request starts a pane server that loads the runtime afresh (one installed since this one started).
|
|
93
|
-
if (error?.code === 'pane_runtime_missing'
|
|
94
|
-
|
|
95
|
-
|
|
106
|
+
if (error?.code === 'pane_runtime_missing') leaving = true;
|
|
107
|
+
refuse(id, error);
|
|
108
|
+
if (!leaving) return;
|
|
109
|
+
if ((await host.listSessions().catch(() => [])).length) { leaving = false; return; }
|
|
110
|
+
leave();
|
|
96
111
|
});
|
|
97
112
|
}
|
|
98
113
|
});
|
|
99
114
|
// A client that goes (its daemon restarting) takes only its viewers with it: the panes stay.
|
|
100
115
|
connection.on('close', () => {
|
|
101
116
|
clients.delete(connection);
|
|
117
|
+
if (leaving && !clients.size) { host.dispose(); process.exit(0); }
|
|
102
118
|
for (const { socketLike, attachment } of streams.values()) { socketLike.emitClose(); attachment?.dispose(); }
|
|
103
119
|
streams.clear();
|
|
104
120
|
scheduleIdleExit();
|
package/substrate/tmux.js
CHANGED
|
@@ -59,12 +59,22 @@ export async function createSession({ command, cwd, env, name, owner, ownerOptio
|
|
|
59
59
|
// set-options just configured. On an already-running server they are simply
|
|
60
60
|
// reapplied.
|
|
61
61
|
const sessionEnv = env ?? {};
|
|
62
|
+
// A tmux server keeps the environment of whoever started it as its global environment, and every pane inherits
|
|
63
|
+
// it. When an agent's session started the server (or is the launcher), that includes the session's own harness
|
|
64
|
+
// identity, so a new pane would resolve as that session. A pane's identity is only what its own session sets:
|
|
65
|
+
// every identity variable the server or the launcher holds and the session does not set is unset in the pane.
|
|
66
|
+
const identity = /^(CLAUDECODE|CLAUDE_CODE_SESSION_ID|CLAUDE_CODE_ENTRYPOINT|CODEX_THREAD_ID|CODEX_SESSION_ID|SUPERCODE_CALLER|SUPERCODE_CALLER_NAME|SUPERCODE_AGENT_[A-Z_]+)$/;
|
|
67
|
+
const serverNames = await runAsync("tmux", tmuxArgs(socket, ["show-environment", "-g"]))
|
|
68
|
+
.then((out) => out.split("\n").map((line) => /^([A-Za-z_][A-Za-z0-9_]*)=/.exec(line)?.[1]).filter((name) => Boolean(name)))
|
|
69
|
+
.catch(() => []); // no server yet: the one this creates starts from this launcher's environment
|
|
70
|
+
const inherited = [...new Set([...serverNames, ...Object.keys(process.env)])]
|
|
71
|
+
.filter((name) => identity.test(name) && !(name in sessionEnv));
|
|
62
72
|
const bootstrapDirectories = [];
|
|
63
73
|
const paneBootstrapArgs = (pane) => {
|
|
64
74
|
const commandArgs = command === undefined ? [] : [command.program, ...(command.arguments ?? [])];
|
|
65
75
|
// With neither command nor overrides, retain tmux's default-shell and
|
|
66
76
|
// default-command behavior rather than choosing a shell for it.
|
|
67
|
-
if (command === undefined && Object.keys(sessionEnv).length === 0)
|
|
77
|
+
if (command === undefined && Object.keys(sessionEnv).length === 0 && inherited.length === 0)
|
|
68
78
|
return [];
|
|
69
79
|
// The complete command rides the private bootstrap file as well as its
|
|
70
80
|
// environment. tmux's command message is bounded (roughly 16 KiB), much
|
|
@@ -72,7 +82,7 @@ export async function createSession({ command, cwd, env, name, owner, ownerOptio
|
|
|
72
82
|
const directory = mkdtempSync(join(tmpdir(), "tmux-session-env-"));
|
|
73
83
|
bootstrapDirectories.push(directory);
|
|
74
84
|
const shell = process.env.SHELL ?? "/bin/sh";
|
|
75
|
-
return writeSessionEnvironmentBootstrap(directory, pane, sessionEnv, commandArgs.length === 0 ? [shell, "-l"] : commandArgs);
|
|
85
|
+
return writeSessionEnvironmentBootstrap(directory, pane, sessionEnv, inherited, commandArgs.length === 0 ? [shell, "-l"] : commandArgs);
|
|
76
86
|
};
|
|
77
87
|
try {
|
|
78
88
|
const startArgs = [...sessionServerOptionArgs(), "new-session", "-d", "-s", name];
|
|
@@ -395,7 +405,7 @@ function tmuxClientEnvironment(source = process.env) {
|
|
|
395
405
|
}
|
|
396
406
|
return environment;
|
|
397
407
|
}
|
|
398
|
-
function writeSessionEnvironmentBootstrap(directory, pane, environment, command) {
|
|
408
|
+
function writeSessionEnvironmentBootstrap(directory, pane, environment, unset, command) {
|
|
399
409
|
const environmentPath = join(directory, `pane-${pane}.env`);
|
|
400
410
|
const wrapperPath = join(directory, `pane-${pane}.sh`);
|
|
401
411
|
const lines = Object.entries(environment).map(([name, value]) => {
|
|
@@ -404,6 +414,7 @@ function writeSessionEnvironmentBootstrap(directory, pane, environment, command)
|
|
|
404
414
|
}
|
|
405
415
|
return `export ${name}=${shellQuote(value)}`;
|
|
406
416
|
});
|
|
417
|
+
lines.unshift(...unset.map((name) => `unset ${name}`));
|
|
407
418
|
writeFileSync(environmentPath, `${lines.join("\n")}\n`, { flag: "wx", mode: 0o600 });
|
|
408
419
|
writeFileSync(wrapperPath, [
|
|
409
420
|
"#!/bin/sh",
|