Skip to content

[Feature] Expose per-session context size + last-turn timestamp to agents via sys_session_get_info (SDK and native parity) #3271

Description

@shkarupa-alex

Problem

An orchestrator driving sub-agents has no way to answer two questions about any session it manages — its own children included:

  1. How full is that session's context right now?
  2. When exactly did its last turn finish?

Both are prerequisites for deliberate context management. Without them, the only available strategy is to keep sending turns until something breaks (see #2967: a full context window bricks a session with "Prompt is too long" and the turn path never auto-compacts), or to compact/respawn on a blind guess.

sys_session_get_info — the one agent-facing surface that returns session metadata — currently returns neither:

sys_session_get_info(session_id="656fe0e7…")
=> {"session_id": "", "status": "idle", "title": "", "agent_id": "",
    "agent_name": "Claude", "runner_id": "", "runner_online": true,
    "host_id": null, "parent_session_id": "", "sub_agent_name": null,
    "reasoning_effort": null, "model": "claude-opus-5", "workspace": null,
    "git_branch": null, "pending_elicitations": [], "pending_elicitation_count": 0}

No token counts, no context window, no timestamps of any kind. sys_session_get_history does not help either — its items are bare {type, role, text} with no timestamps and no usage. So status: "idle" is the entire temporal signal available: it distinguishes "not running" from "running", and nothing else. A session that finished 30 seconds ago and one that finished three hours ago are indistinguishable.

Why this matters

Deciding compact vs. new session. These are different operations with different costs, and picking correctly requires knowing the fill level. Compaction is lossy and, per #3254, repeated compaction can leave a sub-agent silently stalled; starting fresh costs a re-prime of the task context. An orchestrator should be able to see "this child is at 78% of its window" and act before the wall, rather than after.

Prompt-cache economics on Claude subscriptions. The prompt cache has a ~1 hour TTL. That makes elapsed-time-since-last-turn directly load-bearing for a cost decision:

  • Last turn finished 4 minutes ago → the prefix is almost certainly still cached; continuing the existing conversation is dramatically cheaper than re-priming, even at a high context fill.
  • Last turn finished 90 minutes ago → the cache is gone. The next turn re-reads the entire (possibly large) context at full input price. At that point starting a fresh session with a compact hand-off can be cheaper than continuing, and the crossover depends on exactly the two numbers this issue asks for.

Right now that arithmetic is impossible to perform, so the choice is made by vibes.

The data already exists — it just isn't projected

This is a plumbing gap, not a measurement gap.

Context size is already computed by every harness family and normalized to the same context_tokens key (input + cache_creation + cache_read):

harness site
claude-native claude_native_bridge.py:4194, claude_native_forwarder.py:4224
codex-native codex_native_forwarder.py:6066
opencode-native opencode_native_forwarder.py:788
SDK executors inner/openai_agents_sdk_executor.py:1832, and max_context_tokens() on inner/executor.py:557 + per-executor overrides

It is already persisted on the session row as reserved instance-scoped labels (stores/conversation_store/__init__.py:111-112, where the comment calls them "the context-size metrics"):

  • omnigent.last_context_tokens
  • omnigent.last_context_window

written server-side at server/routes/sessions.py:3720-3722 and read back at :22387 / :22475.

And it is already surfaced — to the web UI, not to agents. The session snapshot exposes it as last_total_tokens (server/routes/sessions.py:22385-22389), which drives the context-occupancy ring. #1533 discusses that meter's behavior, so the value is understood to be user-facing already.

Timestamps likewise exist. created_at / updated_at are on conversation rows and on ChildSessionSummary (server/schemas.py:254-255, :777-778), and Usage already carries cache_read_input_tokens / cache_creation_input_tokens (server/schemas.py:821-827, "Consumed by the cache-aware server-side cost path").

The gap is a single projection: _session_get_info_via_rest (runner/tool_dispatch.py:3439) hand-picks ~15 fields from the server snapshot and includes no timestamp and no usage field. The in-process SysSessionGetInfoTool path has the same omission.

Proposed shape

Extend sys_session_get_info (and, in reduced form, the sessions rows of sys_session_list) with:

{
  // … existing fields …
  "context": {
    "tokens": 48213,              // omnigent.last_context_tokens
    "window": 200000,             // omnigent.last_context_window
    "used_fraction": 0.24,
    "as_of": 1785000000,          // when this measurement was taken
    "stale": false                // true when the last turn failed/stalled — see #1533
  },
  "last_turn_completed_at": 1785000000,
  "seconds_since_last_turn": 2317,
  "created_at": 1784990000,
  "updated_at": 1785000000,
  "last_turn_cache": {            // optional but directly answers the TTL question
    "cache_read_input_tokens": 41002,
    "cache_creation_input_tokens": 1180
  }
}

Notes on the shape:

  • as_of / stale are not optional decoration. Context-occupancy meter (context_tokens) freezes on failed turns — only emitted at successful completion, so a stalled/failed turn shows a stale low % (observed 3% on a session actually ~14%) #1533 documents that context_tokens is only emitted on successful turn completion, so a stalled or failed turn leaves a stale low value. A bare number would therefore be actively misleading for a compaction decision. Shipping the measurement with its own timestamp makes staleness detectable by the caller instead of silently wrong.
  • seconds_since_last_turn is derived, but worth returning explicitly: the client has no reliable clock relationship to the server, and the whole point is comparing against a ~1h TTL.
  • Parity across SDK and native is the requirement, not a nice-to-have. An orchestrator like polly mixes both in one tree and must apply one policy across them. Since all harness families already normalize to context_tokens, parity is mostly already paid for.
  • last_turn_cache is the lowest-confidence part of the ask — if the per-turn cache split is awkward to persist, last_turn_completed_at alone is enough to reason about the TTL.

Related

Metadata

Metadata

Assignees

Labels

P2-mediumPriority: bug with workaround, important feature requestcomp:reprComponent: serialization, representation layercomp:runnerComponent: agent runner, execution enginecomp:serverComponent: server, API, session managementenhancementNew feature or requesthelp wantedExtra attention is neededtriagedIssue has been triaged by the bot

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions