Skip to content

fix(terminals): keep a terminal's last-used time across restarts - #1179

Merged
Juliusolsson05 merged 4 commits into
mainfrom
fix/terminal-last-used
Sep 25, 2026
Merged

Juliusolsson05 merged 4 commits into
mainfrom
fix/terminal-last-used

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Fixes #1178

Problem

A terminal's only "last active" time was runtime.terminalForeground.changedAt. It lived in memory and was stamped now on the first foreground observation after any attach. Every restart pulls a foreground snapshot into empty runtimes, so every terminal read "active just now" after a restart, and Close Old Agents could never find a shell nobody had touched in days. A command that finishes inside one poll (1 s) and plain typing never counted at all.

What changes

  • SessionMeta.lastUsedAt (terminals) is persisted with the workspace, so it survives restarts.
  • It moves only on real use:
    • typing or pasting in TerminalLeaf, the one input path for plain shells;
    • a real foreground transition (a command started or finished, a cd).
    • The first observation after an attach is never use.
  • A terminal with no record gets a floor on first sighting: "not used since at least now". Without it, a shell nobody touches after this ships would stay "unknown activity" forever, and Close Old Agents skips unknown rows.
  • Writes are throttled to one a minute per terminal, so typing doesn't autosave on every key. Close Old Agents' finest unit is minutes.
  • Readers: Close Old Agents and Agent Activity both use the record. A parked shell with no runtime now ages too, instead of reading unknown.
  • changedAt keeps its other jobs: marking the pane unread when a command finishes, and the live status.

Review round (one round: Codex, Pi)

Fixed:

  • A terminal nobody is observing yet counts as possibly running. After a restart, a parked tmux shell is not polled until it is woken, so it could be running a dev server. It now has an age, so without this Close Old Agents would have listed it as idle. The row says "not observed yet", and it is excluded unless Include running is ticked. (Codex, major)
  • Ages read "record + 1 minute", a true upper bound on the last use. The throttle drops uses within a minute of the record, so the bare record let a one-minute threshold call a shell used a second ago old. (Codex, major)
  • Use is stamped from the user's own keydown, IME and paste events, not xterm's onData:
    • onData also carries automatic terminal replies, which counted as use (Codex, minor);
    • its buffered branch meant typing while a shell was waking never counted (Codex, major).
  • Undo Close keeps the record (Codex, minor).
  • The reader is renamed terminalLastUsedUpperBound for what it returns (Pi).

Accepted gap (Pi): dictating into a plain shell does not count as use until the dictated command runs (which the foreground change then catches). The dictation hook has no access to the workspace, and adding it isn't worth it here.

Not done, on purpose

tmux session_activity would only cover tmux-backed shells, and it counts output as activity, so a prompt clock or tail -f would keep a forgotten shell "in use" forever.

Verification

  • Unit tests for the helper: the throttle, a clock that goes backwards, non-terminals, and a floor that never moves an existing record.
  • Hook test: the post-restart snapshot is only a floor, and a real change is a use.
  • Close Old Agents tests:
    • a shell unused for 3 days is still found right after a restart;
    • a parked shell with no runtime ages.
  • Mutation check: I restored the old reader and the old "first observation = use" rule. The tests failed both times.

🤖 Generated with Claude Code

Juliusolsson05 and others added 4 commits September 24, 2026 18:00
Refs #1178

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shell's only age was terminalForeground.changedAt: in memory, and stamped
"now" on the first observation after any attach. Every restart pulls a
foreground snapshot into empty runtimes, so every terminal read "active just
now" and Close Old Agents could never find a shell unused for days.

Terminals now carry SessionMeta.lastUsedAt, persisted with the workspace and
moved only by real use: typing or pasting (TerminalLeaf, the one input path
for plain shells), a real foreground transition, never the first observation
after an attach. A record-less terminal gets a floor on first sighting, so a
shell nobody touches after this ships can still become old. Writes are
throttled to one a minute so typing does not autosave per key. Close Old
Agents and Agent Activity both read the record, so a parked shell with no
runtime ages too.

Fixes #1178

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A terminal nobody is observing yet (a parked tmux shell after a restart,
  never re-attached or polled) counts as possibly running. It now has an
  age, so Close Old Agents would otherwise have listed a shell that may be
  running a dev server as idle. The row says "not observed yet", and it is
  excluded unless Include running is ticked.
- Ages read record + one throttle resolution, a true upper bound on the last
  use: the throttle drops uses within a minute of the record, so the bare
  record let a one-minute threshold call a shell used a second ago old.
- Use is stamped from the user's keydown, composition and paste DOM events
  rather than xterm's onData, which also carries automatic terminal replies
  and returned early while the shell was waking.
- Undo Close carries the record with the restored terminal.

Refs #1178

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pi's review: it returns an upper bound (record + one throttle resolution),
not the record, so the next caller that formats it as a timestamp would state
a false minute. Also records the one known imprecision of the first-sample
rule, which only ever errs toward keeping a shell open.

Refs #1178

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05
Juliusolsson05 merged commit 00ef622 into main Sep 25, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the fix/terminal-last-used branch September 25, 2026 01:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(terminals): a terminal's last-used time resets on every reload, so Close Old Agents cannot age terminals

1 participant