Deletes all session persistence that isn't Claude's native transcript store, per
the "only harness-native session management survives" rule.
Backend: delete api/pi/storage.ts (meta.json+messages.json file store), the
api/saved-sessions router (+ unmount), the /pi/sessions REST endpoints, and the
storage.save/loadSession calls in the chat WS handler (in-memory session-manager
stays for live turns; no disk persistence — Claude's transcript is the record).
Also drops the Postgres saved_sessions layer: schema/chat.ts, queries/saved-sessions.ts,
its types and re-exports.
Frontend: delete state/useSavedSessions, ChatList, and the ChatHistory Widget
(all pure saved-session UI); slim ChatHeader to a label; strip the auto-load-latest
+ Save wiring from ChatPanelWrapper and ChatDetailPanel; drop the old resume path
from useChat and SessionListPage; remove the /chat/saved/:id route and the
useInitialData prefetch.
Behavior removed (intended): the Save-session button, email/project panels
auto-resuming the last chat, and /chat/saved/:id. /chat itself is unchanged —
already fully on Claude transcripts. The orphaned saved_sessions Postgres table
is dropped on the next `bun db:push`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bun closes a WS idle for idleTimeout (60s) and only resets its timer on frames
received from the client — but during a chat turn the client only receives, so a
long tool call or a gap between turns tripped the timeout (clean close 1000,
mid-stream cuts, reconnects). Now each chat connection is pinged every 25s in the
WS open handler (cleared in close); the client auto-pongs at the protocol level,
resetting Bun's timer. Covers every client (web, mobile) with one server change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reorganizes email storage: the DB moves from DATA_PATH/<user>/emails.db to
DATA_PATH/<user>/email_accounts/<accountEmail>/emails.db, with a shared
email_accounts/attachment_cache/ (was Gmail/emails/attachments). openEmailDb now
takes (owner, account); a new openUserEmailDb(owner, userId) resolves the user's
configured account (first enabled) for read paths. Threads the account through
email.ts, accounts, resync, queue sync, channel handlers, and the email_db MCP
tool path. Drops the dead getUserEmailDir helper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Sessions panel header gets a pwd selector: the default claude_sessions dir,
plus every directory that already has Claude sessions (auto-discovered by reading
the real cwd back from ~/.claude/projects), plus free-text entry. Switching pwd
refetches the list for that cwd's Claude project group and runs New Chat / resume
in it. Backend: GET /chat/pwds + a ?cwd= param on the session ops; the WS handler
honors a chosen cwd for /chat (default claude_sessions). Browse-modal picker next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single-user platform: the Super Admin's Claude process now uses the real home
(HOME_DIR) instead of DATA_PATH/<email>/home, so its transcript store IS the same
~/.claude the terminal `claude` uses — platform and terminal sessions are
interchangeable (native `/resume` sees them). The session reader resolves the
same home. The generated container CLAUDE.md is no longer written for the Super
Admin (it would pollute the personal global ~/.claude/CLAUDE.md and is stale);
MCP tools still load via --mcp-config, and email/project panels inject their own
prompts. Sandboxed users keep their isolated home.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename appends a {"type":"summary",...} entry to the session's JSONL transcript
(Claude's own format, so the title lives in .claude); the reader takes the last
summary as the title, without a timestamp so it doesn't reorder the list.
PATCH /chat/sessions/:id/title backs it. The list gets inline rename (pencil ->
edit in place) and a two-step delete confirm so a stray click can't nuke a
transcript. Rename verified against a synthetic store.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DELETE /chat/sessions/:id removes Claude's transcript file; the list gets a
per-row delete button. The list also invalidates on turn-complete so new and
continued sessions surface without a manual refresh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
The standalone /chat route now tags its context as 'chat'; the backend runs those
sessions with cwd = DATA_PATH/<email>/claude_sessions (created on demand), so their
transcripts form an isolated Claude project group instead of polluting the home-dir
history. Other chat contexts (email/project panels) keep their own cwd.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First slice of making Claude's session store the source of truth. Adds a reader
(api/chat/claude-sessions.ts) that lists sessions straight from Claude's JSONL
transcripts under $HOME/.claude/projects/<cwd-slug>/, plus GET /chat/sessions
over a dedicated per-user claude_sessions working directory. No bookkeeping
layer — Claude's files are authoritative. Verified against real transcripts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stage 1 of removing Pi (Claude-only). Cuts the non-Claude branches in the chat
WS handler, pipeline executor, and channel send-and-await; deletes the Pi
sidecar, its ecosystem entry, pi-bridge, and the Pi model-listing spawn (now a
static Claude tier list). Adds a guard coercing any legacy non-claude-code model
preference to the Claude default so old settings don't break chat or jobs.
Removes the dead no-op session-save REST route and stale Pi docs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The API and email sidecar both open emails.db; without a busy timeout a write
overlap (resync, or the thread_id backfill) throws "database is locked" and
500s a request. Wait up to 5s for the other writer instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hybrid grouping via a new thread_id column: new mail threads exactly on
References/In-Reply-To (id is sha1(Message-Id), so a referenced id hashes to
the ancestor's own id); already-synced mail is backfilled with a
normalized-subject + counterpart key. Folder views collapse to one row per
thread with a count badge; the reader shows the thread as a collapsible stack.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
exposes the duress lockdown as an endpoint (needs the distress password in the
body, rate-limited) so it can be tripped from a shortcut/webhook, not only by
typing the distress password at the login form.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a duress password (DISTRESS_PASSWORD env): entering it at login trips an
in-memory full lockdown — all new logins (password + passkey) and every existing
session are refused until the server is restarted, and the login itself returns a
normal "invalid credentials" so it gives nothing away. Also add
POST /api/auth/blacklist-token as a clearly-named alias for revoking the current
JWT (same effect as signout).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/download-video now delegates to the ReClip service (its own yt-dlp) and runs as
a background job: POST returns a jobId immediately and the client polls
GET /download-video/:jobId, so a large download no longer holds one long request
open (which was 504-ing behind the reverse proxy). the finished file is streamed
into the user's folder with a title-based name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
a script task that goes silent for a while (e.g. ffmpeg's faststart pass rewrites
a huge file for minutes with no output) would hit Bun's default 120s websocket
idle timeout → close(ws) → killTree killed the task mid-run, corrupting the
output. now the executor pings the socket every 30s while a task runs, resetting
the idle timer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
proc.kill() only signalled the direct child (bash), leaving a running ffmpeg
grandchild orphaned and still encoding. now killTree() walks /proc, SIGTERMs the
whole subtree (bash + ffmpeg), then SIGKILLs any straggler after 2s.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
converting a folder now recursively probes every video and groups episodes by
track layout (audio language+channels, subtitle language). the pickers are
driven off the largest matching group; episodes with a different layout are
listed as skipped and converted separately. adds a /probe-folder endpoint and
passes the majority file list to run.sh as INPUT_INCLUDE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add /file-browser/audio-tracks (list) and raw?audio=N (serve the video with
a chosen audio track via a cached -c copy remux, byte-range seekable). The
player shows an always-visible selector outside the controls; switching
reloads the source and restores the playhead. Works in Chromium, which
doesn't expose the audioTracks API.
Temp remux file keeps the real extension (a .tmp suffix breaks ffmpeg's
muxer selection); concurrent requests for a track share one remux.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add /file-browser/subtitles (list text subtitle tracks) and /subtitle-vtt
(extract one as WebVTT on demand); the VideoRenderer fetches the list,
renders <track> elements, and shows a CC selector to switch/turn off subs.
Track labels prefer title, then handler_name (mp4), then a real language,
falling back to Track N — so untagged tracks aren't shown as "und".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New POST /api/rescan re-runs the boot item setup (ensureItemDirs +
ensureToolLoader) and returns live item counts; the header button calls it
and invalidates the item query caches so the UI refetches from disk.
Also fix ensure-tool-loader to write into OFFICER_ITEMS_DIR/extensions (the
runtime read path) instead of the now-unread DATA_PATH/extensions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the marketplace service dependency and the native/global/user
scope tiers with a single external directory ($OFFICER_ITEMS_DIR) holding
skills, tools, tasks, processes and extensions as plain files.
- tasks move from Postgres to TASK.md files (new file-backed task layer);
task editing now works, which the DB path never supported
- skills/tools/processes collapse into one shared file router (single dir)
- remove the marketplace client (sync-marketplace/sync-version) and the
boot-time sync; pi-bridge/pi-manager/sandbox point at the flat store
- drop the dead tasks + vestigial skills/tools/processes/extensions +
item_chats tables (migration 0004)
- one-time migration script exports DB tasks and consolidates disk items
Migration verified: all 6 tasks round-trip through the runtime parser
identically to their DB rows (pipeline steps, triggers, script impls and
agentic bodies all intact).
NOTE: not yet functionally tested end-to-end — every item (each task mode,
tool, skill, extension) still needs to be run/exercised in the app before
this is trusted. To be done manually.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the /transcode (and dead /transcode-audio) routes and the frontend
mkv/avi transcode wiring. Only browser-native formats (mp4, webm, mov,
m4v, ogv) are classified as video now; other containers fall through to
the generic file view.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
the file browser sends msg.cwd relative to the user's home and it went straight to
Bun.spawn, which resolves it against the server's cwd. when that directory does not
exist posix_spawn reports ENOENT naming the binary rather than the directory, so
"Convert Audio" failed with the misleading "posix_spawn 'bash'".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- the sandbox already drops privileges to the OS user (id -un), so pi-bridge was
the only code path that switched to a per-user unix account
- the four accounts it targeted (andrepadez, john-wick, fedra, miguelbenoliel)
are vestigial: created by scripts/provision-existing-users.sh, with no home
dirs, no files, no processes. the only live account is pastilhas@officer.dev,
which maps to the service user, so isServiceUser was always true and the
sudo -u branch could never fire
- add TODO.md tracking the leftover username plumbing, the useradd script, known
bugs (task-executor relative cwd, bootstrap pi EEXIST), and the ufw 9010 rule
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- add getValidGoogleAccessToken helper and use it in email-cron, email account auth resolver, and the new gmail proxy
- POST /api/integrations/google/gmail-proxy forwards arbitrary gmail rest calls server-side, with auto-refreshed oauth
- pi-manager and claude user-instance inject OFFICER_API_URL + per-session JWT so tools can call back as the user
- claude anthropic proxy uses Authorization: Bearer + preserves any anthropic-beta headers (pro oauth tokens are rejected via x-api-key, and overwriting the beta header broke context_management)
- pi --list-models: fall back to stderr when stdout is empty (pi v0.73.1 writes the table to stderr)
- mcp tool server + pi tool loader: accept type: object inputs so json bodies stay structured
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>