Repository navigation
v0.13.0 — the session context sidebar, the two-layer step view - #214
Conversation
…ld ones docs-local issue #212. Creating a project (ccteam init, web POST /projects, IM/web chat create, satellite import - all via bootstrap_project_at_dir) no longer lays down .claude/settings.local.json. The hooks block it carried only served the retired tmux/rmux terminal protocol; the default stream-json session is hookless (CCTEAM_HOOKLESS, events from stdout, HITL via can_use_tool), so nothing depended on it. The rest of that template (permissions.defaultMode, env.CCTEAM_HOME/CCTEAM_PROJECTS_ROOT, CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS, an always-empty enabledPlugins) either fed the hook subprocesses or is overridden by ccteam's own spawn flags, so the file is not created at all. Existing projects: remove_chat_hooks becomes remove_ccteam_hooks and now recognises every hook shape ccteam ever wrote (<...>/hook.sh <kind>, ccteam [internal] hook, cct hook, ccteam mux hook-emit), so it also catches the bootstrap progress-append/load-context entries the old predicate missed. It runs on bootstrap, on an init re-run, and on project rm --purge; it removes only those entries, keeps every other key and user hook, deletes a file left empty, and never reads or writes .claude/settings.json. Removed as dead: the settings template + render/merge helpers and their exports, rewrite_legacy_hook_commands and remove_cost_accumulate_hooks (no production caller; the new predicate covers their legacy forms). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ssion docs-local issue #210: during an ENOSPC window the single gateway event consumer task exited (its panic message was lost to the full log disk). Its mpsc receiver dropped, every session's event pump read the closed sink as "daemon exited" and stopped silently, and no reply reached IM, web or turns.jsonl again until the daemon restarted. - Consumer: each event is delivered under catch_unwind; a panic is logged at error with event id / channel / sid and the loop goes on. Every awaited platform call (send, progress edit/seed, reactions, split notice, durable send + replay) is bounded by a 120s timeout; a timed-out durable send is recorded Failed in the ledger. A poisoned channel-map lock no longer panics. Consumer exit logs at error. - GatewayEventSink logs at error (sid, event id, channel) when the delivery mpsc is closed, so every pump that stops says so; the stop semantics for real shutdown are unchanged. - fs_atomic::open_jsonl_append terminates a torn last line before appending, so the first record after ENOSPC is not glued onto the fragment. Used by turns.jsonl, progress.jsonl, the durable outbox, pending_turns and experience.jsonl; the outbox and pending_turns now write each record with a single write_all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d settings file docs-local issue #212, checker follow-up to a3cddd9. - Every local stream-json spawn still wrote enabledPlugins."telegram@claude-plugins-official"=false into .claude/settings.local.json, recreating the file on a project's first message. The pin now rides claude's flag-settings layer (`--settings '<json>'` in spawn_spec::build_argv; it outranks user/project/local), so the stream-json path writes no project file. The frozen terminal protocol keeps its own writes. - remove_ccteam_hooks also strips that pin (exactly `false`; a user value stays; an emptied enabledPlugins is pruned), so init re-run and `rm --purge` clean it from older projects. - remove_ccteam_hooks no longer errors on an unreadable / invalid file: it reports Unparseable and leaves the file byte-for-byte. Project creation (bootstrap_project_at_dir: init, web, IM, satellite) warns and continues; `ccteam init` notes it in the receipt; `rm --purge` still refuses. Pre-v0.8.6 projects may still have ccteam hooks in .claude/settings.json; they are inert (CCTEAM_HOOKLESS) and deliberately not touched (red line). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`ccteam start` reports "started" as soon as the daemon's mcp.sock answers an initialize probe, and every MCP client then dials the TCP /mcp that run/mcp-url advertises. The two listeners came up in separate tasks, and the web one does work before binding (the legacy DSH orphan sweep walks /proc), so a client right after start could hit "connection refused" - which is how `ccteam flow run` failed its first hire in flow_run_test on a busy host. ServeOpts gains a `bound` notifier fired once every listener is bound and run/mcp-url + daemon-endpoint.json carry the bound address; the daemon body holds mcp.sock until it fires (or the web task exits, so a failed web still leaves a usable daemon as before). flow_run_test now asserts the advertised port accepts the moment start returns. docs-local issue #214 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the same sid
After `ccteam daemon restart`, a claude stream-json body that still has a
background task running (a run_in_background dev server, a watcher) does
not exit on stdin EOF: real claude 2.1.296 stays alive until that task
ends and then self-continues. The new daemon registers it DETACHED and
waits for it, which is unbounded, so every message to the sid queues
behind it forever. The receipt pointed at `/stop`, which ends the session:
it sets stopped_at, drops the chat's focus routes (the next message mints
a new sid) and strands the queued turns. The session looked permanently
lost.
`/interrupt` (IM and `POST /api/v1/sessions/{sid}/interrupt`) on a
detached sid now ends only that process. The DETACHED entry, the queue
and the focus routes stay, and the body watcher takes the same path it
takes for a body that exits on its own: it recovers what the body said,
rebuilds the sid with --resume (same context) and drains the queued
turns. The receipt names this way out, and the usage docs describe it.
Refs docs-local issue #211.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…en back" from
When `<uuid>.jsonl` exists, start_thread resumed and, on any failure, fell
back to a fresh `--session-id <uuid>` on the SAME uuid. Real claude refuses
that ("Session ID … is already in use", exit 1), so the fallback could never
start. It turned one failed resume into a second failure, and it logged a
chat_session_reset claiming the context was lost when the transcript was
intact.
The local arm now returns the resume failure itself, as a readable,
retryable error. Nothing sticks: the transcript is untouched, the killed
handshake child's body record is cleared (so the next spawn does not
post-mortem a turn that never ran), and the next message tries --resume
again. The remote fallback stays: there the transcript's existence is only
a guess.
Refs docs-local issue #211.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs-local issue #213. Paths whose cost was O(file size) or O(sessions ever) on a per-turn / per-poll / per-start cadence: - IM durable outbox: compacted to the rows still owed (Queued/Failed) at start and whenever an append pushes it past 8 MB; replay streams the file in queue order instead of read_to_string + HashMap of every row. At-least-once is unchanged: every undelivered id keeps its latest row. 86 MB ledger: start 535 ms -> 8 ms after first compaction. - journal::FoldCache: per-file incremental fold over an append-only journal (offset + inode, LRU-bounded); a later ask reads only the appended lines. - claude_stream_json `/goal` + Stop-hook gating (thread_status at every boundary and team-graph poll) moved to turn_gating.rs on FoldCache: 13 MB transcript 12.8 ms -> 9 us per warm call. - turns_mirror::digest: user/status turn counts, answer seq, last turn id as a fold. Replaces full read_all_turns in pump start (x2), every final answer, the per-turn meta refresh (3+ per turn) and the agent wait settle poll (every 20 ms for 2 s). 547 KB turns: 0.96 ms -> 3 us. - list_session_metas: stat-keyed parse cache; a rescan re-reads only meta.json files whose (dev, ino, len, mtime) moved. Serves the web status / team-graph polls, the history sidebar and IM listings. 1263 sessions: 9.8 ms -> 4.7 ms. - web SSE replay ring: only the 128 most recently active sessions keep their 64-frame backlog (next_seq kept, so seqs never restart); previously every sid ever seen kept one for the process lifetime. - progress projection: cost_summary() for budget gates and fleet headers instead of deep-copying every session to read one number. - daemon.log rotated to daemon.log.1 at start once past 16 MB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs-local issue #210 follow-up. The event consumer was one of several one-of-a-kind tasks whose death is global and silent; the same failure class sat in the inbound consumer and the other daemon loops. A clean stop also logged one delivery ERROR per busy session. - New ccteam_im::supervise: contain_panic (per-item, keeps the loop's state and receiver) and supervise_loop (restart a stateless loop after a 1s backoff); both log at error. - Inbound consumer: each message is routed under contain_panic (body extracted into route_inbound_message); channel-map and listener locks are poison-tolerant; its exit logs at error. The gateway event consumer now uses the same helper. - Delegation notifier: each pulse is handled under contain_panic, and the watch-set read is poison-tolerant. Scheduled scheduler, body watcher and idle-release sweeper run under supervise_loop. A panicking IM reload costs the reload, not the daemon's select loop. - ccteam-web: both ring feeders and the binding reaper run under supervise_loop. ccteam-cli hook sink: hook enrichment moved into the blocking task whose panic is already caught per event (now error). - Shutdown: the gateway's event_delivery_closing flag is raised before the consumer is aborted; a sink that finds the consumer gone after that logs at debug, so a clean stop logs no ERROR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A detached body was waited for until it exited, and real claude stays alive after stdin EOF for as long as a background task it started is running. A session nobody steered by hand (a delegated child, a scheduled run, an unwatched chat) stayed stuck behind it, with every turn queued and none delivered. The body watcher now ends the wait on its own, but only while a turn is queued for that sid. A body nobody is waiting on is left alone, so a job it scheduled for later still runs. The adapter reads the vendor record (`detached_activity`; for claude, the tail of the transcript plus the subagent transcripts beside it): - last turn ended and quiet for 30s: the process is kept alive only by background work and can never take the turn; - a turn still open but silent for 30 min: treated the same way (no record: 30 min since the body was detached). A body that is mid-turn and still writing keeps the wait. The process is ended with one SIGKILL to the recorded pid (`session_body::kill_body`), and the same path is used for `/interrupt`. SIGTERM is not used: claude answers it by killing every background task it started (measured on 2.1.296), and those tasks are the user's dev servers, stacks and scheduled jobs. With SIGKILL they keep running, reparented to init, and ccteam signals nothing else. Only the process ends, never the session: sid, ledger and focus routes stay, and the watcher's natural-exit path resumes the same sid with --resume and delivers the queued turns. The ledger records the reason as `ended_idle` or `ended_stalled`. Refs docs-local issue #211. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…f follows rotation docs-local issue #213, follow-up to the outbox compaction. - Every ledger row carries `attempts` (send attempts started; a Queued row is written ahead of each, so a crash mid-send still counts) and `first_queued_ms`. Replay sends an owed row only while attempts < 2 and it was first queued under 24 h ago; otherwise it appends a new terminal `GaveUp` row. A Failed send may still have landed (a timeout says nothing about delivery), so the old "replay everything not Sent" re-sent every failure at every daemon start, forever. Genuinely unsent Queued rows still get their replay (at-least-once kept). - Each replayed send runs under catch_unwind: a row whose send panics is logged and marked GaveUp, and replay continues — before, one poison row aborted the replay and panicked again at every start. - Compaction drops GaveUp rows along with Sent ones. - `ccteam daemon logs -f` reopens the path when the inode changes: a start that rotates daemon.log to daemon.log.1 made the follower replay the old file from its top and miss everything after. - Ledger fixtures in inbound_wiring_test / web_chat_bridge use a current timestamp (ts_ms 1 is now past the age bound); the two restart tests read sent rows before the restart that compacts them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs-local issue #210 follow-up (checker on 3db45ea). - Scheduled scheduler: fire_due_scheduled contains each row's submit; a panic fails the row ("panicked while firing") instead of escaping to the supervisor, which re-ran the same sorted batch every pass (a delivered turn re-sent each second, later due rows starved). A row is marked in flight before its submit and stays excluded until it is completed or failed, so it fires at most once per daemon life. - supervise_loop: exponential restart backoff, 1s doubling to a 60s cap, reset after a run that stayed up 300s, so a deterministic panic cannot flood the log. - idle_release_tick and body_watch_tick contain each session / each post-mortem; the post-mortem batch is taken off the gateway in one go, so one panic no longer drops the rest of it (wrapping only). - ccteam-web rings: poison-tolerant locks, so one panic under a ring lock does not make every later record/replay panic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs-local issue #212, checker follow-ups to 287ecb2. - claude_stream_json_test: start_thread with the fake claude creates no .claude/ (and no settings.local.json) in the project. Proven red by re-adding the ensure_telegram_plugin_disabled call, green without it. - remove_ccteam_hooks reports Unreadable (I/O: EISDIR, EACCES) apart from Unparseable (invalid JSON); the init receipt, bootstrap warning and purge refusal name the right one. - Doc the accepted cost of the Telegram pin scrub: a hand-written `false` is indistinguishable from ccteam's, and stripping it re-enables the plugin for the user's own non-ccteam claude runs in that dir. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A. Bodies are spawned as their own process-group leaders. Ending one (stop, /interrupt, or the automatic bound) now signals the group: SIGTERM, then a group SIGKILL that also sweeps members which outlived the leader. So no orphaned dev server is left holding a port that the resumed session needs. A body that does not lead its own group (spawned by an older build, inside the daemon's group) has only its own pid signalled. The receipt, the IM reply and the docs now say the background processes end with it. B. Every path that brings a session back drains its FIFO before any new line is submitted: the backlog goes out first, in order, on the next message. If the watcher's rebuild fails after a body ends, the reply target is told why and that the queued messages are kept. D. When a line was queued for a dead child and the resume then failed, the line is withdrawn. The error says the message was NOT delivered (so resending cannot deliver it twice) and names `/new` as the way out. The harness error no longer gives advice of its own. C. The ledger records why a detached body ended: `interrupted`, `auto_recovered` or `exited`. The recovered-answer and unrecoverable notes are worded for the reason recorded. Minors: the kill never runs under the gateway lock. REST /interrupt and /stop await it off the lock. The IM /interrupt door hands it to its own task, which wakes the watcher when done. A failed REST interrupt now returns 502 with the cause instead of 404 "unknown session". Refs docs-local issue #211. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e race Follow-ups to the mcp.sock-after-web-bind gate: - The gate was released only after the web serve future returned, so a panic in the web task skipped it: mcp.sock never opened and `start` timed out. A drop guard now releases it on every exit path, unwinding and abort included. - The regression assert only bit on a host whose /proc sweep is slow. A hidden hook (CCTEAM_TEST_WEB_PREBIND_DELAY_MS, read by the daemon binary) stretches the pre-bind window, and daemon_identity_test now fails deterministically with "connection refused" without the gate. - daemon_identity_test's sandbox stops its daemon on drop, so a failing assertion no longer leaks a daemon process. docs-local issue #214 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # crates/ccteam-im/src/gateway.rs
…d says so Owner decision: ending a detached body to keep its session (`/interrupt`, or the automatic bound) ends ONLY the body's process. It is one SIGKILL to the fingerprint-checked pid (`session_body::kill_body`). claude answers SIGTERM by killing every background task it started, and those tasks are the user's dev servers, stacks and scheduled jobs. An explicit `/stop` still ends the whole process group: the session is over, and so is its work. The background processes are never signalled, but they are made visible. Before the kill, the body's descendants are walked in /proc and fingerprinted. Each one still running afterwards is recorded as `orphans` (pid + short cmdline) on the `session_body_exited` ledger event, and the reply target is told: "still running, unattended: pid 1234 `npm run dev`". A port the resumed session cannot bind is no longer a mystery. The ledger reasons are `interrupted`, `ended_idle` and `ended_stalled`. The receipt, the IM reply and the docs say the background processes keep running. Refs docs-local issue #211. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t sticky
D1. end_detached_body_shared no longer overwrites an existing end mark.
The watcher decides off the lock, so it could reach a body already
marked by a user's /stop and relabel that end as its own. The watcher
would then rebuild the session the user had just stopped and drain the
queue into it. Only a stop may take over an interrupt. The exit handler
leaves a stop-marked body to the stop. A stop also drops the sid's
rebuild reservation, so a rebuild already spawning off the lock is
discarded rather than installed after the stop.
D2. When the &mut /interrupt door's kill task fails, it flags the sid. The
next reader of the mark lifts `interrupted`, so a later /interrupt
really ends the body and a later natural exit is not recorded as an
interrupt.
Gap 1. When the watcher's rebuild fails, a waiting delegation parent now
gets an honest failure through the normal completion path
(vendor_error, error_kind resume_failed), not just the reply target.
Gap 2. Project delete ends detached bodies off the gateway lock before
stopping their sessions.
Refs docs-local issue #211.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
N1. A detached body's end is now one hand-over entry per sid: in
progress, then ended (with what it left running) or failed. The
watcher, and the exit handler in the same lock as it removes the entry,
defer while a kill has not handed over. So the report never goes out
without the orphans, and a late hand-over can no longer be reported for
a later incarnation. Each orphan carries its start-time fingerprint and
is re-verified when reported, so a reused pid is never named. A stop
drops any stale entry for the sid.
N2. Orphan command lines are redacted before they reach IM or the ledger:
`key=value` and `--key value` for keys naming a token, secret,
password, passwd, key, auth or credential; `Bearer` values; URL
credentials; known token prefixes (sk-, ghp_, gho_, github_pat_,
xox?-, AKIA); and runs of 32+ base64/hex characters. Control
characters and backticks are replaced and the home directory reads as
`~`. For claude's Bash wrapper, the eval payload is shown instead of
the wrapper.
N3. signal_group's doc is back on signal_group. kill_body re-verifies the
body right before the SIGKILL, because the /proc walk widens the
pid-reuse window. Descendants take their start time from the same
/proc/N/stat read. The doc no longer claims that no transcript line is
cut: /interrupt and ended_stalled can tear the last line, which claude
resumes past.
D1 stays as committed in 9e92aa4: an existing mark is never overwritten,
except that a stop may take over an in-flight interrupt.
Docs: /stop ends the session's current process and that process's
background processes; ones left by an earlier /interrupt or automatic end
are not touched.
Refs docs-local issue #211.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#213 review) docs-local issue #213, checker findings on b76dd80 + 5aa0fac. - list_session_metas cache missed rewrites: atomic_write_durable's temp can land on the inode the previous rename freed, a rewrite keeps its length, and mtime moves on a ~4 ms tick, so two quick rewrites left the stamp unchanged (checker: 170/200 on ext4). Git's racy-timestamp rule: a cached parse is served only if the file's mtime was already 2 s old when it was read; a fresher file is re-read every scan. - Replay to an unconfigured channel spent an attempt without sending, so a row could be given up after zero sends. It now keeps attempts and only refreshes the error (the age bound still ends it). - Compaction bound = max(8 MB, 2x what the last compaction kept): owed rows alone past 8 MB no longer rewrite (+fsync) the whole ledger on every failure. - `daemon logs -f` drains the old file's unread tail before switching to the rotated-in one. - turns_mirror::digest returns None for a present-but-unreadable file; the meta refresh keeps the stored turn_count instead of writing 0, and the other callers fall back as they did before the digest. - Docs: FoldCache states its append-only / never-rewritten-path contract; turn_gating says goal visibility past the 8 MB first-read bound depends on the cache's history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…header secrets
C1. A body's kill and its hand-over now run on their own task. Shared
callers (REST /interrupt, /stop, project delete) await the task's
JoinHandle, so a web client that disconnects mid-kill no longer leaves
`InProgress` behind forever, with the sid stuck detached and its queue
undelivered. Each `InProgress` carries a token and a start time: only
the kill that set it writes the outcome, and one older than 30s is
settled as a failed end.
C2. The redactor now also covers:
- quoted values (`--password='x'`, `TOKEN="x"`, `--token "x"`);
- header and JSON members (`X-Api-Key: x`, `{"token":"x"}`) and Basic
credentials;
- `curl -u user:x`, mysql/mariadb `-px` and `sshpass -p x`;
- `pass` and a whole-word `pwd` as sensitive keys;
- URL userinfo with an empty user (`redis://:pw@h`).
Every example from the review is in the test table.
Minors. If a stop finds its body already gone (an /interrupt's SIGKILL got
there first), it still sweeps the body's process group, but only when
a member still carries the sid in its environment. When the watcher's
rebuild fails, the delegated tasks queued for the sid are withdrawn as
their parents are told; a parent's resend is then the only run, never
a late second completion. A human's queued lines stay.
Refs docs-local issue #211.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # crates/ccteam-im/src/daemon.rs # crates/ccteam-web/src/ring.rs
# Conflicts: # crates/ccteam-im/src/gateway.rs
… path When the watcher cannot bring a session back, the delegated tasks queued for it are withdrawn. Their resume_failed signal used to carry no execution turn, and plan_delegation_delivery answers a boundary nobody's request is bound to with nobody: the parent was never told, and the request stayed outstanding in delegation.json forever. The withdrawn requests are now bound, durably and under the child's store claim, to a turn of their own. That turn is delivered through the ordinary completion path, so the notification has the same identity matching, the same two-phase durability and restart dedup, and the same submission (a live parent is steered, a released one gets the pending FIFO and a resume) as any completion. Each parent hears once that the task was withdrawn and did not run. A REST /stop now runs the whole stop on its own task. A client that disconnects mid-kill no longer leaves a dead body marked `stopping` forever with messages queued behind it. Redaction adds Cookie / Set-Cookie header values, `docker login … -p`, and `echo|printf SECRET | … --password-stdin`. Refs docs-local issue #211. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve docs-local/issues/#215: keep copied debug binaries independent of their build worktree and guard embedding with a regression test. Validation: cargo fmt and make check pass; the embedding regression passes. make test-web remains blocked by the existing ws_last_client_disconnect_stops_pipe_pane FIFO assertion recorded in issues #214 and #215.
A tenant's web `/status` (and every other gateway command) fell through to the vendor because `submit_web_sid` gated the control face on `is_admin`; Claude then queued it behind the running turn. The gate's premise (IM chat ACL coarser than the web's) no longer holds since the v0.8.20 identity convergence: `Gateway::principal` resolves the caller's own web chat to Operator or Tenant and the fleet renders filter through the shared visibility policy. Commands now run on the caller's web chat (`Identity::web_chat_id`) instead of the shared admin chat, and a released session is focused too. HITL approval prompts were emitted without a project slug, so the project-filtered fleet stream showed them to the operator only; the prompt context now carries the session's project. Refs docs-local/issues/#216 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A turn's activity in the web console was one folded counter row; the
per-step summaries reached the browser and were only counted, and the
full content did not exist anywhere in ccteam: stream-json dropped every
`tool_result`, and reasoning was cut to a 200-char preview.
Adapters: tool items carry a vendor-neutral `output`. Claude stream-json
pairs each `tool_result` with its open `tool_use` and completes the same
item; the terminal transcript tail keeps the input in `args` and the
result in `output`; ACP reads `rawOutput`/`content` and now reports the
turn's whole accumulated thought instead of a throttled chunk; Codex
reads aggregated output / MCP results.
Gateway: every activity frame carries a `step_id` unique across pump
lives, and the pump appends each finished step (tool with input+output,
reasoning with its text) to `chat/<sid>/steps.jsonl`, 256 KiB per field.
SSE frames stay small; `GET /api/v1/sessions/{sid}/steps/{step_id}`
serves a step whole, gated like every `/sessions/{sid}/*` route.
SPA: a folded activity row expands into its step list (layer one);
picking a step opens the StepInspector (layer two), docked right on wide
screens and overlaid below 900px, which fetches the full record and
re-asks while the step is not recorded yet.
Refs docs-local/issues/#217
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts requests Lines a parent steers into a child's running turn each become a request bound to that turn, and the planner built one notification per request: excore s1390 steered sixteen lines into one long codex turn of s1403 and got sixteen identical copies of the same answer in one second. A turn has one answer, so a parent is now told once — one notification per parent per boundary, its header naming every request it settles (`answers N requests: …`), the excerpt at the widest mode asked for. Requests are still resolved, recorded and ledgered one by one. A turn that ended without any text wrote no row, and the pump signals a delegation boundary only off a row, so the request bound to it (a "no reply needed" line codex acknowledged silently) stayed `executing` for good and was counted as "still queued" in every later notification. A settled turn that said nothing now gets the same empty, status-bearing closing row a turn released mid-turn gets; the boundary names that row, the parent reads "(no reply)", and the restart reconcile treats such a row as a turn's end too. Refs docs-local/issues/#218 (excore F-GOV-233) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rust 1.99 deprecates fetch_update in favour of try_update, which turns clippy -D warnings red on the gateway test double. Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
Users could only learn where a session's context goes by asking the model
to guess. Claude's get_context_usage control_request already carries the
data behind its own /context (categories, memory files, skills, MCP
tools, agents, the conversation split by tool and attachment, last-request
cache usage), and ccteam called it every turn but kept two numbers.
- harness: vendor-neutral ContextBreakdown + optional
HarnessAdapter::context_breakdown; the Claude stream-json parse is
lenient (only occupancy and window are required) because the protocol
is internal and already richer than the reference schema.
- The turn-end get_context_usage answer is kept whole and persisted as
.ccteam/chat/<sid>/context.json beside status.json, so a released
session still shows its last snapshot; no extra control requests.
- GET /api/v1/sessions/{sid}/context: a resident session is asked live,
otherwise the snapshot is served (live:false); null when the harness
has no breakdown. Gated by gate_sid like every by-sid route.
Refs docs-local/issues/#21.
Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
…ion's window is made of
The `model · ctx N%` chip in the conversation header (and an always-present
toggle next to the cost pill) opens a right sidebar beside the chat that reads
`GET /api/v1/sessions/{sid}/context`: a header line (model, used / window · %,
live vs snapshot · age, Refresh with a spinner state and an inline failure),
one stacked bar of the window (used categories in fixed per-category colors →
autocompact buffer → free; deferred tool schemas listed apart, muted and
uncounted), the category table (tokens desc, rows expand into items with
source / path / not-loaded marks, long paths middle-truncated with a tooltip),
the Messages efficiency view (bucket split, per-tool call / result / total
desc, attachment types desc) and the last request's cache-hit line.
The docked state is remembered per browser (localStorage) so it survives a
session switch; a phone viewport gets a right-edge overlay drawer with a
backdrop and never auto-restores it. The panel re-fetches when a turn ends,
keyed on the page's own SSE boundary frame id — no polling loop. Escape and ×
close it and hand focus back to the toggle. `breakdown: null` renders an
honest explanation of which sessions report one; every sub-section degrades
on its own; unknown fields are ignored. All copy goes through i18n (zh + en)
and says harness, never vendor.
Pure rules (sorting, bar segments, cache ratio, formatting, memory) live in
lib/contextBreakdown.ts with unit tests; the panel has jsdom component tests;
SessionView's SSR tests cover both entry points and the restored-open mount.
Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
Refs docs-local/issues/#21. Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
Ship gate for the post-v0.12.0 cycle (34 commits since the tag). New user-facing capability (minor bump): the web console shows what a session's context window is made of, and every step of a turn opens to its full thinking or tool input and output. README gains both in the console list; docs/usage*.md, docs/orchestration*.md and docs/mcp*.md were updated in their own commits. Version lockstep: workspace `Cargo.toml` 0.12.0 -> 0.13.0 (8 workspace crate lines in `Cargo.lock`, no full re-resolve); `@ccteam/ccteam-ui` version + `ccteam.engine` + the four `@ccteam/engine-*` optionalDependencies + lockfile + `PACKAGE_VERSION` / `ENGINE_VERSION`; embedded `ccteam-ui.tgz` repacked, two-path reproducible. Contents: - local #21 context sidebar: vendor-neutral `ContextBreakdown`, Claude stream-json keeps its turn-end `get_context_usage` answer whole as `chat/<sid>/context.json`; `GET /api/v1/sessions/{sid}/context` (live or snapshot); a docked right sidebar with refresh and turn-end auto-refresh. - Two-layer step view: tool items carry their output across adapters, the gateway records each finished step in `chat/<sid>/steps.jsonl`, served by `GET /api/v1/sessions/{sid}/steps/{step_id}`; tenants get the admin's gateway control face; SPA assets embedded in debug binaries. - Restart recovery and gateway resilience: one bad event or panic no longer stops delivery or a long-lived task; bodies left over from a restart are bounded, owned and ended honestly; outbox replay gives up; mcp.sock opens after the web /mcp listener; costs that grew with lifetime stay bounded. - Project creation writes no Claude hooks; re-init strips old ones. - A2A: one completion per parent per turn; a silent turn settles its requests. - build: `fetch_update` -> `try_update` for rustc 1.99 clippy. Local gate (rustc 1.99.1): fmt clean, clippy 0 warnings, test-baseline 2545/0 (up from 2492), SPA 850/850, plugin 289/289. Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d988bf92f9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| getStep(sid, step.step_id, { signal: ctrl.signal }) | ||
| .then((record) => setFetched({ key, load: { state: "ready", record } })) |
There was a problem hiding this comment.
Refresh an open reasoning inspector as its record grows
When a user opens an ACP reasoning step while the agent is still thinking, the first successful fetch permanently freezes the inspector at that partial text. The ACP translator emits cumulative reasoning updates under the same item ID, and record_finished_step appends newer versions, but stepDone marks thinking steps done immediately, so neither step.step_id nor step.done changes afterward. This effect therefore never fetches the completed reasoning, even after the turn ends, unless the user closes and reopens the inspector. Invalidate the fetched record on reasoning updates or keep refreshing it until the reasoning actually finishes.
Useful? React with 👍 / 👎.
Ship gate for v0.13.0 (post-v0.12.0 cycle, 35 commits incl. the ship gate
d988bf92).GET /api/v1/sessions/{sid}/contextexposes Claude's own context accounting (categories, memory files, skills, MCP tools, agents, the conversation split by tool / attachment, last-request cache usage), snapshotted at turn end for released sessions; the web console shows it in a docked right sidebar with refresh and turn-end auto-refresh.chat/<sid>/steps.jsonl,GET /api/v1/sessions/{sid}/steps/{step_id}).fetch_update→try_updatefor Rust 1.99 clippy.Version lockstep: workspace 0.13.0,
@ccteam/ccteam-ui0.13.0 (+ccteam.engine, engine optionalDependencies), embeddedccteam-ui.tgzrepacked (two-path reproducible).Local gate (rustc 1.99.1): fmt clean · clippy 0 warnings · test-baseline 2545/0 · SPA 850/850 · plugin 289/289.
🤖 Generated with Claude Code