Repository navigation
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # packages/assistant-ui-runtime/src/convertTurnMessages.ts # packages/trueforge-sdk/src/api/types/index.ts # packages/trueforge-sdk/src/serialization/types/index.ts # packages/trueforge-ui/src/containers/AgentSessionTimelineContainer.tsx # packages/trueforge/src/apis/sessions.ts # packages/trueforge/src/app.ts # packages/trueforge/src/main.ts # packages/trueforge/tests/unit/apis/deletedSessionCrud.test.ts # packages/trueforge/tests/unit/apis/sessionHttp.test.ts # python/trueforge_sdk/src/trueforge_sdk/__init__.py # python/trueforge_sdk/src/trueforge_sdk/types/__init__.py
* fix: fold session cost and active duration when a turn pauses Pause credits only the new cost and the closed running interval. Resume and rebuild add later work once, so pause time and the same cost are not billed again. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix typecheck issues * test: measure paused-turn duration from the open running interval Wall-clock created_at to completed_at counted the pause gap, so the store contract expected a few milliseconds off the folded active duration. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: store turn metrics as sibling checkpoint keys The nested metrics_checkpoint object sat inside the checkpoint, and the rebuild seed was named like a value to subtract. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: drop the unused nested metrics checkpoint cleanup Nothing persisted metrics_checkpoint, so the writers and reader do not special-case it. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: name the turn metrics row lock and merge its checkpoint keys Callers of the Postgres read need to see that it takes FOR UPDATE. The three metrics keys are one merge instead of nested jsonb_set calls. Co-authored-by: Cursor <cursoragent@cursor.com> * test: cover pause cost, a later pause, and freeze of paused versus running The shared store suite checked duration but not that cost folds once, a rejected second pause leaves totals unchanged, or a paused freeze adds nothing beyond the pause. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: pass pause metrics into turn persistence A non-terminal persist only stores the cumulative on pause, so it no longer takes the whole orchestrator. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: accrue each running stretch into pause and terminal metrics so cancel can still close the open interval Co-authored-by: Cursor <cursoragent@cursor.com> * fix: write the frozen cancelled state onto turn.done The event log was keeping the caller's metrics-free cancel while the turn row stored checkpoint metrics. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: unblock CI after stretch-based session cost and the main migration order Co-authored-by: Cursor <cursoragent@cursor.com> * Regenerate OpenAPI document and SDKs * Make description for turn metrics common * Regenerate OpenAPI document and SDKs * refactor: name the metrics sum plainly and drop the stretch wording Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: read a freeze's public metrics from the checkpoint Co-authored-by: Cursor <cursoragent@cursor.com> * test: drop the freeze case the contract suite already covers Co-authored-by: Cursor <cursoragent@cursor.com> * fix: hide the activity loader when a turn cannot resume Restore resumeUnavailable so a still-running message without a subscribe path stops spinning the loader and shows the waiting notice instead. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: omit empty checkpoint metrics on a frozen turn Co-authored-by: Cursor <cursoragent@cursor.com> * fix: keep unsubscribable turns waiting instead of falsely completing Guard create-turn reconnect and ensureTurnSubscription when subscribe is missing, finalize cancelled tips locally on cancel, and keep the activity loader from reviving on a stale running message. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: drop inferrable string annotation on abandon reason Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: trueforge-dev-bot[bot] <trueforge-dev-bot[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve conflicts by keeping pause-branch store wiring (resolveTurnExecutionStores, direct provider stores) while adopting main's session-grants read access and session_sharing capability. Co-authored-by: Cursor <cursoragent@cursor.com>
Sandbox installs corrupt node_modules in this workspace. Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve conflicts by keeping turn-execution store resolvers and wiring x-tfg-tools capabilitiesStore through TurnExecutionStores. Co-authored-by: Cursor <cursoragent@cursor.com>
…esources. Move parsing into inlineResources, own ICapabilitiesStore in db/, and wire Empty/Inline stores through turn execution resolvers. Co-authored-by: Cursor <cursoragent@cursor.com>
# Conflicts: # packages/assistant-ui-runtime/src/index.ts # packages/assistant-ui-runtime/src/server/index.ts # packages/trueforge-ui/src/server/types.ts # packages/trueforge/scripts/write-openapi.ts # packages/trueforge/src/apis/agents.ts # packages/trueforge/src/apis/capabilities.ts # packages/trueforge/src/apis/sessions.ts # packages/trueforge/src/app.ts # packages/trueforge/src/db/postgres/types.ts # packages/trueforge/src/db/sqlite/types.ts # packages/trueforge/src/main.ts # packages/trueforge/tests/unit/apis/agents.test.ts # packages/trueforge/tests/unit/apis/capabilities.test.ts # packages/trueforge/tests/unit/apis/deletedSessionCrud.test.ts # packages/trueforge/tests/unit/apis/sessionGrants.test.ts # packages/trueforge/tests/unit/apis/sessionHttp.test.ts
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # packages/trueforge/src/main.ts
Co-authored-by: Cursor <cursoragent@cursor.com>
Bring ha/emit-pause up to date with main, with this branch's migrations ordered last. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the pause migrations after main's sandbox-environment migration, and fill in the memory fields main now requires on paused turns. Co-authored-by: Cursor <cursoragent@cursor.com>
* Rebuild events stream for subscribe on paused turns * fix: date session event sequence migration after pause migrations Stream migrations must run after the pause migrations dated 20261028. Co-authored-by: Cursor <cursoragent@cursor.com> * Restore an expired turn stream only while the turn is paused. 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> * Regenerate OpenAPI document and SDKs * Subscribe to turn streams by event id (#1242) * 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> * Address some review comments * Fix turn stream resume dropping deltas and appending a stale repair. Identical payloads were skipped, an in-progress event id was rejected, and a subscribe repair could land behind a newer tail. Sequence numbers are no longer stored, and an older client can still resume an event-id stream by the count it already applied. Co-authored-by: Cursor <cursoragent@cursor.com> * Regenerate OpenAPI document and SDKs * Repair turn streams by paging listTurnEvents ascending Drop listTurnEventLog. Repair reads durable rows one page at a time in persist order and appends each page only while the stream tail is the entry id the previous page wrote. Co-authored-by: Cursor <cursoragent@cursor.com> * Throw the turn lock timeout directly from withTimeout Pass TurnLockAcquireTimeoutError as the mutex timeout error so the acquire path no longer catches and rewraps E_TIMEOUT. Co-authored-by: Cursor <cursoragent@cursor.com> * Reject after_sequence_number on new-style turn streams 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> * Regenerate OpenAPI document and SDKs * Trim subscribe API descriptions to caller contract Co-authored-by: Cursor <cursoragent@cursor.com> * Regenerate OpenAPI document and SDKs * Restore a missing turn stream from the whole durable log. 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> * Install a restored turn stream atomically and seek after-cursors backward. 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> * Log turn stream restore decisions, page queries, and timings. 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> * Keep interleaved model-message resumes from duplicating text. 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> * Scope resume cursors to the active turn and keep MCP auth cursors across 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> * Fix MCP auth wait after turn rebuild, memory ingest of paused turns, 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> * Send memory exchanges only once closed 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> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: trueforge-dev-bot[bot] <trueforge-dev-bot[bot]@users.noreply.github.com>
🦋 Changeset detectedLatest commit: eb4df5a The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
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 |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit eb4df5a. Configure here.
| : (foldState.threads.get(ROOT_THREAD_ID)?.modelMessageIds ?? []); | ||
| yield withCursor({ content: buildRootAssistantContentForIds(foldState, ids) }); | ||
| yield withSandbox({ content: buildRootAssistantContentForIds(foldState, ids) }); | ||
| return; |
There was a problem hiding this comment.
Sandbox-only stream end skips reconnect
Medium Severity
When the SSE body ends after sandbox.created without turn.done or a pause, streamTurnEvents now yields sandbox metadata and returns instead of throwing TurnStreamDisconnectedError. A transport drop in that window is treated as a finished stream, so subscribe-retry never runs and a still-running turn looks complete.
Reviewed by Cursor Bugbot for commit eb4df5a. Configure here.
| '@truefoundry/trueforge-ui': patch | ||
| --- | ||
|
|
||
| Turns pause while waiting for user input and resume once every required interaction is done, including tool approval policies, questions, and MCP authentication. |
There was a problem hiding this comment.
Multiple changeset files in one PR
Low Severity
This PR adds four .changeset/*.md files. Release process allows only one changeset per PR, with every package bump listed in that single file. Collapse these into one patch changeset covering @truefoundry/trueforge, @truefoundry/trueforge-core, @truefoundry/trueforge-assistant-ui-runtime, and @truefoundry/trueforge-ui.
Additional Locations (2)
Triggered by learned rule: Prereleases use rc tags only
Reviewed by Cursor Bugbot for commit eb4df5a. Configure here.
| const requestHeaders = c.req.header(); | ||
|
|
||
| return deps.activeTurns.withTurnLock({ sessionId, turnId }, async () => { | ||
| const runAsAgent = await referencedRunAsAgent({ |
There was a problem hiding this comment.
Medium: Recheck agent-use permission before resuming
Unlike turn creation, this endpoint checks session ownership but never calls authorizer.canAccessAgent with action: 'use'; referencedRunAsAgent() only loads the agent from storage. A TrueFoundry user whose USE_AGENT permission has been revoked can still submit approvals or answers and trigger reconstruction of their paused named-agent turn with that agent's resource stores, so apply the same agent existence/use check before accepting or forwarding the batch.
| tenant_id: requestContext.tenant_id, | ||
| subject: requestContext.subject, | ||
| roles: requestContext.roles, | ||
| user_credential: requestContext.user_credential, |
There was a problem hiding this comment.
Medium: Caller credentials reach peer logs
This forwards the live user_credential and all inbound HTTP headers, but RequestReplyExecutor.onMessage() logs the complete serialized Redis message when debug logging is enabled. Cross-replica approval and answer requests therefore write bearer tokens and any session cookies into server logs; remove or redact raw-message logging and restrict forwarded headers before carrying credentials through this transport.
| await input.refreshTtl(); | ||
| return 'present'; | ||
| } | ||
| const installed = await input.installIfAbsent(input.readPages()); |
There was a problem hiding this comment.
Medium: Concurrent restores multiply resource use
A session owner can issue parallel subscriptions to an expired paused turn: every request can pass the missing-tail check and read the complete durable log, while the Redis backend builds a separate full temporary stream for each request before renameNX selects a winner. Add a per-stream restore lease or single-flight guard, shared across replicas for Redis, plus a bounded rebuild concurrency budget before consuming readPages() to protect shared database and Redis capacity.
PR overviewThe PR allows turns to pause while waiting for approvals, answers, or MCP authentication, then resume when input arrives. It also supports forwarding input between replicas and restoring event streams for paused turns. Three security issues remain open, with none addressed so far. A session owner whose agent-use permission has been revoked can still resume a paused named-agent turn with that agent's resource stores. Cross-replica input forwarding can expose bearer tokens and cookies in debug logs, while parallel subscriptions can trigger duplicate full-log restores that consume shared database and Redis capacity. Open issues (3)
Fixed/addressed: 0 · PR risk: 7/10 |
| : (foldState.threads.get(ROOT_THREAD_ID)?.modelMessageIds ?? []); | ||
| yield withCursor({ content: buildRootAssistantContentForIds(foldState, ids) }); | ||
| yield withSandbox({ content: buildRootAssistantContentForIds(foldState, ids) }); | ||
| return; |
There was a problem hiding this comment.
🟠 Major Sandbox-only disconnect suppression · bug
The return suppresses TurnStreamDisconnectedError when SSE ends after sandbox.created without content, pause, or turn.done. resumeTurnStream exhausts normally, so callers stop consuming instead of retrying the unfinished turn. Remove the return so the sandbox metadata yield precedes TurnStreamDisconnectedError.
Qodex reviewReview incomplete. Qodex could not finish this review. The findings below are the ones it had already checked.
This text was shortened because it exceeded GitHub's 65,536-character limit. |


Note
High Risk
Rewrites streaming/protocol contracts for paused turns and ingestion; any mismatch between UI hosts and servers can strand approvals, break resume paths, or mis-record memory snapshots.
Overview
Assistant runtime now treats pauses as part of the same turn: it folds
turn.update/ inbound user events, replays histories withLegacyTurnInputItem, tracks subscribe cursors by event id, and drives assistant status via shared constants so MCP auth, approvals, and tool responses stay visible across reconnects while hiding dead-end loaders. Supporting modules gained structured metadata keys, policy-aware fold helpers, and renamed MCP hooks (continueinstead ofresume).API contracts document that
CreateTurnRequestonly carries pure user messages whilecreate_turn_eventsacceptsTurnUserEvent*payloads, paused states expose metrics instead ofaction_required_on_events, tool approval policies are keyed bypolicy, and turn subscription queries gainedfrom_event_id/after_event_idwithafter_sequence_numberdeprecated.Tooling & ops updates include Changesets covering UI/runtime behavior, a Cursor rule mandating unsandboxed
pnpminstalls, and a smallOpenToolCallClosertweak emitting emptyapplied_user_eventswhen auto-closing dummy tool calls.Reviewed by Cursor Bugbot for commit eb4df5a. Bugbot is set up for automated code reviews on this repo. Configure here.