Skip to content

feat(sessions): project per-message model and session transcript path - #3089

Open
devin-ai-integration[bot] wants to merge 5 commits into
masterfrom
devin/1791148933-session-transcript-path
Open

devin-ai-integration[bot] wants to merge 5 commits into
masterfrom
devin/1791148933-session-transcript-path

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Canonical session projection now populates SessionRecord.transcript_path for every host whose session has a real source file (codex, claude, pi, vibe, kimi main wire, cline api-history, kiro workspace/legacy chats, opencode and hermes state DBs), so transcript_path is returned in session search.
  • Codex Message facts now carry the turn_context model in effect for each record (with mid-session switches), closing the last per-message model gap; all other hosts already read their native per-message model.
  • Host adapters pass the source path and in-effect model through normalize context; Session facts ride every conversational record so the fields survive the non-conversational-record projection skip and the optional-field reconcile.

Motivation

Refs #2746 items 5 and 10: the canonical projection never set SessionRecord::transcript_path, and Codex turn_context.model was never attributed to messages (message.model was None). Read-side plumbing (contract fields, search SELECT, SDK mappers) already existed — this fills the write-side gap.

Changes

  • tracedecay-capture: CodexObservationContext (replaces CodexObservationLocation) adds transcript_path + model; Session fact carries the path on every record; Message facts fall back to the context model. Claude/kimi/opencode/vibe/pi/kiro normalizers accept a transcript path and emit a Session fact carrying it (inserted ahead of other facts so it wins canonical_session_fields' first-fact read).
  • tracedecay-sessions: codex CodexContextState tracks model alongside cwd in both the forward observe_context_record path and the backward scan_prior resume walk (stop condition = both fields found). Host drivers pass the real source path; kimi scopes the path to the main-agent wire.jsonl only (sub-agent wires would reconcile-conflict); cline scopes to the api-history stream only.
  • tracedecay-store: canonical_projection keeps the claude Session-fact masking but passes transcript_path through via a masked CanonicalSessionFields.
  • Tests: new transcript_ingest_suite coverage per host asserts session.transcript_path and codex per-message model on real fixture transcripts (incl. a gpt-5.5→gpt-5.6 mid-session switch); new assert_sanitized_path_text_eq helper compares stored paths modulo admission sanitizer redaction (UUID-bearing filenames always redact); updated serialized-payload goldens (pi, opencode, claude observation) for the new field.

Test plan

  • cargo test -p tracedecay-capture -p tracedecay-store -p tracedecay-sessions passes (new + existing)
  • cargo test -p tracedecay --features test-helpers --test transcript_ingest_suite --test session_suite passes (incl. the 3 tests that need the tracedecay CLI binary, built here)
  • Exact-gate: bash scripts/require-exact-test.sh cargo test -p <crate> ... -- --exact run for every new/modified test (14 total)
  • cargo fmt --all -- --check clean; cargo clippy -p tracedecay-capture -p tracedecay-store -p tracedecay-sessions -p tracedecay --all-targets --features test-helpers -- -D warnings clean
  • python3 scripts/linux-test-partitions.py check — 137 targets, each in one partition
  • node scripts/lint-commit-range.mjs --repository . origin/master HEAD clean

Checklist

  • CHANGELOG.md updated (under [Unreleased] if no version bump)
  • No secrets, credentials, or .env files included
  • Breaking changes documented (if any) — none: adds fields that were already in the contract/SDK surfaces

Link to Devin session: https://app.devin.ai/sessions/34b4d5fc2dac426b998cbb1b9fda0b25
Open in Devin Desktop: https://app.devin.ai/desktop/session/34b4d5fc2dac426b998cbb1b9fda0b25?variant=devin
Requested by: @ScriptedAlchemy


Devin Review

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@changeset-bot

changeset-bot Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: e6724ea

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Devin Review found 3 potential issues.

Devin Review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔴 Relocated transcripts block new session messages

When a transcript moves, transcript_path changes on new records while the session retains its old path. Session reconciliation rejects both paths, so new messages remain unprojected.

(Refers to this code)

Learn more

A session row reconciles fields from every projected observation. reconcile_optional rejects two different non-null transcript paths. The new path on every record makes a renamed or relocated transcript disagree with the path already stored for that session. The projection then fails for newly appended messages, even though the source scan admitted them.

Example: A Claude session initially projects from /home/a/session.jsonl. Moving its transcript to /home/b/session.jsonl and appending a message makes the new observation project /home/b/session.jsonl; reconciliation conflicts with /home/a/session.jsonl, so the appended message does not appear in search.

Recommended fix: Give transcript-path changes an explicit relocation policy in the session reconciliation and source-transition paths. Preserve idempotence when the old and new files overlap, and test a move followed by an appended message.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Analysis — the conflict is real but the contract is deliberate and pre-existing on master; this PR only gives it a value to observe:

  • reconcile_optional treats two different non-null values as a SessionReconcileConflict → SessionOutputCollision — for transcript_path that rule was already wired at state.rs:1343 on master (git diff shows this file is untouched by the PR). The pinned test session_reconcile_conflict_names_the_field_without_exposing_its_value asserts "different transcript identities must not merge": divergent paths are read as evidence that two distinct sessions share an id, and the merge fails closed rather than silently re-homing a session.
  • The identical hazard already exists on master for project_path/location_path: any host whose location fact changes mid-stream conflicts the same way. What's new is only exposure — transcript_path was always None, so the arm never fired.
  • Reachability is narrower than the example suggests: for claude the filename is the session id, so a moved file usually implies a new session id (no collision); codex rollouts under sessions/ are append-only and resume forks to a new file + new id. The dangerous case is a path change under a stable session id — real for hosts with relocatable stores (e.g. pi agent-dir relocation, archived rollouts) but an edge, not the common path.
  • The recommended fix (relocation policy: keep-first / last-wins / re-home, with move-then-append idempotence) changes the reconcile invariant and its pinning test — that's product intent, so I'm keeping the contract unchanged in this PR and flagging the decision to the owner rather than unilaterally weakening identity reconciliation. Happy to implement whichever policy is chosen as follow-up.

facts.push(session_fact(Some(header.cwd), None, timestamp));
facts.push(session_fact(
Some(header.cwd),
transcript_path.map(str::to_owned),

@devin-ai-integration devin-ai-integration Bot Oct 4, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🟡 UUID transcripts return unusable file paths

For UUID-named transcripts, transcript_path reaches the session row with its filename redacted. Callers cannot open the returned path to read the transcript.

Learn more

The new session path goes into a canonical observation before observation sanitization. The sanitizer replaces high-entropy filename spans with [TraceDecay redacted: high-entropy token], then the projector copies the sanitized string to SessionRecord.transcript_path. The added integration assertions explicitly accept this substitution for Pi and Kiro. A returned string with that substitution no longer names the source file.

Example: A Pi source named 2026-09-25T16-00-00-000Z_5f0c2a8e-3b1d-4c7e-9a2f-6d8e1b4c7a90.jsonl projects a path with the UUID replaced by a redaction marker; opening that returned path fails.

Recommended fix: Define whether transcript_path is a usable file locator or sanitized display text. If it is a locator, use an authorized source-path lookup keyed by durable source identity instead of deriving it from sanitized observation payloads; cover UUID-bearing filenames in an end-to-end read test.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Analysis — this is a real semantic boundary worth naming, but the redaction is a deliberate pre-existing contract, not an artifact of this change:

  • RecordSanitizerV1::observation_v1 sanitizes the whole canonical envelope at admission, before anything persists. Stored text is always the sanitized form — the ingest suite asserts modulo-redaction via assert_sanitized_path_text_eq precisely so the privacy layer is never weakened to make a test pass.
  • For the UUID-named file hosts covered here (pi, claude, codex, kiro), the redacted component is recoverable: session_id is the file stem, so dirname(transcript_path) + session_id + extension reconstructs the real path (e.g. pi 5f0c2a8e-… → …/2026-09-25T16-00-00-000Z_5f0c2a8e-….jsonl). For opencode/hermes the path is a state DB that survives sanitization verbatim.
  • So transcript_path today is exact for non-UUID sources and a stem-redacted locator for UUID sources. Whether the row should instead expose an authorized source-identity lookup (unsanitized locator) or stay sanitized display text is a product decision — the field never existed before, so either choice establishes the contract rather than weakening one. I've flagged this to the owner; happy to implement the source-identity lookup if we want a true locator.

Comment thread crates/tracedecay-capture/src/pi.rs Outdated
ScriptedAlchemy and others added 4 commits October 4, 2026 21:46
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1791148933-session-transcript-path branch from a6864cf to ba36efe Compare October 4, 2026 21:50
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant