Commit Graph
17 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 be266da9e2 let an idle browser go without taking the agent with it
Officer's hour-long idle timer was doing two unrelated jobs: collecting its own in-memory binding, which
is its business, and terminating the agent, which is the sidecar's. It could not do the first without
the second, because `unsub` was a closure reachable only through `kill`.

So a browser that went away killed a live agent an hour later — including one the sidecar had
deliberately protected. The sidecar already refuses to collect a session that is mid-turn or holding
background tasks: `task:started` disarms its idle GC, and `armIdle` re-checks and re-arms rather than
firing once. Officer had no view of any of that. A laptop running out of battery overnight took a
`run_in_background` job with it for no reason.

`detach` now sits beside `kill` on both streaming handles, and `_sidecarUnsub` — declared and called for
a long time, never once assigned — is populated at all three sites. `releaseSession` unsubscribes and
forgets the record without killing; the idle timer points at it. `deleteSession` is unchanged, so an
explicit disconnect still ends the session.

The third assignment site was not in the plan: `adoptOrphanedSession` sets `_claudeKill` but nothing
else, so an adopted session that later idled out would have dropped its record while the listener stayed
subscribed — a leak of one per adopt-then-leave.

No double subscription: releasing unsubscribes first, so a returning browser either adopts with a fresh
listener or starts a first turn with none behind it.

Step 1 of docs/chat-session-lifetime.md. Step 2 (a list verb, so running sessions can be found after a
restart) is still open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 00:32:10 +01:00
pastilhasandClaude Opus 5 6d0d103c78 carry attached images through to the model
The composer already uploaded an image, split its data URL and put the bytes on the wire as
`images`. Nothing on the server read them. The `chat` ClientMessage had no such field, and the
prompt reached the sidecar as a bare string, so all the model ever saw was the client-generated
`[Attached image: …]` placeholder — a label describing a picture it was never shown.

The transport was never the obstacle: `query()` consumes an async iterable of user messages whose
`content` is an Anthropic `MessageParam`, and only `pushTurn` hardcoding a string kept it to text.
So `images` is threaded through the four hops that dropped it and turned into native image content
blocks at the end, renaming `mediaType` to the API's `media_type` at that last step.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 02:54:39 +00:00
pastilhasandClaude Opus 4.8 238c3b8097 make the agent sidecar the writer of record for chat output
officer's registration socket silently drops sends when it isn't OPEN
(sidecar/connect.ts:send — no queue, no error, no return value). the agent pushed
raw parser events over that socket and officer translated and persisted them, so
everything a turn produced while officer was restarting went nowhere: the turn kept
running, the output was gone, and a reconnecting client replayed a log that simply
had no rows for those seconds. stage 1 kept the agent alive across a restart; this
is what makes its output survive one too.

move the translation and the write into the sidecar:

- turn-stream.ts is the stateful ChatEvent -> browser-message translator lifted out
  of websocket.ts (delta buffering, flush before tool:start and result). pure and
  synchronous, so it is unit tested — 12 tests, 100% lines.
- session-log.ts commits each message to chat_session_events and only then hands it
  to officer, with its cursor id attached. per-session promise chain: translation is
  synchronous and therefore in arrival order, and only the commit is queued, so
  cursor ids are assigned in the order events actually happened. a delta that
  overtook the assistant:text in front of it would make the client commit its stream
  buffer at the wrong point, so deltas go through the same queue even though they are
  never written.
- claude:event on the wire becomes claude:message: a finished browser-facing message
  plus its seq. officer relays it verbatim and folds it into the in-memory session
  for sync:messages. it no longer builds or persists chat messages for this harness.

