Skip to content

feat(terminal): store and expose OSC 7501 program status records #8278

Open

Description

@benngarcia

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:

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 (with kind=permission|question|auth), error or clear;
  • hierarchical ids for sub-tasks (an agent's subagents, for example);
  • app, plus a one-line base64 msg and title.

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

  1. Parse and store. The terminal layer handles OSC 7501 and keeps one record per terminal and id, following the spec:
    • the 4 KiB limits;
    • control characters in msg/title are refused;
    • working and blocked records drop when the process exits or on OSC 133 A;
    • feature detection answers OSC 7501 ; ?.
  2. 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 --json gains programStatus: [{ 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.
  3. Optionally, use blocked records 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

  1. github-actions commented on Oct 7, 2026

    @github-actions
    Contributor

    This is a feature request, not a bug. Nothing in the codebase handles OSC 7501 today: I searched for 7501 and 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 use blocked records 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 refuse msg/title values that contain control characters, while the title scanner strips them, so that one rule needs to change.
    • The result is stored on TerminalSession.title (terminal.ts:565-566, "Lives and dies with the process"). A programStatus: 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 while shellReadyState === "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 clear working/blocked records 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 at terminal.ts:2612), so the signal is there.

    Clearing records on process exit. onExit at terminal.ts:3419 is the natural place to drop working/blocked records.

    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) and listTerminalSessions (terminal.ts:901).
    • The tRPC procedure terminal.list (packages/host-service/src/trpc/router/terminal/terminal.ts:137). Adding an optional field is additive, but read packages/trpc/AGENTS.md first, because released clients still call this procedure.
    • The CLI command packages/cli/src/commands/terminals/list/command.ts, which passes result through 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 outputs Start/Stop/PermissionRequest/Failed, and through packages/shared/src/agent-status.ts. A blocked record could feed the same PermissionRequest path. 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

    1. Records don't survive a host-service restart. The sessions map 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).
    2. Which wins: hooks or 7501. Once Claude Code and Codex send both, the issue should say which source the board trusts. This connects to feat(sdk): expose terminal agent lifecycle status #7007 and feat(sdk): expose typed pending terminal-agent interactions #7009.
    3. Output should pass through unchanged. Unlike the 133;A scan, the 7501 scanner should leave bytes alone. xterm.js already ignores unknown OSC sequences, and stripping them would break other clients.

    How to verify / test sketch

    A bun:test next to terminal-title-scanner.test.ts could cover the parser on its own:

    • feed "\x1b]7501;state=blocked;kind=question;id=1;msg=<base64>\x07" split across two chunks and check that one record comes out;
    • check that a sequence over 4096 bytes is dropped, and that a msg which decodes to text with control characters is refused;
    • check that 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 real onOutput path. The test/helpers/fake-pty.ts setup in host-service is the closest existing harness.

  2. aramalipoor commented on Oct 19, 2026

    @aramalipoor

    what reads the agent state on the other end? something on your phone, or a dashboard?

  3. aramalipoor commented on Oct 27, 2026

    @aramalipoor

    is that agent state mostly for you, or for others on your team too?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions