Skip to content

fix(provider): OpenRouter generation ID collection and provider attribution - #295

Merged
astefanutti merged 3 commits into
fullsend-ai:mainfrom
Aliciapet11:fix/openrouter-generation-ids
Nov 5, 2026
Merged

astefanutti merged 3 commits into
fullsend-ai:mainfrom
Aliciapet11:fix/openrouter-generation-ids

Conversation

@Aliciapet11

@Aliciapet11 Aliciapet11 commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Fixes OpenRouter generation IDs are lost before provider attribution #296.
  • Separates Claude message_ids from OpenRouter generation_ids.
  • Collects top-level requestId values beginning with gen-* from:
    • root Claude Code streams
    • subagent transcripts
    • persisted Claude session transcripts
  • Passes only generation_ids to the OpenRouter provider session and backfill.
  • Fixes Claude session-directory path encoding so persisted transcripts are found before cleanup.
  • Handles null usage fields without crashing token accounting.
  • Adds regression tests for generation-ID extraction, session-transcript reading, null usage parsing, and provider reconciliation.

After sharing the results of the POC, we got Michael’s feedback about needing to report the specific provider that served each request. While digging into that, we found that the harness was collecting Claude’s msg_* IDs instead of OpenRouter’s gen-* generation IDs. A live run confirmed the symptom: message_ids were populated, but generation_ids was empty, no OpenRouter ledger rows were written, and the routing audit showed 0 audited.

Further investigation showed that Claude Code’s raw stream-json output did not always include the top-level requestId, even though the persisted Claude session transcript did. The harness was deleting that session transcript before extracting IDs, so the gen-* IDs were lost before OpenRouter backfill could use them. The same live test also exposed a crash when OpenRouter emitted null usage fields; that is fixed here too.

Validation

  • 282 focused tests passed
  • Live one-case RFE Creator run with GLM 5.3 Flash collected 40 generation IDs, wrote 40 ledger rows, and completed the routing audit.

Review follow-ups

  • Root-cause clarification: the old cleanup encoding produced a double leading dash and never matched a real Claude session directory. Transcripts were therefore not being deleted before extraction; they were never read, and session directories accumulated. The corrected encoding makes cleanup actually delete the session directory, which is why the transcript is now read before cleanup. Users may see long-accumulated session directories disappear on subsequent runs.
  • Routing-audit behavior: a pinned OpenRouter run with zero generation rows now reports audit_complete: false rather than appearing compliant. Report, compare, ANOVA, and MLflow consumers flag those runs as incomplete.
  • EvalHub provenance: the returned provenance artifact now carries message_ids_count for Claude turn accounting and generation_ids_count for OpenRouter request coverage. Documentation directs consumers to generation_ids_count for generation-backed cost and routing coverage.
  • Harbor follow-up: Harbor: export persisted Claude session transcripts for OpenRouter generation IDs #298 tracks exporting persisted Claude session transcripts when stream-json omits requestId.
  • Spec 014 records the 2026-11-03 live-run evidence, marks probe Fix Bash interception that could block all Bash calls #12 superseded, and documents the legacy message.id fallback.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix OpenRouter generation ID collection and provider attribution

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Separate Claude message IDs from OpenRouter generation IDs so provider costs use the correct
 requests.
• Recover generation IDs from session and subagent transcripts before cleanup; tolerate null token
 usage.
• Correct backfill, reconciliation, and routing-audit behavior, with regression coverage and updated
 documentation.
Diagram

graph TD
  Streams["Claude streams"] --> Capture["ID capture"] --> Results["Run results"] --> Session["Provider session"] --> Ledger["Generation ledger"] --> Reconcile["Cost reconciliation"] --> Audit["Routing audit"]
  Transcripts["Session transcripts"] --> Capture
  Results --> Reconcile
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Capture IDs at the provider proxy
  • ➕ Could record generation IDs independently of Claude stream and transcript formats.
  • ➖ Requires an additional interception component and changes provider connection behavior across runners.

Recommendation: Use the PR's stream-plus-transcript collection strategy: it recovers IDs absent from live output without introducing a proxy, while retaining legacy-result fallback for offline backfill. A proxy is worth considering only if Claude's persisted transcripts also prove unreliable.

Files changed (24) +411 / -92

Bug fix (12) +245 / -45
base.pyGive run results a separate generation ID field +3/-5

Give run results a separate generation ID field

• Adds generation_ids to RunResult and identifies message_ids as Claude IDs for turn accounting.

agent_eval/agent/base.py

claude_code.pyRecover and sight OpenRouter IDs in Claude runs +39/-14

Recover and sight OpenRouter IDs in Claude runs

• Sights top-level generation IDs during streaming and collects IDs from the persisted session before cleanup, including on failure paths. Keeps Claude message IDs separate and passes only generation IDs to provider provenance.

agent_eval/agent/claude_code.py

fullsend.pyCollect generation IDs across Fullsend iterations +4/-1

Collect generation IDs across Fullsend iterations

• Merges generation IDs from iteration output and subagent transcripts into the Fullsend RunResult.

agent_eval/agent/fullsend.py

stream_capture.pyExtract generation IDs and tolerate null usage +131/-10

Extract generation IDs and tolerate null usage

• Adds top-level requestId extraction from streams, subagent transcripts, and persisted Claude sessions, plus session-directory encoding and session-ID lookup. Treats explicit null token counts as zero.

agent_eval/agent/stream_capture.py

adapter.pyCarry generation IDs through Evalhub results +8/-3

Carry generation IDs through Evalhub results

• Adds generation IDs to case and aggregate payloads and uses their count for returned provenance, with fallback for older results.

agent_eval/evalhub/adapter.py

results.pySeparate Harbor trial message and generation IDs +19/-2

Separate Harbor trial message and generation IDs

• Extracts and aggregates generation IDs across trial transcripts and steps. Trial pricing uses generation IDs, retaining a fallback for legacy trial records.

agent_eval/harbor/results.py

run.pyBackfill Harbor trials by generation ID +3/-1

Backfill Harbor trials by generation ID

• Sights each trial's generation IDs in the provider session and includes their union in the run result.

agent_eval/harbor/run.py

audit.pyMark empty pinned routing audits incomplete +4/-1

Mark empty pinned routing audits incomplete

• Prevents a pinned run with no audited generation rows from reporting a complete routing audit.

agent_eval/providers/openrouter/audit.py

backfill.pyPrefer generation IDs for offline backfill +14/-2

Prefer generation IDs for offline backfill

• Reads generation_ids from case and run results, falling back to legacy message_ids only when the new field is absent.

agent_eval/providers/openrouter/backfill.py

session.pyClarify provider-session generation ID inputs +5/-5

Clarify provider-session generation ID inputs

• Renames sighting inputs to generation_ids to reflect the IDs sent to OpenRouter backfill.

agent_eval/providers/openrouter/session.py

reconcile.pyBase provider coverage on generation IDs +4/-1

Base provider coverage on generation IDs

• Uses generation_ids as the default ledger-coverage key set while preserving legacy message_ids fallback and explicit caller overrides.

agent_eval/providers/reconcile.py

execute.pyPersist generation IDs in evaluation results +11/-0

Persist generation IDs in evaluation results

• Writes and aggregates generation IDs for single-case, batch, multi-step, parallel, and failure results.

skills/eval-run/scripts/execute.py

Tests (9) +153 / -38
conftest.pySupport request IDs in assistant test events +3/-1

Support request IDs in assistant test events

• Allows the assistant-event fixture to emit a top-level requestId independently of its message ID.

tests/conftest.py

openrouter_fakes.pyModel distinct IDs in the fake Claude CLI +5/-2

Model distinct IDs in the fake Claude CLI

• Emits separate Claude message IDs and OpenRouter generation request IDs for direct-run tests.

tests/openrouter_fakes.py

test_claude_cli_direct.pyVerify direct Claude run ID separation +6/-3

Verify direct Claude run ID separation

• Checks that provider sightings receive generation IDs while RunResult retains distinct message IDs, including on failure.

tests/test_claude_cli_direct.py

test_fullsend_runner.pyVerify Fullsend generation ID aggregation +8/-5

Verify Fullsend generation ID aggregation

• Adds request IDs to root and subagent fixtures and checks both ID lists across iterations.

tests/test_fullsend_runner.py

test_harbor_results.pyVerify Harbor trial generation IDs and pricing +20/-11

Verify Harbor trial generation IDs and pricing

• Checks separate ID extraction, multi-step deduplication, and per-trial pricing against generation IDs.

tests/test_harbor_results.py

test_openrouter_backfill_cli.pyTest current and legacy backfill ID fields +15/-2

Test current and legacy backfill ID fields

• Updates interrupted-run fixtures and confirms generation_ids takes precedence over legacy message_ids.

tests/test_openrouter_backfill_cli.py

test_openrouter_session.pyTest provider sessions with distinct ID lists +8/-3

Test provider sessions with distinct ID lists

• Updates reconciliation and crash-path fixtures to carry Claude message IDs separately from generation IDs.

tests/test_openrouter_session.py

test_providers_reconcile.pyTest generation-based coverage and empty audits +12/-0

Test generation-based coverage and empty audits

• Checks that generation IDs determine request coverage and that a pinned run without generation rows has an incomplete audit.

tests/test_providers_reconcile.py

test_stream_capture.pyTest transcript extraction and null token usage +76/-11

Test transcript extraction and null token usage

• Covers session and subagent generation-ID recovery, ID separation, session-ID selection, and explicit null usage fields.

tests/test_stream_capture.py

Documentation (3) +13 / -9
spec.mdCorrect the OpenRouter ID contract +7/-6

Correct the OpenRouter ID contract

• Documents Claude message IDs as turn-accounting keys and top-level generation request IDs as cost-truth keys.

specs/014-openrouter-provider/spec.md

openrouter.mdExplain which streamed IDs OpenRouter backfills +3/-1

Explain which streamed IDs OpenRouter backfills

• Clarifies that top-level generation request IDs are sighted while nested Claude message IDs remain turn-counting keys.

website/guides/openrouter.md

runs-directory.mdDocument separate persisted ID fields +3/-2

Document separate persisted ID fields

• Describes generation_ids, legacy result compatibility, and its role in ledger reconciliation.

website/reference/runs-directory.md

@fullsend-ai-review

fullsend-ai-review Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 5:23 PM UTC · Completed 5:43 PM UTC

Commit: ec42ade · View workflow run →

Runtime: pi · Model: sonnet → claude-sonnet-5-5 · Effort: high · Cost: $2.68

@qodo-code-review

qodo-code-review Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (1)

Grey Divider


Action required

1. Cleanup lacks root and home guards 📜 Skill insight ⛨ Security
Description
_cleanup_session passes session_dir to shutil.rmtree after only an existence/type check,
without resolving it and rejecting / or Path.home() immediately before deletion. The changed
session-directory encoding feeds this deletion path during normal runs, so any unexpected path
resolution that widens session_dir reaches recursive cleanup without the required boundary check.
Code

agent_eval/agent/claude_code.py[834]

+        encoded = claude_session_dir_name(workspace)
Relevance

●●● Strong

Recent security precedents consistently accept resolved containment checks around filesystem
operations.

PR-#194
PR-#263

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires a resolved root/home rejection immediately before every recursive deletion
derived from run state or configuration. The changed encoding is used to construct session_dir,
while the deletion checks only existence and directory type before calling shutil.rmtree; the
current encoding normally keeps the target under the Claude projects directory but does not provide
the required deletion-boundary guard.