gap detection, which is what the durable log is for. chat_session_events.id is a
global bigserial, so two consecutive events of one session are not consecutive ids
and a client cannot tell a contiguous replay from one with a hole in it. each durable
message now carries prevSeq — the cursor of the previous message in the same session —
which is inside the persisted payload, so it survives replay. useChat compares it
against the cursor it holds before advancing, and surfaces a visible marker on a
mismatch: a conversation that silently skips a tool call or half an answer reads as
the assistant having done something inexplicable. only checked once a cursor exists,
because opening a session from history legitimately starts mid-chain (events are swept
after 7 days, the transcript is not).

a failed write delivers live with no seq, so the client sees the message but does not
advance past something it cannot replay, and the next successful write chains from the
cursor the client still holds.

pipeline steps pass durable: false. their sessionKey is a throwaway uuid no browser
will ever replay and the job's own event log is its record, so writing those rows only
grows the table.

opencode still goes through officer's createEventHandler, now labelled as such. that
is the sidecars-opencode branch.

this fixes R4 from CLAUDE_SIDECAR_ISOLATION.md. R3 and R5 already worked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 04:51:52 +00:00
pastilhasandClaude Opus 4.8 62dc4c1a5c run the agent as a pm2 peer instead of a child of officer
the process that runs claude (sidecar/claude/user-instance.ts) had no pm2 entry
and was spawned on demand by the main server, with stdout/stderr inherited. that
made every agent session a grandchild of officer, so pm2's tree-kill took the
session down on every `pm2 restart officer` — the single thing that makes it
impossible to work on the platform while an agent is running.

give it its own entry (officer-agent) and delete the spawn machinery:
ensureClaudeSidecar, spawnAndWaitForRegistration, the 50ms registration poll and
the per-email claudeProcs/claudeSpawnWaiters maps, ~77 lines. officer now spawns
no sidecar at all.

for that to work the sidecar had to stop needing officer to start:

- it resolves the owner from the database (getOwnerUser) instead of reading
  CLAUDE_USER_EMAIL out of the env officer built. single-user is a hard
  invariant, so there is nothing to fan out over. CLAUDE_USER_EMAIL still wins
  when set, for manual runs, and a fresh install waits for bootstrap rather
  than exiting into a restart loop.
- it reads the anthropic proxy secret from the proxy sidecar's own state file
  rather than being handed it in env. lazily, because ensureProxySecret
  persists on a 30s debounce and pm2 starts both processes together.

it registers as 'agent' with capability 'claude', so the registry finds it the
way it finds every other sidecar. that removes the email argument from
killClaude, interruptClaude and clearClaudeSession, which only ever existed to
locate a per-email sidecar by name.

what officer keeps is a short wait-for-capability, because pm2 brings peers up
together and the first request after a boot can beat the sidecar's registration.

also align the two officer port fallbacks in the sidecar (5000 for the socket,
9010 for the rest base) — same instance, so they cannot disagree.

this fixes R1 and R2 from CLAUDE_SIDECAR_ISOLATION.md. events produced while
officer is down are still lost; that is stage 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 04:34:01 +00:00
pastilhasandClaude Opus 4.8 449f28b1e5 chat: persistent Agent SDK session per chat — decouple worker from turn (Phase 1)
Root fix for orphaned background tasks: the platform drove Claude Code as a
one-shot `claude -p` per turn (stdin ignored, process exits at turn end), so
run_in_background/Monitor work — and its task_notification — had no live harness
to return to. Now each chat session runs ONE long-lived Agent SDK query() with
streaming input; turns are user messages pushed onto it, and the session stays
warm between turns.

- claude-manager: persistent `query({ prompt: AsyncIterable, options })` per
  sessionKey (bypassPermissions, --resume, mcp via extraArgs, CLAUDECODE stripped).
  Single consumer loop maps every SDK message → ChatEvent, incl. post-turn
  task_started / task_notification. interrupt() = stop-turn; abort() = kill-session;
  30-min idle GC.
- stream-parser: processMessage() (object-level, reused by the SDK loop) + task
  message handling. ChatEvent/ServerMessage gain task:started / task:notification.
