Skip to content

LocationActivity evicts running location-scoped background shells at the 60-minute idle TTL, silently stranding the sessions waiting on them #52908

Closed as not planned

Description

@mpj

Repro (opencode 2.0.18, measured 2026-10-02)

A session created in a directory; POST /api/shell spawns sleep 5400 there.

Time (UTC) Event
20:43 session created; background sleep 5400 spawned through POST /api/shell?location[directory]=…
~21:43 location services evicted directory=… (INFO)
21:51:33 GET /api/shell/{id} → ShellNotFoundError, HTTP 404

What happened

A background shell spawned through POST /api/shell?location[directory]=… is killed when LocationActivity evicts its idle location, and the eviction is silent but for one INFO line. A session waiting on that shell's completion to wake it is stranded: it looks idle and healthy, but nothing is left to wake it.

Why it bites

Agents hold long waits in these shells — CI watches, pollers, "wake me when this process exits" strokes — and expect the shell's completion to remain observable afterwards. Two properties combine to make that unsafe past an hour: (1) the TTL is hard-coded (timeToLive??"60 minutes") and refreshed only by durable session events, so a running shell cannot keep its own location alive; (2) the sweep also interrupts agent loops still active in the location (s.interrupt(id, {reason:"inactivity", awaitSettlement:true})), so even an in-flight multi-hour foreground operation is at risk. A caller can lose the artifact it is blocked on and gets no signal at all — "silently reaped" is indistinguishable from "still running" until someone notices an empty shell bar.

Related work

fix(core): preserve approvals across location cleanup (#46509) made cleanup skip locations with outstanding references, because a permission wait exceeding the timeout lost its request. A running background shell looks like the same kind of outstanding reference from a caller's point of view.

What would fix it (any one)

  1. Treat a running background shell as location activity, or as an outstanding reference deferring cleanup (the fix(core): preserve approvals across location cleanup #46509 shape);
  2. make the idle TTL configurable;
  3. at minimum, make eviction of a location holding running shells visible — a WARN with the shell ids, and/or a status: "reaped" record rather than a bare 404 — so callers can tell "reaped" from "never existed".

Our workaround

We hold such waits in a detached process outside the server and push a wake on completion (POST /api/session/{id}/prompt, which admirably admits input to a previously idle session and runs the loop). Unnecessary with (1) or (2).

Activity

  1. github-actions commented on Oct 3, 2026

    @github-actions
    Contributor

    This issue does not fully meet our contributing guidelines.

    What needs to be fixed:

    • Please edit the issue to use the Bug Report template format and put the report under its required Description field. The current custom headings omit that field; your reproduction steps and OpenCode version are already helpful.

    Please edit this issue to address the above within 72 hours, or it will be automatically closed.


    This issue might be a duplicate of existing issues. Please check:

    • #51828: reports background shells terminated by 60-minute Location inactivity eviction.
    • #48691: reports terminals killed by 60-minute location eviction.
    • #51343: reports the same eviction interrupting active sessions and rejecting pending questions.

    If you believe this was flagged incorrectly, please let a maintainer know.

  2. SamSpiri commented on Oct 3, 2026

    @SamSpiri

    Second independent instance, DB-verified (v2.0.22)

    Independent confirmation of the exact failure, with the completion notification proven absent from storage.

    Setup: worker session ses_efeb61f29ffeWnthm3xB8kevap in a location (directory) whose only activity was the dispatched turn.

    Timeline (local time):

    Time Event
    18:02:34 shell tool call, background: true, timeout 0 → job sh_10280cd3f001TXnljvbScsSUtv
    18:03:05 worker idle (outcome succeeded) — last durable event in the location
    ~19:03 60-min idle TTL elapses → sweep evicts location services evicted (directory)
    19:11:33 job actually finishes (/tmp/.../real2_summary.txt mtime; tee finalised)
    20:11:15 session woken manually by the user

    Evidence the notification was never emitted:

    • session_message: no synthetic row with shellID=sh_10280cd3f... exists anywhere in the DB. If the watcher had delivered, the row would be present (rows are append-only; a later user prompt cannot consume/remove it).
    • session_inbox / session_pending for the session are empty — nothing was left undelivered, so this is not a "synthetic admitted but wake dropped" case.
    • The shell .out file (sh_10280cd3f...out) is gone, consistent with shell.ts:234 Effect.onInterrupt(() => shell.remove(info.id)) running when the watcher fiber is interrupted at eviction.
    • No server restart/crash in the window (NRestarts=0, no start/exit in journal) — resumeSuspendedSessions did not run.

    Control: a second background: true job in the same session, sh_103051812..., ran ~17 min (< 60) and did deliver: synthetic row with metadata {source:"shell", shellID, jobID, state:"completed", exit:0} at 20:29:02. So the path works when the location survives; it only breaks when the job outlives the idle TTL.

    Multiplier detail: the threshold is idle time since the last durable event in the location, swept at 1-min granularity — not job runtime. A background job that ends before ~60 min of location inactivity is safe; one that outlives it loses its completion notification and strands the waiting session, exactly as described here.

    The chain in source for this instance: notifyWhenDone is Effect.forkIn(scope, …) into the location/plugin scope (tool/plugin/shell.ts:154-186); location-activity.ts:51,77-81 interrupts owners with reason:"inactivity" then locations.invalidate(ref), closing that scope and interrupting the watcher before sessions.synthetic(...) runs. packages/core/test/location-activity.test.ts:176-178 already asserts a waiting session is evicted after 1 + 62 minutes of mocked time.

  3. github-actions commented on Oct 6, 2026

    @github-actions
    Contributor

    This issue has been automatically closed because it was not updated to meet our contributing guidelines within the 72-hour window.

    Feel free to open a new issue that follows our issue templates.

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

Metadata

Metadata

Assignees

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