Skip to content

fix: preserve pre-turn spawn identity - #3

Merged
EtanHey merged 2 commits into
mainfrom
t3layer/p2-native-adapter
Jul 31, 2026
Merged

fix: preserve pre-turn spawn identity#3
EtanHey merged 2 commits into
mainfrom
t3layer/p2-native-adapter

Conversation

@EtanHey

@EtanHey EtanHey commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

  • preserve the newest native user-message ID before T3 assigns a provider turn ID
  • accept an acknowledged spawn only when every available thread/project/message identity agrees
  • add regressions for the real post-dispatch projection window and conflicting identities

Why

A live production-adapter spawn was accepted and executed by T3, but the immediate read model had latestTurn: null while the initial user message was already durable. The facade rejected that valid intermediate snapshot as transport_unavailable.

Verification

  • Bun 1.3.11: 126 tests passed, 0 failed, 316 assertions
  • typecheck passed
  • changed-file Prettier check passed
  • diff check and Gitleaks passed
  • independent adversarial review: SHIP_SLICE
  • local CodeRabbit committed-diff review: 0 findings

Live gate

This fixes the boundary found by the real isolated T3 RPC proof. The full two-turn live proof will be rerun after merge; this PR does not claim that gate is complete.


Note

Medium Risk
Changes fail-closed identity checks on spawn and ambiguous send recovery; incorrect matching could accept wrong dispatches or reject valid ones, but scope is limited to reconciliation paths with new regression tests.

Overview
Fixes false transport_unavailable when spawn succeeds but the read model still has latestTurn: null while the initial user message is already visible.

The native adapter now exposes latestUserMessageId on NativeThreadSnapshot (newest user message from orchestration detail, including before a provider turn exists). Spawn and ambiguous-send reconciliation use matchesMessageIdentity, which treats latestUserMessageId and latestTurn.userMessageId as identity sources: at least one must be present and any present values must match the dispatched messageId. Conflicting sources are rejected instead of accepted.

Tests cover the pre-turn spawn window, conflicting identity on send, and adapter projection for unassigned turns.

Reviewed by Cursor Bugbot for commit 1adf16e. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Bug Fixes
    • Improved thread spawning for sessions that begin before a provider-assigned turn exists.
    • Thread identity validation now consistently recognizes matching initial user messages.
    • Prevented inconsistent message identities from being accepted during spawn validation.
    • Initial thread state is now preserved accurately, including the latest user message ID and the absence of a latest turn when applicable.

Note

Preserve pre-turn spawn identity using latestUserMessageId in thread snapshots

  • Adds latestUserMessageId to NativeThreadSnapshot by scanning messages in reverse for the most recent user-role message, independent of whether a provider turn has been assigned.
  • Introduces matchesMessageIdentity helper in facade.ts that checks both latestUserMessageId and latestTurn.userMessageId, requiring all present sources to agree.
  • Replaces direct latestTurn?.userMessageId comparisons in spawn and send reconciliation with matchesMessageIdentity, so identity is accepted when either source matches and fails closed when sources conflict.
  • Behavioral Change: ambiguous-dispatch recovery now rejects when latestUserMessageId and latestTurn.userMessageId are both present but disagree, where previously only latestTurn.userMessageId was checked.

Macroscope summarized 1adf16e.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds latestUserMessageId to native thread snapshots. Native snapshot conversion derives it from thread history. Spawn identity validation accepts matching IDs from the snapshot or latest turn and rejects missing or inconsistent identities. Tests cover starting sessions without assigned turns.

Changes

Initial user message identity

Layer / File(s) Summary
Snapshot identity contract and validation
src/facade.ts
NativeThreadSnapshot now exposes an optional latestUserMessageId. Spawn validation checks all observed message IDs and requires them to match the requested ID.
Native snapshot message projection
src/nativeRuntime.ts
Snapshot conversion finds the latest user-authored message across the thread history and includes its ID when available.
Initial-state regression coverage
test/facade.spawn.test.ts, test/native-runtime-adapter.test.ts
Tests cover matching identities, inconsistent identities, and starting sessions without a provider-assigned turn.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • EtanHey/t3layer#1: Directly introduced the related snapshot contract and spawn identity validation.
  • EtanHey/t3layer#2: Also modifies native runtime snapshot conversion.

Poem

A rabbit checks the message trail,
Before the turn IDs set sail.
The starting state stays clear and bright,
Matching IDs make records right.
No latest turn? The ID remains—
Safe in the snapshot, free of pains.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: preserving spawn identity before a provider turn exists.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3layer/p2-native-adapter

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.

@EtanHey

EtanHey commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@EtanHey

EtanHey commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@EtanHey

EtanHey commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

