Skip to content

feat: port upstream #883 — adopt Workflow v5 stream handling (WorkflowChatTransport) #117

Description

@dennisonbertram

Part 3 of 4 deferred upstream ports (epic #119). Port of upstream PR #883
(commit 712e1f49, "Update Workflow beta stream handling"). Decision:
GO-WITH-CONDITIONS per docs/plans/upstream-883-reflection.md. Protocol
questions are closed by the POC (POC/upstream-883-workflow-v5/FINDINGS.md,
10/10 PASS); the residual risk is at the integration seam (real auth on resume
GET, real-device iOS recovery, double-resume, and the workflow runtime
major bump). This is part 3 of 4 deferred upstream ports.

Sequencing and dependencies

Why this matters

Mid-stream stream drops (Vercel Function timeout, mobile network drop, tab
backgrounding) are a real user-facing failure: today the user can lose assistant
output or see duplicated/replayed content, and the fork papers over this with a
bespoke stall-timer probe loop. Upstream #883 moves stream recovery into the
@workflow/ai WorkflowChatTransport, which counts UI chunks and resumes from
the exact next chunk via an indexed GET (startIndex). The user-visible outcome:
on a mid-stream POST drop the client deterministically resumes with zero
duplicate chunks and zero gaps
instead of relying on a 4s wall-clock heuristic.
For operators it replaces a fragile timer with a transport-managed protocol and a
correlatable x-workflow-stream-tail-index header.

Beyond user value, this is a prerequisite for upstream #884 (parallel work in
chat.ts), which soft-depends on the v5 transport contract (the "single
totally-ordered chunk log" invariant the v5 startIndex protocol indexes).

User/operator path protected

  • User path: an active assistant stream whose underlying POST connection
    drops mid-stream (function timeout / network drop) resumes automatically and
    the rendered conversation contains no duplicated or missing content.
  • User path (mobile/iOS): the "tap stop" action stops the stream on the
    first tap (the previously-shipped "need to tap stop 3 times on iOS" bug must
    not regress); backgrounding/foregrounding mid-stream resumes cleanly.
  • User path: page refresh during a live stream resumes and de-dupes (parity
    with today — replays from index 0).
  • Operator path: stream lifecycle is correlatable across POST and resume GET
    via chatId, x-workflow-run-id, and x-workflow-stream-tail-index.

Behavior contract

  1. Mid-stream POST drop → seamless resume. Given a chat streaming an
    assistant reply, when the POST stream drops without a finish chunk, then the
    transport auto-issues GET /api/chat/{chatId}/stream?startIndex=<chunkIndex>
    and resumes from the next chunk with zero duplicate chunks and zero gaps.
  2. Refresh resume → parity. Given a live stream, when the user refreshes the
    page, then resumeStream() replays from index 0 and the client de-dupes by
    message id (identical to today; this is parity, not a regression, and not an
    improvement unless a negative initialStartIndex is opted into later).
  3. 204 is swallowed. Given the GET resume route returns 204 (no active
    stream), when the transport reconnects, then the override returns null and
    no error is surfaced to the UI.
  4. iOS stop still aborts. Given a user taps "stop" mid-stream, when stop
    fires, then the stream aborts on the first tap and auto-recovery does not
    immediately re-attach to the still-running server stream (userStoppedRef
    suppression intact).
  5. Server/client chunk index alignment. Given the durable readable is a
    single append-only chunk log, when the route is called with startIndex=K,
    then run.getReadable({ startIndex: K }) seeks the same absolute chunk the
    transport's global chunkIndex counted (no re-emitted start/text-start
    preamble).

