Skip to content

[fix] Last-message-only reconstruction: fix ordering, config wiring, and robustness - #5500

Closed
ardaerzin wants to merge 5 commits into
feat/sessions-last-message-onlyfrom
fix/sessions-reconstruction-qa
Closed

[fix] Last-message-only reconstruction: fix ordering, config wiring, and robustness#5500
ardaerzin wants to merge 5 commits into
feat/sessions-last-message-onlyfrom
fix/sessions-reconstruction-qa

Conversation

@ardaerzin

Copy link
Copy Markdown
Contributor

Context

Stacked on #5486. The differential QA on that PR (comment) found four defects that a pass/fail gate can't see, plus CodeRabbit flagged the same config bug and a couple of robustness gaps. This PR fixes them. All four feature flags stay off by default, so nothing changes unless they're set.

What was broken and how it's fixed

1 + 2. Reconstruction ran in the wrong place (one root cause, two symptoms).
Reconstruction happened inside runTurn, but two things that read request.messages already ran outside it: the turn's own prompt was persisted first (server.ts), and the keep-alive history fingerprint was computed on the inbound (minimal) history.

  • On a fresh turn the record log already held the current prompt, so reconstruction returned it and the code appended the inbound tail on top. The prompt appeared twice ([reconstruct] records=1 priorMessages=1 inbound=1). This fired even in full-history mode, because turn one always carries exactly one message.
  • Every warm continuation fingerprinted the empty prior-conversation and mismatched what the previous park predicted, so it evicted to cold on every turn.

Fix: hoist reconstruction to the session-owned run handler, before the prompt persist and before the fingerprint. At that point the record log holds only prior turns, so rebuilt-history + inbound-tail is the full conversation exactly once, and the fingerprint sees the same full history a full-history client would have sent (so warm continuations match). plan is then built from the full request, so runTurn no longer recomputes the transcript. The guard is also tightened to exactly one trailing user message, so an empty or assistant-only inbound (an approval resume) never rebuilds.

Before: messages: [{user: "PONG"}, {user: "PONG"}] on turn one; warm sessions evicted to cold every turn.
After: turn one sends one message; warm continuations stay warm.

3. Smart truncation dropped the record instead of truncating it.
SessionsConfig was defined in env.py but never attached to AgentaConfig, so env.agenta.sessions.records.smart_truncation raised AttributeError inside the publish try/except, which logged and returned False. With AGENTA_RECORDS_SMART_TRUNCATION on, an over-64KB record was dropped entirely — worse than the legacy path it replaced. Now records are the only copy of the conversation, so that's lost context. Fix: wire the sub-namespace onto AgentaConfig and add a regression guard for the config path.

4. The dropped-record signal was never consumed.
takePersistFailures had no caller: in durable mode a dropped record was counted into a per-session map that nothing read, so the signal was lost and the map grew unread. Fix: consume it at the turn-end drain (flush) — warn when the durable log is incomplete and clear the counter.

Records fetch could hang the turn (CodeRabbit).
fetchSessionRecords sits on the turn's critical path (the prompt waits on it). Added an AbortSignal.timeout (env-tunable, 5s default) so a stalled query fails fast to the inbound-history fallback instead of waiting on Undici's long request-level timeout.

Flags couldn't reach the runner.
The runner service has no env_file by design, so the session flags never reached it and the feature could only be toggled in unit tests. Forwarded them explicitly in the oss/ee dev compose runner env; all default OFF.

Tests

  • Runner: 1266 unit tests pass (typecheck clean). Added guard regressions for empty / assistant-only inbound and a flush-consumer test.
  • API: sessions suite passes; added a regression test that env.agenta.sessions.records.smart_truncation resolves.
  • Not live-QA'd yet (I can't run the stack). The ordering and fingerprint reasoning is verified against the code, but the warm-continuation and duplicate-prompt fixes want a real run to confirm.

What to QA

