Commit Graph
10 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 6235662492 fix(chat): an answered question is not a failed tool call
The answer channel is a denial — the only PreToolUse decision that can carry a
result — and the harness stamps every denied tool `is_error`. So a perfectly
good answer arrived looking like a failure: red in the transcript, and read by
the agent as something that went wrong rather than as the reply to its own
question. Confirmed on the first live question through the new channel, which
came back as `<error>Show it in Officer</error>`.

The parser already sees the tool_use that opened the question, so it records the
id and clears the flag when the matching result lands. A timeout or a lost
session lands here too and is equally not an error: its output is a sentence
explaining itself, which the agent should read rather than retry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:52:44 +01:00
pastilhasandClaude Opus 5 77f90a91f0 show the model that answered instead of the bare harness string
the picker rendered the literal `claude-code` for any chat that named no
tier. that is DEFAULT_MODEL — the bare harness — and it routes correctly
(no --model is passed, so the cli picks for itself) but names no catalogue
entry, so radix matched no item and the fallback from 773f0e1c printed the
raw string's last segment.

officer cannot answer this because it deliberately did not choose. the
harness does, in its system.init `model` field, which the parser was
discarding. so: emit it as a `model:resolved` chat event, translate it to an
officer id in turn-stream, and forward it to the client only when the turn
named no tier.

that last condition is the load-bearing one. the alias map is lossy in one
direction — opus[1m] and opusplan both answer as claude-opus-5 — so
forwarding unconditionally would silently relabel a user's own choice as
plain opus.

display only, and three ways: not durable (like session:claude), never
recorded to chat_session_models, and never written into the client's
selectedModel or the reattach ref. storing it would pin a session to
whatever the cli defaulted to on its first turn, which is the freedom the
bare harness exists to preserve.

untested against a live turn — the parser and translator have unit tests,
the websocket gate does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 03:58:35 +00:00
pastilhasandClaude Opus 5 6b4339052a reattach a refreshed browser to a running turn
Refreshing mid-turn appeared to kill the agent's output. It never did: the
session survives a dropped socket, the agent keeps generating into it and keeps
committing durable events, and `close` only detaches the socket and arms an
hour-long idle timer. What broke was purely delivery — and the reconnect path
that would have fixed it could not fire, because the browser came back having
forgotten officer's session key. It lived in page state. The only id left was
Claude's transcript uuid in the URL, and nothing accepted that.

So accept it. `attach` carries the uuid, and the agent's on-disk session map —
the single record relating the two — turns it back into the key everything else
is written in terms of. The uuid now also goes out at `system.init` rather than
only at `result`, which is what makes the first turn recoverable at all: until
now a chat had no address until it had finished, and a long first turn is
exactly the one worth reconnecting to.

`sync:live` deliberately carries no messages. The harness writes its transcript
as it goes, so the HTTP load on landing already supplies the past; sending the
server's record of the same messages on top of it would duplicate them, and
there is no shared id to reconcile the two by. Attach hands over the rest of the
turn, the half-written paragraph the transcript cannot hold, and the session's
cursor head — that last one so a *later* drop replays from the head instead of
re-delivering the whole conversation from zero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:59:26 +00:00
pastilhasandClaude Opus 5 969b2f3762 show compaction as it happens and open tool calls while they run
Compaction was the one thing the harness does that emitted nothing at all while it
ran, and it can run for minutes — silence that reads as a hung turn, which costs a
server restart to discover it wasn't. The sidecar now reports both ends: the start
from the PreCompact hook, the finish from the compact_boundary message with the
token count, both durable so a reload or a reconnect still sees them.

Tool rows open themselves while they run and hold for five seconds after their
result, so the inputs are on screen at the moment the call is made rather than
after the fact. The clock lives outside React, keyed by tool call id: rows are
virtualised, so unmounting is not the call ending, and a fast call can render its
start and its result together — a row that only opens when it catches the pending
state never opens for exactly the quickest calls. A click outranks the clock for
as long as the row lives. A failure behaves identically and differs only in colour,
so it stays findable by scanning and nameable in conversation.

Shell logs move off the green-on-black pre onto the shared code surface, which is
the one block that had no copy button and the one you most often want to hand to
someone else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 05:10:22 +00:00
pastilhasandClaude Opus 5 7b6ca5f4ca attribute subagent output to the task that spawned it
the harness stamps every message a subagent produces with parent_tool_use_id.
the sidecar wrote it outgoing and nothing ever read it coming back, so a
subagent's prose and tool calls were spliced into the main transcript as if the
agent you are talking to had produced them — and worse, its deltas were appended
to the same text buffer, so two voices were concatenated inside one bubble.

both buffering layers (stream-parser's textBuffer and turn-stream's buffer) are
now maps keyed by parent, and parentToolUseId rides on ChatEvent, ServerMessage
and Message. useChat nests parented output under the Task row that spawned it;
ToolActivity draws the trace inside the expanded panel.

background tasks get the same treatment from the other end: task:started and
task:notification were two unrelated fake assistant bubbles minutes apart, and
are now one role:'task' row correlated by taskId that appears pending and
resolves in place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 01:48:21 +00:00
pastilhasandClaude Opus 5 47d03de307 chat: carry claude's own session uuid on the result event
The sidecar knew which transcript file a turn had landed in and kept it to itself —
setClaudeSession fed --resume and nothing else. A session officer started was therefore
unaddressable from the platform side. Put it on the result event so it crosses the wire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 00:28:39 +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
pastilhasandClaude Opus 4.8 669692355d chat: rename the pi-mono provider router + purge residual pi names (Stage 4b/2)
Renames the AI-harness/provider settings router into the chat namespace and
clears the remaining "pi" identifiers from the chat stack.

- server-settings/pi-mono.ts → chat-providers.ts; piMonoRouter → chatProvidersRouter;
  route /server-settings/pi-mono → /server-settings/chat-providers (+ all callers)
- piId → providerId (PROVIDERS map + AIHarnessesSection UI), PiProvider → ChatProvider,
  PI_MONO_* query keys → CHAT_PROVIDERS_*, installPiMono → installAgent
- data-path: PI_CONFIG_DIR → AGENT_CONFIG_DIR (path ~/.pi/agent unchanged);
  drop dead getPiMonoDir/getPiMonoSessionDir exports
- settings: flip the vestigial defaultProvider literal 'pi' → 'chat' (never read;
  only defaultModel drives behavior); access-policy config key 'pi-access-policy'
  → 'chat-access-policy'
- misc: ModelSelector fallback label, TaskDefaults model grouping, CapabilityPage
  chat var, a stale stream-parser comment

Intentionally left (genuine external `pi`/opencode references, not ours to rename):
the `pi` binary install/version flow (@mariozechner/pi-coding-agent, `which pi`),
the ~/.pi/agent config path, PI_TOOLS_DIRS/PI_SEARXNG_URL runtime env-var contract,
TOOL.md `targets: pi` metadata, and the "Pi Mono" installer UI label.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 15:52:58 +00: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.6 c38d5b0ea1 extract stream parser module with tests, add mcp tool call logging
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 13:49:26 +00:00