- API: the sidecar event subscription is now SESSION-scoped (no longer unsubscribes
  on 'result'), so background events after turn-end still reach the client. First
  turn opens the session; later turns push onto it. handleStop → interrupt (keeps
  session warm); disconnect/deleteSession → kill.
- protocol/sidecar-registry/user-instance: claude:interrupt command + interruptClaude.
- client: render task:started / task:notification in the transcript.

Verified end-to-end through the real chat WS: a run_in_background task's completion
arrives ~6s AFTER the turn's result; multi-turn on one warm session works.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 00:00:11 +00:00
brunorezioandClaude Opus 5 044aacf4d5 remove the dead multi-user surface
Officer is single-user: the server owner is the only account, created once by
/auth/bootstrap. Everything that existed to serve additional users was
unreachable, so it is gone rather than left looking like it does something.

Accounts: drop the invite / resend-invite / delete / list-users routes and the
Users settings screen, the inert /auth/signup handler, and the account
verification chain it fed (verify, resend-verification, VerifyScreen, the
UserInvite + VerifyAdmin + VerifyRegistration templates). /auth/verify-token
survives for password resets only, and now requires a reset-password token
rather than accepting any signed JWT.

Roles: drop the users.role column and the four-value USER_ROLES enum. The
permissions table granted every role identical methods, and every
role === 'Super Admin' check was permanently true. The JWT no longer carries a
role claim.

Sandbox: remove sidecar/sandbox.ts and its five call sites. bwrap was selected
only for non-Super-Admin users, so it never ran. It was also not a usable agent
jail as written — --share-net, the project root (with .env) bound read-only,
and runuser dropping to the server's own uid. Rebuilding it for agent
containment would be a different construction, and git history keeps this one.

getHomeDir keeps its DATA_PATH meaning; the new getOwnerHomeDir resolves the
owner's real login home, which is what terminals, chats and task runs use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:30:20 +01:00
pastilhasandClaude Opus 4.8 49df1c0b0c chat: rename the pi chat transport + model list to chat (Stage 4b/1)
Pure rename, no behavior change. Moves the misnamed "pi" chat harness into the
chat namespace:

- api/pi/{websocket,session-manager,types,logger,list-models} → api/chat/
- merge api/pi/rest.ts into api/chat/chat.ts (/pi/models → /chat/models,
  /pi/stt → /chat/stt); drop the piRestRouter mount
- PiEvent → ChatEvent, piWebsocket → chatWebsocket, listPiModels → listChatModels
- WS route /api/pi/chat/ws → /api/chat/ws, provider tag 'pi' → 'chat'
- frontend: useChat/useAudioRecording URLs, usePiModels→useModels /
  useVisiblePiModels→useVisibleModels / useEnabledPiModels→useEnabledModels,
  'PI_MODELS' query key → 'CHAT_MODELS', attachments provider 'pi-mono' → 'chat'

