fix(session-flow): preserve grouping key + call/result previews in observer distillation - #1497
Conversation
…server distillation summarize_record() stripped every tool call to a bare name and every tool result to a bare count, so the headless running-retro analysis prompt -- despite instructing the analyzer to group tool-use events by API message id and check for a dependency before flagging a missed-batching Efficiency finding -- had no field it could actually compute either claim from; it could only ever drop the finding. Adds `mid` (the transcript's own API message id, when present) as the batching/sequencing grouping key, and bounded (80-char) `calls[].in` / `results[].out` previews keyed by call id so a later call's input can be checked against an earlier call's output for a genuine dependency. Both are omitted (not padded) when absent, keeping the distillation's token-cheap cost bound intact. `_analysis_prompt` updated to reference the new fields. Follow-up from #1473 (PR #1482) and Codex's review of it, filed as #1485 and scoped to the schema change PR #1482 deferred. Neither #1481 nor #1482 (both still open) touches `summarize_record()`; `_analysis_prompt` will need a routine rebase reconciliation with #1482's compute-don't-assert text once that PR merges, since both add adjacent prose to the same function. Closes #1485 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 44s —— View job Code Review — fix(session-flow): preserve grouping key + call/result previews in observer distillation
Overall: This is a well-motivated, tightly scoped fix. The root cause diagnosis is accurate — Findings1. Truthy guard on
|
|
Claude finished @kyle-sexton's task in 2m 17s —— View job Security Review — PR #1497Todo
FindingsIMPORTANT — Prompt injection surface expanded via tool result content previewsConfidence: PLAUSIBLE Location: out["results"] = [{"id": c.get("tool_use_id"),
"out": _preview(c.get("content"))}
for c in tool_results]Before this PR, tool results appeared in the observations file only as an integer count ( Tool result content can originate from external, third-party-controlled sources:
80 characters is more than sufficient for a prompt injection payload — Blast radius: The analysis run is sandboxed to Existing mitigations (all pre-PR, not introduced here):
What isn't mitigated: None of those three controls prevent a sufficiently crafted injection from influencing what text the analysis returns, which is what gets written to the ledger. The Note on pre-existing risk: The Suggested hardening:
No findings in
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 70e02e867d
ℹ️ 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".
…tion ambiguity Two review findings on PR #1497 (this branch), both addressed directly: - Code review finding #1: `if mid:` dropped a falsy-but-valid message id (e.g. a future numeric `0` id format). Changed to `if mid is not None:` so `mid` is omitted only when the record genuinely carries none. - Codex inline finding "Mark truncated previews as uncomputable": `_preview()` silently sliced at 80 chars, so a dependency-bearing value past the limit looked like a clean "no match" rather than "unknown" -- letting the analysis prompt assert independence it couldn't actually compute, reproducing the exact asserted-and-wrong failure mode this whole feature exists to prevent. `_preview()` now ends a cut value in a trailing marker ("(ellipsis)"), and `_analysis_prompt` instructs the analyzer to treat a marker-terminated preview as an unknown dependency check, not a clean absence, and drop the claim rather than assert it. - Also folded in Codex's clarity nit on the dependency-matching instruction (the result's `id` matches the EARLIER call's `calls[].id`, not the later one being checked). Two other findings (an inline P2 on observations-file permissions on the `/tmp` multi-user fallback path, and a security-review IMPORTANT/PLAUSIBLE finding on `results[].out` widening the prompt-injection surface via external tool-result content) are valid but orthogonal to #1485's own acceptance criteria -- filed as a deferred follow-up: #1499. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 52s —— View job PR Review — fix(session-flow): preserve grouping key + call/result previews in observer distillationTodo
Overall: This is a well-executed, tightly scoped fix. The root cause analysis is accurate — Status of previous review findings
New findings in
|
|
Claude finished @kyle-sexton's task in 3m 49s —— View job Security Review — PR #1497
HEAD reviewed: Scope: IMPORTANT — Prompt injection surface expanded by
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 129db8ee89
ℹ️ 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".
…act result text Pre-handoff review surfaced four gaps a synthetic-fixture-only test suite couldn't catch on its own: - Verified `message.id` actually exists in real transcripts (checked 150+ live session files) rather than trusting the design assumption. It does -- but the scan also found the real batching pattern is the OPPOSITE of what the initial test assumed: a single API message routinely spans MULTIPLE assistant records (0/150+ files had more than one tool_use block in a single record), so `mid` is what makes batching computable, not record adjacency. Added a test for the pattern that's actually common and kept the original as a documented schema-level case. - Measured the real token-cost impact against real transcripts instead of asserting it away: the new fields grow the observations file ~70-150% on tool-heavy sessions, driven almost entirely by the preview content itself (the point of the fix). Cut every avoidable byte without losing capability: `_short_id()` correlates by an 8-char id tail instead of the full opaque id (mid/calls/results ids cut ~55-58%), the redundant `tool_results` count is dropped (`len(results)` already carries it), and `_preview()` now extracts a tool_result content-block list's actual text instead of JSON-dumping the block wrapper (a real transcript's tool_result.content is a list about as often as a plain string). CHANGELOG updated with the honest measured numbers instead of an unqualified "no regression" claim. - `_TRUNC_MARKER` changed from "..." (ellipsis) to ASCII "..." -- the original non-ASCII marker flows through the headless analysis run's `subprocess.run(..., input=prompt, text=True)` with no explicit `encoding=` on this call, which could raise `UnicodeEncodeError` outside the retained exception handling on a code page that can't represent it. - Added an explicit bullet (CHANGELOG + will land in the PR description) confirming delegation needed no new field -- `tools` already carries Task/Agent tool names -- since the issue title names it alongside sequencing/batching/dependency. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 4m 49s —— View job PR Review — fix(session-flow): preserve grouping key + call/result previews in observer distillationTodo
HEAD reviewed: Overall: This is a high-quality, unusually well-grounded commit: it validates assumptions against real transcript data (150+ files), measures token cost before claiming there's no regression, and fixes a real encoding risk. The implementation is sound. One open concern from the prior Codex P2 review round carries forward; the other changes are clean. Status of open items from previous reviews
Finding 1 —
|
|
Claude finished @kyle-sexton's task in 4m 42s —— View job Security Review — PR #1497HEAD reviewed: Scope: IMPORTANT —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dde29df559
ℹ️ 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".
…ng check The prior "one message spans multiple records" test paired a text-only record with a tool-bearing one, so it never actually exercised two calls sharing a `mid` -- the exact invariant the analysis prompt's batching computation depends on. Confirmed the invariant against real data before fixing the test: of 6,352 tool-bearing message ids across 200 live session transcripts, 1,412 (~22%) span 2+ tool-bearing records; a positive-control check against this repo's own session transcripts found 580/5,431 (~11%). Batching genuinely happens and is genuinely detectable via shared `mid`. Rewrote the test so both records carry a tool_use and assert `r1["mid"] == r2["mid"]` directly, and updated the CHANGELOG's verification claim from the softer "150+ transcripts" to these actual counts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 43s —— View job PR Review — fix(session-flow): preserve grouping key + call/result previews in observer distillationTodo
HEAD reviewed: Scope of this review: The fourth commit ( Latest commit (a46d77f): cleanThe commit fixes a real test gap: the prior "one message spans multiple records" test paired a text-only record with a tool-bearing one, so it never actually verified cross-record The CHANGELOG is updated from the softer "150+ transcripts" claim to concrete measurement: 6,352 tool-bearing message ids across 200 sessions, 1,412 (~22%) spanning 2+ tool-bearing records; 580/5,431 (~11%) positive control in this repo's own transcripts. This is the right call — a claim backed by counts that can be reproduced is materially more trustworthy than a floor count. No issues found in this commit. Status of prior-round findings
Finding 1 — No-
|
|
Claude finished @kyle-sexton's task in 2m 47s —— View job Security Review — PR #1497HEAD reviewed: Scope: IMPORTANT —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a46d77ff27
ℹ️ 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".
Composes #1482's compute-don't-assert rule set with this branch's field vocabulary in `_analysis_prompt`, rather than letting either side win. - #1482 owns the rule set: the "(mandatory -- you have no delegation prompt to fall back on)" framing, the occurrence-count rule behind an "Emerging pattern" finding, the `"ran sequentially," "no subagent delegation"` calibration examples, and the control/resource/side-effect widening of "dependency". This branch's rewrite had dropped all of those; they are back. - This branch owns the field vocabulary: `mid`, `calls[].in`, `results[].out`, and the truncation-marker-means-unknown rule. Where the two overlap -- #1482's "the observations may not carry a message-id field at all" -- this branch supersedes it, because `summarize_record()` now emits `mid`. The absent-key rule survives, rescoped from the schema to the per-record case. - #1482 named "delegation" in the mandate but gave no computation path once its `"no subagent delegation"` example is read against the new fields. Pointed it at `tools`, which already carries the tool-call names. `test_observer.py`: both sides added a class named `AnalysisPrompt`, so the merged module silently kept only the second and discarded this branch's five prompt tests. Folded into one class. `test_message_id_absence_caveat_present` asserted the exact prompt string this branch falsifies; rewritten against the absent-key rule it was actually guarding. `test_redaction_rule_still_present` dropped as a literal duplicate of `test_mandatory_redaction_pass_not_crowded_out`. CHANGELOG entry renumbered 0.17.4 -> 0.17.7 above main's 0.17.6/0.17.5/0.17.4, matching the `plugin.json` bump.
Second merge: main advanced past the first one while it was being resolved. Picks up #1496 (`_pid_alive` tasklist UTF-8, session-flow 0.17.7) and #1497's sibling handoff/loop work (0.17.8), plus #1506/#1508. Version-only conflict: this branch's entry renumbered 0.17.7 -> 0.17.9 above main's 0.17.8, with `plugin.json` bumped to match. `observer.py` and `test_observer.py` auto-merged -- #1496 touches `_pid_alive`, which neither `summarize_record()` nor `_analysis_prompt` is coupled to; its `test_tasklist_call_is_utf8_explicit` is present and passing in the merged suite (55 tests, all green).
|
Claude finished @kyle-sexton's task in 4m 33s —— View job PR Review — fix(session-flow): preserve grouping key + call/result previews in observer distillationTodo
HEAD reviewed: What the latest merge brings in: PR #1496 ( Finding 1 —
|
| Finding | Status |
|---|---|
if mid: drops falsy ids (round 1) |
✅ Fixed — if mid is not None: |
| Truncated preview indistinguishable from absent (Codex P2, round 1) | ✅ Fixed — _TRUNC_MARKER + prompt instruction |
| Prompt ambiguity on "EARLIER vs later call" (round 1) | ✅ Fixed |
"..." gameable by natural tool output (security CONFIRMED, rounds 2-4) |
|
No-mid contradiction in _analysis_prompt (rounds 4+, Codex P2 round 3) |
|
say silently truncated without marker (Codex P2 round 3) |
Non-blocking, open |
| Call-input overlap for resource dependencies (Codex P2 round 4) | Non-blocking, open |
/tmp fallback permissions |
Deferred to #1499 |
results[].out prompt injection surface |
Deferred to #1499 |
Positives in the latest merge
observer.pyandtest_observer.pyauto-merged cleanly — no conflict artifacts, no duplicated text from the_analysis_promptcomposition.- The
_pid_alivetasklist change from fix(session-flow): make _pid_alive's tasklist subprocess.run UTF-8-explicit #1496 (test_tasklist_call_is_utf8_explicit) is present and non-overlapping; the 55-test suite is green. - Version renaming (0.17.7 → 0.17.9 above main's 0.17.8) and CHANGELOG conflict resolution are accurate and tidy.
Verdict: Two actionable items remain from prior rounds. Finding 1 is now additionally concrete — the code comment is factually wrong about the current state of the subprocess call, independently of the marker choice. Both can be resolved in a single commit. The merge itself is clean.
|
Claude finished @kyle-sexton's task in 3m 59s —— View job Security Review — PR #1497HEAD reviewed: Scope: IMPORTANT —
|
| Finding | Severity | Confidence | Status |
|---|---|---|---|
"..." marker gameable — stale comment, encoding already explicit |
IMPORTANT | CONFIRMED | Open |
results[].out prompt-injection surface + clean-prose extraction |
IMPORTANT | PLAUSIBLE | Deferred → #1499 |
no-mid contradiction in _analysis_prompt |
— | PLAUSIBLE | Open |
| Observations file world-readable | SUGGESTION | PLAUSIBLE | Deferred → #1499 |
The leading finding is the _TRUNC_MARKER one: the comment claiming encoding= isn't explicit is wrong in the current HEAD (commit 558c0d3 already added it), so the justification for using "..." instead of "…" no longer holds, and "..." is meaningfully gameable by everyday tool output. Restoring "…" and correcting the comment is a two-line fix. The no-mid prompt contradiction is the other item worth addressing before merge.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 77a8245a70
ℹ️ 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".
…he dependency check
Four findings from Codex's review of the distillation schema, all in the
mechanism this PR itself introduces.
The truncation marker was in-band, so a complete tool result that naturally
ends in `...` ("Processing complete...") was indistinguishable from a cut one.
The prompt reads that signal to decide unknown-vs-absent, so the ambiguity
suppressed computable findings in one direction and could license an
asserted-and-wrong one in the other. `_preview()` now returns `(text, cut)`
and each `calls`/`results` entry carries `cut: true` only when truncated.
`say` had the same defect with no signal at all: the prompt reads that
narration for a control/resource/side-effect dependency, and it was silently
cut at 160 characters. `say`/`human` now go through the same `_bounded()`
helper and carry `say_cut`/`human_cut`.
The grouping rule contradicted itself -- a missing `mid` was listed both as
"ran as separate, sequential turns" and as uncomputable. A missing grouping
key is now uniformly uncomputable and explicitly never evidence of sequential
execution, in the prompt and in `summarize_record()`'s docstring.
The dependency check compared the later call's input only against earlier
results and narration, so a side-effecting pair (`mkdir /tmp/out` then a
`Write` into it) -- empty result, no narration -- read as an independent
missed batch. It now also compares the earlier call's own `calls[].in`
against the later call's for a shared resource.
|
Claude encountered an error after 3m 22s —— View job PR Review — fix(session-flow): preserve grouping key + call/result previews in observer distillationTodo
HEAD reviewed: What the latest commit brings: Four findings from Codex's reviews of the distillation schema, all in the mechanism this PR itself introduces. The in-band truncation marker is gone, narration gets cut flags, the no- Status of all prior-round findings
Finding 1 —
|
|
Claude finished @kyle-sexton's task in 2m 39s —— View job Security Review — PR #1497HEAD reviewed: Scope: Status of prior security findingsAll previously-open blocking items from the earlier review rounds are resolved in the current HEAD:
Remaining finding —
|
|
Warning Automated review did not complete — this is an infrastructure failure, not a review verdict. Treat any Claude comment on this PR (including a placeholder like "I'll analyze this and get back to you") as incomplete, not "no findings."
Re-running the job, or pushing a new commit, will retry the review. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 15a6982044
ℹ️ 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".
…-block previews Two more findings from Codex, both cases where a distilled result looked complete and independent when it was neither. `is_error` was dropped, so a failed call and a successful empty one distilled identically. An error-then-retry pair then sits in two `mid` groups with no data dependency to find and routes as a missed batch, even though the retry could only be chosen after the failure. A `results` entry now carries `err: true`, and the prompt's dependency check treats a retry after a failed call as control-dependent. A `tool_result.content` mixing a text block with an image or document block kept only the text and reported the preview complete -- the JSON fallback runs only when no text was extracted at all. The omitted block can carry the dependency, so a mixed list is now reported cut even when the text fits the limit. An all-text list is unaffected.
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8a7c437611
ℹ️ 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".
… error retry The retry rule accepted tool-name equality as proof, so a failed `Read` of one file followed by a `Read` of a different file in the same turn was classified control-dependent -- suppressing a genuine missed-batching finding. The rule now requires the two calls' input previews to name the same resource or repeat the same arguments. The failure direction matters here: every other guard in this prompt errs toward dropping a claim, but this one errs toward asserting a dependency that isn't there, which costs a real finding rather than a false one.
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ed0c097604
ℹ️ 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".
…gible failure pairs The previous commit's "same resource or same arguments" bar was too tight in the other direction: a corrected-argument retry (`git stats` -> `git status`) shares no resource and repeats no argument, yet the second call could only be chosen after seeing the first fail. Retry evidence is now three forms -- shared resource, repeated arguments, or a visible correction of the failed input -- with tool-name equality still excluded. And the residual case is settled rather than left to the analyzer's discretion: where a failure sits in the pair and none of that evidence is legible, the pair is UNKNOWN, not independent, so the claim is dropped. That matches every other guard in this prompt and removes the asymmetry the last commit's message called out, instead of trading one wrong direction for the other.
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 1s —— View job I'll analyze this and get back to you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c8456b97d0
ℹ️ 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".
…l schema The 70-150% figure was measured before the review rounds changed the schema -- the truncation marker was removed and four flags were added since. Re-ran the measurement against the current code, distilling 24 real transcripts with 20+ tool-bearing records each through both this version and the version it replaces: 61.9% aggregate, per-file mean 63.0%, median 61.4%, range 43.7-116.0%. Lower than the original claim, but the point of restating it is that the number now describes the schema that actually ships rather than an earlier one.
# Conflicts: # plugins/session-flow/CHANGELOG.md
|
Claude encountered an error after 1s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 1s —— View job I'll analyze this and get back to you. |
`summarize_record()` extracted only dict `text` blocks from a user `content` list, but retro's canonical `parse_transcript.py` counts a bare string in that list as a human message. A prompt encoded as `content: ["next request"]` therefore emitted neither `human` nor `turn_boundary`, so in a session with no `stop_hook_summary` records the analysis prompt's "same user turn" precondition saw no boundary and calls answering prompts on either side of it became a candidate missed-batching pair. Both shapes now read as a human message, so the two parsers state one rule.
# Conflicts: # plugins/session-flow/CHANGELOG.md
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 72d10d6353
ℹ️ 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".
…le text An image- or document-only prompt yields no extractable narration, so keying the boundary off `human` text left it invisible in transcripts with no `stop_hook_summary` at that point -- letting the dependency check span a new human request and report calls on opposite sides as a missed batch. Any user record carrying content that is not a `tool_result` is now a boundary: text, a bare string, an image, a document, or a block type that does not exist yet. A pure tool-result record stays inside the turn, since marking those would split every genuinely batched turn and suppress real findings. This replaces enumerating recognized human-content shapes, so a future block type needs no further change here.
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 54abf1ff43
ℹ️ 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".
…pendent `Edit foo.py` then `Bash pytest` shares no path, echoes nothing, and need not be narrated, so the resource comparison -- which needs a shared named resource -- structurally cannot see it and the analyzer could report the most common sequential pair in a coding session as a missed batch. The dependency check gains a fifth place: a later call that verifies the whole working state (build, test, lint, typecheck, VCS) is dependent on any earlier call in the pair that mutated state. An undecidable mutation drops as unknown rather than routing, matching every other guard in the prompt.
# Conflicts: # plugins/session-flow/CHANGELOG.md
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
…lint failure (G52) - parse_transcript.py: removes three wildcard `case _:` arms whose bodies were `continue` or `pass`. An unmatched `match` already falls through, and each arm was the last case of a match that is itself the last statement of its enclosing loop, so `continue` and falling out are the same edge. - check-usage-limit-reset.py: adds a `noqa: E402` for the `zoneinfo` import, fixing a lint failure that predates this run. - Test-side dedup: three duplicated subprocess spawns collapse to one helper, two duplicated patch fixtures become one context manager, and an env-dict build adopts the spread idiom the same file already uses elsewhere. - Comment passes across the group, including one plan reference and two history-narration blocks. Removed narration, preserved here: "Multi-session tests (Phase C -- retro-pre-commit-chain)"; "used to raise UnicodeDecodeError inside subprocess's reader thread when a fixed utf-8 decoder was used, leaving out.stdout as None"; "summarize_record() now carries a message id as mid"; "Existing single-session warning vs error semantics preserved."; and "verified against real transcripts (see PR #1497's measurement)", where the rejected `PR #N` form became the sanctioned bare `(#1497)`. Verified by an independent fresh-context refutation verifier: - The `case _:` removal was checked structurally, not by eye. An AST walk proves the match is the last statement of its loop with nothing after it and no `for...else`, so the `continue` cannot have been skipping following work. The removed nodes are exactly three wildcard arms with no guard and no binding. - A 36-invocation differential corpus compared stdout bytes, stderr bytes, exit code, JSON values and recursive key order, including unmatched-then-matched and matched-then-unmatched orderings in one content list, which is the case a `continue` regression would expose. Zero divergences. Instrumentation confirmed the corpus actually reaches all three removed arms, and a 2,000-seed fuzz found no divergence, including 44 seeds where both versions raise the same exception. - The `_analysis_prompt` f-string, whose exact substrings 13 tests assert on, is byte-identical across 48 input combinations, which is the real risk when a formatter re-wraps an implicit concatenation. - The merged subprocess helper is equivalent only because the caller passes `input` as a keyword; the two original stubs had different signatures. Verified. - Mutation testing killed 8 of 13 mutants, and each of the 5 survivors was shown pre-existing by applying the equivalent mutation to the origin/main form. The refactor is coverage-neutral. The verifier REFUTED the justification originally written for the pragma, and this commit carries the correction. The worker's comment claimed the import must follow the tzdata bootstrap. It need not: CPython resolves `tzdata` lazily inside `ZoneInfo(key)` construction, not at `import zoneinfo` time, and the bootstrap mutates `sys.path` long before any zone is constructed. The verifier built both hoisted variants and ran them with sensitivity controls proving the probe could detect a break; all resolved the zone from the vendored bundle identically. The comment no longer asserts a necessity that does not hold. Hoisting the import and dropping the pragma outright is the better end state and is left as a follow-up, since it is a code change this verification pass did not cover. Recorded for the run report: 93% of this diff is formatter churn. The repo's own `ruff-format` PostToolUse hook reformatted two previously-unformatted files whole, turning an 83-line intentional change into 1,192 lines. `ruff format` is not a CI gate here (ruff is pinned in requirements but no workflow invokes it), and `main` carries 45 unformatted Python files. Provably inert, but it will keep inflating diffs; worth a run-level decision to either format those 45 in one dedicated commit or disable the hook for the sweep. Incidental pre-existing crash found by the fuzzer, unchanged by this diff and present identically on origin/main: `parse_main_transcript` raises AttributeError when a compact_boundary event carries an explicit `"compactMetadata": null`, because the `{}` default only applies to a missing key. Worth filing separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LRWf55tWpxcSdpxEMh2eJD
Closes #1485
Summary
summarize_record()(the function that produces the distilled observations the headlessrunning-retro analysis actually receives) stripped every tool call down to its bare name and
every tool result down to a bare count — so
observer.py's headless_analysis_prompt, despiteinstructing the analyzer to "group tool-use events by API message id" and check for a dependency
before flagging a missed-batching Efficiency finding, had no field it could actually compute
either claim from. It could only ever drop the finding, never compute it.
midto each distilled assistant event: a bounded correlation key derived from thetranscript's own API message id, when the raw record carries one. Tool-use events sharing one
midcame from the SAME assistant turn (their calls were batched into one API call); events withdifferent
mids — or nomidat all, which happens when the raw record didn't carry one — ran asseparate, sequential turns.
id:
calls[].inon the assistant event,results[].outon the user event. This lets a latercall's input be checked against an earlier call's output for a genuine data dependency before a
sequential/unbatched pair is flagged as a missed batch — the same dependency check PR fix(session-flow): add compute-don't-assert rule to running-retro analysis prompts #1482 added
to the prompt text, but which had nothing to compute against until now.
dependency" —
tools(unchanged by this PR) already carries the tool-call names a delegationfinding needs (a
Task/Agenttool name in the list), so the actual gap this PR closes issequencing/batching/dependency only.
_analysis_promptupdated to referencemid/calls/resultsexplicitly, in both the Methodsection and the trailing "Do:" summary, including a rule that a preview ending in the truncation
marker must be treated as an unknown dependency check — never a clean absence — so a cut preview
that happened to hide a real dependency can't license an asserted-and-wrong finding.
session-flowversion bumped 0.17.3 → 0.17.4 with a matching CHANGELOG entry.Scope note (read before merge):
summarize_record()is untouched by both #1472's fix (PR #1481)and #1473's fix (PR #1482) — verified against both PRs' diffs before starting, so there is no
functional overlap.
_analysis_promptis touched by PR #1482 (it adds the compute-don't-assertcaveat text to the same function); that PR is still open/unmerged, so this branch was cut before its
changes existed and does not include them. Expect a routine rebase/merge conflict in
_analysis_promptwhen both land — not a functional conflict, since #1482 adds the compute-don't-assertrule and this PR adds the fields that rule needs to compute against; whichever merges second will
need to fold the two prose additions together.
Verified against real data, not just synthetic fixtures. Before finalizing, checked the design's
load-bearing assumptions against live session transcripts on this machine rather than trusting the
schema design from memory — including the specific discriminating check of whether shared
midactually correlates batched tool calls, not just any two records that happen to share an id:
message.idis genuinely present on every assistant record checked.a single API message routinely spans multiple transcript records (0 of 150+ files sampled had
more than one
tool_useblock in a single record) — so an early cut of the "spans multiple records"test only proved a text-only record and a tool-bearing record could share a
mid, not that twotool-bearing records ever do, which is the actual invariant the analysis prompt's batching
computation depends on.
transcripts, 1,412 (~22%) span 2+ tool-bearing records. Confirmed again as a positive control against
this repo's own session transcripts (the sessions that did this PR's own work, which are known to
have batched tool calls): 580 of 5,431 tool-bearing message ids (~11%) span 2+ tool-bearing records.
Batching genuinely happens and is genuinely detectable via shared
mid— not a coincidentalcorrelation from an under-specified test. Both the "multiple tool_use blocks in one record" case
(schema-valid, just not commonly observed live) and the "one message's tool calls span multiple
records" case (the common real pattern, now tested against two tool-bearing records asserting
r1["mid"] == r2["mid"]directly) are covered by tests.tool_result'scontentis a list of content blocks about as often as it's a plain string._preview()now extracts the block list's actualtextinstead of JSON-dumping the wrapperstructure (
[{"type":"text","text":...}]), which was quietly burning preview budget on syntaxinstead of content.
Honest token-cost accounting (acceptance criterion 4). Measuring
summarize_record()outputagainst real transcripts instead of asserting the cost away: the new fields grow the distilled
observations file 61.9% in aggregate on tool-heavy sessions (per-file mean 63.0%, median 61.4%,
range 43.7-116.0%), re-measured against the FINAL schema over 24 real session transcripts carrying
20+ tool-bearing records each, distilled through both this branch and the version it replaces. (An
earlier 70-150% figure in this body was measured before the review rounds removed the truncation
marker and added the cut/err flags; it described a schema that no longer ships.) The growth is
almost entirely the preview content itself — which is the actual point of the fix, not overhead. What is
overhead was cut wherever it didn't reduce the analyzer's computing capability:
_short_id()correlates by an 8-char id tail instead of the full opaque id (cut id-field bytes ~55-58% on the
measured sample), the
tool_resultscount field is dropped as redundant withlen(results), andthe content-block-list fix above shaved a further few percent. This is a real, bounded (never
unbounded — every field has a hard cap), single-analysis-call cost against a cheap model
(
claude-haiku-4-5by default), not runaway growth — but it is not "no regression" either, and theCHANGELOG says so plainly rather than leaving an unqualified claim for a reviewer to take on faith.
Test plan
Distillationtests intest_observer.py:midpresent when the raw recordcarries a message id and absent when it doesn't;
calls/resultsfields omitted when thereare no tool calls/results (no schema noise on a text-only turn); both the schema-valid
"multiple tool_use in one record" case and the real-world "one message spans multiple records"
case share one
mid;tool_result.contentas a block list extracts its text; a bounded-previewtest confirming an oversized tool input is truncated to the preview limit, not carried in full.
ShortIdtest class:Nonepasses through, a short id is unchanged, a long id istruncated to the tail, the shortening is deterministic (required for
calls[].id<->results[].idcorrelation to work at all), and a non-string id is stringified.test_sequencing_and_dependency_round_trip: builds observations from realtranscript-shaped records for a genuinely dependent Write-then-Read pair and confirms both
acceptance criteria in one round trip — the events are recognizable as sequential (different
mids) and the dependency is findable purely from the bounded previews, without ever readingthe raw transcript.
AnalysisPrompttest class asserting_analysis_prompt's output references the newmid/calls/resultsfields, the batched/sequential grouping language, thedrop-if-uncomputable fallback, the truncation-marker-means-unknown rule, and that the
pre-existing mandatory redaction instruction wasn't crowded out.
test_observer.pysuite locally after every commit: 47/47 passing.message.idpresence, the block-vs-stringtool_result.contentshape, and the actualbatching invariant (2+ tool-bearing records sharing one
mid, confirmed at 1,412/6,352 (~22%)across 200 live sessions and 580/5,431 (~11%) as a same-repo positive control) against real
session transcripts on this machine (see "Verified against real data" above) rather than
trusting the schema design from memory.
the final post-review schema (24 transcripts with 20+ tool-bearing records each) rather than
letting a number measured against an earlier schema stand — see "Honest token-cost accounting"
above for the actual figures.
markdownlint-cli2against the editedCHANGELOG.md: 0 issues.Post-review updates (four rounds):
claude[bot]'s code review andchatgpt-codex-connector[bot]'s inline review both landed realfindings:
if mid:dropped a falsy-but-valid message id (fixed:if mid is not None:), and apreview silently sliced at 80 chars made a dependency-bearing value past the limit look like a
clean "no match" instead of "unknown" (first fixed with a trailing marker; that marker was itself
later found ambiguous and replaced — see round 4 below). Also folded in a clarity
nit on which call a result's
idmatches. Two more findings (observations-file permissions on the/tmpmulti-user fallback, and the security review's finding onresults[].outwidening theprompt-injection surface via external tool-result content) are valid but orthogonal to this item's
acceptance criteria — deferred as a follow-up: session-flow/running-retro: observer's persisted preview content widens local-read and injection exposure without owner-only permissions or extra redaction #1499.
A pre-handoff self-review caught that the design's two load-bearing assumptions (
message.idpresence,
tool_result.contentshape) had only been asserted, never checked against real data,and that the token-cost claim had only been spot-checked on one field rather than measured in
aggregate — see "Verified against real data" and "Honest token-cost accounting" above for what
that surfaced and how it was addressed (id shortening, redundant-field removal, content-block text
extraction, and an honest CHANGELOG number instead of an unqualified claim). Also fixed
the truncation marker from a non-ASCII ellipsis to ASCII (the marker has since been removed
entirely — see round 4).
A second self-review caught that round 2's "150+ transcripts" verification had shown message ids
can span multiple records, but not that two records sharing a
midare ever both tool-bearing —the actual invariant
mid-based batching detection depends on. The initial "spans multiplerecords" test paired a text-only record with a tool-bearing one, so it never exercised that case
either. Ran the discriminating check directly and rewrote the test against two tool-bearing
records — see "Verified against real data" above for the resulting counts (1,412/6,352 and the
580/5,431 same-repo positive control) confirming the design holds.
Merging
main(which landed PR fix(session-flow): add compute-don't-assert rule to running-retro analysis prompts #1482) and a further Codex review round. The merge composesfix(session-flow): add compute-don't-assert rule to running-retro analysis prompts #1482's rule set — the occurrence-count rule, the
(mandatory — you have no delegation prompt to fall back on)framing, the control/resource/side-effect widening — with this branch's fieldvocabulary; fix(session-flow): add compute-don't-assert rule to running-retro analysis prompts #1482's "the observations may not carry a message-id field at all" is superseded,
since
summarize_record()now emits one. Both sides had also added a test class namedAnalysisPrompt, so the merged module silently kept only the second and discarded five of thisbranch's prompt tests; folded into one class. The review round then landed six more findings, all
in the mechanism this PR introduces, all fixed:
it (
Processing complete...). Replaced by an out-of-bandcutflag;_TRUNC_MARKERis gone.saywas silently cut at 160 chars although the dependency check reads it. Nowsay_cut/human_cut, via the same bounding helper.midand called that caseuncomputable. A missing grouping key is now uniformly uncomputable.
side-effecting pair (
mkdirthen aWriteinto that directory) read as independent. It nowalso compares the two calls' inputs.
is_errorwas dropped, so an error-then-retry pair read as an unbatched sibling. Aresultsentry now carries
err, and a retry is control-dependent — recognized by shared resource,repeated arguments, or a visible correction, never by tool name alone; where none of that is
legible the pair is unknown and the claim is dropped.
reported cut.
turn (no intervening
human/turn_boundary) before the dependency test runs at all.Related
restores the actual capability fix(session-flow): add compute-don't-assert rule to running-retro analysis prompts #1482 deferred — see the scope note above for the expected merge
interaction)
_run_analysis's subprocess spawn — verified no overlap)prompt-injection-surface hardening)
This was generated by AI during work-loop execution.
🤖 Generated with Claude Code