Repository navigation
Description
What problem are you trying to solve?
I run 10–17 agent sessions at once in Superset across four hosts. A program outside Superset needs to know the state of each terminal's agent: working, waiting on me, or finished. Today it has three bad options:
- read Superset's hook-derived state, which doesn't exist on a public surface yet (feat(sdk): expose terminal agent lifecycle status #7007 is open);
- add its own lifecycle hooks next to Superset's, giving two owners of the same lifecycle;
- scrape the screen.
Mitchell Hashimoto's program status protocol, OSC 7501 (post, spec), standardizes that state. The program running in the terminal reports it, and the terminal owns it:
state:idle,working,done,blocked(withkind=permission|question|auth),errororclear;- hierarchical
ids for sub-tasks (an agent's subagents, for example); app, plus a one-line base64msgandtitle.
The author posted on X (2026-10-07) that a day after publication it was integrated in Amp, Factory, TUIOS and libghostty, with in-progress PRs or stated support from several agents and terminals. I haven't verified each of those independently. Ghostty's parser has merged (ghostty#14560).
Once Claude Code and Codex emit it, the terminal holding their output is the only party that knows each agent's state. The spec gives programs no way to read records back ("there is no way to read records"). So a terminal host has to keep the records and expose them for anything else to use them.
Proposed solution
- Parse and store. The terminal layer handles
OSC 7501and keeps one record per terminal andid, following the spec:- the 4 KiB limits;
- control characters in
msg/titleare refused; workingandblockedrecords drop when the process exits or on OSC 133A;- feature detection answers
OSC 7501 ; ?.
- Expose records next to the lifecycle status feat(sdk): expose terminal agent lifecycle status #7007 adds, on the same workspace-scoped terminal listing. For example,
superset terminals list --jsongainsprogramStatus: [{ id, state, kind?, app?, title?, msg?, progress?, updatedAt }]. Records are untrusted program output, so they are labeled with the terminal they came from, as the spec's security section requires. - Optionally, use
blockedrecords to drive the board's "Needs attention" column and notifications. They are a standard signal that doesn't depend on any one agent's hooks, and the spec asks terminals to rate-limit anything a record triggers outside the terminal.
This overlaps with #7009 (a typed pending interaction). A blocked record with kind=question and a msg is the cross-agent version of the same thing, so 7501 could be the provider-neutral source #7009 asks for.
I'm happy to send a PR for the parser and record store, plus the field on the terminal listing, if the shape above works for you.
Workarounds you've tried
- Claude Code Stop and SessionStart hooks that POST to a local service. This works for Claude Code alone, adds a second set of lifecycle hooks next to Superset's, and has to be rebuilt for every agent.
superset terminals read, i.e. screen text. That's not a contract.
Activity
github-actions commentedon Oct 7, 2026
This is a feature request, not a bug. Nothing in the codebase handles OSC 7501 today: I searched for
7501and found no matches outside this issue. The host service already has the hooks this would need, and a PR that follows the shape you describe would fit the existing code well.Summary
You run many agent sessions across several hosts and want a program outside Superset to see each terminal's agent state (working, blocked, done) without adding its own hooks or reading the screen. The proposal is for Superset's terminal layer to parse OSC 7501 program-status sequences, keep one record per terminal and
id, and add those records to the workspace-scoped terminal listing (superset terminals list --json). The board's "Needs attention" column could optionally useblockedrecords too.Where this would go
PTY output path (parse and store). The host service already reads every PTY output chunk in
packages/host-service/src/terminal/terminal.ts:3360-3418(onOutput), and it already reads one OSC type there:scanForTerminalTitle(packages/shared/src/terminal-title-scanner.ts:159) is a byte-level OSC scanner. It caps sequences at 4096 bytes (MAX_OSC_SEQUENCE_BYTES, line 10), handles BEL/ST/C1 terminators and sequences split across chunks, and strips control characters (normalizeTerminalTitle, line 37). A 7501 scanner can copy most of this. The spec says to refusemsg/titlevalues that contain control characters, while the title scanner strips them, so that one rule needs to change.TerminalSession.title(terminal.ts:565-566, "Lives and dies with the process"). AprogramStatus: Map<id, record>field next to it would fit the existing pattern.Clearing records on OSC 133
A.scanForShellReady(packages/shared/src/shell-ready-scanner.ts:59) only runs whileshellReadyState === "pending", plus a short late-marker window (terminal.ts:3373-3395). It also removes the marker from the output it returns. It can't be reused as-is to clearworking/blockedrecords on every prompt, so this needs its own passive 133;A check that leaves output unchanged. The shell wrappers send 133;A from precmd on each prompt (see the comment atterminal.ts:2612), so the signal is there.Clearing records on process exit.
onExitatterminal.ts:3419is the natural place to dropworking/blockedrecords.Answering feature detection (
OSC 7501 ; ?).answerDsrCursorQueries(terminal.ts:1631) is the precedent: the host writes the reply into the PTY. That function stays silent while a renderer is attached, because xterm.js answers DSR itself. xterm.js has no 7501 handler, so for 7501 the host would have to answer every time, attached or not. Doing it in the renderer would be wrong: with several attached clients, the program would get several answers.Exposing records. Three layers need changes:
TerminalSessionSummary(terminal.ts:844) andlistTerminalSessions(terminal.ts:901).terminal.list(packages/host-service/src/trpc/router/terminal/terminal.ts:137). Adding an optional field is additive, but readpackages/trpc/AGENTS.mdfirst, because released clients still call this procedure.packages/cli/src/commands/terminals/list/command.ts, which passesresultthrough as-is, so it would need no change.Optional "Needs attention" mapping. Hook-derived state today goes through
mapEventType(packages/host-service/src/events/map-event-type.ts), which outputsStart/Stop/PermissionRequest/Failed, and throughpackages/shared/src/agent-status.ts. Ablockedrecord could feed the samePermissionRequestpath. That would need a rate limit, as you noted from the spec, and a rule for which source wins when both hooks and 7501 report on the same terminal.Points to settle before a PR
sessionsmap is in-memory.listLiveTerminalSessions(terminal.ts:943) fills in sessions it only knows from the daemon using DB rows, and those have no OSC title (terminal.ts:1005). 7501 records would be lost the same way until the program sends a new one. That may be fine because records are live state, but the listing should say so (for example, an empty array rather than a missing field).How to verify / test sketch
A
bun:testnext toterminal-title-scanner.test.tscould cover the parser on its own:"\x1b]7501;state=blocked;kind=question;id=1;msg=<base64>\x07"split across two chunks and check that one record comes out;msgwhich decodes to text with control characters is refused;ESC]7501;?ESC\is reported as a query, not a record.Store behaviour (clearing on 133;A and on exit, the
?reply written to the PTY) needs the realonOutputpath. Thetest/helpers/fake-pty.tssetup in host-service is the closest existing harness.aramalipoor commentedon Oct 19, 2026
what reads the agent state on the other end? something on your phone, or a dashboard?
aramalipoor commentedon Oct 27, 2026
is that agent state mostly for you, or for others on your team too?