From 43b0a9d3196596dacbbfd2fb8f9e89fb16fc6098 Mon Sep 17 00:00:00 2001 From: Vikas Singhal Date: Mon, 13 Jul 2026 13:53:36 +0530 Subject: [PATCH] fix(session): take-over sets status='running' so file attach works (v0.138.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Taking over an unattended run (claimSession / POST /interactive) flipped headless→0, set claimed_by, and cleared the resume sentinel — but never set status='running' the way the resume path (markResumed) does. A take-over can race the Stop-hook turn-end teardown that already moved the run to 'done', so the claimed-and-attached session kept a terminal status and everything gated on status==='running' (notably attachFile) rejected it with "session is not live". Take-over now forces status='running' (pane resurrects on re-open via the already-cleared sentinel). The console's attach/drag/paste gate also keys off the pane being attached/live (isLive + the just-took-over override) instead of raw status, so it no longer lags a poll behind a take-over; the server stays the hard authority. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 14 ++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- src/terminal.ts | 8 +++++++- web/src/App.tsx | 12 ++++++++---- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 314ae1d..77db760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,20 @@ new version heading in the same commit. ## [Unreleased] +## [0.138.2] — 2026-07-13 +### Fixed +- **Taking over an unattended session no longer breaks file attach with "session is not live."** Take-over + (`claimSession`, `POST /api/sessions/:id/interactive`) flipped `headless→0`, set `claimed_by`, and cleared + the resume sentinel — but, unlike the resume path (`markResumed`), it never set `status = 'running'`. A + take-over can race the Stop-hook turn-end teardown, which may have already moved the run to `done`; the + claimed-and-attached run then kept a terminal status, so everything gated on `status === 'running'` — + notably `attachFile` — rejected the now-live, steerable session as "not live." Take-over now forces + `status = 'running'` (the pane resurrects on re-open via the already-cleared sentinel), matching resume. + The console's 📎 attach/drag/paste gate also now keys off the pane being **attached/live** (the same + `isLive` rule the green dot uses, plus the just-took-over override) instead of raw `status`, so it stops + lagging a poll behind a take-over; the server stays the hard authority. + (`TerminalManager.claimSession`, `ImageDropZone` in `web/src/App.tsx`.) + ## [0.138.1] — 2026-07-13 ### Fixed - **The Profile page now loads instead of bouncing to the Inbox.** `profile` was added to the `Route` diff --git a/package-lock.json b/package-lock.json index 71f78b3..81eadfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agent-os", - "version": "0.138.1", + "version": "0.138.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agent-os", - "version": "0.138.1", + "version": "0.138.2", "license": "MIT", "bin": { "agent-os": "bin/agent-os" diff --git a/package.json b/package.json index d861d86..ed5604c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-os", - "version": "0.138.1", + "version": "0.138.2", "description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.", "license": "MIT", "type": "commonjs", diff --git a/src/terminal.ts b/src/terminal.ts index 45baafc..afc6390 100644 --- a/src/terminal.ts +++ b/src/terminal.ts @@ -1098,7 +1098,13 @@ export class TerminalManager { if (row.claimed_by) return { ok: true }; // already taken over — the pane is already sticky/attachable // A prior stop must not veto the deliberate take-over; clear any sentinel so a re-open resurrects. this.allowResume(sessionId); - this.db.prepare("UPDATE term_sessions SET headless = 0, claimed_by = ?, claimed_at = ?, updated_at = ? WHERE id = ?") + // Force status back to 'running' (like markResumed does for the resume path). A take-over can race the + // Stop-hook turn-end teardown, which may have already flipped an unattended run to 'done'; without this, + // the claimed run keeps a terminal status and everything gated on `status === 'running'` — notably + // attachFile ("session is not live") — wrongly rejects the now-attached, steerable session. The sentinel + // is already cleared above, so a re-open resurrects the pane; 'running' is the one flag resume set that + // claim was missing. + this.db.prepare("UPDATE term_sessions SET headless = 0, status = 'running', claimed_by = ?, claimed_at = ?, updated_at = ? WHERE id = ?") .run(by, Date.now(), Date.now(), sessionId); // No kill, no relaunch — the live pane keeps streaming; the caller opens ttyd and attaches to it. this.audit(sessionId, by, 'session.claimed', { agent: row.agent }); diff --git a/web/src/App.tsx b/web/src/App.tsx index 46dc2e9..a29dea2 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1821,7 +1821,8 @@ function TerminalFrame({ session, tmux, onActivity }: { session?: Session; tmux: {takingOver ? 'taking over…' : 'Take over'} )} - onActivity(session.id) : undefined} + onActivity(session.id) : undefined} fontSize={fontSize} setFontSize={setFontSize}> @@ -1835,14 +1836,17 @@ function TerminalFrame({ session, tmux, onActivity }: { session?: Session; tmux: * (file paste). Each uploads the bytes; the server saves them in the agent's folder and types the path * into the running claude. Now that the terminal is same-document (no iframe), drops/pastes over it * bubble to our window handlers directly — no cross-document interception needed. */ -function ImageDropZone({ session, children, onActivity, fontSize, setFontSize }: { - session?: Session; children: ReactNode; onActivity?: () => void +function ImageDropZone({ session, attachable, children, onActivity, fontSize, setFontSize }: { + session?: Session; attachable?: boolean; children: ReactNode; onActivity?: () => void fontSize: number; setFontSize: (f: number | ((s: number) => number)) => void }) { const [drag, setDrag] = useState(false) const [help, setHelp] = useState(false) const [toast, setToast] = useState<{ kind: 'ok' | 'err' | 'busy'; text: string } | null>(null) - const live = session?.status === 'running' + // Gate on the pane being attached/live — the same `isLive` notion the green dot uses, plus the + // just-took-over override — NOT raw `status === 'running'`, which lags a poll behind a take-over and + // reads terminal on a still-attached run. The server (`attachFile`) stays the hard authority. + const live = Boolean(attachable) const wrapRef = useRef(null) // Keep the latest upload closure reachable from imperative (addEventListener) handlers without // re-binding them every render.