Needs all four flags on. Turn 1 "my name is Arda", turn 2 "what's my name?".

  • The /invoke body carries a single message and the runner logs [reconstruct] … inbound=1 with no duplicated prompt (turn 1 should not reconstruct at all — records are empty before the prompt is persisted).
  • Multi-turn warm session: no [keepalive] mismatch (history) … evict + cold lines; turn 2+ latency stays at warm levels.
  • A tool approval mid-conversation, approve, reload: the run resumes and the approval binds (the resume's assistant-only tail must not trigger a rebuild).
  • Regression with flags on: a normal single-turn chat, streaming, replay all look unchanged.

Note

The one QA finding not addressed here is the FE gating (agentRequest.ts): last-message-only should gate on a server-confirmed durable-history state rather than a non-empty session id, so a session with no records retains full history. That's a larger FE/BE contract change and belongs in its own PR.

…gerprinting

Reconstruction ran inside runTurn, but two things that read request.messages already ran
outside it: the turn's own prompt was persisted first, and the keep-alive history
fingerprint was computed on the inbound (minimal) history. So on a fresh turn the record
log already held the current prompt (reconstruction returned it, then the inbound tail was
appended -> the prompt appeared twice), and every warm continuation mismatched the
fingerprint the previous park predicted, evicting to cold.

Hoist reconstruction to the session-owned run handler, before the prompt persist and before
the fingerprint. At that point the log holds only prior turns (rebuilt history + inbound tail
is the full conversation exactly once), and the fingerprint sees the same full history a
full-history client would send, so warm continuations match. plan is then built from the full
request, so runTurn no longer recomputes the transcript. Also tighten the reconstruct guard to
exactly one trailing user message (empty / assistant-only inbound must not rebuild).
fetchSessionRecords sits on the turn's critical path (the prompt waits on it), so add an
AbortSignal.timeout (env-tunable) to fail fast to the inbound-history fallback instead of
hanging on Undici's long request-level timeout.

takePersistFailures had no caller: the per-session drop count was written in durable mode and
never read, so the signal was lost and the map grew unread. Consume it at the turn-end drain
(flush) — warn when the durable log is incomplete and clear the counter. Add guard regressions
for empty / assistant-only inbound and a flush-consumer test.
…esolves

The publish path reads env.agenta.sessions.records.smart_truncation, but SessionsConfig was
defined and never wired onto AgentaConfig. With AGENTA_RECORDS_SMART_TRUNCATION on, that read
raised AttributeError inside the publish try/except, which logged and returned False — so an
over-64KB record was dropped entirely instead of structure-truncated, losing conversation
context now that records are the source of truth. Wire the sub-namespace and add a regression
guard for the config path.
The runner service has no env_file by design, so AGENTA_SESSIONS_RECONSTRUCT /
AGENTA_RECORDS_DURABLE / AGENTA_RECORDS_INGEST_MAX_RETRIES never reached it and the feature
could only be toggled in unit tests. Forward them explicitly in the oss/ee dev compose runner
env; all default OFF (empty -> disabled), so nothing changes unless set.
@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jul 24, 2026
@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jul 24, 2026 11:40pm

Request Review

@dosubot dosubot Bot added the Bug Report Something isn't working label Jul 24, 2026
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 525b2fd3-a2fe-4bba-acc1-19b359cb2279

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sessions-reconstruction-qa

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…uction seam

Reconstruction from records supplies model context only in the cold-evicted case; warm and
cold-not-evicted turns resume natively. On warm turns reconstruction runs only to realign the
keep-alive fingerprint. Also record that the seam moved to the run handler (before persist and
fingerprint).
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Jul 24, 2026
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Status Destroyed (PR closed)

Updated at 2026-07-24T23:59:22.774Z

@ardaerzin

Copy link
Copy Markdown
Contributor Author

Closing as a duplicate. I built this independently before seeing Mahmoud's QA notes — the same fixes are already up as focused PRs: #5488 (compose flags), #5489 (double prompt), #5490 (warm eviction), #5491 (SessionsConfig). Those are the canonical ones; this one restructured the reconstruction seam more broadly and overlaps them. The only bits not covered there are minor (a records-query fetch timeout and consuming takePersistFailures at turn end) — leaving those for a separate call rather than piling on.

@ardaerzin ardaerzin closed this Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Report Something isn't working size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant