fix(session-flow): add compute-don't-assert rule to running-retro analysis prompts - #1482
Conversation
…lysis prompts An independent fresh-context validation of a running-retro observer ledger found the checkpoint analyzer accurate on findings that harvested the session's own self-declared observations but 0/2 on independently inferred structural claims -- one asserted-and-wrong finding (tool-call sequencing claimed from a narrative read, contradicted by grouping the same calls by API message id) routed as a tracker issue a human would have filed for a non-problem. Add a compute-don't-assert rule to both callers of the checkpoint method: the in-session checkpoint delegation (checkpoint.md's Method section, new step 4, plus its delegation prompt template's "Do:" summary) and the detached observer's headless analysis prompt (observer.py's _analysis_prompt, which has no delegation prompt to fall back on and needed the rule stated inline). Any claim about transcript/tool-call structure -- sequencing, batching, delegation, or an occurrence count -- must now be computed from the observation/transcript records (message-id grouping, actual counts) rather than asserted from a narrative impression, and dropped rather than asserted uncomputed when it can't be derived. Scoped to recommendation 1 of #1473 only; recommendation 2 (raise the default analysis model tier) is split out as #1480, and recommendation 3 (disclose partial-transcript coverage in the ledger header) is tracked under #1471. Closes #1473 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 3s —— View job PR Review
Summary: Clean, well-scoped fix. The change correctly addresses recommendation 1 of #1473 with no scope creep, and the fallback behavior is defensively correct. One consistency gap worth noting — not a blocker. FindingsMinor: message-ID grouping instruction is unachievable in the headless path Derive an ordering or batching claim (e.g. "ran sequentially," "no subagent \
delegation") by grouping tool-use events by API message id and inspecting the grouping. \The The fallback clause ( A clarifying note would make this explicit rather than implicit: # Suggested addition after the message-id sentence:
Derive an ordering or batching claim ... by grouping tool-use events by API message id
and inspecting the grouping. Note: distilled observations may not carry message ids; \
if the field is absent, treat that claim as uncomputable and drop it.Everything else looks correct
Delegation prompt template — The
CHANGELOG — Accurate, appropriately detailed, correctly references #1473. The description of the root cause ("0/2 on independently inferred structural claims — one routed as a tracker issue a human would have filed for a non-problem") gives useful context for readers evaluating whether the fix is worth the diff. Version bump — 0.17.3 → 0.17.4 is correct for a bug fix. Both CLAUDE.md compliance — No hardcoded paths, no PII/secrets, plugin-form-safe, versioned correctly. The single finding is a clarity/guidance issue rather than a correctness bug — the fallback clause ensures the headless agent drops uncomputable claims even without the clarification. Ready to merge once the note is considered (or accepted as-is given the defensive fallback already covers it). |
|
Claude finished @kyle-sexton's task in 1m 26s —— View job Security Review — PR #1482Status: Complete Todo:
FindingsNo security issues found in the changes introduced by this PR. The five changed files consist of:
|
| Surface | Risk | Mitigated? |
|---|---|---|
{session_id} interpolated into Claude prompt |
Attacker-controlled --session-id arg could inject LLM instructions |
Tool is local; caller already has code execution; no host-system escalation possible |
{observations} / {checkpoint} (file paths) in prompt |
Model directed to read unexpected paths | Mitigated: --add-dir restricts Read tool to work_dir + checkpoint_dir only |
| Transcript observation data reaching the model | Prompt injection via crafted transcript content | Mitigated: trust-boundary declaration (line 629–631) tells model observations are untrusted data, not instructions; --tools Read prevents any write/exec tools being driven by injected content |
self.session_id written to ledger frontmatter (line 512) |
YAML frontmatter corruption via newlines in session_id | Pre-existing; not introduced by this PR; low severity (local ledger file, no deserialization chain) |
None of these are introduced by this PR, and all have reasonable mitigations for a local-execution tool.
Summary: The PR is a prompt-text-only change. It adds defensive instructions to an AI analysis prompt and corresponding tests. No new injection surfaces, no permission changes, no new dependencies, no workflow modifications. Clean from a security standpoint.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2f76525dc8
ℹ️ 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".
claude[bot]'s PR review on #1482 correctly found summarize_record() never preserves a message-id field into the distilled observations the detached observer's headless analysis receives, so the compute-don't-assert rule's "group by API message id" guidance pointed at a field that never exists in that path's own input. Add an explicit caveat: an absent message-id field reads as uncomputable (drop the claim), never as license to assert from impression. Covered by a new test asserting both the prompt text and that summarize_record()'s output genuinely carries no id key. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 45s —— View job PR Review — follow-up commit
|
| Assertion | Matched text |
|---|---|
"Compute, don't assert" |
line 640 |
"sequencing" |
line 642 |
"batching" |
line 642 |
"delegation" |
line 642 |
"occurrence count" |
line 642 |
"message id" |
line 645 "API message id" |
"drop the claim" |
line 650 |
"may not carry a message-id field" |
line 646 |
"MANDATORY redaction pass" |
line 635 |
All match. PR is correct and complete.
|
Claude finished @kyle-sexton's task in 1m 24s —— View job Security Review — PR #1482Status: Complete
No security issues found in the changes introduced by this PR.The five changed files consist of:
|
| Surface | Risk | Mitigated? |
|---|---|---|
{session_id} interpolated into Claude prompt |
Attacker-controlled --session-id arg could inject LLM instructions |
Tool is local; caller already has code execution; no host-system escalation possible |
{observations} / {checkpoint} (file paths) in prompt |
Model directed to read unexpected paths | Mitigated: --add-dir restricts Read tool to work_dir + checkpoint_dir only (lines 434–456) |
| Transcript observation data reaching the model | Prompt injection via crafted transcript content | Mitigated: trust-boundary declaration tells model observations are untrusted data, not instructions; --tools Read --strict-mcp-config prevents write/exec tools being driven by injected content |
self.session_id written to ledger frontmatter (line 512) |
YAML frontmatter corruption via newlines in session_id | Pre-existing; not introduced by this PR; low severity (local ledger file, no deserialization chain) |
None of these are introduced by this PR, and all have reasonable mitigations for a local-execution tool.
Summary: The PR is a prompt-text-only change across commits 2f76525 and b42283c. It adds defensive instructions to an AI analysis prompt and corresponding tests. No new injection surfaces, no permission changes, no new dependencies, no workflow modifications. Clean from a security standpoint.
|
This was generated by AI during work-loop execution. VALID (fixed now) — confirmed: Fixed in b42283c: added an explicit caveat immediately after the message-id sentence in
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b42283c082
ℹ️ 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".
…batching Codex's PR #1482 review found the compute-don't-assert rule closed the structural-fact class (was a claim about sequencing/batching/delegation actually computed) but left a related gap open: a correctly *computed* sequencing fact is not by itself proof of a missed batching opportunity -- calls that ran in separate message-id groups can be genuinely dependent (a later call's input consumes an earlier call's result), which makes the sequential execution correct, not a miss. Extend the same discipline to the judgment built on top of a computed structural fact: both checkpoint.md's Method step 4 (+ its delegation Do: line) and observer.py's _analysis_prompt now require checking the calls' tool inputs/results for a data dependency before routing a computed sequential/unbatched claim as an Efficiency finding. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 5s —— View job PR Review — follow-up commit
|
|
Claude finished @kyle-sexton's task in 1m 27s —— View job Security Review — PR #1482
No security issues found in the changes introduced by this PR. Changed files reviewed
|
| Surface | Risk | Mitigated? |
|---|---|---|
{session_id} interpolated into Claude prompt (line 661) |
Attacker-controlled --session-id arg could inject LLM instructions |
Tool is local; caller already has code execution — no host-system escalation possible |
{observations} path in prompt (line 659) |
Model directed to read at attacker-controlled path | Mitigated: --add-dir restricts Read tool to work_dir + checkpoint_dir only (lines 434–456) |
| Transcript observation data reaching the model | Prompt injection via crafted transcript content | Mitigated: trust-boundary declaration (line 629–631) tells model observations are untrusted data; --tools Read --strict-mcp-config prevents write/exec tools being driven by injected content |
self.session_id written to YAML frontmatter (line 512) |
YAML frontmatter corruption via newlines in session_id | Pre-existing; not introduced or worsened by this PR; low severity (local ledger file, no deserialization chain) |
None of these are introduced by this PR, and none are worsened by it.
test_observer.py
The new AnalysisPrompt test class calls _analysis_prompt() with hardcoded safe values (/abs/obs.jsonl, /abs/checkpoint.md, sid) and asserts string containment. No new attack surface.
Summary: The PR is a prompt-text-only change across all three commits. It adds defensive instructions to the AI analysis prompt and corresponding tests. No new injection surfaces, no permission changes, no new dependencies, no workflow modifications. Clean from a security standpoint.
CI's comment-hygiene gate flags tracker references embedded in code comments (they rot once the PR merges/renumbers). Two test docstring comments cited "PR #1482" directly; reworded both to describe the underlying reason without the tracker reference. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 24s —— View job PR Review — commit
|
| File | State |
|---|---|
plugin.json |
Version bump 0.17.3 → 0.17.4 ✓ |
CHANGELOG.md |
Accurate entry covering all four commits ✓ |
checkpoint.md |
Step 4 well-articulated; renumbering 4-7 → 5-8 correct ✓ |
observer.py |
Rule present, message-id caveat present, dependency-check present ✓ |
test_observer.py |
4 test methods, all assertions match actual prompt text ✓ |
Ready to merge.
|
Claude finished @kyle-sexton's task in 1m 30s —— View job Security Review — PR #1482
No security issues found in the changes introduced by this PR. Changed files reviewed
|
| Surface | Risk | Mitigated? |
|---|---|---|
{session_id} interpolated into Claude prompt (line 660) |
Attacker-controlled --session-id arg could inject LLM instructions |
Tool is local; caller already has code execution — no host-system escalation possible |
{observations} path in prompt (line 659) |
Model directed to read at attacker-controlled path | Mitigated: --add-dir restricts Read tool to work_dir + checkpoint_dir only (lines 434–456) |
| Subprocess call (line 467) | Shell injection | Mitigated: subprocess.run(cmd, ...) uses a list, never shell=True |
| Transcript observation data reaching the model | Prompt injection via crafted transcript content | Mitigated: trust-boundary declaration (lines 629–631) tells model observations are untrusted data, not instructions; --tools Read --strict-mcp-config prevents write/exec tools being driven by injected content |
self.session_id written to YAML frontmatter (line 512) |
YAML frontmatter corruption via newlines in session_id | Pre-existing; not introduced or worsened by this PR; low severity (local ledger file, no deserialization chain) |
None of these are introduced by this PR, and none are worsened by it.
test_observer.py
The new AnalysisPrompt test class calls _analysis_prompt() with hardcoded safe values (/abs/obs.jsonl, /abs/checkpoint.md, sid) and asserts string containment. No new attack surface.
Summary: The PR is a prompt-text-only change across all four commits. It adds defensive instructions to the AI analysis prompt and corresponding tests. No new injection surfaces, no permission changes, no new dependencies, no workflow modifications. Clean from a security standpoint.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 356ba7221f
ℹ️ 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".
…flow A second Codex review pass found the dependency-check instruction added in the previous commit only defined "dependency" as a later call's input consuming an earlier call's result -- a control, resource, or side-effect dependency (e.g. a directory created before a file is written into it, an edit made before a test that exercises it runs) is just as real a reason two calls had to run sequentially, and the narrower wording could still let a false missed-batching finding through. Broaden the dependency check in both checkpoint.md's Method step 4 (+ its Do: line) and observer.py's _analysis_prompt to cover data, control, resource, and side-effect dependencies, not data flow alone. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 29s —— View job PR Review — commit
|
| Surface | Before | After |
|---|---|---|
checkpoint.md Method step 4 |
"a later call's input consumes an earlier call's result" | "data, control, resource, or side-effect — dependency between them" |
checkpoint.md delegation Do: |
"check for a genuine data dependency" | "check for a genuine data, control, resource, or side-effect dependency" |
observer.py main block |
same narrow form | same broadened form |
observer.py Do: summary |
"checking for a data dependency" | "checking for a data, control, resource, or side-effect dependency" |
The concrete examples added (a directory created before a file is written into it, an edit made before a test that exercises it runs) are well-chosen — they are the exact failure-mode pattern the Codex finding described and are distinct from result-to-input flow.
Test assertions
Three new assertions added to test_dependency_check_before_batching_finding_present:
| Assertion | Matched text |
|---|---|
"missed batching opportunity" |
observer.py line 652 ✓ |
"dependency" |
observer.py lines 658, 670 ✓ |
"control" |
observer.py lines 655, 670 ✓ |
"resource" |
observer.py lines 655, 670 ✓ |
"side-effect" |
observer.py lines 655, 670 ✓ |
All match. The prior regression guards (test_redaction_rule_still_present, test_message_id_absence_caveat_present) are untouched and still valid.
Remaining structural limitation (unchanged from prior review)
The headless path still can't compute dependency checks of any kind — summarize_record() carries only tool names, not inputs, results, or side-effect information. This means the dependency check is uncomputable on the headless path for data, control, resource, and side-effect dependencies alike. The drop-if-uncomputable clause fires correctly in all cases (absent context → uncomputable → drop the Efficiency finding), so the behavior is conservative and correct. This gap was identified in the 8f29a28 review, already filed as #1485, and is not worsened by this commit.
Full PR state at HEAD (eda8a2c)
All five commits together correctly implement recommendation 1 of #1473. The PR is correct and complete.
| File | State |
|---|---|
plugin.json |
Version bump 0.17.3 → 0.17.4 ✓ |
CHANGELOG.md |
Accurate entry covering all five commits ✓ |
checkpoint.md |
Step 4 well-articulated; delegation Do: consistent ✓ |
observer.py |
Rule, message-id caveat, and dependency check all present and broadened ✓ |
test_observer.py |
All assertions match actual prompt text ✓ |
Ready to merge.
|
Claude finished @kyle-sexton's task in 1m 31s —— View job Security Review — PR #1482
No security issues found in the changes introduced by this PR. Changed files reviewed
|
| Surface | Risk | Mitigated? |
|---|---|---|
{session_id} interpolated into Claude prompt (line 663) |
Attacker-controlled --session-id arg could inject LLM instructions |
Tool is local; caller already has code execution — no host-system escalation possible |
{observations} / {checkpoint} paths in prompt (lines 662, 666) |
Model directed to read at attacker-controlled path | Mitigated: --add-dir restricts Read tool to work_dir + checkpoint_dir only (lines 434–456) |
subprocess.run(cmd, ...) (line 467) |
Shell injection | Mitigated: uses a list, never shell=True; prompt goes via input= to stdin, not into cmd |
| Transcript observation data reaching the model | Prompt injection via crafted transcript content | Mitigated: trust-boundary declaration (lines 629–631) tells model observations are untrusted data; --tools Read --strict-mcp-config prevents write/exec tools being driven by injected content |
self.session_id written to YAML frontmatter (line 512) |
YAML frontmatter corruption via embedded newlines in session_id | Pre-existing; not introduced or worsened by this PR; low severity (local ledger file, no deserialization chain) |
None of these are introduced by this PR, and none are worsened by it.
test_observer.py
The new AnalysisPrompt test class calls _analysis_prompt() with hardcoded safe values (/abs/obs.jsonl, /abs/checkpoint.md, sid) and asserts string containment against the return value. No new attack surface.
Summary: The PR is a prompt-text-only change across all five commits. It adds defensive instructions to the AI analysis prompt and corresponding tests. No new injection surfaces, no permission changes, no new dependencies, no workflow modifications. Clean from a security standpoint.
|
This was generated by AI during work-loop execution. VALID (deferred) — confirmed: Same interim mitigation as the message-id gap: the drop-if-uncomputable clause is broad enough ("if the observations don't carry what's needed to compute a structural claim, drop the claim") that it already covers this — a model reading the block as a unit should drop the Efficiency finding rather than infer dependency from tool names alone, which is the correct/conservative outcome. Rather than leave it implicit, I widened the follow-up already filed for the message-id gap (#1485) to also cover this — same underlying schema limitation ( |
…-assert-running-retro # Conflicts: # plugins/session-flow/CHANGELOG.md
|
Claude finished @kyle-sexton's task in 1m 38s —— View job Security Review — PR #1482
No security issues found in the changes introduced by this PR. Changed files reviewed
|
| Surface | Risk | Mitigated? |
|---|---|---|
{session_id} interpolated into Claude prompt (line 685) |
Attacker-controlled --session-id arg could inject LLM instructions |
Tool is local; caller already has code execution — no host-system escalation possible |
{observations} / {checkpoint} paths in prompt (lines 684, 688) |
Model directed to read at attacker-controlled path | Mitigated: --add-dir restricts Read tool to work_dir + checkpoint_dir only (lines 434–456) |
subprocess.run(cmd, ...) (line 488) |
Shell injection | Mitigated: uses a list, never shell=True; prompt goes via input= to stdin, not into cmd |
| Transcript observation data reaching the model | Prompt injection via crafted transcript content | Mitigated: trust-boundary declaration (lines 651–653) tells model observations are untrusted data, not instructions; --tools Read --strict-mcp-config (lines 452–454) prevents write/exec tools being driven by injected content |
self.session_id written to YAML frontmatter (line 534) |
YAML frontmatter corruption via embedded newlines in session_id | Pre-existing; not introduced or worsened by this PR; low severity (local ledger file, no deserialization chain) |
None of these are introduced by this PR, and none are worsened by it.
test_observer.py
The new AnalysisPrompt test class calls _analysis_prompt() with hardcoded safe values (/abs/obs.jsonl, /abs/checkpoint.md, sid) and asserts string containment against the return value. No new attack surface.
Summary: The PR is a prompt-text-only change. It adds defensive instructions to an AI analysis prompt and corresponding tests. No new injection surfaces, no permission changes, no new dependencies, no workflow modifications. Clean from a security standpoint.
|
Claude finished @kyle-sexton's task in 2m 16s —— View job PR Review — merge commit
|
| Assertion | Location in prompt |
|---|---|
"Compute, don't assert" |
line 662 |
"sequencing" / "batching" / "delegation" / "occurrence count" |
lines 664–665 |
"message id" |
line 667 |
"may not carry a message-id field" |
line 668 |
"drop the claim" |
line 672 |
"missed batching opportunity" |
line 674 |
"dependency" |
lines 676, 680 |
"control" / "resource" / "side-effect" |
lines 677, 680 |
"MANDATORY redaction pass" |
line 657 |
The test_message_id_absence_caveat_present structural assertion (assertNotIn("id", rec) against summarize_record() output) remains accurate — summarize_record() at lines 51–93 never emits an id key. ✓
CLAUDE.md compliance — No hardcoded paths, no PII/secrets, plugin-form-safe (${CLAUDE_PLUGIN_ROOT} conventions unaffected), versioned correctly. ✓
Security — No new interpolated variables in the _analysis_prompt f-string; only static string literals added. Pre-existing injection surfaces ({session_id}, {observations}, {checkpoint}) unchanged and mitigated (local execution, --add-dir restriction, trust-boundary declaration, --tools Read). ✓
The single observation (line-break in the Do: template) is cosmetic and non-blocking. Ready to merge.
…plicit (#1496) *This was generated by AI during work-loop execution.* Closes #1483 ## Summary - `observer.py`'s `_pid_alive` Windows liveness check called `subprocess.run(["tasklist", ...], capture_output=True, text=True)` with no explicit `encoding=`, so Python decoded `tasklist`'s output using the platform code page (cp1252 on Windows) instead of UTF-8 — the same class of defect #1472 already fixed for `_run_analysis`'s subprocess call (PR #1481). Currently harmless (the only check is an ASCII integer substring match against the output), but left implicit it risked the same silent-corruption pattern resurfacing. Added `encoding="utf-8", errors="replace"` to match #1472's fix. - Completed the broader sweep #1483's brief called for: every `subprocess.run`/`subprocess.Popen` and `open()`/`read_text`/`write_text` call across `session-flow`'s production scripts (`observer.py`, `arm_observer.py`, `retro/scripts/parse_transcript.py`) was audited. No other gaps found — every remaining text-mode call site is already `encoding="utf-8"`-explicit, or intentionally binary (`"rb"` mode / a raw `os.open` file descriptor with no text decoding involved). - **Scope note:** this branch was cut from `main` before PR #1481 (fix for #1472) and PR #1482 merged, so it deliberately does **not** touch `_run_analysis`'s `subprocess.run` call (line ~467) — that call site is already fixed on #1481's branch. This PR only adds the `_pid_alive` fix #1481's bot review flagged as a separate, pre-existing gap. - Version bump: `session-flow` 0.17.3 → 0.17.4, with a matching `CHANGELOG.md` entry. Note this will likely collide with #1481's own 0.17.3 → 0.17.4 bump if both are open at merge time — whichever merges second will need a routine version-bump rebase, unsurprising for two sibling fixes to the same file cut from the same base. ## Test plan - Added `test_pid_alive_tasklist_call_is_utf8_explicit` to `test_observer.py`, which forces the Windows (`os.name == "nt"`) branch regardless of host platform (so it isn't skipped on Linux/mac CI) and asserts `encoding="utf-8"` and `errors="replace"` are passed to `subprocess.run`. - Ran the full `test_observer.py` suite: 24/24 passing. - `ruff check` on `observer.py` and `test_observer.py`: clean. - Validated `plugin.json` parses as JSON after the version bump. ## Related - #1472 — parent issue; its body's "Out of scope" note named this exact follow-on audit. - PR #1481 — the fix for #1472, whose bot review surfaced `_pid_alive`'s gap and is the fix this PR's `_pid_alive` change is patterned after. Not touched by this PR (see scope note above). - #1473 / PR #1482 — sibling in-flight work touching the same file family; not required by or conflicting with this change beyond the routine same-file merge-order note above. --- **Update:** applied both cosmetic (non-blocking) findings from the automated PR review — shortened the `_pid_alive` `encoding=` rationale comment to one line, and moved the new test out of the `Locking` class into a dedicated `PidAlive` class. No behavior change; 24/24 tests still passing, `ruff` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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.
…server distillation (#1497) Closes #1485 ## Summary - `summarize_record()` (the function that produces the distilled observations the headless running-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`, 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, never compute it. - Adds `mid` to each distilled assistant event: a bounded correlation key derived from the transcript's own API message id, when the raw record carries one. Tool-use events sharing one `mid` came from the SAME assistant turn (their calls were batched into one API call); events with different `mid`s — or no `mid` at all, which happens when the raw record didn't carry one — ran as separate, sequential turns. - Adds bounded (80-char) previews of each tool call's input/result, keyed by a bounded correlation id: `calls[].in` on the assistant event, `results[].out` on the user event. This lets a later call'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 #1482 added to the prompt text, but which had nothing to compute against until now. - **Delegation needed no new field.** The issue title names "sequencing/batching/delegation/ dependency" — `tools` (unchanged by this PR) already carries the tool-call names a delegation finding needs (a `Task`/`Agent` tool name in the list), so the actual gap this PR closes is sequencing/batching/dependency only. - `_analysis_prompt` updated to reference `mid`/`calls`/`results` explicitly, in both the Method section 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-flow` version 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_prompt` *is* touched by PR #1482 (it adds the compute-don't-assert caveat 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_prompt` when both land — not a functional conflict, since #1482 adds the compute-don't-assert *rule* 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 `mid` actually correlates *batched* tool calls, not just any two records that happen to share an id: - `message.id` is genuinely present on every assistant record checked. - The real batching pattern is the *opposite* of what an early version of this PR's own test assumed: a single API message routinely spans **multiple** transcript records (0 of 150+ files sampled had more than one `tool_use` block 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 two *tool-bearing* records ever do, which is the actual invariant the analysis prompt's batching computation depends on. - Ran the discriminating check directly: of 6,352 tool-bearing message ids across 200 live session 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 coincidental correlation 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. - A `tool_result`'s `content` is a list of content blocks about as often as it's a plain string. `_preview()` now extracts the block list's actual `text` instead of JSON-dumping the wrapper structure (`[{"type":"text","text":...}]`), which was quietly burning preview budget on syntax instead of content. **Honest token-cost accounting (acceptance criterion 4).** Measuring `summarize_record()` output against 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_results` count field is dropped as redundant with `len(results)`, and the 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-5` by default), not runaway growth — but it is not "no regression" either, and the CHANGELOG says so plainly rather than leaving an unqualified claim for a reviewer to take on faith. ## Test plan - [x] Added/extended `Distillation` tests in `test_observer.py`: `mid` present when the raw record carries a message id and absent when it doesn't; `calls`/`results` fields omitted when there are 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.content` as a block list extracts its text; a bounded-preview test confirming an oversized tool input is truncated to the preview limit, not carried in full. - [x] Added `ShortId` test class: `None` passes through, a short id is unchanged, a long id is truncated to the tail, the shortening is deterministic (required for `calls[].id` <-> `results[].id` correlation to work at all), and a non-string id is stringified. - [x] Added `test_sequencing_and_dependency_round_trip`: builds observations from real transcript-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 `mid`s) and the dependency is findable purely from the bounded previews, without ever reading the raw transcript. - [x] Added an `AnalysisPrompt` test class asserting `_analysis_prompt`'s output references the new `mid`/`calls`/`results` fields, the batched/sequential grouping language, the drop-if-uncomputable fallback, the truncation-marker-means-unknown rule, and that the pre-existing mandatory redaction instruction wasn't crowded out. - [x] Ran the full `test_observer.py` suite locally after every commit: 47/47 passing. - [x] Verified `message.id` presence, the block-vs-string `tool_result.content` shape, and the actual batching 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. - [x] Measured the aggregate token-cost impact against real transcripts, and RE-measured it against 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. - [x] `markdownlint-cli2` against the edited `CHANGELOG.md`: 0 issues. **Post-review updates (four rounds):** 1. `claude[bot]`'s code review and `chatgpt-codex-connector[bot]`'s inline review both landed real findings: `if mid:` dropped a falsy-but-valid message id (fixed: `if mid is not None:`), and a preview 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 `id` matches. Two more findings (observations-file permissions on the `/tmp` multi-user fallback, and the security review's finding on `results[].out` widening the prompt-injection surface via external tool-result content) are valid but orthogonal to this item's acceptance criteria — deferred as a follow-up: #1499. 2. A pre-handoff self-review caught that the design's two load-bearing assumptions (`message.id` presence, `tool_result.content` shape) 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). 3. 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 `mid` are ever both tool-bearing — the actual invariant `mid`-based batching detection depends on. The initial "spans multiple records" 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. 4. Merging `main` (which landed PR #1482) and a further Codex review round. The merge composes #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 field vocabulary; #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 named `AnalysisPrompt`, so the merged module silently kept only the second and discarded five of this branch's prompt tests; folded into one class. The review round then landed six more findings, all in the mechanism this PR introduces, all fixed: - The truncation marker was in-band and so indistinguishable from a value that genuinely ends in it (`Processing complete...`). Replaced by an out-of-band `cut` flag; `_TRUNC_MARKER` is gone. - `say` was silently cut at 160 chars although the dependency check reads it. Now `say_cut` / `human_cut`, via the same bounding helper. - The grouping rule both asserted sequential execution for a missing `mid` and called that case uncomputable. A missing grouping key is now uniformly uncomputable. - The dependency check compared the later input only against earlier results and narration, so a side-effecting pair (`mkdir` then a `Write` into that directory) read as independent. It now also compares the two calls' inputs. - `is_error` was dropped, so an error-then-retry pair read as an unbatched sibling. A `results` entry 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. - A mixed content-block result (text + image) kept only the text and reported complete; it is now reported cut. - Cross-turn pairs could never have been batched, so a candidate pair must sit inside one user turn (no intervening `human` / `turn_boundary`) before the dependency test runs at all. ## Related - #1473 (recommendation 1 — the compute-don't-assert rule this gap was found underneath) - PR #1482 (added the drop-if-uncomputable caveats as interim mitigations for this same gap; this PR restores the actual capability #1482 deferred — see the scope note above for the expected merge interaction) - PR #1481 (unrelated fix in the same file — `_run_analysis`'s subprocess spawn — verified no overlap) - #1499 (deferred follow-up filed from this PR's reviews — observations-file permissions + prompt-injection-surface hardening) --- *This was generated by AI during work-loop execution.* 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Closes #1473
Summary
running-retro's checkpoint analyzer had no rule requiring structural transcript claims (tool-call sequencing/batching/delegation, "emerging pattern" occurrence counts) to be computed rather than asserted from a narrative read — an independent fresh-context validation found the analyzer accurate on findings that harvest the session's own self-declared observations, but 0/2 on independently inferred structural claims. One asserted-and-wrong claim routed as a tracker issue a human would have filed for a non-problem.checkpoint.md's Method section (new step 4) and its delegation prompt template's "Do:" summary, plusobserver.py's_analysis_prompt(the detached observer has no delegation prompt to fall back on, so the rule is stated inline there too).claude[bot]and Codex — found three real gaps across four follow-up commits, all fixed in this PR):summarize_record()never preserves one) — an absent field is uncomputable, not license to assert from impression.summarize_record()) is a schema change beyond this PR's scope — filed as session-flow/running-retro: detached observer's distilled observations never preserve enough structure to compute sequencing/batching/delegation/dependency claims #1485.session-flowversion bumped 0.17.3 → 0.17.4 with a matching CHANGELOG entry.Test plan
AnalysisPrompttest class totest_observer.pyasserting the compute-don't-assert rule text (named failure modes: sequencing, batching, delegation, occurrence count, message-id grouping, drop-if-uncomputable), the message-id-absence caveat, and the dependency-check-before-batching-finding requirement (including the data/control/resource/side-effect breadth) are all present in_analysis_prompt's output — plus a regression guard that the pre-existing mandatory redaction instruction wasn't crowded out.test_observer.pysuite locally after every commit: 27/27 passing.markdownlint-cli2against the editedcheckpoint.md: 0 issues (checked after every markdown edit).plugins/session-flow/for other references to checkpoint.md's internal Method step numbers (renumbered 4-7 → 5-8 by the insertion) — none found;SKILL.md's "step N" references are its own separate flow numbering, unaffected.review / reviewandsecurity-review / security-review; every bot-review finding (3 from Codex, 1 from claude[bot] not already covered by the fixed ones) verified against actual code, classified, reacted to, replied to, and — for inline threads — resolved. One VALID-but-deferred finding routed to a follow-up issue (session-flow/running-retro: detached observer's distilled observations never preserve enough structure to compute sequencing/batching/delegation/dependency claims #1485) rather than expanding this PR's scope.Related
Refs #1480 — recommendation 2 (raise the default analysis model tier), split out as a separate cost/quality decision, not touched here.
Refs #1471 — recommendation 3 (disclose partial-transcript ledger coverage truncation), tracked there rather than duplicated here.
Refs #1485 — follow-up filed from this PR's review, widened to cover both bot findings: preserve enough structure in the observer's distilled observations (a grouping key, plus tool-call inputs/result content) to compute — not just drop — sequencing/batching/delegation/dependency findings on the headless path.
This was generated by AI during work-loop execution.
🤖 Generated with Claude Code