@volter/supercode-terminal 0.2.72 → 0.2.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/index.d.ts CHANGED
@@ -58,7 +58,7 @@ export class TmuxTerminalHost {
58
58
  env?: Record<string, string>;
59
59
  name?: string;
60
60
  }): Promise<TerminalSession>;
61
- capture(sessionId: string, options?: { lines?: number; preserveEscapes?: boolean }): string;
61
+ capture(sessionId: string, options?: { lines?: number; preserveEscapes?: boolean }): Promise<string>;
62
62
  sendInput(
63
63
  sessionId: string,
64
64
  data: string,
@@ -258,6 +258,7 @@ export function composerReadyForInput(kind: string, screen: string | undefined):
258
258
  export function composerHoldsDraft(kind: string, screen: string | undefined): boolean | null;
259
259
 
260
260
  export { classifyAttentionSignal, observeLifecycle, gatherLifecycleObservation, createLifecycleStore, snapshotProcessTree, snapshotProcessTreeAsync } from './substrate/session-lifecycle.js';
261
+ export { HARNESS_MARKERS, HARNESS_CHILD_SESSION_MARKERS, isHarnessMarker, withoutHarnessMarkers, dropOwnHarnessMarkers } from './substrate/harness-env.js';
261
262
  export type {
262
263
  LifecycleSnapshot,
263
264
  LifecycleStore,
package/index.mjs CHANGED
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
9
9
  import { createServer as defaultCreateServer } from 'node:http';
10
10
 
11
11
  import { NativeTerminalSessionHost } from './substrate/native-host.js';
12
+ import { withoutHarnessMarkers } from './substrate/harness-env.js';
12
13
  import * as defaultTmux from './substrate/tmux.js';
13
14
  import { openLocalTerminal as defaultOpenLocalTerminal } from './substrate/local-terminal.js';
14
15
 
@@ -293,14 +294,24 @@ export class TmuxTerminalHost {
293
294
  return created;
294
295
  }
295
296
 
296
- capture(sessionId, options = {}) {
297
- const entry = this.#entry(sessionId);
298
- return this.#tmux.capturePane({
299
- lines: options.lines,
300
- preserveEscapes: options.preserveEscapes === true,
301
- socket: this.#socket,
302
- target: `${entry.name}:0.0`,
303
- });
297
+ // Asynchronous, and one tmux process: the daemon reads every pane's screen each time it judges attention, and a
298
+ // synchronous read (a session listing and a capture per pane, each a spawnSync) held its event loop for all of them
299
+ // back to back, so a local client's hello waited behind the whole pass. A session that is gone is told from the
300
+ // failed capture, by the same listing as before.
301
+ async capture(sessionId, options = {}) {
302
+ const entry = this.#catalog.get(sessionId);
303
+ if (!entry) throw new TerminalHostError('session_not_found', 'The tmux session no longer exists.');
304
+ try {
305
+ return await this.#tmux.capturePaneAsync({
306
+ lines: options.lines,
307
+ preserveEscapes: options.preserveEscapes === true,
308
+ socket: this.#socket,
309
+ target: `${entry.name}:0.0`,
310
+ });
311
+ } catch (error) {
312
+ this.#entry(sessionId);
313
+ throw error;
314
+ }
304
315
  }
305
316
 
306
317
  async sendInput(sessionId, data, options = {}) {
@@ -672,7 +683,7 @@ export class PtyTerminalHost {
672
683
  // a half-installed package): the pane server answers this code by ending itself, and the next one loads afresh.
673
684
  throw new TerminalHostError('pane_runtime_missing', `The pane runtime could not load: ${errorMessage(error)}`);
674
685
  }
675
- const env = { ...process.env, ...(input.env ?? {}) };
686
+ const env = { ...withoutHarnessMarkers(process.env), ...(input.env ?? {}) };
676
687
  const { file, args } = windowsLaunch(input.command.program, input.command.arguments ?? [], env);
677
688
  // The watchdog is running before the pane is, and the pane is on its list the moment it exists.
678
689
  this.#startWatchdog();
@@ -1230,7 +1241,7 @@ export class PaneServerHost {
1230
1241
  } catch (error) {
1231
1242
  // No server on the pipe yet: start one, detached from this daemon, and try again as it comes up.
1232
1243
  if (attempt === 0 && (error.code === 'ENOENT' || error.code === 'ECONNREFUSED')) {
1233
- const child = spawn(process.execPath, [this.#script(), this.#pipe], { detached: true, stdio: 'ignore', windowsHide: true, env: process.env });
1244
+ const child = spawn(process.execPath, [this.#script(), this.#pipe], { detached: true, stdio: 'ignore', windowsHide: true, env: withoutHarnessMarkers(process.env) });
1234
1245
  child.on('error', () => {});
1235
1246
  child.unref();
1236
1247
  }
@@ -2164,3 +2175,4 @@ function randomToken() {
2164
2175
  // every surface that triages panes shares one precision rather than each
2165
2176
  // inventing its own patterns.
2166
2177
  export { classifyAttentionSignal, observeLifecycle, gatherLifecycleObservation, createLifecycleStore, snapshotProcessTree, snapshotProcessTreeAsync } from './substrate/session-lifecycle.js';
2178
+ export { HARNESS_MARKERS, HARNESS_CHILD_SESSION_MARKERS, isHarnessMarker, withoutHarnessMarkers, dropOwnHarnessMarkers } from './substrate/harness-env.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-terminal",
3
- "version": "0.2.72",
3
+ "version": "0.2.74",
4
4
  "type": "module",
5
5
  "description": "Optional leak-safe terminal and tmux host adapter for Volter Harness frontends",
6
6
  "exports": {
@@ -29,6 +29,7 @@
29
29
  "types": "./react/stream.d.ts",
30
30
  "import": "./react/stream.mjs"
31
31
  },
32
+ "./harness-markers.json": "./substrate/harness-markers.json",
32
33
  "./docker-exec": {
33
34
  "types": "./substrate/docker-exec.d.ts",
34
35
  "import": "./substrate/docker-exec.js"
@@ -0,0 +1,12 @@
1
+ /** What a harness takes itself for a nested session by. Each is read for truth, so an empty value is no mark. */
2
+ export declare const HARNESS_CHILD_SESSION_MARKERS: readonly string[];
3
+ /** Every mark: the child-session ones, and what names the session a command ran for. */
4
+ export declare const HARNESS_MARKERS: readonly string[];
5
+ export declare function isHarnessMarker(name: string): boolean;
6
+ /**
7
+ * The environment a process supercode starts gets: `env` without the calling harness's own marks. `keep` names what
8
+ * the launch sets on purpose.
9
+ */
10
+ export declare function withoutHarnessMarkers<T extends Record<string, string | undefined>>(env: T, keep?: Iterable<string>): T;
11
+ /** Drops the marks from this process's own environment, so nothing it starts later can inherit them. */
12
+ export declare function dropOwnHarnessMarkers(env?: NodeJS.ProcessEnv): void;
@@ -0,0 +1,29 @@
1
+ import { readFileSync } from "node:fs";
2
+ // A coding harness marks what its tools run: Claude Code exports CLAUDECODE, CLAUDE_CODE_CHILD_SESSION and the rest
3
+ // to every command of a session, Codex its thread. A process supercode starts that outlives the command (a terminal
4
+ // app, an attach client, a pane, the daemon) is not that session's child: carrying the marks, every shell a person
5
+ // opens in that terminal starts harnesses that take themselves for a nested session (Claude Code then saves no
6
+ // transcript). The names are harness-markers.json beside the built module, the one list; the CLI reads the same file
7
+ // (crates/cli/src/harness_env.rs).
8
+ const listed = JSON.parse(readFileSync(new URL("./harness-markers.json", import.meta.url), "utf8"));
9
+ /** What a harness takes itself for a nested session by. Each is read for truth, so an empty value is no mark. */
10
+ export const HARNESS_CHILD_SESSION_MARKERS = Object.freeze([...listed.child_session]);
11
+ /** Every mark: the child-session ones, and what names the session a command ran for. */
12
+ export const HARNESS_MARKERS = Object.freeze([...listed.child_session, ...listed.session]);
13
+ const markers = new Set(HARNESS_MARKERS);
14
+ export function isHarnessMarker(name) {
15
+ return markers.has(name);
16
+ }
17
+ /**
18
+ * The environment a process supercode starts gets: `env` without the calling harness's own marks. `keep` names what
19
+ * the launch sets on purpose.
20
+ */
21
+ export function withoutHarnessMarkers(env, keep = []) {
22
+ const kept = new Set(keep);
23
+ return Object.fromEntries(Object.entries(env).filter(([name]) => !markers.has(name) || kept.has(name)));
24
+ }
25
+ /** Drops the marks from this process's own environment, so nothing it starts later can inherit them. */
26
+ export function dropOwnHarnessMarkers(env = process.env) {
27
+ for (const name of HARNESS_MARKERS)
28
+ delete env[name];
29
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "child_session": [
3
+ "CLAUDECODE",
4
+ "CLAUDE_CODE_CHILD_SESSION"
5
+ ],
6
+ "session": [
7
+ "CLAUDE_CODE_ENTRYPOINT",
8
+ "CLAUDE_CODE_SESSION_ID",
9
+ "CLAUDE_CODE_SESSION_ATTENDED",
10
+ "CLAUDE_CODE_EXECPATH",
11
+ "CLAUDE_CODE_MESSAGING_SOCKET",
12
+ "CLAUDE_CODE_MESSAGING_TOKEN",
13
+ "CLAUDE_CODE_INVOKED_SKILLS",
14
+ "CLAUDE_PID",
15
+ "CLAUDE_EFFORT",
16
+ "AI_AGENT",
17
+ "CODEX_THREAD_ID",
18
+ "CODEX_SESSION_ID",
19
+ "CODEX_SANDBOX",
20
+ "CODEX_SANDBOX_NETWORK_DISABLED",
21
+ "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"
22
+ ]
23
+ }
@@ -1,4 +1,5 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
+ import { withoutHarnessMarkers } from "../harness-env.js";
2
3
  function runEnvironment(options) {
3
4
  if (options.inheritEnv === false) {
4
5
  return options.env ?? {};
@@ -181,7 +182,7 @@ async function spawnAsync(command, args, options, input) {
181
182
  });
182
183
  }
183
184
  export function spawnInherited(command, args) {
184
- const child = spawn(command, args, { stdio: "inherit" });
185
+ const child = spawn(command, args, { stdio: "inherit", env: withoutHarnessMarkers(process.env) });
185
186
  child.on("exit", (code, signal) => {
186
187
  if (signal) {
187
188
  process.kill(process.pid, signal);
@@ -1,5 +1,6 @@
1
1
  import pty from "@homebridge/node-pty-prebuilt-multiarch";
2
2
  import fs from "node:fs";
3
+ import { withoutHarnessMarkers } from "./harness-env.js";
3
4
  import { assertSession, getStatusLineCount, getWindowSize } from "./tmux.js";
4
5
  export class TmuxTerminalAttachError extends Error {
5
6
  phase;
@@ -46,7 +47,7 @@ export function attachTmuxTerminalSocket(options) {
46
47
  cols,
47
48
  cwd: options.cwd ?? process.cwd(),
48
49
  env: {
49
- ...process.env,
50
+ ...withoutHarnessMarkers(process.env),
50
51
  TERM: "screen-256color"
51
52
  },
52
53
  rows
package/substrate/tmux.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { isHarnessMarker } from "./harness-env.js";
4
5
  import { requireCommand, run, runAsync, runWithInputAsync, spawnInherited } from "./internal/exec.js";
5
6
  import { snapshotDescendantPids } from "./internal/process-tree.js";
6
7
  const TMUX_INSTALL_HINT = "Install it with: brew install tmux";
@@ -63,12 +64,14 @@ export async function createSession({ command, cwd, env, name, owner, ownerOptio
63
64
  // it. When an agent's session started the server (or is the launcher), that includes the session's own harness
64
65
  // identity, so a new pane would resolve as that session. A pane's identity is only what its own session sets:
65
66
  // 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_LAUNCHED_BY|SUPERCODE_AGENT_[A-Z_]+)$/;
67
+ // The harnesses' own marks are the shared list (harness-env); supercode's caller and agent variables are its own.
68
+ const supercodeIdentity = /^(SUPERCODE_CALLER|SUPERCODE_CALLER_NAME|SUPERCODE_LAUNCHED_BY|SUPERCODE_AGENT_[A-Z_]+)$/;
69
+ const identity = (name) => isHarnessMarker(name) || supercodeIdentity.test(name);
67
70
  const serverNames = await runAsync("tmux", tmuxArgs(socket, ["show-environment", "-g"]))
68
71
  .then((out) => out.split("\n").map((line) => /^([A-Za-z_][A-Za-z0-9_]*)=/.exec(line)?.[1]).filter((name) => Boolean(name)))
69
72
  .catch(() => []); // no server yet: the one this creates starts from this launcher's environment
70
73
  const inherited = [...new Set([...serverNames, ...Object.keys(process.env)])]
71
- .filter((name) => identity.test(name) && !(name in sessionEnv));
74
+ .filter((name) => identity(name) && !(name in sessionEnv));
72
75
  const bootstrapDirectories = [];
73
76
  const paneBootstrapArgs = (pane) => {
74
77
  const commandArgs = command === undefined ? [] : [command.program, ...(command.arguments ?? [])];