Skip to content

Rebuild events stream for subscribe on paused turns - #1165

Merged
chiragjn merged 33 commits into
ha/emit-pausefrom
cj-turn-stream-rebuild
Nov 6, 2026
Merged

chiragjn merged 33 commits into
ha/emit-pausefrom
cj-turn-stream-rebuild

Conversation

@chiragjn

@chiragjn chiragjn commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Note

Medium Risk
Touches live turn streaming, subscribe API semantics, Redis stream layout, and memory ingestion selection—all user-visible if wrong. Changes are well-tested but span core runtime and persistence.

Overview
Turn subscribe and resume now use event-id cursors (from_event_id / after_event_id), deprecating integer after_sequence_number on new streams; the API, OpenAPI, generated SDK, and UI adapter are updated accordingly, and SSE no longer advertises built-in reconnection.

Server-side event streams were reworked: live Redis/in-memory logs use millisecond entry ids, can restore from the durable log when a paused turn’s stream is gone, validate resume cursors, and only attach legacy sequence numbers for old streams; create/subscribe paths and drain logic were updated, plus per-turn lock acquire timeout (TURN_LOCK_ACQUIRE_TIMEOUT_MS).

The assistant-ui runtime tracks subscribe cursors (including MCP auth and overlapping model messages), stores them on active streams, and passes them through subscribeToTurn; sequence numbers are optional, and tests cover the new replay/progress behavior.

Memory ingest now sends only closed exchanges, drops pause_timeout_ms and paused-range session scanning, and adjusts store queries and contracts; configuration/docs reflect the change.

Agent threads carry pendingMCPAuth when rehydrated so a rebuilt paused turn waits for user MCP continue without re-prompting; related session-store contract tests were extended.

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

@changeset-bot

changeset-bot Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 455dfb1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@truefoundry/trueforge Patch

Not sure what this means? Click here to learn what changesets are.

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

chiragjn and others added 14 commits October 26, 2026 13:19
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	packages/trueforge/src/runtime/activeTurns.ts
Stream migrations must run after the pause migrations dated 20261028.

Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve storeContractSuite import conflict by keeping TerminalTurnState
(stream rebuild) and TurnNonTerminalMetrics (pause emit), and adapt
HEAD-only last_turn_details tests to the pause-branch metrics fields.

Co-authored-by: Cursor <cursoragent@cursor.com>
Running and finished turns keep the existing stream or fall back to list events, and subscribe does not rebuild the turn.

Co-authored-by: Cursor <cursoragent@cursor.com>
* Subscribe and repair turn streams by event id.

New live and repaired streams use Redis ids {ulidMillisecond}-{n} and do not assign sequence numbers. Subscribe accepts from_event_id or after_event_id. after_sequence_number remains only for older integer streams.

* Fix event-id resume on legacy streams, repair, and reconnect.

Scan leftover sequence streams from the start, retry a distinct shared-id payload after a timestamp conflict, and match repair rows without depending on JSON key order. Replay mcp.auth_required, and do not treat that inclusive shell as reconnect progress.

* Minor fixes from self review

* Regenerate OpenAPI document and SDKs

* Simplify redis lua scripts

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: trueforge-dev-bot[bot] <trueforge-dev-bot[bot]@users.noreply.github.com>

@qodex-ai qodex-ai 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.

Inline findings below. See the walkthrough comment for the summary.

Comment thread packages/trueforge/src/apis/turns.ts Resolved
@qodex-ai

qodex-ai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Qodex review

Reviewed units u1. Documents closed-only ingestion and removal of pause_timeout_ms.

Effort: ●●●●● (5/5) · 3 findings: 1 major, 2 minor

Since last review: 0 addressed, 0 new.

Scope: the changes since the last review (delta: only what changed since the previously reviewed commit 2db18f8 is reviewed; findings are anchored on the whole change).

Qodex review threads: 3 open, 15 resolved by Qodex, 4 resolved by a reviewer.

3 findings still present from earlier reviews; inline comments not duplicated. See the collapsed detail.

Walkthrough

Notes

  • Code around the change is partial: 152 symbols cut at the packet byte cap.

Still present from earlier reviews

These findings were already reported on this PR and remain unresolved. The original inline comments are still attached at their first-seen lines.

What Qodex checked

Lenses (openqodex review with checked answers, all dimensions)

  • Bugs and correctness
  • Security (auth, injection, SSRF, secrets, headers)
  • Performance regressions
  • Maintainability
  • Style and consistency

Scanners

  • actionlint did not run
  • bandit did not run
  • brakeman did not run
  • cargo-deny did not run
  • checkov did not run
  • gitleaks 8.21.2
  • golangci did not run
  • hadolint did not run
  • kube-linter did not run
  • kubeconform did not run
  • osv-scanner 2.6.0
  • oxlint 1.86.0
  • rubocop did not run
  • ruff did not run
  • semgrep 1.94.0
  • shellcheck did not run
  • sqlfluff did not run
  • sqllint did not run
  • squawk did not run
  • tflint did not run
  • trivy did not run
  • zizmor did not run
  • 0 scanner candidates raised or dropped by the reviewer

Quality filters

  • PR description: minimal (present: length; missing: checklist, issue link)
  • Repo card last refreshed 8 days ago — may not reflect very recent code structure changes
  • Per-category confidence floor (bug/security/performance 0.7, maintainability 0.71, style 0.75)
  • Severity threshold from .qodex.yaml: minor
  • Path excludes (none configured in .qodex.yaml)
  • Disabled rules (none configured in .qodex.yaml)

Verification (M1b probes against PR preview)

  • Preview deployment discovered (no successful deploy for this commit)
  • Probes (skipped, no preview to probe)

Cross-review dedup

  • Delta review: delta: only what changed since the previously reviewed commit 2db18f8 is reviewed; findings are anchored on the whole change
  • 3 findings carried over from earlier reviews (inline comment not duplicated)

Output

  • Walkthrough comment posted
  • Inline comments (no in-diff findings to attach)

Reviewer: shown 55 of 55 changed hunks, opened 6 files (7 changed files never opened), 13 tool calls served, 6 model calls, 0 correction rounds, 55.5s. The library does not report tool calls asked apart from those it served, or a step cap.
Reviewed by Qodex · 107.8s

Every dimension is reviewed. Bugs, security, and performance post inline; low-severity style and convention notes are collected in the group above rather than dropped. Mark a finding resolved or flag a false positive in the Qodex Pull Requests view.

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

Stale Bugbot comment from a previous run.

Comment thread packages/trueforge/src/apis/turns.ts Resolved
Comment thread .changeset/event-id-subscribe-cursor.md Resolved
Comment thread packages/trueforge/src/apis/turns.ts Resolved

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

Stale Bugbot comment from a previous run.

Only leftover {sequence}-1 streams accept the legacy cursor; new streams
return 400 for after_sequence_number and Last-Event-ID. Drop the
skip-count compat path and mark subscribe non-resumable for Fern so the
SDK stops reconnecting with Last-Event-ID.

Co-authored-by: Cursor <cursoragent@cursor.com>

@qodex-ai qodex-ai 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.

Inline findings below. See the walkthrough comment for the summary.

Comment thread packages/trueforge/src/apis/turns.ts Resolved
trueforge-dev-bot Bot and others added 4 commits November 4, 2026 04:49
Co-authored-by: Cursor <cursoragent@cursor.com>
Partial repair cannot match emit order, so an existing stream is left untouched. Seeks compare a dedicated id field instead of decoding payloads, and the Redis owner key is removed so ownership stays on the turn row.

Co-authored-by: Cursor <cursoragent@cursor.com>

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

Stale Bugbot comment from a previous run.

chiragjn and others added 2 commits November 5, 2026 10:18
…ward.

A restore writes the durable log to a private key and renames it onto the stream only while the stream is still absent, so a stream is never left as a prefix. An after_event_id seek walks down from the tip, so a recent cursor settles in one step.

Co-authored-by: Cursor <cursoragent@cursor.com>
Restore logs the missing-stream rebuild, each durable page query, the install or lost race, and failures. Counts and timings stay in the message text so they are not indexed as log fields. Request-scoped ids come from the log context.

