feat: session connections, citations, and the human in the loop - #37
Merged
Conversation
Codex 0.147 moved its UI-facing user and assistant records from the legacy event_msg.user_message / agent_message events to canonical event_msg.item_completed TurnItems. ccx listed those sessions with (no summary), zero messages, and a blank web conversation while tool calls still rendered, which read as a renderer bug and was a source selection regression. - Add a Codex-native TurnItem wire adapter (turn_items.go). - Full parse detects completed conversation items first, then picks one message source for the whole rollout so hybrid files cannot render both. Quick parse counts both variants in a single scan and prefers the completed totals and summary. - Keep ignoring raw response_item.message: its user-role records can carry injected instruction and environment envelopes that would show up as human prompts. - Use completed item IDs as message UUIDs so export and drill-down anchors stay stable. - Bump CacheFormatVersion 3 -> 4 so an upgraded binary cannot keep serving blank cached parses. Fixture is synthetic and sanitized; covers canonical selection, legacy/hybrid dedup, count and summary parity, stable IDs, model metadata, and instruction-envelope exclusion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dogfooding "when did we first mention semantica" exposed four defects at once: content matching was substring-only (47 hits, 46 of them "semantically", and no way to tell 0 real hits from 46), results carried no earliest-match time and ranked only by hit count, a cold scan took 6m18s on one core in total silence, and a summary hit short-circuited the content scan of the one session that mattered. - -w/--word: ASCII word boundary on whichever side of the query starts or ends with a word character, applied to names, summaries, conversation text and --raw lines. CJK queries are unaffected. - first_hit per content result: FIRST column, RFC3339 in --json, and --sort first|last|hits (hits stays the default). - --hits: one citation row per matching message — time, session, role, message id, quote — oldest first, capped by -n with a visible "showing N of M". Under --raw the unit is a transcript line anchored by its own uuid/type/timestamp. - Bounded worker pool (up to 8), non-allocating case-fold prefilter that stops at the first hit, stderr progress on a TTY: 1m14s -> 7.6s warm on a 3.5 GB store. - Summary hits keep their content evidence instead of short-circuiting. - -n shorthand for --limit on sessions, projects and log. Remaining open from the same dogfood: history.jsonl is still not a searchable source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…files Sessions were islands. The joins between them — the handoff a later session picked up, the fork that carried a conversation into a new file, the second agent on the same files at the same time, the session that said "see 736a7bac" — are all in the transcripts, but nothing computed them (docs/design/0006-session-connections.md). - New `ccx related [session]`: the anchor's connections to the other sessions of its workspace, deterministic and evidence-backed: forked_from/fork_of (shared message ids), mentions/mentioned_by (id prefix in conversation text, quoted), handoff_from/handoff_to (baton file written by one, read by the other later), builds_on/built_on_by (file edited by one, then touched by the other), overlaps, previous/next. Strength is a band; path lists are capped with count kept and `truncated` set; --json is ccx.related.v1 with message id, time, path, quote per relation. - `ccx trace --full` carries the same list as `related`. - Profiles are built once per workspace session on the search worker pool with TTY progress; the parse cache makes repeats ~1s. - Fix: heredoc bodies were scanned as shell redirects, so a Go `if n > 0` or a markdown `> 2026-08-18` inside `python3 - <<'EOF'` became "edited files" in trace files_edited (and would have been junk builds_on evidence). Bodies are stripped before the redirect scan; regression test names the symptom. Tests: related_test.go (handoff/builds_on direction and evidence pairing, fork + mentions incl. tool-result exclusion, overlap window, ordering, self-skip, path cap, baton paths, id prefix); TestExtractRedirectPathsIgnoresHeredocBodies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`ccx log` is the time-sliced evidence layer, but it disagreed with the parser about what a human said: - Claude user-role lines that no human typed — slash-command markers, <local-command-*> echoes, task notifications, injected meta (skill bodies), compaction carriers — were all `user_prompt`. Today's slice reported 233 prompts; 79 were typed by a person. sessionlog now reuses parser.ClassifyUserText (exported) plus isMeta / isCompactSummary, so kinds match the full parser: command, command_output, notification, meta, compact_summary. - Codex 0.147 rollouts rendered the real conversation (event_msg.item_completed UserMessage/AgentMessage) as bare `item_completed` rows with no text, while raw response_item messages — including the injected AGENTS.md envelope — showed as `user_prompt`. sessionlog now decodes TurnItems via codex.DecodeCompletedTurnMessage (exported from turn_items.go) and applies the parser's one-source-per-rollout rule (docs/design/0004): in a rollout with completed items, legacy events become `legacy_message` and raw response_item messages `model_input`/`model_output`. Nothing is dropped; session kinds and preview are re-tallied so metrics count the conversation once. With the kinds trustworthy, two filters make the firehose a timeline: `--kind K1,K2` keeps record kinds; `--match PHRASE [-w]` keeps records whose raw transcript line contains the phrase (grep parity, the time-bounded complement of `search --hits`). `metrics.records` stays scope-wide, `records_matched` is the narrowed count. ccx log --scope today --all --kind user_prompt # the humans in the loop ccx log --scope month --all --match deadman -w # when a term came up Tests: TestCollectCodex0147ItemCompletedIsTheConversation, TestCollectCodexLegacyRolloutUnchanged, TestCollectClaudeUserRoleNoiseIsNotAPrompt (kinds, metrics, --kind, --match on the raw line). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tool-only steps
The human in the loop leaves two marks that ccx misread. The
"[Request interrupted by user]" marker (the human pressed stop) is a
plain user-role text line, so it classified as a prompt and opened a
fake turn — `u: [Request interrupted by user for tool use]`. A
permission-prompt rejection ("The user doesn't want to proceed with
this tool use") is a tool_result with is_error, so it counted as a
tool error. One real store holds 650 interruptions across 321
sessions and 145 rejections; none were visible as what they are.
- parser: KindInterrupt (harness marker via ClassifyUserText; never
an exchange anchor, so no fake turn) and IsToolDenial.
- trace: Turn/Step/TraceStats gain interrupts and denials; the
rejected call's evidence is marked denied (not an error — it did
not run); outline header ("1 interrupt, 2 denied"), turn badges,
step badges ("[4t 1! 1d]"), OutlineTurn/OutlineStep fields.
- trace: a narration-less step gets a headline from its tools —
"(no narration) Bash x3, Read" — instead of a bare badge row
(2026-08-17 dogfood finding 3).
- log: kinds `interrupt` and `tool_denied`; Claude tool_result rows
now preview their content instead of the literal word tool_result.
Tests: TestClassifyUserTextHarnessWrappers (+interrupt cases),
TestIsToolDenial, TestAnalyzeCountsInterruptsAndDenials,
TestOutlineLabelsToolOnlySteps, sessionlog kinds test extended.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ts context search --hits and trace hand out message ids, but nothing in the CLI could open one; drill-down meant the web page or raw grep (open since docs/devlog/2026-08-03-content-search-noise.org finding 4). render.WindowSession slices the wire-order message list around the target (exact id or unique prefix; ambiguity is an error, not a guess), detaches children so the window is exactly what it says, and never mutates the cached parse. `ccx view <session> --at ID` renders that window, prints "message N of M" on stderr, and keeps the target even under --brief. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude Code and Codex append every human prompt to history.jsonl, and those files outlive session cleanup — in one real store prompts reach back to 2025-09-28 while the oldest session file is 2025-12-07. For "when did we first say X" that is the longest-lived evidence there is. --content now scans both history files (formats differ: claude display/project/sessionId/timestamp-ms vs codex session_id/ts/text). Only prompts whose session id is not in the store surface, so a prompt is cited once — from the session while it exists, from history after cleanup. Rows are type `prompt` with FIRST, a [user] quote, and under --hits a history:line anchor; --sort first interleaves them into the timeline. Live: `search --content -w --sort first temporal_chaggr` -> first mention 2025-09-28 00:54 from history (sessions long gone). Tests: TestScanPromptHistory (both formats, known-session skip, anonymous prompts, missing file, result shape). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… roadmap status Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…race Move the workspace relation (list every session of the anchor's workspace, profile on a worker pool, relate) from the CLI into trace.RelateWorkspace over a narrow SessionSource interface, so the CLI, the web API, and tests share one implementation. The CLI wrapper keeps its stderr progress. New endpoint returns the ccx.related.v1 envelope (related, total, shown, warnings; ?limit=N) — the same shape as `ccx related --json` — so a session-page panel or an agent reading the API sees exactly what the CLI prints. On demand only: it costs a parse of every workspace session (cached after the first call). Test: TestHandleAPIRelated (builds_on + previous between two workspace sessions, 404s for unknown session and malformed path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codex's counterpart of Claude Code's "[Request interrupted by user]": event_msg.turn_aborted with reason interrupted (144 in one real store) now becomes a KindInterrupt marker in the full parse and an `interrupt` record in sessionlog, so trace interrupt counts and `log --kind interrupt` cover both providers. Other abort reasons are left alone. Tests: TestParseSessionCodexTurnAbortedIsInterrupt; legacy rollout log test extended. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The MESSAGE column cut every id to 8 characters, which is fine for a
uuid and wrong for the synthetic ids: `codex-thinking-90` and
`codex-thinking-331` both printed as `codex-th`, and a prompt-history
anchor at line 4426 printed as `line:442` — a different line that
happens to exist. So the citation was not merely unpasteable into
`ccx view --at` ("prefix is ambiguous"); it pointed somewhere else.
Shorten only ids whose first 8 characters are hex; print the rest
whole. They are short to begin with, so the column stays narrow.
This was referenced Aug 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Evidence features from the Semantica/MineContext study (designs
0005,
0006) plus the Codex 0.147
rollout fixes. 13 commits, +4390/-213 across 43 files.
What ships
Sessions stop being islands.
ccx related [session]derives theanchor's connections to the rest of its workspace, deterministically
and with evidence you can walk to:
forked_from/fork_of(sharedmessage ids),
mentions/mentioned_by(an id named in conversationtext, quoted),
handoff_from/handoff_to(a baton file written byone, read by the other later),
builds_on/built_on_by(a sharedworkspace file),
overlaps,previous/next. Strength is a band,never a score. Same envelope from
--json,ccx trace --full, andGET /api/related/<project>/<session>(ccx.related.v1).Claims can point at their evidence.
search --hitsemits one rowper matching message (time, session, role, message id, quote);
view --at MESSAGE_ID [--context N]opens that id with itsneighbours and says where it sits (
message 58 of 393). Ambiguousprefixes are an error, not a guess.
Search answers "when".
FIRSTcolumn +first_hitin JSON +--sort first|last|hits;-w/--wordfor whole-word matching;--contentalso scanshistory.jsonl, so "when did we first say X"reaches past the session-cleanup horizon. The scan is ~10x faster
(parallel) and shows progress on a terminal.
The human in the loop is first-class. Interrupts and permission
denials parse as their own kinds instead of fake prompts and generic
tool errors — 650 interruptions across 321 sessions and 145
rejections were invisible before.
loggains--kindand--match PHRASE [-w].Codex 0.147 renders again — conversations moved to
TurnItems;turn_aborted(interrupted)counts as an interrupt.Verification
go build ./...,go vet ./...,gofmt -lcleango test ./...green;go test -racegreen on the five packagesthat gained concurrency (
cmd,trace,sessionlog,provider/codex,render)relatedreturns9 connections with cross-provider evidence (CC + CX);
log --scope today --all --kind user_promptnarrows 24,491 records to 106;view --atround-trips a citation printed bysearch --hitsReview finding, fixed here
search --hitscut every message id to 8 characters. Unambiguous fora uuid, wrong for the synthetic ids:
codex-thinking-90andcodex-thinking-331both printed ascodex-th, and a prompt-historyanchor at line 4426 printed as
line:442— a line that exists and isnot the one cited. The citation was not merely unpasteable into
view --at; it pointed elsewhere. Fixed in 9970a18 with a test.Follow-ups (not filed)
ccx log <unknown-project>reports "0 source log files" with nowarning — pre-existing, same class as the friction fixed in 0fcfa62.
view --atlanding on a thinking message shows[thinking collapsed...];--atcould imply--show-thinkingwhen the target is one.