agent_eval/agent/claude_code.py[831-837]
agent_eval/agent/stream_capture.py[291-307]
Skill: eval-run

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_cleanup_session` recursively deletes the computed Claude session directory without resolving the target and rejecting the filesystem root or the current user's home directory immediately before deletion.

## Fix Focus Areas
- agent_eval/agent/claude_code.py[831-837]

## Recommended Fix
Resolve `session_dir` immediately before `shutil.rmtree`, reject targets equal to `Path('/').resolve()` or `Path.home().resolve()`, and then perform the recursive deletion only after the safety check passes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


2. Session reads can escape allowed storage ✓ Resolved

Resolving commit: 4116ce1

Resolution reason
The commit validates session IDs as safe single path segments, resolves and validates the
projects/session roots, rejects symlinked directories and transcript files, and checks resolved
transcript containment before reading or recursively scanning them.
Description
claude_session_generation_ids joins the unvalidated session identifier into transcript paths and
recursively reads the resulting directory without resolving or checking containment. A session
identifier containing an absolute path, traversal component, or symlink can redirect rglob() and
open() outside the expected Claude projects directory.

3. Runs with preference-only routing always fail strict audit 🐞 Bug ≡ Correctness
Description
The new audit_complete = unattributed == 0 and (audited > 0 or not declared) checks whether
declared holds any non-empty routing spec. But audited only goes up for rows whose spec is
pinned (only, or order with allow_fallbacks: false), so an order that allows fallbacks, a
quantizations-only entry, or an entry for a model this run never used is declared yet can never be
audited. Every such run now reports audit_complete: false, which makes --strict-routing exit 2
('routing audit incomplete'), makes compare/anova refuse to pool the run, and gets the run flagged
as unaudited in reports and MLflow tags, even though nothing was pinned and nothing went wrong.
Code

agent_eval/providers/openrouter/audit.py[R120-122]

+    # A pinned run with zero generation rows means collection or backfill
+    # failed, not that every request was compliant.
+    audit_complete = unattributed == 0 and (audited > 0 or not declared)
Relevance

●●● Strong

Recent routing precedents accept correcting audit coverage and handling declared routing policies
accurately.

PR-#228
PR-#229
PR-#230

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
pinned_providers returns None for an order that allows fallbacks and for a quantizations-only
spec. In the row loop, a row with pins is None hits continue before audited += 1. declared
is built from every non-empty spec in table.models, and is_empty is true only when all fields
are None. So a declared-but-unpinned table leaves audited == 0 with declared non-empty, and
audit_complete is False. execute.py then adds a strict failure for that, and
compare/anova/mlflow/report all treat False as unaudited.

agent_eval/providers/openrouter/audit.py[19-29]
agent_eval/providers/openrouter/audit.py[50-56]
agent_eval/providers/openrouter/audit.py[86-91]
agent_eval/providers/openrouter/audit.py[110-122]
agent_eval/providers/openrouter/routing.py[157-164]
skills/eval-run/scripts/execute.py[1000-1006]
skills/eval-compare/scripts/compare.py[166-166]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`routing_audit` marks the audit incomplete whenever `declared` is non-empty and `audited == 0`. `declared` includes unpinned preference specs (an `order` that allows fallbacks, quantizations only, `sort`, ...) and specs for models the run never used, and rows under those keys are never audited. Every such run reports `audit_complete: false` and fails `--strict-routing`.

## Fix Focus Areas
- agent_eval/providers/openrouter/audit.py[110-122]

## Recommended Fix
Build a set of pinned routing keys, i.e. keys in `declared` for which `pinned_providers(table.for_model(key))` is not None. Apply the zero-rows rule only to those, for example: `audit_complete = unattributed == 0 and (audited > 0 or not pinned_keys)`. Better still, restrict it to pinned keys the run actually requested, such as the plan's skill/subagent routing keys. Add tests for a preference-only table and for a table that pins a model the run did not use.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗



Remediation recommended

4. Timed-out runs lose their persisted OpenRouter ids ✓ Resolved

Resolving commit: d895f34

Resolution reason
extract_session_id now prefers the latest result session_id and, if none exists, falls back to the
system/init event's session_id (second loop). A test covers the init-only fallback. This addresses
the finding's premise that init-stage session IDs are ignored, so truncated streams can still locate
the persisted transcript.
Description
extract_session_id reads session_id only from result events, ignoring IDs already present on
system/init and assistant events. When a timeout or exception truncates the stream before a result
event, the persisted-transcript lookup receives None, so transcript-only billed gen-* IDs never
reach the provider binding or backfill and the run's spend remains unpriced and unaudited.


Informational

5. A null usage block still crashes token accounting 🐞 Bug ☼ Reliability
Description
extract_usage still reads obj.get("message", {}).get("usage", {}), which returns None when the
event contains "usage": null, so the following u.get(...) raises AttributeError. The same
happens with a null per-model stats entry in modelUsage. On the success path extract_usage
runs outside any try, so a provider that nulls the whole usage object (not just its fields) still
crashes the case, which the PR's null-usage fix was meant to prevent.
Code

agent_eval/agent/stream_capture.py[R117-120]

+            fb_input += u.get("input_tokens") or 0
+            fb_output += u.get("output_tokens") or 0
+            fb_cache_read += u.get("cache_read_input_tokens") or 0
+            fb_cache_create += u.get("cache_creation_input_tokens") or 0
Relevance

●●● Strong

Recent precedents accept defensive normalization of malformed or null usage structures.

PR-#183

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Only the leaf values were made null-safe with or 0; the containing dict lookups `.get("usage",
{}) and stats.get` are not guarded against a None value.

agent_eval/agent/stream_capture.py[106-139]
agent_eval/agent/claude_code.py[616-617]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A `"usage": null` or a null `modelUsage` entry still makes `extract_usage` raise AttributeError.

## Fix Focus Areas
- agent_eval/agent/stream_capture.py[106-139]

## Recommended Fix
Use `msg = obj.get("message") or {}` and `u = msg.get("usage") or {}`, and skip or empty-default `stats` when it is not a dict (`stats = stats if isinstance(stats, dict) else {}`). Add a test that feeds a stream with `"usage": null`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


Grey Divider

Context sources
✅ Compliance rules (platform): 106 rules
✅ Skills: 5 invoked
  eval-analyze
  eval-anova
  eval-mlflow
  eval-run
  eval-dataset
✅ Web pages:
  +2 more
✅ Cross-repo context — repo relationships
  Explored: repo: fullsend-ai/rfe-creator (sha: c9ea4d3c) — View relationship
✅ REVIEW.md
Review mode: Auto: 🧠 Deep: Cross-cutting provider attribution changes span many independent runtime and reconciliation paths.

Grey Divider

Tip of the day
💡 Did you know, you can turn these tips off under Display > Comment parts

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread agent_eval/agent/stream_capture.py Resolved
if not projects_dir.exists():
return
encoded = "-" + str(workspace).replace("/", "-")
encoded = claude_session_dir_name(workspace)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Cleanup lacks root and home guards 📜 Skill insight ⛨ Security

_cleanup_session passes session_dir to shutil.rmtree after only an existence/type check,
without resolving it and rejecting / or Path.home() immediately before deletion. The changed
session-directory encoding feeds this deletion path during normal runs, so any unexpected path
resolution that widens session_dir reaches recursive cleanup without the required boundary check.
Agent Prompt
## Issue description
`_cleanup_session` recursively deletes the computed Claude session directory without resolving the target and rejecting the filesystem root or the current user's home directory immediately before deletion.

## Fix Focus Areas
- agent_eval/agent/claude_code.py[831-837]

## Recommended Fix
Resolve `session_dir` immediately before `shutil.rmtree`, reject targets equal to `Path('/').resolve()` or `Path.home().resolve()`, and then perform the recursive deletion only after the safety check passes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

Comment on lines +120 to +122
# A pinned run with zero generation rows means collection or backfill
# failed, not that every request was compliant.
audit_complete = unattributed == 0 and (audited > 0 or not declared)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Runs with preference-only routing always fail strict audit 🐞 Bug ≡ Correctness

The new audit_complete = unattributed == 0 and (audited > 0 or not declared) checks whether
declared holds any non-empty routing spec. But audited only goes up for rows whose spec is
pinned (only, or order with allow_fallbacks: false), so an order that allows fallbacks, a
quantizations-only entry, or an entry for a model this run never used is declared yet can never be
audited. Every such run now reports audit_complete: false, which makes --strict-routing exit 2
('routing audit incomplete'), makes compare/anova refuse to pool the run, and gets the run flagged
as unaudited in reports and MLflow tags, even though nothing was pinned and nothing went wrong.
Agent Prompt
## Issue description
`routing_audit` marks the audit incomplete whenever `declared` is non-empty and `audited == 0`. `declared` includes unpinned preference specs (an `order` that allows fallbacks, quantizations only, `sort`, ...) and specs for models the run never used, and rows under those keys are never audited. Every such run reports `audit_complete: false` and fails `--strict-routing`.

## Fix Focus Areas
- agent_eval/providers/openrouter/audit.py[110-122]

## Recommended Fix
Build a set of pinned routing keys, i.e. keys in `declared` for which `pinned_providers(table.for_model(key))` is not None. Apply the zero-rows rule only to those, for example: `audit_complete = unattributed == 0 and (audited > 0 or not pinned_keys)`. Better still, restrict it to pinned keys the run actually requested, such as the plan's skill/subagent routing keys. Add tests for a preference-only table and for a table that pins a model the run did not use.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

Comment thread agent_eval/agent/stream_capture.py Resolved
Comment on lines +117 to +120
fb_input += u.get("input_tokens") or 0
fb_output += u.get("output_tokens") or 0
fb_cache_read += u.get("cache_read_input_tokens") or 0
fb_cache_create += u.get("cache_creation_input_tokens") or 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

5. A null usage block still crashes token accounting 🐞 Bug ☼ Reliability

extract_usage still reads obj.get("message", {}).get("usage", {}), which returns None when the
event contains "usage": null, so the following u.get(...) raises AttributeError. The same
happens with a null per-model stats entry in modelUsage. On the success path extract_usage
runs outside any try, so a provider that nulls the whole usage object (not just its fields) still
crashes the case, which the PR's null-usage fix was meant to prevent.
Agent Prompt
## Issue description
A `"usage": null` or a null `modelUsage` entry still makes `extract_usage` raise AttributeError.

## Fix Focus Areas
- agent_eval/agent/stream_capture.py[106-139]

## Recommended Fix
Use `msg = obj.get("message") or {}` and `u = msg.get("usage") or {}`, and skip or empty-default `stats` when it is not a dict (`stats = stats if isinstance(stats, dict) else {}`). Add a test that feeds a stream with `"usage": null`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

@fullsend-ai-review fullsend-ai-review Bot added the risk/moderate PR risk: moderate label Nov 3, 2026
@fullsend-ai-review

fullsend-ai-review Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Risk Assessment: moderate (2/5)

Details

Moderate risk driven by a large blast radius across 26 files and a protected script edit in execute.py, balanced by an experienced contributor, low 30-day commit churn, no CI or dependency changes, accompanying unit test coverage, and full alignment with linked issue #296.

Previous run

<!-- fullsend:risk-assessment -->
Risk Assessment: moderate (2/5)

Details

Moderate risk driven by a large blast radius across 25 files and a protected script edit in execute.py, balanced by an experienced contributor, low 30-day commit churn, no CI or dependency changes, and accompanying unit test coverage.

Previous run (2)

<!-- fullsend:risk-assessment -->
Risk Assessment: moderate (2/5)

Details

Moderate risk driven by 24 changed files with large blast radius and a protected script edit, balanced by an experienced contributor, low 30-day commit churn, no CI or dependency changes, and accompanying unit test coverage.

@fullsend-ai-review

fullsend-ai-review Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Review

Findings

Medium

  • [protected-path] skills/eval-run/scripts/execute.py — This PR modifies a protected governance/infrastructure path. The PR links issue OpenRouter generation IDs are lost before provider attribution #296, which explicitly authorizes the execute.py change (persisting and aggregating generation_ids alongside message_ids), so context is sufficient. Human approval is always required for protected-path changes regardless of context.

Low

  • [edge-case] agent_eval/agent/stream_capture.py:319 — claude_session_generation_ids reads persisted transcripts from Path.home()/.claude/projects unless projects_dir is passed, and none of the ClaudeCodeRunner call sites pass it. If a user overrides CLAUDE_CONFIG_DIR (or HOME) through runner.env/extra_env, ids that appear only in the persisted transcript are lost; stream and subagent ids are still collected. The base-branch _cleanup_session already makes the same assumption, and the harness never sets CLAUDE_CONFIG_DIR itself, so this only affects deliberate overrides.
    Remediation: derive the projects directory from the subprocess environment _build_env produces (honoring CLAUDE_CONFIG_DIR, then HOME), pass it as projects_dir, and use the same directory in _cleanup_session. Add a runner-level test with a non-default config directory.

Earlier findings

  • agent_eval/agent/stream_capture.py:283, edge-case — resolved: extract_session_id now falls back to the system/init session id when no result event exists; covered by test_falls_back_to_init_session_id_before_result.
  • agent_eval/agent/stream_capture.py:284, edge-case — resolved: same init-event fallback (stream_capture.py lines 289–299).
  • agent_eval/harbor/results.py:121, api-contract — reclassified to info: Harbor still extracts ids only from captured stream-json; persisted-transcript export is documented as follow-up work (Harbor: export persisted Claude session transcripts for OpenRouter generation IDs #298, spec probe fix: improve execution mode detection in eval-analyze #28).
  • agent_eval/evalhub/adapter.py:260, backward-incompatible — reclassified to info: the new generation_ids_count key and the changed message_ids meaning are now documented in website/guides/evalhub.md and website/reference/runs-directory.md, and the provenance travels in a free-form metadata dict.
  • agent_eval/evalhub/adapter.py:262, backward-incompatible — resolved: duplicate of the finding above; an older pod never emits generation_ids_count, so the missing key already identifies it.

Re-review note: One or more prior blocking findings remain open and must still be addressed.

Blocking prior findings:


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

2 finding(s) below the low severity threshold were not posted.

Previous run

<!-- fullsend:review-findings-v2:eyJ2ZXJzaW9uIjoyLCJhY3Rpb24iOiJyZXF1ZXN0LWNoYW5nZXMiLCJmaW5kaW5ncyI6W3sic2V2ZXJpdHkiOiJtZWRpdW0iLCJjYXRlZ29yeSI6ImVkZ2UtY2FzZSIsImZpbGUiOiJhZ2VudF9ldmFsL2FnZW50L3N0cmVhbV9jYXB0dXJlLnB5IiwiaWQiOiJmXzdkMDAxYmM2Nzc3MzVlYTkiLCJsaW5lIjoyODMsImFjdGlvbmFibGUiOnRydWUsImRpbWVuc2lvbiI6ImNvcnJlY3RuZXNzIn0seyJzZXZlcml0eSI6ImxvdyIsImNhdGVnb3J5IjoiYXBpLWNvbnRyYWN0IiwiZmlsZSI6ImFnZW50X2V2YWwvaGFyYm9yL3Jlc3VsdHMucHkiLCJpZCI6ImZfNDBjOTE5NDNkYjlmZjI2ZSIsImxpbmUiOjEyMSwiYWN0aW9uYWJsZSI6dHJ1ZSwiZGltZW5zaW9uIjoiY29ycmVjdG5lc3MifSx7InNldmVyaXR5IjoibG93IiwiY2F0ZWdvcnkiOiJiYWNrd2FyZC1pbmNvbXBhdGlibGUiLCJmaWxlIjoiYWdlbnRfZXZhbC9ldmFsaHViL2FkYXB0ZXIucHkiLCJpZCI6ImZfNDkwYzM0MGJmMTczNGQ4YiIsImxpbmUiOjI2MCwiYWN0aW9uYWJsZSI6dHJ1ZSwiZGltZW5zaW9uIjoiY3Jvc3MtcmVwby1jb250cmFjdHMifSx7InNldmVyaXR5IjoibWVkaXVtIiwiY2F0ZWdvcnkiOiJlZGdlLWNhc2UiLCJmaWxlIjoiYWdlbnRfZXZhbC9hZ2VudC9zdHJlYW1fY2FwdHVyZS5weSIsImlkIjoiZl85OTU2NmY0NGRiMmZjYzRjIiwibGluZSI6Mjg0LCJhY3Rpb25hYmxlIjp0cnVlLCJkaW1lbnNpb24iOiJjb3JyZWN0bmVzcyJ9LHsic2V2ZXJpdHkiOiJsb3ciLCJjYXRlZ29yeSI6ImJhY2t3YXJkLWluY29tcGF0aWJsZSIsImZpbGUiOiJhZ2VudF9ldmFsL2V2YWxodWIvYWRhcHRlci5weSIsImlkIjoiZl9hM2Y0ZTg2YzYwNGUxMjQxIiwibGluZSI6MjYyLCJhY3Rpb25hYmxlIjp0cnVlLCJkaW1lbnNpb24iOiJjcm9zcy1yZXBvLWNvbnRyYWN0cyJ9LHsic2V2ZXJpdHkiOiJsb3ciLCJjYXRlZ29yeSI6ImluamVjdGlvbi12dWxuIiwiZmlsZSI6ImFnZW50X2V2YWwvYWdlbnQvc3RyZWFtX2NhcHR1cmUucHkiLCJpZCI6ImZfZWMzMWRhMmZhMjkzOTk2ZSIsImxpbmUiOjMwOCwiYWN0aW9uYWJsZSI6dHJ1ZSwiZGltZW5zaW9uIjoic2VjdXJpdHkifSx7InNldmVyaXR5IjoibG93IiwiY2F0ZWdvcnkiOiJtaXNzaW5nLWF1dGhvcml6YXRpb24iLCJmaWxlIjoiYWdlbnRfZXZhbC9hZ2VudC9iYXNlLnB5IiwiaWQiOiJmX2IzZTliZTYzYzFjZWNkMGEiLCJhY3Rpb25hYmxlIjpmYWxzZSwiZGltZW5zaW9uIjoiaW50ZW50LWNvaGVyZW5jZSJ9XSwiZGlzcG9zaXRpb25zIjpbeyJpZCI6ImZfNDBjOTE5NDNkYjlmZjI2ZSIsInN0YXR1cyI6InJlY2xhc3NpZmllZCJ9LHsiaWQiOiJmXzk5NTY2ZjQ0ZGIyZmNjNGMiLCJzdGF0dXMiOiJvcGVuIn0seyJpZCI6ImZfZWMzMWRhMmZhMjkzOTk2ZSIsInN0YXR1cyI6InJlc29sdmVkX2J5X2NoYW5nZSJ9LHsiaWQiOiJmX2IzZTliZTYzYzFjZWNkMGEiLCJzdGF0dXMiOiJyZXNvbHZlZF9ieV9jaGFuZ2UifSx7ImlkIjoiZl9hM2Y0ZTg2YzYwNGUxMjQxIiwic3RhdHVzIjoib3BlbiJ9XX0= -->

Review

Findings

Medium

  • [edge-case] agent_eval/agent/stream_capture.py:283 — extract_session_id skips every event that is not type == "result". A timeout or crash before the final result event can still leave a session_id in the init/assistant events and billed generations in the persisted transcript. Both failure branches in ClaudeCodeRunner call this helper, so with no result event it returns None and claude_session_generation_ids returns an empty set. If stdout carried no requestId, the partial run's generation IDs and costs are lost, even though this PR adds failure-path recovery for them. The session directory is still on disk in those branches because _cleanup_session runs only on the success path.
    Remediation: Prefer the latest result event's session_id, but fall back to a valid session_id from earlier init/assistant events when no result event exists. Add a test with a truncated stream and a persisted session transcript.
  • [protected-path] skills/eval-run/scripts/execute.py — This PR modifies a protected path (skills/). The PR links OpenRouter generation IDs are lost before provider attribution #296, which explicitly authorizes the execute.py change that persists and aggregates generation_ids, and the description explains the rationale. Human approval is still always required for protected-path changes, regardless of context.

Low

  • [api-contract] agent_eval/harbor/results.py:121 — Harbor still collects generation IDs only from the captured stream-json transcript. If Claude emits msg_* message ids without a top-level requestId, neither the new requestId extraction nor the legacy gen-* fallback finds an ID. The local runner can recover IDs from the persisted session transcript, but Harbor has no equivalent: _agent_transcript_metrics reads only claude-code.txt or codex.txt. This is not a regression, since the base code passed message_ids to sight_trial and had the same gap.
    Remediation: Capture persisted Claude session transcripts before the Harbor container is removed and merge their requestId values into the trial's generation_ids, with a regression test.
  • [backward-incompatible] agent_eval/evalhub/adapter.py:260 — The EvalHub provenance field message_ids_count used to count OpenRouter gen-* ids and now counts Claude msg_* ids. An external consumer using it as OpenRouter coverage would silently get a different number. generation_ids_count is added alongside it, but the field is not mentioned in the docs.
    Remediation: Document the changed meaning of message_ids_count in the EvalHub/provenance docs or release notes, and point consumers to generation_ids_count.

Earlier findings

  • agent_eval/harbor/results.py:121, api-contract — reclassified from medium to low: the gap predates this PR (base code had the same failure) and the PR only improves Harbor coverage.
  • agent_eval/agent/stream_capture.py, injection-vuln — resolved: session_id is now validated against [A-Za-z0-9_-]+, and symlinked session dirs and transcripts are rejected. Resolved session and transcript paths must stay inside their expected roots.
  • agent_eval/agent/base.py, missing-authorization — resolved: the PR links OpenRouter generation IDs are lost before provider attribution #296, which specifies the separate generation_ids field.

Labels: PR fixes a bug in OpenRouter generation ID collection


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (2)

<!-- fullsend:review-findings-v2:eyJ2ZXJzaW9uIjoyLCJhY3Rpb24iOiJyZXF1ZXN0LWNoYW5nZXMiLCJmaW5kaW5ncyI6W3sic2V2ZXJpdHkiOiJtZWRpdW0iLCJjYXRlZ29yeSI6ImFwaS1jb250cmFjdCIsImZpbGUiOiJhZ2VudF9ldmFsL2hhcmJvci9yZXN1bHRzLnB5IiwiaWQiOiJmXzQwYzkxOTQzZGI5ZmYyNmUiLCJsaW5lIjoxMjEsImFjdGlvbmFibGUiOnRydWUsImRpbWVuc2lvbiI6ImNvcnJlY3RuZXNzIn0seyJzZXZlcml0eSI6Im1lZGl1bSIsImNhdGVnb3J5IjoiZWRnZS1jYXNlIiwiZmlsZSI6ImFnZW50X2V2YWwvYWdlbnQvc3RyZWFtX2NhcHR1cmUucHkiLCJpZCI6ImZfOTk1NjZmNDRkYjJmY2M0YyIsImxpbmUiOjI4NCwiYWN0aW9uYWJsZSI6dHJ1ZSwiZGltZW5zaW9uIjoiY29ycmVjdG5lc3MifSx7InNldmVyaXR5IjoibG93IiwiY2F0ZWdvcnkiOiJpbmplY3Rpb24tdnVsbiIsImZpbGUiOiJhZ2VudF9ldmFsL2FnZW50L3N0cmVhbV9jYXB0dXJlLnB5IiwiaWQiOiJmX2VjMzFkYTJmYTI5Mzk5NmUiLCJsaW5lIjozMDgsImFjdGlvbmFibGUiOnRydWUsImRpbWVuc2lvbiI6InNlY3VyaXR5In0seyJzZXZlcml0eSI6ImxvdyIsImNhdGVnb3J5IjoibWlzc2luZy1hdXRob3JpemF0aW9uIiwiZmlsZSI6ImFnZW50X2V2YWwvYWdlbnQvYmFzZS5weSIsImlkIjoiZl9iM2U5YmU2M2MxY2VjZDBhIiwiYWN0aW9uYWJsZSI6ZmFsc2UsImRpbWVuc2lvbiI6ImludGVudC1jb2hlcmVuY2UifSx7InNldmVyaXR5IjoibG93IiwiY2F0ZWdvcnkiOiJiYWNrd2FyZC1pbmNvbXBhdGlibGUiLCJmaWxlIjoiYWdlbnRfZXZhbC9ldmFsaHViL2FkYXB0ZXIucHkiLCJpZCI6ImZfYTNmNGU4NmM2MDRlMTI0MSIsImxpbmUiOjI2MiwiYWN0aW9uYWJsZSI6dHJ1ZSwiZGltZW5zaW9uIjoiY3Jvc3MtcmVwby1jb250cmFjdHMifV19 -->

Review

Findings

High

  • [protected-path] skills/eval-run/scripts/execute.py — This PR modifies a protected path (skills/) and has no linked issue authorizing the change to skill/infrastructure files. Human approval is always required for protected-path changes.
    Remediation: Link an issue authorizing the execute.py change (or explain why it is required) and obtain human maintainer approval.

Medium

  • [api-contract] agent_eval/harbor/results.py:121 — Harbor still derives generation IDs only from the captured stream-json. On Claude Code versions where requestId appears only in the persisted session transcript, generation_ids stays empty on the Harbor path, harbor/run.py sights no generations, and backfill/routing attribution remain broken there.
    Remediation: Export persisted Claude session transcripts from Harbor trials and collect their requestId values before sight_trial(), unioned with IDs from stream-json; add a Harbor test with msg-... stream IDs and generation IDs only in persisted transcripts.
  • [edge-case] agent_eval/agent/stream_capture.py:284 — extract_session_id reads session_id only from a result event, but the timeout and exception paths in claude_code.py rely on it. A run killed or crashing before its first result event returns None even though the init event carried the session ID, so already-billed root generations are lost from RunResult.generation_ids.
    Remediation: Fall back to the session_id from the system/init event (or preallocate it before launch); add a test for interruption before any result event.

Low

  • [injection-vuln] agent_eval/agent/stream_capture.py:308 — The persisted-transcript lookup is not confined to the session directory: session_id is only checked for being non-empty, so absolute paths or ../ escape it, and symlinks are followed. Impact is limited (only gen- requestId strings are extracted), but extra IDs could be imported and polled with the run's OpenRouter key.
    Remediation: Validate session_id format, reject separators/absolute paths, ensure resolved paths stay inside the session directory, reject symlinks; add regression tests.
  • [backward-incompatible] agent_eval/evalhub/adapter.py:262 — message_ids_count now counts generation_ids when that key is present (always written, [] when none), so it is 0 on runs without collected generation IDs; the field name and the comment at line 50 still say message ids. External consumers would see a silent meaning change.
    Remediation: Keep message_ids_count on message_ids and add generation_ids_count, or rename/document it; update the comment at line 50.
  • [missing-authorization] agent_eval/agent/base.py — No linked issue or work item establishes authorized scope for this 24-file change (mitigated: it is a scoped correction within spec 014 and updates that spec and related docs).
    Remediation: Link the PR to the authorizing issue or work item.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

fullsend-ai-review[bot]

This comment was marked as outdated.

Signed-off-by: apetruni <apetruni@redhat.com>
@Aliciapet11
Aliciapet11 force-pushed the fix/openrouter-generation-ids branch from 109a8f5 to 83cac99 Compare November 4, 2026 09:57
@fullsend-ai-review

fullsend-ai-review Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ❌ Failure (validation failed after 1 iteration(s)) · Started 9:59 AM UTC · Completed 10:22 AM UTC

Commit: 83cac99 · View workflow run →

Runtime: pi · Model: sonnet → claude-sonnet-5-5 · Effort: high · Cost: $0.52

Signed-off-by: apetruni <apetruni@redhat.com>
@astefanutti

Copy link
Copy Markdown
Collaborator

/fs-review

@fullsend-ai-review

fullsend-ai-review Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:08 PM UTC · Completed 12:20 PM UTC

Commit: 83cac99 · View workflow run →

Runtime: pi · Model: sonnet → claude-sonnet-5-5 · Effort: high · Cost: $3.01

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added the kind/bug Something isn't working label Nov 4, 2026

@astefanutti astefanutti left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed against main at d87a7ab. Verified locally on 83cac99: the full suite passes (2880), no new lint findings in the changed files, mkdocs build --strict is clean, and the branch merges cleanly onto today's main. #296 covers the execute.py protected-path edit.

Direction is right and the plumbing is consistent. Separating Claude's msg_* ids (turn accounting) from OpenRouter's gen-* ids (cost truth, attribution) is the correct model, and generation_ids reaches every consumer — runner, provider session, backfill, reconcile, Harbor, EvalHub, execute.py — with legacy fallbacks for old run results. The persisted-transcript reader is hardened (session-id format, symlink rejection, containment). The null-usage fix is right. On Claude Code 2.1.314 I can confirm the premise locally: the transcript's top-level requestId and message.id are different identifiers.

Before merge (details inline):

  1. The local runner has no fallback for the old id shape. _extract_transcript_metrics (Harbor) accepts a gen- message.id when requestId is absent; assistant_event_ids, extract_generation_ids and subagent_generation_ids do not. Any Claude Code + OpenRouter combination that still echoes the generation id as message.id — what spec 014 probe #12 verified in September — would now collect zero generation ids locally, the symptom this PR fixes. Same fallback in stream_capture.py, plus a test with the legacy event shape.
  2. extract_session_id reads only result events (fullsend's finding above, agreed): on a timeout or crash the persisted transcript is never read on exactly the paths where stream-json is least complete. Fall back to the system/init event's session_id.
  3. Spec 014 evidence. The edit replaces a statement labelled VERIFIED (probe #12) without recording what changed. Label the new claim VERIFIED with the live-run evidence (date, Claude Code version, model, and the observation that requestId was in the persisted transcript but not always in stream-json), mark probe #12 as superseded, and update the rows that still name message_ids as the key set (cost_confidence / cost_coverage at ~423-424, the architecture table at ~111). One question that decides the Harbor follow-up: under which condition does stream-json omit requestId?

Worth recording, no code change asked:

  • The root-cause narrative in the description is slightly off. The old cleanup encoded the path with a double leading dash ("-" + "/Users/..."), so it never matched a real session directory: transcripts were not deleted before extraction, they were never read — and session directories accumulated. The new encoding makes cleanup actually delete, which is why reading before cleanup now matters. Worth saying so, and expect long-accumulated session directories to start disappearing per run.
  • The audit_complete change is good and intended: a pinned run with zero generation rows now reads as incomplete in report, compare, anova and MLflow instead of compliant. Mention it in the description / release note. Same for fullsend's point on message_ids_count in the EvalHub provenance: a sentence in the docs pointing consumers to generation_ids_count.
  • Harbor trials capture stream-json only (fullsend's finding, agreed): if a container's Claude Code omits requestId, generation_ids stays empty there. Separate issue — exporting the session transcript from the trial.
  • Qodo's remaining root/home guard before rmtree: the encoding cannot produce either path, so a one-line session_dir != projects_dir guard is only there to close the thread.
  • The PR title is not a conventional commit; squash-merge as fix(provider): … so semantic-release cuts the patch.

return ids


def assistant_event_ids(obj):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No fallback for the older shape here (nor in extract_generation_ids at 352 and subagent_generation_ids at 237), while harbor/results.py:122 has one: when requestId is absent and message.id starts with gen-, that id is the generation id (spec 014 probe #12, verified 2026-09-16). Without it, a CLI/OpenRouter combination that still echoes gen ids as message.id collects zero generation ids on the local runner. Suggest the same rule in all three helpers and a test with the legacy event shape (message.id: gen-…, no requestId).

return sorted(ids)


def extract_session_id(stdout_lines):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Agree with the review bot above: on a timeout or crash there may be no result event, so the persisted transcript is never read on exactly the paths where stdout is least complete. The system/init event carries session_id as well — prefer the latest result, fall back to init, with a test for a truncated stream plus a persisted transcript.

`gen-…` generation id (probe #12: 2 per turn, including the `generate_session_title`
background call, whose cost is real spend and is therefore included), so each id is handed
to the `generation.py` worker **as the stream is read** (a per-message callback the
(:165) transcripts. Current OpenRouter transcripts carry the generation id in the

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This replaces a VERIFIED statement (probe #12, 2026-09-16: message.id was the gen-… id) without saying what changed. Please label the new claim VERIFIED with the live-run evidence — date, Claude Code version, model, and that requestId was present in the persisted transcript but not always in stream-json — and annotate probe #12 in the checklist as superseded. The cost_confidence / cost_coverage rows (~423-424) and the architecture table (~111) still describe message_ids as the key set. Also: under which condition does stream-json omit requestId? That answer decides the Harbor follow-up.

if not projects_dir.exists():
return
encoded = "-" + str(workspace).replace("/", "-")
encoded = claude_session_dir_name(workspace)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good fix. For the record: the old encoding ("-" + str(workspace).replace("/", "-")) produced a double leading dash, so cleanup never matched a real session directory — the transcripts were not being deleted before extraction, they were never read, and session directories accumulated. With this change cleanup really deletes, which is why reading the transcript first now matters. Worth a sentence in the description; users will see long-accumulated session directories disappear per run.

if k in ("order", "only", "ignore", "allow_fallbacks", "quantizations")}
# A pinned run with zero generation rows means collection or backfill
# failed, not that every request was compliant.
audit_complete = unattributed == 0 and (audited > 0 or not declared)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Intended and correct: a pinned run with zero generation rows now reads as incomplete rather than compliant. Note that report.py, compare.py, analyze.py and log_results.py all key on audit_complete is False, so such runs will now be flagged in every consumer. Worth a line in the description / release note.

@Aliciapet11 Aliciapet11 changed the title Fix OpenRouter generation ID collection and provider attribution fix(provider): OpenRouter generation ID collection and provider attribution Nov 5, 2026
Signed-off-by: apetruni <apetruni@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:00 PM UTC · Completed 1:11 PM UTC

Commit: d895f34 · View workflow run →

Runtime: pi · Model: sonnet → claude-sonnet-5-5 · Effort: high · Cost: $3.07

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.

return set()
try:
projects = (Path(projects_dir) if projects_dir is not None
else Path.home() / ".claude" / "projects").resolve(strict=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] edge-case

claude_session_generation_ids reads persisted transcripts from Path.home()/.claude/projects unless projects_dir is passed, and no ClaudeCodeRunner call site passes it. If a user overrides CLAUDE_CONFIG_DIR (or HOME) via runner.env/extra_env, ids present only in the persisted transcript are lost; stream and subagent ids are still collected. The base-branch _cleanup_session makes the same assumption.

Suggested fix: Derive the projects directory from the subprocess environment _build_env produces (CLAUDE_CONFIG_DIR, then HOME), pass it as projects_dir, use the same directory in _cleanup_session, and add a runner-level test with a non-default config directory.

@@ -255,6 +258,7 @@ def _finish_provider_plan(session, run_dir: Path, case_results: list) -> dict:
final = payload
provenance = {k: final.get(k) for k in PROVENANCE_FIELDS if k in final}
provenance["message_ids_count"] = len(final.get("message_ids") or [])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] backward-incompatible

Previously reported finding remains open: low backward-incompatible in agent_eval/evalhub/adapter.py:260.

@Aliciapet11

Copy link
Copy Markdown
Contributor Author

Thanks @astefanutti , I addressed your review comments in d895f34:

@astefanutti astefanutti left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @Aliciapet11!

@astefanutti
astefanutti merged commit c8687a9 into fullsend-ai:main Nov 5, 2026
33 checks passed
github-actions Bot pushed a commit that referenced this pull request Nov 5, 2026
## [1.62.2](v1.62.1...v1.62.2) (2026-11-05)

### Bug Fixes

* **provider:** OpenRouter generation ID collection and provider attribution ([#295](#295)) ([c8687a9](c8687a9))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/bug Something isn't working risk/moderate PR risk: moderate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenRouter generation IDs are lost before provider attribution

2 participants