The /pi-mono provider/harness settings router is renamed separately (next commit).
Note: the WS route change requires the mobile app to point at /api/chat/ws.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 15:47:40 +00:00
pastilhasandClaude Opus 4.8 9565cd9462 chat: click-to-resume /chat sessions via Claude --resume
Clicking a session in the list loads its transcript (GET /chat/sessions/:id,
parsed from Claude's JSONL into display messages) and continues the actual Claude
session: a resumeSessionId is threaded chat handler -> send-claude-code -> sidecar
-> claude-manager, which passes --resume <uuid> (in-memory session mapping still
takes precedence for live turns). Parser verified against real transcripts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 10:11:58 +00:00
pastilhasandClaude Opus 4.6 9578110e8b shared bwrap sandbox, skip for super admin, extend to pi and terminals
- extract buildSandboxPrefix/buildRunuserSuffix into shared sandbox.ts
- super admin bypasses bwrap for full host access (claude, pi, terminal)
- member pi processes now use bwrap instead of sudo -u
- member terminals now use bwrap instead of sudo -u
- mount /run for systemd-resolved DNS inside sandbox
- pass role through claude spawn params and channel types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 00:06:44 +00:00
pastilhasandClaude Opus 4.6 a0d9ea63f9 sidecar self-registration: sidecars connect to API server instead of vice versa
Flips the connection model so sidecars register themselves with the API
server via WebSocket at /api/sidecar/register, enabling dynamic discovery,
location independence, and automatic reconnection from either side.

- Add registration protocol types and PTY command/event types
- Create sidecar-registry.ts (replaces sidecar-client.ts) as passive registry
- Create sidecar connector (connect.ts) with exponential backoff reconnect
- Convert process sidecar from WS server to WS client
- Convert PTY sidecar from WS server to multiplexed WS client
- Simplify terminal bridge to thin adapter using registry
- Add PTY sidecar as PM2-managed process
- Update all consumer imports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:48:19 +00:00
pastilhasandClaude Opus 4.6 86c2d6333a process sidecar: independent process manager for long-running work
Introduces a separate Bun process (port 5100) that owns all spawned
processes and long-running work, so the API server can restart freely
without disrupting active sessions.

The sidecar owns:
- Anthropic proxy (port 5051) with persisted secret across restarts
- Claude Code process spawning and session tracking (--resume support)
- Pi agent spawning and RPC lifecycle (prompt/abort/thinking)
- Job queue engine (lane processing, retries, notifications)

The API server becomes a thin client that forwards commands over a
single WebSocket connection with auto-reconnect. send-claude-code.ts
goes from 550 lines of spawn logic to 73 lines of sidecar delegation.

State persisted to data/sidecar/state.json every 30s and on shutdown.
Lockfile prevents duplicate instances. See SIDECAR.md for full docs
and manual testing procedures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 07:36:47 +00:00
pastilhasandClaude Opus 4.6 c54ff9da28 bypass anthropic proxy for users with own claude credentials
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 04:35:34 +00:00
pastilhasandClaude Opus 4.6 038fd16fb2 anthropic auth proxy for multi-user claude code
Local HTTP proxy on 127.0.0.1:5051 intercepts Claude Code API requests
from sandboxed member users, injects the real OAuth token server-side,
and forwards to Anthropic. Users only see a proxy secret, never the
real credentials.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 03:40:14 +00:00
pastilhasandClaude Opus 4.6 d5fd3f58b1 add claude code model selection (opus, sonnet, haiku)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:58:31 +00:00
pastilhas ef13f96d36 fix(pi): add snap node compatibility diagnostics and documentation
- Added detailed error logging to detect snap node compatibility issues
- When Pi process exits with code 1, log helpful diagnostic info including node path
- Add hint to check for snap node and reinstall via apt/nvm
- Create SNAP_NODE_COMPATIBILITY.md with full troubleshooting guide
- Document root cause: snap node has file descriptor incompatibility with Bun.spawn stdin pipes
- Provide clear installation instructions for NodeSource and nvm alternatives
2026-03-04 01:45:36 +00:00
pastilhasandClaude Opus 4.6 40a9768cb3 claude-code streaming chat, desktop remote viewer, new-automation route, tiktok task v4, misc fixes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 20:00:45 +00:00
pastilhasandClaude Opus 4.6 0068244356 claude-code as channel model, container trust/permissions fixes, terminal cwd fix
- add claude-code as virtual model in channel messaging (telegram/discord/whatsapp)
- new send-claude-code.ts: docker exec claude -p with session resumption
- route claude-code model in sendAndAwait before Pi pipeline
- append claude-code to listPiModels output
- fix container .claude mount (rw for sub-mounts), hooks format (matcher-based)
- pre-seed hasTrustDialogAccepted and bypassPermissions in container settings
- git init in entrypoint to skip workspace trust prompt
- fix ~/~ double-tilde in CommandTerminalWrapper cwd resolution
- remove --continue from claude-code panel command

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 20:00:45 +00:00