Co-authored-by: Cursor <cursoragent@cursor.com>
@chiragjn
chiragjn force-pushed the cj-turn-stream-rebuild branch from 6527f22 to e6556d2 Compare November 6, 2026 04:02
pages: AsyncIterable<readonly T[]>,
streamTTLSeconds: number | undefined,
): Promise<boolean> {
const tempKey = `${this.streamId}:restore:${randomUUID()}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium: Bound concurrent log restoration

When a paused turn's stream has expired, parallel authorized /subscribe requests can all pass the absent-stream check and build complete copies under separate UUID keys; renameNX only selects a winner after those copies have been allocated, allowing a burst to multiply a large log's Redis footprint and pressure shared resources. Acquire a per-stream restore lease shared across replicas, recheck existence after acquiring it, and bound overall restore concurrency.

@qodex-ai qodex-ai 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.

Inline findings below. See the walkthrough comment for the summary.

}
await Promise.all(writes);
wrote = true;
await this.expireKey(tempKey, streamTTLSeconds);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Minor Partial restore after temporary key expiration · bug

installIfAbsent expires tempKey while durable page queries remain pending. If a query exceeds streamTTLSeconds, subsequent xAdd calls recreate tempKey without earlier pages. renameNX then installs the incomplete stream and reports success. Subscribers miss earlier restored events despite the successful restore result. Keep tempKey alive throughout page retrieval, or detect expiration and restart the complete restore before renameNX.

Bring in the main sync and pause migration ordering from ha/emit-pause.

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	packages/trueforge/src/apis/turns.ts
@chiragjn
chiragjn force-pushed the cj-turn-stream-rebuild branch from e6556d2 to 48a41df Compare November 6, 2026 04:10

@qodex-ai qodex-ai 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.

Inline findings below. See the walkthrough comment for the summary.

Comment thread packages/trueforge/src/apis/turns.ts Resolved
const reply: unknown = await this.redis.eval(PUT_STREAM_ENTRY, {
keys: [this.streamId],
arguments: [
this.entryMillisecondFor(event),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 Major Legacy resume cursors stop advancing · bug

put appends timestamp entries to existing legacy streams. readEvent returns these entries without sequence_number, although subscribe still accepts after_sequence_number. Legacy clients cannot advance their sequence cursors and replay the appended events on every reconnect. Preserve sequence-based writes and sequence_number for existing legacy streams until those streams expire.

}
await Promise.all(writes);
wrote = true;
await this.expireKey(tempKey, streamTTLSeconds);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Minor Temporary restore expiry discards earlier pages · bug

installIfAbsent expires tempKey after each page. A subsequent page read exceeding streamTTLSeconds lets Redis delete earlier pages before xAdd recreates tempKey. renameNX installs incomplete history, and applyEventLogRestore preserves that incomplete stream on subsequent restores. Keep tempKey alive throughout page reads, or detect expiry and restart the entire restore before renameNX.

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

Stale Bugbot comment from a previous run.

Comment thread packages/trueforge/src/apis/turns.ts Resolved
A resume starts at the oldest open message shell, and reconnect progress is counted per cursor so replaying the same events still exhausts the retry budget. Test fixtures include the memory catalog and configuration switch.

Co-authored-by: Cursor <cursoragent@cursor.com>

@qodex-ai qodex-ai 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.

Inline findings below. See the walkthrough comment for the summary.

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

Stale Bugbot comment from a previous run.

chiragjn and others added 2 commits November 6, 2026 10:48
…oss pauses

- Pick the earliest open model message only from the current turn so a
  resume cursor from another turn cannot leak in.
- Keep a finished model.message when a replayed shell arrives, so
  replays do not truncate tool arguments while approvals are pending.
- Walk history newest-first for the reconnect cursor and pin it to
  mcp.auth_required until the auth prompt is answered or the turn moves on.

Co-authored-by: Cursor <cursoragent@cursor.com>
…and early sandbox id

- Restore pending_mcp_auth onto rebuilt AgentThreads so a turn paused for
  MCP auth does not run before user.mcp_auth_continue arrives.
- Memory ingest treats a tip turn as paused when status is 'paused' and
  uses turn.updated_at as the pause time, matching the new paused state.
- Carry the open tip's sandbox id onto the pre-getTurn progress paint.
- Accept any state shape in cursorFromTurnEvents and narrow it locally.

Co-authored-by: Cursor <cursoragent@cursor.com>

@qodex-ai qodex-ai 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.

Inline findings below. See the walkthrough comment for the summary.

Co-authored-by: Cursor <cursoragent@cursor.com>

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2db18f8. Configure here.

Comment thread packages/trueforge/src/apis/turns.ts Resolved
A turn paused on the user can resume on the same turn id, so ingesting it early
moved the checkpoint onto a turn whose final reply was then skipped. Only closed
exchanges are ingested now; the pause timeout and partial sends are removed.

Co-authored-by: Cursor <cursoragent@cursor.com>
@chiragjn
chiragjn merged commit eb4df5a into ha/emit-pause Nov 6, 2026
15 checks passed
@chiragjn
chiragjn deleted the cj-turn-stream-rebuild branch November 6, 2026 18:44
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.

2 participants