Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
8 changes: 7 additions & 1 deletion src/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
12 changes: 8 additions & 4 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1821,7 +1821,8 @@ function TerminalFrame({ session, tmux, onActivity }: { session?: Session; tmux:
<Terminal className="h-3.5 w-3.5" /> {takingOver ? 'taking over…' : 'Take over'}
</button>
)}
<ImageDropZone session={session} onActivity={onActivity && session?.id ? () => onActivity(session.id) : undefined}
<ImageDropZone session={session} attachable={Boolean(session) && (isLive(session!) || overrideAttach)}
onActivity={onActivity && session?.id ? () => onActivity(session.id) : undefined}
fontSize={fontSize} setFontSize={setFontSize}>
<Xterm key={nonce} wsUrl={wsUrl} fontSize={fontSize} copyOnSelect />
</ImageDropZone>
Expand All @@ -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<HTMLDivElement>(null)
// Keep the latest upload closure reachable from imperative (addEventListener) handlers without
// re-binding them every render.
Expand Down
Loading