P2 live gate is now PASS before merge, superseding the earlier pending statement in the PR body.

  • Exact compatible T3 source: 9ed5c80046179f6a8299263ad542ef178d1c465b
  • T3Layer head: 524dec3d1ffda650bad2f3b07e6a0bf91f81d579
  • Strict public RPC descriptor matched t3-rpc-v1:effect@4.0.0-beta.102:json-websocket
  • Real native flow completed: spawn → terminal P2_FIRST_TURN_OK → send → terminal P2_FOLLOW_UP_OK
  • Thread: b9d0901f-9f01-4e27-bced-beff6b891913
  • Follow-up receipt sequence 48; terminal sequence 54; assistant messages persisted non-streaming
  • Ephemeral auth session was revoked at 2026-07-31T04:44:02.964Z
  • Evidence report confirms credentialMaterialPersisted: false

The durable native user messages intentionally have turn_id = NULL; the hardened adapter and proof bind their exact IDs through latestUserMessageId, while completion/assistant evidence remains latest-turn-bound. Final independent Codex/Terra adversarial evaluation is running now.

Comment thread src/facade.ts
Comment on lines +273 to +285
function matchesMessageIdentity(
snapshot: NativeThreadSnapshot,
messageId: string,
): boolean {
const observedMessageIds = [
snapshot.latestUserMessageId,
snapshot.latestTurn?.userMessageId,
].filter((candidate): candidate is string => candidate !== undefined);
return (
observedMessageIds.length > 0 &&
observedMessageIds.every((candidate) => candidate === messageId)
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High src/facade.ts:273

matchesMessageIdentity requires latestUserMessageId and latestTurn.userMessageId to be equal, but during ambiguous send recovery the new user message can be durable while latestTurn still reflects the previous completed turn. In that projection window latestUserMessageId matches the retried command but latestTurn.userMessageId identifies the prior turn, so the .every() check returns false and rejects a dispatch that actually succeeded, surfacing the ambiguous error instead of a recovered receipt. Consider not treating the prior turn's userMessageId as conflicting evidence for send — for example, matching when any candidate equals messageId rather than requiring all to agree.

Suggested change
function matchesMessageIdentity(
snapshot: NativeThreadSnapshot,
messageId: string,
): boolean {
const observedMessageIds = [
snapshot.latestUserMessageId,
snapshot.latestTurn?.userMessageId,
].filter((candidate): candidate is string => candidate !== undefined);
return (
observedMessageIds.length > 0 &&
observedMessageIds.every((candidate) => candidate === messageId)
);
}
function matchesMessageIdentity(
snapshot: NativeThreadSnapshot,
messageId: string,
): boolean {
const observedMessageIds = [
snapshot.latestUserMessageId,
snapshot.latestTurn?.userMessageId,
].filter((candidate): candidate is string => candidate !== undefined);
return observedMessageIds.some((candidate) => candidate === messageId);
}
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/facade.ts around lines 273-285:

`matchesMessageIdentity` requires `latestUserMessageId` and `latestTurn.userMessageId` to be equal, but during ambiguous `send` recovery the new user message can be durable while `latestTurn` still reflects the previous completed turn. In that projection window `latestUserMessageId` matches the retried command but `latestTurn.userMessageId` identifies the prior turn, so the `.every()` check returns `false` and rejects a dispatch that actually succeeded, surfacing the ambiguous error instead of a recovered receipt. Consider not treating the prior turn's `userMessageId` as conflicting evidence for `send` — for example, matching when any candidate equals `messageId` rather than requiring all to agree.

@EtanHey

EtanHey commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

Follow-up to the first live receipt: the final adversarial evaluator found and blocked an ambiguous-send recovery edge case. It is now fixed test-first in 1adf16e1121c33bb9afc19953eee9fe91f1fbf47.

Fresh exact-head live gate is PASS:

  • Thread f534fce2-f01f-4295-a9e4-da5b37f7201a
  • First exact user ID 0dcb31b8-6148-426d-8e11-34af314da2dd, terminal sequence 69, marker P2_FIRST_TURN_OK
  • Follow-up exact user ID 445a9891-121d-42b9-9476-56795b8b005d, receipt sequence 73, terminal sequence 79, marker P2_FOLLOW_UP_OK
  • Both assistants persisted complete/non-streaming; native user turn_id remains intentionally NULL
  • One-use auth session a22bf5ba-0cec-489b-91c0-33766a89a9ce revoked at 2026-07-31T04:53:51.470Z
  • Evidence: status: passed, credentialMaterialPersisted: false

A fresh independent evaluator is reproducing its former failing ambiguous-send case against this exact head before merge.

@EtanHey
EtanHey merged commit 7f609fc into main Jul 31, 2026
3 checks passed
@EtanHey
EtanHey deleted the t3layer/p2-native-adapter branch August 3, 2026 10:04
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