Skip to content

feat: session connections, citations, and the human in the loop - #37

Merged
lroolle merged 13 commits into
mainfrom
feat/search-evidence-codex-0147
Aug 20, 2026
Merged

feat: session connections, citations, and the human in the loop#37
lroolle merged 13 commits into
mainfrom
feat/search-evidence-codex-0147

Conversation

@lroolle

@lroolle lroolle commented Aug 20, 2026

Copy link
Copy Markdown
Member

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 the
anchor's connections to the rest of its workspace, deterministically
and with evidence you can walk to: forked_from/fork_of (shared
message ids), mentions/mentioned_by (an id named in conversation
text, quoted), handoff_from/handoff_to (a baton file written by
one, read by the other later), builds_on/built_on_by (a shared
workspace file), overlaps, previous/next. Strength is a band,
never a score. Same envelope from --json, ccx trace --full, and
GET /api/related/<project>/<session> (ccx.related.v1).

Claims can point at their evidence. search --hits emits one row
per matching message (time, session, role, message id, quote);
view --at MESSAGE_ID [--context N] opens that id with its
neighbours and says where it sits (message 58 of 393). Ambiguous
prefixes are an error, not a guess.

Search answers "when". FIRST column + first_hit in JSON +
--sort first|last|hits; -w/--word for whole-word matching;
--content also scans history.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. log gains --kind and
--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 -l clean
  • go test ./... green; go test -race green on the five packages
    that gained concurrency (cmd, trace, sessionlog,
    provider/codex, render)
  • Dogfooded against a real 20-session workspace: related returns
    9 connections with cross-provider evidence (CC + CX); log --scope today --all --kind user_prompt narrows 24,491 records to 106;
    view --at round-trips a citation printed by search --hits

Review finding, fixed here

search --hits cut every message id to 8 characters. Unambiguous for
a uuid, 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 line that exists and is
not 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 no
    warning — pre-existing, same class as the friction fixed in 0fcfa62.
  • view --at landing on a thinking message shows
    [thinking collapsed...]; --at could imply --show-thinking
    when the target is one.

lroolle and others added 13 commits August 18, 2026 03:16
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.
@lroolle
lroolle merged commit 8dcb339 into main Aug 20, 2026
2 checks passed
@lroolle
lroolle deleted the feat/search-evidence-codex-0147 branch August 20, 2026 03:07
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