Skip to content

v0.13.0 — the session context sidebar, the two-layer step view - #214

Merged
firstintent merged 35 commits into
mainfrom
dev
Oct 21, 2026
Merged

firstintent merged 35 commits into
mainfrom
dev

Conversation

@firstintent

@firstintent firstintent commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Ship gate for v0.13.0 (post-v0.12.0 cycle, 35 commits incl. the ship gate d988bf92).

  • Session context sidebar (local M0.22: team factory — author + publish ccteam teams as Claude Code plugins #21): GET /api/v1/sessions/{sid}/context exposes 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.
  • Two-layer step view: a turn's folded activity row opens into its steps, each step into its full thinking or tool input/output (chat/<sid>/steps.jsonl, GET /api/v1/sessions/{sid}/steps/{step_id}).
  • Web: tenants get the admin's gateway control face; SPA assets embedded in debug binaries.
  • Restart recovery, detached-body ownership, outbox replay, scheduled-fire containment and bounded long-running costs in the IM gateway; mcp.sock opens only after the web /mcp listener.
  • Project creation writes no Claude hooks; re-init strips old ones.
  • A2A: one completion per parent per turn.
  • Toolchain: fetch_update → try_update for Rust 1.99 clippy.

Version lockstep: workspace 0.13.0, @ccteam/ccteam-ui 0.13.0 (+ ccteam.engine, engine optionalDependencies), embedded ccteam-ui.tgz repacked (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

randd1024 and others added 30 commits October 14, 2026 00:26
…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>
@firstintent firstintent changed the title dev → main: post-v0.12.0 cycle v0.13.0 — the session context sidebar, the two-layer step view Oct 21, 2026
@firstintent
firstintent marked this pull request as ready for review October 21, 2026 18:05
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T18:12:57.741347Z d988bf9 Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +42 to +43
getStep(sid, step.step_id, { signal: ctrl.signal })
.then((record) => setFetched({ key, load: { state: "ready", record } }))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@firstintent
firstintent merged commit a7e7c76 into main Oct 21, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant