From 5bd89f0b8c0ac0dc48b4aea4b04c8524baaccc5a Mon Sep 17 00:00:00 2001 From: prmartinow Date: Sat, 11 Jul 2026 19:41:01 +0700 Subject: [PATCH 1/2] feat(pty): propagate active sessionId and AGENT_TTY_ACTIVE to spawned processes This exposes AGENT_TTY_ACTIVE=true and AGENT_TTY_SESSION_ID= in the child PTY environment, allowing scripts and utilities inside the session to detect and inspect the active agent-tty context. --- src/host/hostMain.ts | 1 + src/pty/createPty.ts | 10 +++++++++- test/unit/pty/createPty.test.ts | 20 ++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/host/hostMain.ts b/src/host/hostMain.ts index 4ded84a..8a31897 100644 --- a/src/host/hostMain.ts +++ b/src/host/hostMain.ts @@ -245,6 +245,7 @@ export async function runHost(sessionId: string): Promise { rows: manifest.rows, env: manifest.env ?? {}, term: manifest.term ?? 'xterm-256color', + sessionId, }); invariant( diff --git a/src/pty/createPty.ts b/src/pty/createPty.ts index fbf5005..97c5897 100644 --- a/src/pty/createPty.ts +++ b/src/pty/createPty.ts @@ -16,6 +16,7 @@ export interface PtyOptions { rows: number; env: Record; term: string; + sessionId?: string; } const EXECUTABLE_PERMISSION_MASK = 0o111; @@ -96,6 +97,7 @@ export function resolvePtyEnv( env: Record, term: string, baseEnv: Record = process.env, + sessionId?: string, ): Record { const resolved: Record = {}; for (const [key, value] of Object.entries(baseEnv)) { @@ -113,6 +115,12 @@ export function resolvePtyEnv( Object.assign(resolved, env); resolved.TERM = term; + + resolved.AGENT_TTY_ACTIVE = 'true'; + if (sessionId) { + resolved.AGENT_TTY_SESSION_ID = sessionId; + } + return resolved; } @@ -144,6 +152,6 @@ export function createPty(options: PtyOptions): IPty { cwd, cols, rows, - env: resolvePtyEnv(env, term), + env: resolvePtyEnv(env, term, process.env, options.sessionId), }); } diff --git a/test/unit/pty/createPty.test.ts b/test/unit/pty/createPty.test.ts index 62c0e71..477069b 100644 --- a/test/unit/pty/createPty.test.ts +++ b/test/unit/pty/createPty.test.ts @@ -64,4 +64,24 @@ describe('resolvePtyEnv', () => { expect(resolved.BAZ).toBe('qux'); expect(Object.prototype.hasOwnProperty.call(resolved, 'EMPTY')).toBe(false); }); + + it('unconditionally sets AGENT_TTY_ACTIVE to true', () => { + const resolved = resolvePtyEnv({}, 'xterm-256color', {}); + expect(resolved.AGENT_TTY_ACTIVE).toBe('true'); + }); + + it('sets AGENT_TTY_SESSION_ID when sessionId is provided', () => { + const resolved = resolvePtyEnv( + {}, + 'xterm-256color', + {}, + 'test-session-123', + ); + expect(resolved.AGENT_TTY_SESSION_ID).toBe('test-session-123'); + }); + + it('does not set AGENT_TTY_SESSION_ID when sessionId is not provided', () => { + const resolved = resolvePtyEnv({}, 'xterm-256color', {}); + expect(resolved.AGENT_TTY_SESSION_ID).toBeUndefined(); + }); }); From 7b13952a57a0143d071450ea7a7c3bb7fa2dc139 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 24 Sep 2026 09:48:44 +0000 Subject: [PATCH 2/2] fix(pty): let --env override injected session vars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inject AGENT_TTY_ACTIVE and AGENT_TTY_SESSION_ID after the inherited environment but before the caller-supplied env, so an explicit `create --env AGENT_TTY_ACTIVE=... / AGENT_TTY_SESSION_ID=...` wins as docs/USAGE.md promises, while a nested session still replaces an id inherited from its outer session. Add unit tests for the override and inherited-id cases, an integration test that matches the shell's id against the create envelope, and document both variables in the resolvePtyEnv comment and USAGE.md. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_ --- docs/USAGE.md | 11 ++++++- src/pty/createPty.ts | 21 +++++++------ test/integration/pty-basics.test.ts | 49 +++++++++++++++++++++++++++++ test/unit/pty/createPty.test.ts | 36 +++++++++++++++++++++ 4 files changed, 107 insertions(+), 10 deletions(-) diff --git a/docs/USAGE.md b/docs/USAGE.md index 6a0d765..49fda7e 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -261,7 +261,16 @@ Avoid writing automated sessions into the default `~/.agent-tty` unless you inte `create` spawns the shell with your inherited environment plus `TERM` (from `--term`) and a default `PROMPT_EOL_MARK=` (empty). The empty `PROMPT_EOL_MARK` suppresses the inverse-video `%` that `zsh` prints at the end of any output without a trailing newline; without it, agent-tty's hidden per-`run` completion marker leaves a stray `%` in snapshots, screenshots, and recordings. The variable is zsh-only and inert in other shells. -Any `--env` value always wins, so you can opt back into the shell's native behavior per session: +The shell also receives two session variables, so scripts inside a session can detect agent-tty and find their own session: + +| Variable | Value | +| ---------------------- | --------------------------------------------------------- | +| `AGENT_TTY_ACTIVE` | `true`. | +| `AGENT_TTY_SESSION_ID` | The session's ID (a ULID, as printed by `create --json`). | + +Both replace inherited values, so a session created from inside another session reports its own ID, not the outer one. Like the `PROMPT_EOL_MARK` default, they are set at spawn time and are not stored in the session's recorded `env`. + +Any `--env` value always wins, so you can opt back into the shell's native behavior or override a session variable per session: ```bash # Restore zsh's styled default marker: diff --git a/src/pty/createPty.ts b/src/pty/createPty.ts index 97c5897..2f98e37 100644 --- a/src/pty/createPty.ts +++ b/src/pty/createPty.ts @@ -86,12 +86,16 @@ const PROMPT_EOL_MARK_ENV_KEY = 'PROMPT_EOL_MARK'; * Resolves the environment handed to the spawned PTY shell. * * Precedence, lowest to highest: the inherited process environment (minus - * host-only internals), then the `PROMPT_EOL_MARK=''` default, then the caller-supplied `env` (so a `--env` - * value always wins — even an explicit empty one), then `TERM`. The default sits - * after the inherited environment so it also overrides any inherited - * `PROMPT_EOL_MARK`, keeping captures deterministic regardless of the launching - * shell. The presence check is against `env` (the user-explicit set) rather than - * the merged result, so an inherited value never counts as opting out. + * host-only internals), then the `PROMPT_EOL_MARK=''` default, then the + * generated session metadata (`AGENT_TTY_ACTIVE=true` and, when `sessionId` is + * given, `AGENT_TTY_SESSION_ID`), then the caller-supplied `env` (so a `--env` + * value always wins — even an explicit empty one), then `TERM`. The defaults sit + * after the inherited environment so they also override inherited values: a + * nested session reports its own id rather than the outer session's, and + * captures stay deterministic regardless of the launching shell. The + * `PROMPT_EOL_MARK` presence check is against `env` (the user-explicit set) + * rather than the merged result, so an inherited value never counts as opting + * out. */ export function resolvePtyEnv( env: Record, @@ -113,14 +117,13 @@ export function resolvePtyEnv( resolved[PROMPT_EOL_MARK_ENV_KEY] = ''; } - Object.assign(resolved, env); - resolved.TERM = term; - resolved.AGENT_TTY_ACTIVE = 'true'; if (sessionId) { resolved.AGENT_TTY_SESSION_ID = sessionId; } + Object.assign(resolved, env); + resolved.TERM = term; return resolved; } diff --git a/test/integration/pty-basics.test.ts b/test/integration/pty-basics.test.ts index 5f4bbfa..2f92ca1 100644 --- a/test/integration/pty-basics.test.ts +++ b/test/integration/pty-basics.test.ts @@ -213,6 +213,55 @@ describe('pty-basics integration', { timeout: 30000 }, () => { } }); + it('exposes the session id and AGENT_TTY_ACTIVE to the spawned shell', () => { + let sessionId = ''; + + try { + // An id inherited from an outer session must not leak into this one. + const createResult = runCli( + [ + 'create', + '--json', + '--', + '/bin/sh', + '-c', + 'printf "active=%s id=%s\\n" "${AGENT_TTY_ACTIVE-unset}" "${AGENT_TTY_SESSION_ID-unset}"; exec cat', + ], + { AGENT_TTY_HOME: testHome, AGENT_TTY_SESSION_ID: 'outer-session' }, + ); + expect(createResult.status).toBe(0); + expect(createResult.stderr).toBe(''); + const envelope = JSON.parse(createResult.stdout) as SuccessEnvelope<{ + sessionId: string; + }>; + expect(envelope.ok).toBe(true); + sessionId = envelope.result.sessionId; + expect(sessionId.length).toBeGreaterThan(0); + + const waitResult = runCli( + [ + 'wait', + sessionId, + '--text', + `active=true id=${sessionId}`, + '--timeout', + '10000', + '--json', + ], + { AGENT_TTY_HOME: testHome }, + 60_000, + ); + expect(waitResult.status).toBe(0); + expect(waitResult.stderr).toBe(''); + expect(JSON.parse(waitResult.stdout)).toMatchObject({ + ok: true, + result: { matched: true, timedOut: false }, + }); + } finally { + destroySession(testHome, sessionId); + } + }); + it('resize records resize and inspect reflects new dimensions', async () => { let sessionId = ''; diff --git a/test/unit/pty/createPty.test.ts b/test/unit/pty/createPty.test.ts index 477069b..3d477a5 100644 --- a/test/unit/pty/createPty.test.ts +++ b/test/unit/pty/createPty.test.ts @@ -84,4 +84,40 @@ describe('resolvePtyEnv', () => { const resolved = resolvePtyEnv({}, 'xterm-256color', {}); expect(resolved.AGENT_TTY_SESSION_ID).toBeUndefined(); }); + + it('replaces an inherited outer session id with the session id', () => { + const resolved = resolvePtyEnv( + {}, + 'xterm-256color', + { AGENT_TTY_ACTIVE: 'outer', AGENT_TTY_SESSION_ID: 'outer-session' }, + 'inner-session', + ); + + expect(resolved.AGENT_TTY_ACTIVE).toBe('true'); + expect(resolved.AGENT_TTY_SESSION_ID).toBe('inner-session'); + }); + + it('lets caller-supplied env override the injected session variables', () => { + const resolved = resolvePtyEnv( + { AGENT_TTY_ACTIVE: 'custom', AGENT_TTY_SESSION_ID: 'custom-id' }, + 'xterm-256color', + { AGENT_TTY_SESSION_ID: 'outer-session' }, + 'inner-session', + ); + + expect(resolved.AGENT_TTY_ACTIVE).toBe('custom'); + expect(resolved.AGENT_TTY_SESSION_ID).toBe('custom-id'); + }); + + it('lets an explicit empty caller value override the injected session variables', () => { + const resolved = resolvePtyEnv( + { AGENT_TTY_ACTIVE: '', AGENT_TTY_SESSION_ID: '' }, + 'xterm-256color', + {}, + 'inner-session', + ); + + expect(resolved.AGENT_TTY_ACTIVE).toBe(''); + expect(resolved.AGENT_TTY_SESSION_ID).toBe(''); + }); });