Skip to content

fix(web): stabilize fork action during streaming - #1399

Merged
tiann merged 1 commit into
tiann:mainfrom
KorenKrita:agent/fix-pi-fork-footer-jitter
Aug 7, 2026
Merged

fix(web): stabilize fork action during streaming#1399
tiann merged 1 commit into
tiann:mainfrom
KorenKrita:agent/fix-pi-fork-footer-jitter

Conversation

@KorenKrita

Copy link
Copy Markdown
Contributor

Summary

  • keep the Fork action attached to the latest completed conversation boundary while the main agent is streaming
  • exclude the active turn, starting at the latest invoked user message, from completed-boundary selection
  • cover Pi-like streaming shapes where agent events split assistant output into transient visible groups

Root cause

latestCompletedBoundaryId was derived from every current visibleBlocks snapshot. During Pi streaming, agent events can split assistant output into multiple temporary groups, so the candidate moved between message cards. The Fork button was mounted and unmounted as a result, shifting the timestamp beside it.

Validation

  • bun run typecheck
  • bun run test:web (241 files, 2157 tests)
  • focused assistant-runtime.test.ts regression coverage

Closes #1396

@KorenKrita
KorenKrita force-pushed the agent/fix-pi-fork-footer-jitter branch from a242d73 to 502312c Compare August 6, 2026 23:56

@github-actions github-actions 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.

Findings

  • [Major] Keep the completed boundary stable while the new prompt is still queued — SessionChat filters uninvoked user rows out of visibleBlocks at web/src/components/SessionChat.tsx:1195, but the hub sets thinking=true as soon as the send is queued. At web/src/lib/assistant-runtime.ts:384, findLastIndex then selects the previous completed turn's user as the active-turn start, so its assistant response is excluded and the Fork action moves backward or disappears until the CLI stamps invokedAt.

    Suggested fix:

    export function findLatestCompletedBoundaryId(
        blocks: readonly VisibleChatBlock[],
        isRunning: boolean,
        activeTurnStartedAt: number | null
    ): string | null {
        const assigned = assignThreadMessageIds(blocks)
        let limit = assigned.length
    
        if (isRunning) {
            const activeTurnStart = assigned.findLastIndex(({ block }) => (
                visibleBlockRole(block) === 'user'
                && (
                    activeTurnStartedAt == null
                        ? block.invokedAt != null
                        : (block.invokedAt ?? block.createdAt) >= activeTurnStartedAt
                )
            ))
            if (activeTurnStart >= 0) limit = activeTurnStart
            else if (activeTurnStartedAt == null) return null
        }
        // existing candidate scan
    }
    
    findLatestCompletedBoundaryId(
        visibleBlocks,
        props.session.thinking,
        props.session.activeTurnStartedAt
    )

Summary

  • Review mode: initial
  • One major boundary-selection gap remains during the queued-to-invoked handoff.

Testing

  • Not run (automation). Add a regression case with completed [u1, a1], isRunning=true, and an active-turn timestamp newer than both visible blocks; the expected boundary remains agent-text:a1.

HAPI Bot

Comment thread web/src/lib/assistant-runtime.ts Outdated
@KorenKrita
KorenKrita force-pushed the agent/fix-pi-fork-footer-jitter branch from 502312c to ef25ea6 Compare August 7, 2026 00:05

@github-actions github-actions 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.

Findings

  • [Major] Keep the boundary at the first user message of a steered turn - while Pi is streaming, an immediate follow-up is delivered as a steer and becomes another invoked user row without ending the main thinking state. activeTurnStartedAt remains the original prompt timestamp (hub/src/sync/sessionCache.ts:492), but findLastIndex selects the later steer, so assistant blocks already streamed within the same active turn are scanned as "completed" and the Fork action moves onto that transient card. Evidence: web/src/lib/assistant-runtime.ts:385.

    Suggested fix:

    const activeTurnStart = activeTurnStartedAt === null
        ? assigned.findLastIndex(({ block }) => (
            visibleBlockRole(block) === 'user' && block.invokedAt != null
        ))
        : assigned.findIndex(({ block }) => (
            visibleBlockRole(block) === 'user'
            && (block.invokedAt ?? block.createdAt) >= activeTurnStartedAt
        ))

Summary
Review mode: follow-up after new commits

  • The prior queued-before-invocation gap is addressed, but Pi steering can still advance the boundary into the active turn.

Testing

  • Not run (automation). Repository execution is prohibited by this review workflow; GitHub test and pr-review checks were pending when reviewed. Add a regression with [completedUser, completedAssistant, activeUser, streamingAssistant, steerUser], isRunning=true, and activeTurnStartedAt equal to activeUser so the result remains completedAssistant.

HAPI Bot

Comment thread web/src/lib/assistant-runtime.ts Outdated
@KorenKrita
KorenKrita force-pushed the agent/fix-pi-fork-footer-jitter branch from ef25ea6 to 586ae5e Compare August 7, 2026 00:15

@github-actions github-actions 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.

Findings

  • [Major] Cut off active output even when the active user row is outside the tail window — a fresh tail request loads only 200 messages and the client later caps regular rows at 400 (web/src/lib/message-window-store.ts:39, web/src/lib/message-window-store.ts:623). A long running turn can therefore omit its starting user row while retaining newer assistant output. When that happens, activeTurnStart is -1; because the explicit-marker branch leaves limit unchanged, the scan promotes the first visible streaming assistant card to the current Fork boundary, recreating the movement this PR is intended to stop. Evidence web/src/lib/assistant-runtime.ts:393.

    Suggested fix:

    if (activeTurnStart >= 0) {
        limit = activeTurnStart
    } else if (activeTurnStartedAt === null) {
        return null
    } else {
        const firstActiveBlock = assigned.findIndex(({ block }) =>
            (block.invokedAt ?? block.createdAt) >= activeTurnStartedAt
        )
        if (firstActiveBlock >= 0) limit = firstActiveBlock
    }

Summary
Review mode: follow-up after new commits

  • The previous steering finding is resolved. One active-turn truncation path still exposes transient streaming output as the current fork boundary.

Testing

  • Not run (automation; executing PR code is prohibited by this review workflow). GitHub test and pr-review checks were in progress when reviewed. Add a regression where the active user row is absent but a visible assistant block has createdAt >= activeTurnStartedAt; the result must remain the prior completed boundary, or null when no prior boundary is visible.

HAPI Bot

Comment thread web/src/lib/assistant-runtime.ts
@KorenKrita
KorenKrita force-pushed the agent/fix-pi-fork-footer-jitter branch from 586ae5e to b37bc7d Compare August 7, 2026 00:27

@github-actions github-actions 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.

Findings

  • None.

Summary
Review mode: follow-up after new commits

  • The previous Major finding at web/src/lib/assistant-runtime.ts:393 is resolved by the timestamp-based fallback cutoff when the active user row is outside the tail window. No additional correctness, security, regression, data-loss, performance, or maintainability issues found in the latest full diff.
  • Residual risk: browser-level rendering behavior for assistant-card joining during live Pi streaming is covered indirectly by helper tests, not an end-to-end streaming UI test.

Testing

  • Not run locally (automation; executing PR code is prohibited by this review workflow). GitHub test check passed. Focused coverage exists in web/src/lib/assistant-runtime.test.ts:139 for streaming reshapes, queued prompts, steering, tail truncation, completion, and missing markers.

HAPI Bot

@tiann
tiann merged commit feb4099 into tiann:main Aug 7, 2026
2 checks passed
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.

Fork action and timestamp jitter during Pi streaming

2 participants