Product and design spec

  • Entry point: transparent to the user — no new UI control. Recovery happens
    inside AbortableChatTransport and GET /api/chat/[chatId]/stream.
  • Primary flow: stream renders incrementally; on a drop, content continues
    appending after a brief gap with no visible duplication or "Load failed"
    banner for the recoverable (POST-drop) case.
  • States:
    • streaming — chunks render via experimental_throttle (75ms).
    • recovering (POST drop) — transport-internal reconnect; no user-visible
      error.
    • recovering (refresh/visibility)resumeStream() from index 0; de-dupes
      by id.
    • no active stream — GET returns 204; resume is a cheap no-op (returns
      null).
    • error (non-204, e.g. 401/403/expired session) — surfaces as a clean error
      that routes through the surviving retry-error branch; must NOT wedge into
      an infinite probe loop.
  • Accessibility: no change to focus order or ARIA; streamed text continues to
    append to the existing assistant message region.
  • Copy: no new copy. Existing error/retry copy unchanged.
  • Permissions: resume GET stays behind requireAuthenticatedUser +
    requireOwnedChatById; ownership and auth are unchanged.
  • Error/empty/loading: empty (no active stream) → 204/no-op; loading →
    normal streaming; error (non-204) → existing error banner + retry path.

Integration spec

Source of truth

Upstream commit 712e1f49 (PR #883) touches 7 files (3 load-bearing source
files + 2 test files + package.json + bun.lock):

File Upstream change Fork state Reconciliation action
apps/web/package.json workflow ^4.2.0-beta.72 → ^5.0.0-beta.5; add @workflow/ai ^5.0.0-beta.4 workflow ^4.2.0-beta.72 (resolves 4.2.4); @workflow/ai absent Pin exact: workflow: "5.0.0-beta.5", @workflow/ai: "5.0.0-beta.4" (no carets per reflection §5)
apps/web/lib/abortable-chat-transport.ts Swap base class DefaultChatTransport (ai) → WorkflowChatTransport (@workflow/ai); add body intersection + prepareSendMessagesRequest fold + reconnectToStream 204-swallow override Still DefaultChatTransport (byte-identical to 712e1f49^; verified by reflection doc §0) Apply patch as-is; keep abort wrapper; keep prepareReconnectToStreamRequest chatId override (fork-only); keep 204-swallow
apps/web/app/api/chat/[chatId]/stream/route.ts Add parseStartIndex, run.getReadable({ startIndex }), getTailIndex(), x-workflow-stream-tail-index header; GET now accepts request param Byte-identical to 712e1f49^ (verified) Apply patch as-is
apps/web/app/sessions/[sessionId]/chats/[chatId]/stream-recovery-policy.ts Collapse stall-detection branch of getStreamRecoveryDecision to return "none"; shouldScheduleStallRecovery → always return false Has the full stall-timer branch (byte-identical to 712e1f49^) Apply patch as-is
apps/web/app/api/chat/[chatId]/stream/route.test.ts Adds startIndex passthrough, x-workflow-stream-tail-index, invalid-startIndex400 tests May have diverged Update tests-first (RED commit)
apps/web/app/sessions/[sessionId]/chats/[chatId]/stream-recovery-policy.test.ts Updates stall-branch expectations to "none"/false Has the old "probe"/true expectations Update tests-first (RED commit)
bun.lock Re-resolves all @workflow/* from v4.2.x to v5.0.0-beta.5 constellation; adds @workflow/ai@5.0.0-beta.4; drops configVersion: 1 On @workflow/core@4.2.4; @workflow/ai absent Regenerate deliberately after pinning; reconcile configVersion: 1 drop

Fork-only files NOT in the diff — keep unchanged

File Why it must be kept unchanged
apps/web/app/sessions/[sessionId]/chats/[chatId]/hooks/use-session-chat-runtime.ts Contains iOS userStoppedRef mitigation (lines 155–249) and speculative mount-resume probe (lines 296–354); upstream has no counterpart; deleting either would regress iOS stop behavior
apps/web/app/sessions/[sessionId]/chats/[chatId]/hooks/use-stream-recovery.ts Keeps the surviving visibility-probe branch post-#883; the stall-timer effect goes inert but is NOT deleted (follow-up cleanup scope)

Key fork-specific overrides that must survive the transport re-base

  1. prepareReconnectToStreamRequest: ({ id }) => ({ api: /api/chat/${id}/stream }) — this is what makes our DB-chatId reconnect model work (overrides v5 default runId-derived URL). POC Scenario 1 verified it routes the resume GET to /api/chat/{chatId}/stream, not a runId URL.
  2. reconnectToStream 204-swallow override — v5 throws Failed to fetch chat: 204 on no-active-stream; the override returns null (POC Sub-goal 4 confirmed this is still required under v5).
  3. AbortableChatTransport.abort() — still needed because WorkflowChatTransport.reconnectToStream does not thread an abort signal through its internal fetch.

Workflow producer surface (chat.ts) — unchanged by #883

chat.ts uses getWritable<UIMessageChunk>() (line 1009), getRun (line 2187),
claimActiveStream (line 1043), getWorkflowMetadata. The #883 diff changes no
producer code. However, the workflow runtime major bump (v4.2.4 → v5 beta)
is a broader surface and must be validated end-to-end by a full CI run and
managed-runtime smoke.

AI-SDK version skew — confirmed no forced bump

@workflow/ai peers ai: "^6". Fork resolves ai@6.0.168 via catalog
^6.0.165. @workflow/ai@5.0.0-beta.4 also peers @ai-sdk/provider: "^3.0.0";
fork satisfies this via @ai-sdk/provider@3.0.8 in bun.lock. No ai or
@ai-sdk/* upgrade is forced.
The ~32 other from "ai" import sites in
apps/web (generateText, streamText, InferUIMessageChunk, UIMessage types, etc.)
are unaffected. Confirmed by reflection doc §4 and POC "Dependency / peer
surprises."

Dependency pinning — drop carets, pin exact

Per reflection doc §5 and the dependency-pinning hazard:

// apps/web/package.json — exact pins, no carets
"@workflow/ai": "5.0.0-beta.4",
"workflow":     "5.0.0-beta.5"

Rationale: @workflow/ai@5.0.0-beta.6 peers workflow ^5.0.0-beta.8; with
caret ranges on both, a fresh bun install can float to mismatched beta minors
of API-unstable packages. The transport .d.ts is byte-identical beta.4→beta.6
(POC verified), but this is not guaranteed for future betas. Pin exact; let the
lockfile be the single source of truth.

WorkflowChatTransportOptions API surface (POC-proven)

The flat interface (does NOT extend HttpChatTransportInitOptions):

  • Option is named fetch (NOT customFetch — any stale docs saying otherwise
    are wrong for this beta line).
  • headers, credentials, body are NOT native options — the fork adds body
    via intersection and folds it through prepareSendMessagesRequest.
  • prepareReconnectToStreamRequest and prepareSendMessagesRequest are the AI
    SDK 6 types re-exported — same callback shape the fork already uses.

In scope

  • Bump workflow"5.0.0-beta.5" (exact) and add @workflow/ai
    "5.0.0-beta.4" (exact) in apps/web/package.json; regenerate bun.lock.
  • Re-base AbortableChatTransport onto WorkflowChatTransport; keep the abort
    wrapper, the body intersection + prepareSendMessagesRequest fold, the
    204-swallow reconnectToStream override, and the prepareReconnectToStreamRequest
    chatId override.
  • Add startIndex parsing (+ 400 on invalid) and the
    x-workflow-stream-tail-index response header to the GET stream route.
  • Collapse stream-recovery-policy.ts stall branch to return "none" and
    shouldScheduleStallRecovery → always false (upstream as-is); update
    stream-recovery-policy.test.ts and stream/route.test.ts tests-first.
  • Keep all fork-only recovery logic (iOS userStoppedRef, speculative
    mount-resume probe, DB-chatId reconnect model, visibility-probe branch).
  • Prove integration: resume GET under real auth (expired session → graceful,
    non-wedging error), real-device iOS stop/background/refresh, and a
    managed-runtime smoke against the v5 runtime.

Out of scope

  • Dead-code cleanup of the now-vestigial STREAM_RECOVERY_STALL_MS,
    getStreamRecoveryDelayMs, and the inert stall-timer effect / inFlightStartedAt
    plumbing in use-stream-recovery.ts. These stay (still exported/referenced)
    and should be removed in a separate, clearly-scoped follow-up so the port diff
    stays reviewable.
  • Negative initialStartIndex opt-in for cheap refresh-resume (skipping
    already-rendered chunks on refresh). The capability exists (proven in POC
    Scenario 3) but is optional and not required for parity.
  • Wiring onChatEnd / onChatSendMessage observability hooks.
  • fix: #879 honor guardrails embedded in the run's definitionSnapshot #884's parallelization of chat.ts. This ticket is part 3 of 4 deferred
    upstream ports; fix: #879 honor guardrails embedded in the run's definitionSnapshot #884 soft-depends on the v5 transport contract and must honor
    the "single totally-ordered chunk log" invariant (parallel computation,
    serialized emission into one getWritable) — record that constraint in the
    feat: port upstream #884 — parallelize chat workflow startup #118 ticket, do not implement it here.
  • Any ai / @ai-sdk/* upgrade (none is forced by @workflow/ai).

Research and context sources

  • Upstream commit 712e1f49 (PR feat: unblock authenticated production canary and wire journey harnesses (#866) #883, "Update Workflow beta stream
    handling")
    — 7-file diff being ported (3 load-bearing source files +
    2 test files + package.json + bun.lock). Our 3 changed source files are
    byte-identical to upstream's pre-feat: unblock authenticated production canary and wire journey harnesses (#866) #883 state, so the patch applies cleanly.
  • docs/plans/upstream-883-reflection.md — deep reflection;
    GO-WITH-CONDITIONS verdict; the 11-item conditions checklist folded into
    Definition of done below; identifies the real risk (fork-only client recovery
    the patch does not touch) and the double-resume hazard.
  • POC/upstream-883-workflow-v5/FINDINGS.md — 10/10 PASS. Definitive
    v5 transport surface (fetch not customFetch; flat options interface;
    defaults), abort/resume proof, chunk-index semantics, and DB-chatId
    compatibility.
  • Vercel Workflow v5 docs — transport options, run.getReadable({ startIndex })
    / getTailIndex() semantics, and the runId+localStorage reconnect model the
    fork overrides. Fetch current docs via ctx7 / Context7 (Vercel Workflow)
    before relying on training data, since the betas are fast-moving.
  • Fork sources read during scoping: apps/web/lib/abortable-chat-transport.ts,
    apps/web/app/api/chat/[chatId]/stream/route.ts,
    apps/web/app/sessions/[sessionId]/chats/[chatId]/stream-recovery-policy.ts,
    apps/web/app/sessions/[sessionId]/chats/[chatId]/hooks/use-session-chat-runtime.ts.

Agent todo checklist

  • Re-run the POC harness (POC/upstream-883-workflow-v5) against the
    versions that actually resolve to confirm the protocol surface still holds.
  • Pin exact betas in apps/web/package.json: workflow: "5.0.0-beta.5",
    @workflow/ai: "5.0.0-beta.4" (no carets). Regenerate bun.lock
    deliberately; confirm ai stays 6.0.168 and no @ai-sdk/* skew is
    forced.
  • Add failing tests first: update stream-recovery-policy.test.ts for the
    collapsed stall branch (expects "probe" → should be "none"; expects
    true → should be false); add startIndex + x-workflow-stream-tail-index
    + invalid-startIndex400 coverage to stream/route.test.ts. Confirm
    RED.
  • Apply the three-file patch: re-base abortable-chat-transport.ts onto
    WorkflowChatTransport (keep abort wrapper, body intersection,
    prepareSendMessagesRequest fold, 204-swallow reconnectToStream
    override
    , prepareReconnectToStreamRequest chatId override).
  • Update stream/route.ts: parse startIndex (reject non-integer with
    400), call run.getReadable({ startIndex }), await getTailIndex(),
    return x-workflow-stream-tail-index header.
  • Collapse stream-recovery-policy.ts stall branch to return "none" and
    shouldScheduleStallRecoveryfalse (upstream as-is). Keep
    retry-error and visibility-probe branches.
  • Confirm use-session-chat-runtime.ts fork-only logic is UNCHANGED:
    prepareReconnectToStreamRequest chatId override, userStoppedRef
    iOS mitigation, speculative mount-resume probe.
  • Add/confirm a test (or network trace) that the resume GET hits
    /api/chat/{chatId}/stream, not a runId URL.
  • Add a test that a non-204 resume response (401/403) degrades to a clean
    error and does NOT wedge the recovery/probe loop.
  • Run bun test for the two touched suites, then bun --bun run ci
    (full v5-runtime CI) and git diff --check.
  • Managed-runtime smoke against the v5 runtime end-to-end
    (start/getRun/getReadable/getWritable/cancel).
  • Real-device iOS smoke: (a) stop mid-stream = one tap; (b)
    background/foreground mid-stream = resume, no duplicate content; (c)
    refresh mid-stream = replays-from-0 + de-dupes.
  • Capture observability/proof evidence; commit RED then GREEN; push to the
    fork; open the PR.
  • Record the "single totally-ordered chunk log" invariant constraint in the
    feat: port upstream #884 — parallelize chat workflow startup #118 ticket before or alongside this PR.

Tests to add first

  1. Mid-stream POST drop, zero-duplicate resume (transport/integration): POST
    delivers chunks 0..N, drops without finish; assert the transport reconnects
    with startIndex=N+1 and the delivered chunk set has no duplicates and no
    gaps. (Mirrors POC Scenario 1.)
  2. Refresh resume replays from index 0 (parity): resumeStream() sends no
    startIndex; assert replay-from-0 + de-dupe by id (matches today).
  3. 204 swallowed: GET resume route returns 204; assert the
    reconnectToStream override returns null and no error surfaces.
  4. iOS stop mitigation: explicit stop sets userStoppedRef; assert auto
    retryChatStream({ auto: true }) clears the error and does NOT re-attach.
  5. Server-side chunk index alignment: GET .../stream?startIndex=8
    run.getReadable({ startIndex: 8 }) receives 8, response carries
    x-workflow-stream-tail-index; invalid startIndex400. (Mirrors
    upstream route.test.ts additions.)

Observability and user feedback

  • User-visible status: recoverable POST-drop resumes with no error banner and
    no duplicated text; non-204 resume errors surface the existing retry banner.
  • Responsible surfaces: GET /api/chat/[chatId]/stream (route) and
    AbortableChatTransport (client transport) own stream-lifecycle signals.
  • Correlation IDs: every stream lifecycle event correlates by chatId,
    x-workflow-run-id (POST response header → chat.activeStreamId), and
    x-workflow-stream-tail-index (GET resume response header). Resume requests
    hit GET /api/chat/{chatId}/stream?startIndex=<int>.
  • Events/fields: route logs the resume entry with { chatId, workflowRunId (activeStreamId), startIndex, tailIndex, status }; transport-level abort and
    reconnect are attributable to chatId. Typed error kinds: errorKind: "resume-204" (swallowed → no-op), resume-auth (401/403), resume-failed
    (other non-OK). The single ordered getWritable<UIMessageChunk>() chunk log is
    the index the resume seeks — keep it one totally-ordered sequence.
  • Redaction: never log message/prompt content, session cookies, or provider
    tokens; log only ids, indexes, status, and error kind.
  • Debug recipe: grep '"chatId":"<id>"' logs | grep 'stream' to follow a
    POST→drop→resume sequence; confirm startIndex on the resume GET advances and
    x-workflow-stream-tail-index is present.
  • Caveat for metrics: the v5 resume-skip means the client may legitimately
    NOT see chunks 0..K on a reconnect — confirm no client-side metric counts on
    full-stream delivery.
  • Managed-runtime proof: because this is a workflow-runtime major bump
    (broader surface than the transport-only POC), acceptance requires a full
    bun --bun run ci against the v5 runtime + a managed-runtime smoke per
    docs/process/managed-runtime-proof-standard.md (target Level 2: local-or-live
    sandbox proof exercising start/getRun/getReadable/getWritable/cancel
    end-to-end, with provenance, command evidence, verification, and limitations
    recorded).

Regression harness plan

  • Route contract test (stream/route.test.ts): startIndex passthrough,
    x-workflow-stream-tail-index header, invalid-startIndex400. Fail-before:
    no startIndex/header handling. Pass-after: covered.
  • Policy test (stream-recovery-policy.test.ts): collapsed stall branch
    returns "none"; shouldScheduleStallRecovery returns false. Fail-before:
    expects "probe"/true. Pass-after: "none"/false.
  • Transport behavior test: mid-stream-drop reconnect delivers each chunk
    exactly once (no dup, no gap); 204 → null.
  • Managed-runtime smoke against the v5 runtime (manual proof captured in the
    PR if no automated harness reaches the real runtime) — required because the
    POC ran against a Node http harness, not our real stack.
  • Manual real-device iOS smoke (no continuous harness practical): stop /
    background-foreground / refresh, captured as PR evidence.

TDD audit trail

  • RED commit: failing stream/route.test.ts (+startIndex/header/400) and
    updated stream-recovery-policy.test.ts (collapsed branch), committed
    test-only on the work branch; record the RED command + expected failure reason.
  • GREEN commit: the three-file patch + dependency pins + regenerated
    bun.lock that turns the targeted tests green; record the GREEN command and
    the adjacent-suite + bun --bun run ci results.
  • REGRESSION commit: confirm the full suite (all touched suites) still passes
    after any follow-up integration-proof adjustments.

Regression risks and concerns

  • Double-resume (highest fork-specific risk): v5's internal
    reconnectToStreamIterator AND our visibility-probe / speculative
    mount-resume loops can both fire. The SDK should reconcile by message id, but
    this is UNPROVEN on a real device. Watch for visible jank / chunk replay on top
    of a live stream.
  • iOS "tap stop 3 times": the userStoppedRef mitigation guards a
    previously-shipped bug upstream never exercised (upstream has no
    use-session-chat-runtime.ts). Removing it to "match upstream" would regress
    iOS stop. KEEP it; prove one-tap-stop on a real device.
  • Resume GET under real auth: the POC harness had no auth. An expired-session
    reconnect throws a non-204 (e.g. Failed to fetch chat: 401) that the 204-only
    override does NOT swallow — must degrade to a clean error via the surviving
    retry-error branch, not an infinite probe loop. Add an explicit test.
  • workflow runtime major bump: feat: unblock authenticated production canary and wire journey harnesses (#866) #883's diff changes no producer code, but
    v4.2.4 → v5 beta is a broader surface (start, getRun, getReadable,
    getWritable, getWorkflowMetadata, claimActiveStream durability,
    cancel). The POC only validated the transport in isolation — a full CI run +
    managed-runtime smoke is required, not optional.
  • Beta peer drift: @workflow/ai@5.0.0-beta.4 peers workflow ^5.0.0-beta.5, but @workflow/ai@5.0.0-beta.6 peers ^5.0.0-beta.8. Caret
    ranges on both can float to mismatched beta minors on a fresh install — hence
    exact pins.
  • Test churn: both touched test files exist in the fork and may have
    diverged; land the updates tests-first.
  • Chunk-index desync for feat: port upstream #884 — parallelize chat workflow startup #118/fix: #879 honor guardrails embedded in the run's definitionSnapshot #884: any future change that introduces
    multiple writables or reorders chunks post-hoc would desync the v5 resume
    index — record the single-ordered-log invariant in the feat: port upstream #884 — parallelize chat workflow startup #118 ticket.

Deploy or migration impact

  • Dependency bump only (workflow major + new @workflow/ai); no database
    migration.
    Latest migration is 0047_cheerful_thunderbolt_ross.sql.
    Regenerate bun.lock (note: upstream lockfile drops configVersion: 1
    reconcile deliberately during regeneration).
  • Migrations still run on bun run build, but this change adds none.
  • Risk profile: the v5 runtime is a broader surface than the transport.
    Require integration proof before deploy: resume under real auth/expired
    session; real-device iOS stop/background/refresh; managed-runtime smoke.
  • Rollback: revert the dependency pins + the three-file patch + bun.lock;
    the byte-identical pre-feat: unblock authenticated production canary and wire journey harnesses (#866) #883 base means revert is clean.

Definition of done

Folds the reflection's 11-item conditions checklist (docs/plans/upstream-883-reflection.md §8):

  • Pin exact betas: workflow: "5.0.0-beta.5" and @workflow/ai: "5.0.0-beta.4" (no carets); bun.lock regenerated deliberately; POC
    harness re-run against the resolved versions.
  • Three-file patch applied as-is: transport re-base, route
    startIndex/tail-index header, policy branch collapse.
  • 204-swallow reconnectToStream override kept: compiles; no-active-stream
    path returns null, not an error.
  • prepareReconnectToStreamRequest: ({ id }) => ({ api: /api/chat/${id}/stream }) kept:
    test/trace confirms the resume GET hits the chatId URL, not a runId URL.
  • Fork-only recovery logic NOT deleted: iOS userStoppedRef mitigation
    and speculative mount-resume probe in use-session-chat-runtime.ts, and
    the surviving visibility-probe branch in use-stream-recovery.ts,
    preserved.
  • Tests-first for the touched test files: stream-recovery-policy.test.ts
    updated for the deleted stall branch; stream/route.test.ts covers
    startIndex + x-workflow-stream-tail-index + invalid→400.
  • Resume GET proven under real auth: an expired-session reconnect returns
    a non-204 status that degrades to a clean error and does NOT wedge the
    recovery loop.
  • Real-device iOS smoke (highest-priority manual check): (a) stop
    mid-stream → one tap; (b) background/foreground mid-stream → resume with no
    duplicate content; (c) refresh mid-stream → replays-from-0 + de-dupes
    (parity with today).
  • Full bun --bun run ci + managed-runtime smoke against the v5 runtime
    (not just the transport): start/getRun/getReadable/getWritable/
    cancel exercised end-to-end, with proof evidence captured per the Managed
    Runtime Proof Standard (target Level 2).
  • Follow-up recorded (separate, optional): delete the vestigial
    STREAM_RECOVERY_STALL_MS / getStreamRecoveryDelayMs / dead stall-timer
    effect in a scoped cleanup after the port lands green.
  • feat: port upstream #884 — parallelize chat workflow startup #118 constraint recorded: the "single totally-ordered chunk log"
    invariant (parallel computation, serialized emission into one
    getWritable) noted in the feat: port upstream #884 — parallelize chat workflow startup #118 ticket.
  • git diff --check clean; RED and GREEN commits captured in the TDD audit
    trail; change committed, pushed to the fork, and a PR opened against
    dennisonbertram/fork-open-agents.

Assumptions and alternatives

Assumptions

  1. The exact version pins (workflow@5.0.0-beta.5, @workflow/ai@5.0.0-beta.4)
    are still installable from the npm registry. If they have been yanked, the
    latest stable beta in the ^5.0.0-beta.5 / ^5.0.0-beta.4 range should be
    used after re-running the POC typecheck harness to verify the transport .d.ts
    is still byte-compatible.
  2. The three fork source files remain byte-identical to 712e1f49^ (the
    reflection confirmed this; the current git show confirms the fork has not
    touched these files since). Any divergence discovered at port time means the
    patch must be hand-applied rather than cherry-picked.
  3. The AI SDK (ai@6.0.168, @ai-sdk/react@3.0.170) stays pinned. If a
    subsequent grooming step bumps ai, re-verify the @workflow/ai peer
    constraint.

Alternatives considered

  • Preserve fork recovery vs adopt upstream WorkflowChatTransport recovery:
    Chosen approach is BOTH — adopt the v5 transport for mid-stream-drop recovery
    while keeping all fork-only recovery logic (iOS, speculative probe, visibility
    probe) that upstream never exercised. Upstream-only approach (delete fork
    recovery logic) would regress iOS stop behavior. Fork-only approach (keep
    DefaultChatTransport) foregoes the deterministic chunk-index resume protocol.
  • Caret ranges vs exact pins for beta deps: Chosen is exact pins. The
    dependency-pinning hazard (mismatched beta peers on fresh install) is well-documented
    in the reflection. The alternative (caret ranges, rely on lockfile) is riskier
    for fast-moving betas in CI environments that may reinstall.
  • Negative initialStartIndex for refresh-resume: Out of scope for parity.
    Would require opting into a new v5 capability that the current behavior contract
    does not require and that has not been user-tested.

Grooming clarifications (2026-05-31)

Groomed by: issue-groomer autonomous agent, read-only research pass.
Grooming verdict: STATUS:READY — the reflection doc provides a GO-WITH-CONDITIONS
decision backed by a 10/10 POC, all conditions are documentable as a concrete
implementation plan (the DoD above), and no genuine unresolved strategic decision
remains.

What this grooming pass confirmed (grounding against live fork state):

  • apps/web/lib/abortable-chat-transport.ts still imports DefaultChatTransport
    from "ai" (byte-identical to pre-feat: unblock authenticated production canary and wire journey harnesses (#866) #883 upstream as claimed). Direct read
    confirmed.
  • apps/web/package.json still has workflow: "^4.2.0-beta.72" and no
    @workflow/ai entry. Direct read confirmed.
  • Latest migration is 0047_cheerful_thunderbolt_ross.sql — no migration needed
    for this port. Confirmed.
  • apps/web/app/api/chat/[chatId]/stream/route.ts byte-identical to pre-feat: unblock authenticated production canary and wire journey harnesses (#866) #883:
    no startIndex parsing, no x-workflow-stream-tail-index header,
    getReadable() called with no arguments. Direct read confirmed.
  • apps/web/app/sessions/[sessionId]/chats/[chatId]/stream-recovery-policy.ts
    still has the full stall-detection branch (lines 60–72) and
    shouldScheduleStallRecovery returns a real computation. Direct read confirmed.
  • use-session-chat-runtime.ts contains the iOS userStoppedRef mitigation
    (lines 155–249) and speculative mount-resume probe (lines 296–354). Direct read
    confirmed. These are fork-only; upstream commit 712e1f4 does not touch this file.
  • Upstream commit 712e1f49 is reachable; git show 712e1f49 --stat confirmed
    the 7-file diff (+244/-73 lines).
  • bun.lock in the upstream commit shows the full @workflow/* constellation
    bumping from v4 to v5.0.0-beta.5, and @workflow/ai@5.0.0-beta.4 being added.
    Fork's bun.lock does not have @workflow/ai. Confirmed.

What required no update: the existing issue body (as groomed in the prior
session) is already highly complete and accurate. This grooming pass validated
the body against current fork state and upstream commit reality. The only
substantive additions in this pass: (a) explicit sequencing table (depends on /
blocks); (b) confirmed migration number (0047); (c) this provenance block.

Risks flagged for human awareness (not blockers):

  1. Real-device iOS smoke and double-resume interaction (transport auto-reconnect
    • visibility-probe + speculative-probe) are UNPROVEN on real hardware. The DoD
      requires this proof before the PR can be considered done. No automated harness
      can substitute.
  2. The workflow runtime v5 major bump covers more surface than the POC validated
    (POC tested transport only, not start/getRun/claimActiveStream/cancel).
    Full managed-runtime smoke is required in the green phase, not optional.
  3. Beta version availability: if workflow@5.0.0-beta.5 or
    @workflow/ai@5.0.0-beta.4 have been yanked from npm by the time the port
    begins, the implementer must re-run the POC typecheck with the next available
    compatible beta and confirm the .d.ts surface is still compatible.

Provenance: research sources — docs/plans/upstream-883-reflection.md (full
read), docs/context/command-intent.md (full read), gh issue view 117 (full
read), git show 712e1f49 --stat + full diff (confirmed), direct reads of
abortable-chat-transport.ts, stream/route.ts, stream-recovery-policy.ts,
use-session-chat-runtime.ts, package.json, migration directory,
POC/upstream-883-workflow-v5/FINDINGS.md (header).


Guardrail: created on dennisonbertram/fork-open-agents (the fork). Do not push
any artifact to vercel-labs/open-agents.

Metadata

Metadata

Labels

status:readyGroomed and ready for autonomous implementationtype:featureFeature work

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions