Skip to content

feat(runner): rebase JP's session turns/streams runner onto the decomposed runner#5376

Open
mmabrouk wants to merge 3 commits into
sessions-rebase/backendfrom
sessions-rebase/runner
Open

feat(runner): rebase JP's session turns/streams runner onto the decomposed runner#5376
mmabrouk wants to merge 3 commits into
sessions-rebase/backendfrom
sessions-rebase/runner

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Jul 18, 2026

Copy link
Copy Markdown
Member

What this is

The runner third of JP's #5363, rebased onto v0.105.5 and ported into the post-decomposition module layout (#5369). JP wrote these edits against the old monolithic sandbox_agent.ts (2,477 lines); main has since split it into environment.ts / run-turn.ts / runtime-contracts.ts / … behind a 44-line facade. This PR re-homes JP's hunks into those modules.

Stacked on the backend PR (#5375); the base is sessions-rebase/backend, so this diff shows only the runner delta.

How JP's monolith edits were re-homed

  • The SandboxAgentDeps interface change (rename syncHarnessSessionDurableappendSessionTurn; drop the write/clear sandbox-pointer deps) → runtime-contracts.ts.
  • The two acquireEnvironment branches (the reconnect-failure path and the post-hydrate pointer write, both now append-only with no pre-turn pointer PUT) → environment.ts.
  • The turn-completion sync (syncHarnessSessionDurable replaced by appendSessionTurn with the streamId gate, workflow references, sandbox id, and trace id) → run-turn.ts.
  • The sandbox-reconnect.ts and session-continuity-durable.ts rewrites apply as-is. protocol.ts, sessions/alive.ts, sessions/persist.ts, version.ts, and the tests are JP's versions verbatim.

Ported from feat/sessions-extensions@1532ec7fe5 (JP); attribution is in the commit.

Reconciliations with the release

  • server.ts — kept main's session-identity / session-pool import split (from the decomposition) and applied JP's watchdog changes: startAliveWatchdog is now awaited and takes an onInterrupted callback that aborts the controller (W7.4), request.streamId is threaded from the heartbeat, and buildPersistingEmitter gets turnId + span_id.
  • tracing/otel.ts, package.json, pnpm-lock.yaml — kept main's. The OTel 2.x bump JP carried is already in main, and main additionally has the [5357] fix(runner): recognize client-tool settles as a resume in cold replay #5364 trace-id-on-done feature that JP's branch predates. Keeping main's preserves both behaviors, so no port was needed.

Testing

  • pnpm run typecheck: clean.
  • pnpm test: 1202/1202 pass (77 files; the +10 over main's 1192 are JP's new session tests).
  • pnpm run build:extension: ok.

For JP

Mechanical rebase of your #5363 runner delta onto the decomposed runner, on a branch I own (sessions-rebase/runner). Please review the re-homing and the two reconciliations — I did not touch your branches.

https://claude.ai/code/session_01DnWRxU3dCJ11hgDidm26vW


QA finding fixed: record-ingest 422 (span_id typed as UUID)

QA found the runner's record ingest failed 100% with HTTP 422: the API typed span_id as UUID, while the runner correctly sends the real 16-hex OTel span id (runContext.trace.span_id). See debug/qa-sessions-rebase/FINDING-record-ingest-422.md. The API-side fix lands in the backend lane (#5375); there is no runner behavior change — the runner keeps sending the real span id verbatim.

This lane adds a persist contract test (commit df38c4a709) whose fetch stub enforces the API contract — span_id must be a 16-hex OTel span id or the stub returns 422 — so a regression to a non-span-shaped span_id fails the test instead of silently dropping records. The old stub returned 200 for any body and hid the mismatch; the turn/span tagging test now uses a real 16-hex span id. pnpm test is now 1204/1204 (was 1202).


Post-QA amendment (Step 1 of the approvals-incident fixes):

Two amendments landed on these lanes after live QA. First, the runner computed turn_index once per environment acquire, so every warm turn re-inserted the same index and the append 500ed (ix_session_turns_project_id_session_id_turn_index); the index is now computed at turn start from the shared continuity store, which also fixes the two-pooled-environments case. We confirmed the intended semantics: turn_index is a true conversation-turn counter, and the code now carries that invariant as a comment. Second, the turns DAO now maps the duplicate-key IntegrityError to EntityCreationConflict, so a duplicate append reads as 409 instead of an anonymous 500. The dev database's session_turns table was wiped because every row written while the bug was live carried acquire-counter indexes; sessions rebuild their rows on their next completed turn.

@vercel

vercel Bot commented Jul 18, 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 19, 2026 6:19pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 18, 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: 0c101612-540c-48f2-b815-165eb50fd0c8

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
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.48% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly summarizes the main change: rebasing the session turns/streams runner onto the decomposed runner.
Description check ✅ Passed The description is directly related to the changeset and explains the re-homing, reconciliation, and testing of the runner updates.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sessions-rebase/runner

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
services/runner/src/sessions/alive.ts (1)

198-218: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Serialize heartbeats with release().

The async interval permits overlapping requests, and release() only clears future ticks. An in-flight is_running: true heartbeat can therefore complete after the final is_running: false heartbeat and leave the stream marked running. Track and await the active beat before sending the release heartbeat, or replace the interval with a serialized loop.

services/runner/src/engines/sandbox_agent/run-turn.ts (1)

543-574: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Advance and persist completed turns without an agentSessionId.

The outer guard suppresses both counter advancement and the append when a harness omits its optional native session ID. This can reuse turn_index and leave durable sandbox/continuity state stale.

Record the harness session conditionally, but always advance and append a successful session-owned turn.

Proposed fix
     if (
       stopReason !== "paused" &&
       env.continuityTurnIndex !== undefined &&
-      sessionId &&
-      env.session?.agentSessionId
+      sessionId
     ) {
-      (deps.sessionContinuityStore ?? sessionContinuityStore).record(
-        sessionId,
-        plan.harness,
-        env.session.agentSessionId,
-        env.continuityTurnIndex,
-      );
+      const continuityStore =
+        deps.sessionContinuityStore ?? sessionContinuityStore;
+      const agentSessionId = env.session?.agentSessionId;
+      if (agentSessionId) {
+        continuityStore.record(
+          sessionId,
+          plan.harness,
+          agentSessionId,
+          env.continuityTurnIndex,
+        );
+      } else {
+        continuityStore.restoreLatestTurn(
+          sessionId,
+          env.continuityTurnIndex,
+        );
+      }
+
       const syncCred = runCredential(request);
       if (syncCred && request.streamId) {
         const workflowRefs = buildWorkflowReferences(
           request.runContext?.workflow,
         );
         void (deps.appendSessionTurn ?? appendSessionTurn)(
           sessionId,
           plan.harness,
           env.continuityTurnIndex,
           {
             streamId: request.streamId,
-            agentSessionId: env.session.agentSessionId,
+            agentSessionId,
             sandboxId: env.sandbox?.sandboxId,
🧹 Nitpick comments (2)
services/runner/tests/unit/server.test.ts (1)

46-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract duplicated listen helper to tests/utils/.

The listen test helper is duplicated across these test files. As per coding guidelines, shared test helpers and fixtures should be placed under tests/utils/. Consider extracting this function into a shared utility module to keep the test environment setup DRY.

  • services/runner/tests/unit/server.test.ts#L46-L52: Remove this function and import it from tests/utils/.
  • services/runner/tests/acceptance/server-contract.test.ts#L44-L49: Remove this function and use the extracted helper.

Source: Coding guidelines

services/runner/tests/unit/session-alive-interrupt.test.ts (1)

55-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise interruption through the interval heartbeat.

This starts a second watchdog with an already-interrupted first response; it does not test the stated “next beat reports cancellation” path. Use fake timers, start with true, switch to false, advance one interval, and assert the callback fires once.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d0a8f85c-70dc-4169-b2ee-f458e5222027

📥 Commits

Reviewing files that changed from the base of the PR and between 386c63e and 28ce10a.

📒 Files selected for processing (19)
  • services/runner/src/engines/sandbox_agent/environment.ts
  • services/runner/src/engines/sandbox_agent/run-turn.ts
  • services/runner/src/engines/sandbox_agent/runtime-contracts.ts
  • services/runner/src/engines/sandbox_agent/sandbox-reconnect.ts
  • services/runner/src/engines/sandbox_agent/session-continuity-durable.ts
  • services/runner/src/protocol.ts
  • services/runner/src/server.ts
  • services/runner/src/sessions/alive.ts
  • services/runner/src/sessions/persist.ts
  • services/runner/src/version.ts
  • services/runner/tests/acceptance/server-contract.test.ts
  • services/runner/tests/integration/server-smoke.test.ts
  • services/runner/tests/unit/sandbox-lifecycle.test.ts
  • services/runner/tests/unit/sandbox-reconnect.test.ts
  • services/runner/tests/unit/server.test.ts
  • services/runner/tests/unit/session-alive-interrupt.test.ts
  • services/runner/tests/unit/session-alive.test.ts
  • services/runner/tests/unit/session-continuity-durable.test.ts
  • services/runner/tests/unit/session-persist.test.ts

Comment on lines +923 to +925
// The heartbeat response already carries the session_streams row id — free, no extra
// round-trip. Thread it onto the request so the engine's turn-append write has it.
request.streamId = watchdog.streamId();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Propagate stream IDs discovered after the first heartbeat.

This snapshots the accessor only once. After a fail-open first heartbeat, a later successful beat updates the watchdog but leaves request.streamId undefined, causing run-turn.ts to skip the durable turn append. Update the request through a watchdog callback, or read the latest ID at the append boundary.

Comment on lines +194 to +196
// Await the FIRST beat so streamId is ready before the caller starts the turn.
const first = await sendHeartbeat(sessionId, turnId, credential);
handleBeat(first);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound the first heartbeat before awaiting it on the run-start path.

sendHeartbeat() has no timeout, so a stalled API response now prevents the agent turn from starting indefinitely. Add a finite request deadline while retaining the existing fail-open result.

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reviewed the full runner delta (19 files) line by line against the decomposition. Verdict: the re-homing is faithful and the design is genuinely better than what it replaces.

What I verified:

  • The three monolith hunks landed in the right modules: the SandboxAgentDeps narrowing in runtime-contracts.ts (write/clear pointer deps correctly deleted, read side kept for the reconnect ladder), the two acquireEnvironment branches in environment.ts, and the turn-completion append in run-turn.ts.
  • The pointer-write deletion is sound reasoning, not just a port: with append-only turns, the fresh sandbox's own turn row supersedes the dead pointer by ordering, so the old staleness guard genuinely dissolves. The explanatory comments left in place are exactly right.
  • server.ts keeps the decomposition's import split while adding the real behavioral win of this PR: startAliveWatchdog is awaited, and onInterrupted wires the control-plane cancel to controller.abort(), so a kill/steer against a session finally reaches the in-flight run. The heartbeat's stream_id rides the existing round-trip, no extra call.
  • Tests: 1204/1204 including the new persist contract test whose stub enforces the API's 16-hex span-id contract (the exact seam the live QA caught).

One non-blocking note (inline): the fire-and-forget .catch(() => {}) on the turn append is fine only as long as sessions/persist.ts logs terminal failure after its retries; it does today, keep it that way, because a silent drop here is precisely how the 422 stayed invisible.

},
{ authorization: syncCred, log: logger },
);
).catch(() => {});

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The swallow here is acceptable only because sessions/persist.ts logs the terminal failure after its retries. Worth a one-line comment saying so, since this exact silent-drop shape is how the span-id 422 went unnoticed until live QA.

@mmabrouk
mmabrouk force-pushed the sessions-rebase/backend branch from 9e2044c to 3f975df Compare July 18, 2026 17:42
@mmabrouk
mmabrouk force-pushed the sessions-rebase/runner branch from df38c4a to 3e0edee Compare July 18, 2026 17:42
@mmabrouk
mmabrouk marked this pull request as ready for review July 18, 2026 19:10
@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Jul 18, 2026
@mmabrouk
mmabrouk requested a review from jp-agenta July 18, 2026 19:10
@mmabrouk
mmabrouk force-pushed the sessions-rebase/backend branch from 3f975df to b90e9e8 Compare July 18, 2026 21:05
@mmabrouk
mmabrouk force-pushed the sessions-rebase/runner branch from 3e0edee to 46f5f6e Compare July 18, 2026 21:06
@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-production-3bee.up.railway.app/w
Project agenta-oss-pr-5376
Image tag pr-5376-bbadcd2
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-07-19T18:28:18.233Z

mmabrouk added 3 commits July 19, 2026 20:14
Rebases the runner third of JP's feat/sessions-extensions (PR #5363) onto
current main, ported into the post-decomposition module layout (PR #5369).

JP wrote these against the old monolithic sandbox_agent.ts; they are re-homed
by region into the decomposed modules:
- The SandboxAgentDeps interface change (syncHarnessSessionDurable renamed to
  appendSessionTurn; the write/clear sandbox-pointer deps dropped) goes into
  runtime-contracts.ts.
- The two acquireEnvironment branches (the reconnect-failure path and the
  post-hydrate pointer write, both now append-only with no pre-turn pointer PUT)
  go into environment.ts.
- The turn-completion sync (syncHarnessSessionDurable replaced by appendSessionTurn
  with the streamId gate, workflow references, sandbox id, and trace id) goes into
  run-turn.ts.
The sandbox-reconnect.ts and session-continuity-durable.ts rewrites apply as-is.
protocol.ts, sessions/alive.ts, sessions/persist.ts, version.ts, and the tests
are JP's versions verbatim.

Ported from feat/sessions-extensions@1532ec7fe5 (JP).

Reconciliations with the release main:
- server.ts: kept main's session-identity / session-pool import split from the
  decomposition and applied JP's watchdog changes (startAliveWatchdog is now
  awaited and takes an onInterrupted callback that aborts the controller,
  request.streamId is threaded from the heartbeat, and buildPersistingEmitter
  gets turnId and span_id).
- tracing/otel.ts, package.json, pnpm-lock.yaml: kept main's. The OTel 2.x bump
  JP carried is already in main, and main additionally has the #5364
  trace-id-on-done feature that JP's branch predates.

Verified: pnpm run typecheck clean, pnpm test 1202/1202 pass, build:extension ok.

Claude-Session: https://claude.ai/code/session_01DnWRxU3dCJ11hgDidm26vW
…persist test

Add a persist test whose fetch stub enforces the API contract (span_id must be a
16-hex OTel span id, else 422), so a regression to a non-span-shaped span_id fails
here instead of silently dropping records. The old stub returned 200 for any body
and hid the mismatch. Also switch the turn/span tagging test to a real 16-hex span
id.

Claude-Session: https://claude.ai/code/session_01DnWRxU3dCJ11hgDidm26vW
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant