Skip to content

feat: remote delivery ordering with persistent process segments#1435

Merged
zerob13 merged 3 commits intodevfrom
codex/remote-message-with-emoji
Apr 8, 2026
Merged

feat: remote delivery ordering with persistent process segments#1435
zerob13 merged 3 commits intodevfrom
codex/remote-message-with-emoji

Conversation

@zerob13
Copy link
Copy Markdown
Collaborator

@zerob13 zerob13 commented Apr 8, 2026

Summary

  • Replace the remote trace + answer dual-track delivery flow with ordered delivery segments that follow assistant block order across Telegram and Feishu
  • Keep process logs persistent, append later tool/answer phases as new messages, and avoid collapsing final output back into the first answer message
  • Add shared tool-call preview summarization so desktop tool blocks and remote process logs use the same argument preview logic
  • Update the remote process log spec and extend runtime/store/renderer tests to lock in ordered segment behavior
  • Implementation approach: model remote output as ordered process | answer | terminal segments in the snapshot, then let each transport sync segments by key so only the current segment tail stays editable while later phases append in order

Testing

  • pnpm vitest run test/main/presenter/remoteControlPresenter/remoteBlockRenderer.test.ts test/main/presenter/remoteControlPresenter/remoteConversationRunner.test.ts test/main/presenter/remoteControlPresenter/remoteBindingStore.test.ts test/main/presenter/remoteControlPresenter/telegramPoller.test.ts test/main/presenter/remoteControlPresenter/feishuRuntime.test.ts
  • pnpm run format
  • pnpm run i18n
  • pnpm run lint
  • pnpm run typecheck

Summary by CodeRabbit

  • New Features

    • Remote conversations in Telegram and Feishu now show an ordered, persistent process log of tool calls, assistant answers, and final updates; tool-call previews are summarized and truncated for compact display alongside streaming text.
  • Bug Fixes

    • Prevents duplicate final updates, appends later segments without overwriting earlier answers, and suppresses redundant “no assistant response” fallbacks for tool-only turns.
  • Documentation

    • Added a formal specification for the Remote Process Log behavior and formatting.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

📝 Walkthrough

Walkthrough

Replaces ephemeral remote status/content messages with an ordered, persistent segments delivery model (kinds: process/answer/terminal) across Feishu and Telegram runtimes, adds trace/segment builders and centralized tool-call summary logic, and updates persistence, syncing, and tests to use the new segments shape.

Changes

Cohort / File(s) Summary
Spec & Types
docs/specs/remote-process-log/spec.md, src/main/presenter/remoteControlPresenter/types.ts, src/shared/lib/toolCallSummary.ts
Adds Remote Process Log spec, new RemoteDeliverySegment type, and a shared summarizeToolCallPreview() utility.
Block rendering & trace builders
src/main/presenter/remoteControlPresenter/services/remoteBlockRenderer.ts
Adds buildRemoteTraceText() and buildRemoteDeliverySegments(), REMOTE_NO_RESPONSE_TEXT, and helpers for trace formatting and segment construction.
Conversation snapshot & runner
src/main/presenter/remoteControlPresenter/services/remoteConversationRunner.ts
RemoteConversationSnapshot now includes traceText and deliverySegments; snapshots use shared trace text and delivery-segment construction and unify fallback text handling.
Delivery state store
src/main/presenter/remoteControlPresenter/services/remoteBindingStore.ts
Refactors persisted RemoteDeliveryState to a segments array (per-segment messageIds/lastText) and updates clone/read/write and compatibility checks.
Feishu runtime
src/main/presenter/remoteControlPresenter/feishu/feishuRuntime.ts
Replaces status/content sync with segment-based syncing (deliverySegments), appends terminal segments, updates persistence/rehydration, and removes legacy deletion flows.
Telegram poller/runtime
src/main/presenter/remoteControlPresenter/telegram/telegramPoller.ts
Mirrors Telegram delivery to segment model, derives segments from snapshot (legacy fallback to trace/text), syncs per-segment messages with compatibility checks, and removes legacy message-id/deletion logic.
UI component
src/renderer/src/components/message/MessageBlockToolCall.vue
Replaces local tool-call preview logic with imported summarizeToolCallPreview() and removes older inline-summary helpers.
Tests
test/main/.../feishuRuntime.test.ts, test/main/.../telegramPoller.test.ts, test/main/.../remoteBlockRenderer.test.ts, test/main/.../remoteBindingStore.test.ts, test/main/.../remoteConversationRunner.test.ts
Updates tests to assert segments shape, adds coverage for trace text, segment merging, terminal-appending, tool-only turns, null messageId preservation, and removes expectations about deleted temporary status messages.

Sequence Diagram

sequenceDiagram
    participant Conv as Conversation Runner
    participant Render as Block Renderer
    participant Snapshot as Delivery Snapshot
    participant Runtime as Feishu/Telegram Runtime
    participant Remote as Remote Platform

    Conv->>Render: buildRemoteDeliverySegments(blocks)
    Render->>Render: coalesce tool calls & answers into segments
    Render-->>Snapshot: deliverySegments[]

    Conv->>Render: buildRemoteTraceText(blocks)
    Render-->>Snapshot: traceText

    Snapshot-->>Runtime: RemoteConversationSnapshot (deliverySegments, traceText)

    Runtime->>Runtime: syncDeliverySegments()
    loop for each segment in order
        Runtime->>Remote: sendText / editText for segment
        Remote-->>Runtime: messageId(s)
        Runtime->>Runtime: persist segment.messageIds
    end

    alt final/timeout => append terminal
        Runtime->>Runtime: determine terminal text (finalText/fullText/text or timeout)
        Runtime->>Remote: sendText(terminal)
        Remote-->>Runtime: terminalMessageId
        Runtime->>Runtime: persist terminal segment
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped through traces, nibbling each line,

process, answer, terminal — ordered just fine.
No fleeting “thinking…” left in the grass,
Persistent segments now hold every pass.
I thumped a tiny key and left a carrot-sign.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main architectural change: converting to ordered delivery segments with persistent process tracking instead of temporary dual-track output.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/remote-message-with-emoji

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 and usage tips.

@zerob13 zerob13 changed the title Fix remote delivery ordering with persistent process segments feat: remote delivery ordering with persistent process segments Apr 8, 2026
@zerob13 zerob13 marked this pull request as ready for review April 8, 2026 06:23
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/main/presenter/remoteControlPresenter/services/remoteConversationRunner.ts (1)

493-511: Minor inconsistency in no-response text.

Line 500 uses an inline string 'No assistant response was produced.' while lines 504 and 507 use the REMOTE_NO_RESPONSE_TEXT constant. Consider using the constant consistently.

♻️ Proposed fix for consistency
       if (!trackedMessage) {
         const completed = !activeGeneration && session.status !== 'generating'
         if (completed) {
           this.bindingStore.clearActiveEvent(endpointKey)
         }
         return {
           messageId: null,
-          text: completed ? 'No assistant response was produced.' : '',
+          text: completed ? REMOTE_NO_RESPONSE_TEXT : '',
           traceText: '',
           deliverySegments: [],
           statusText: completed ? '' : buildRemoteStatusText([]),
           finalText: completed ? REMOTE_NO_RESPONSE_TEXT : '',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/presenter/remoteControlPresenter/services/remoteConversationRunner.ts`
around lines 493 - 511, Replace the inline string 'No assistant response was
produced.' with the existing constant REMOTE_NO_RESPONSE_TEXT for consistency;
locate the early-return block that checks trackedMessage (around the completed
calculation and this.bindingStore.clearActiveEvent(endpointKey)) and update the
fields text, finalText, and fullText to use REMOTE_NO_RESPONSE_TEXT so all
no-response outputs are uniform (ensure statusText still uses
buildRemoteStatusText([]) when not completed).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/presenter/remoteControlPresenter/feishu/feishuRuntime.ts`:
- Around line 618-625: The messageId collection logic in the block using
existing, nextChunks and messageIds is compacting away missing ids which breaks
alignment with chunkFeishuText(normalized) and causes later updateText() calls
to target the wrong segment; inside the loop that calls
this.deps.client.sendText(target, chunk) (and in the similar loops at the other
locations), preserve positional holes by pushing a placeholder (e.g. null) into
messageIds when sendText() returns null (or alternatively, if you prefer the
other behavior, mark the whole segment as non-editable as soon as any sendText()
returns null), and ensure downstream code that reads messageIds expects and
respects those null slots when calling updateText() so indices remain aligned
with nextChunks/chunkFeishuText(normalized).
- Around line 537-564: In appendTerminalDeliverySegment: currently you bail out
if any segment has kind === 'answer', which prevents adding terminal failure
text after a partial streamed answer; change the logic to only skip appending
when the last answer segment's text equals normalized (i.e., duplicate
terminal), otherwise allow adding the terminal segment; keep the existing checks
for sourceMessageId, normalized empty, and REMOTE_NO_RESPONSE_TEXT, and
reference the function appendTerminalDeliverySegment and the segment properties
kind/text/sourceMessageId to locate and update the condition.

In `@src/main/presenter/remoteControlPresenter/telegram/telegramPoller.ts`:
- Around line 497-524: The current appendTerminalDeliverySegment function
short-circuits whenever any 'answer' segment exists, which drops terminal
failures after partial answers; change the logic to only skip adding the
terminal when the last segment is an 'answer' whose text matches the normalized
terminal text. In appendTerminalDeliverySegment, replace the
segments.some((segment) => segment.kind === 'answer') check with a check that
gets the last element (e.g., const last = segments[segments.length - 1]) and
returns segments only if last?.kind === 'answer' && last.text === normalized;
leave the REMOTE_NO_RESPONSE_TEXT guard and the terminal push behavior
unchanged.

---

Nitpick comments:
In
`@src/main/presenter/remoteControlPresenter/services/remoteConversationRunner.ts`:
- Around line 493-511: Replace the inline string 'No assistant response was
produced.' with the existing constant REMOTE_NO_RESPONSE_TEXT for consistency;
locate the early-return block that checks trackedMessage (around the completed
calculation and this.bindingStore.clearActiveEvent(endpointKey)) and update the
fields text, finalText, and fullText to use REMOTE_NO_RESPONSE_TEXT so all
no-response outputs are uniform (ensure statusText still uses
buildRemoteStatusText([]) when not completed).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d1e89d24-b512-4f10-a875-7f58f7643dc1

📥 Commits

Reviewing files that changed from the base of the PR and between 624cefc and f1833bc.

📒 Files selected for processing (14)
  • docs/specs/remote-process-log/spec.md
  • src/main/presenter/remoteControlPresenter/feishu/feishuRuntime.ts
  • src/main/presenter/remoteControlPresenter/services/remoteBindingStore.ts
  • src/main/presenter/remoteControlPresenter/services/remoteBlockRenderer.ts
  • src/main/presenter/remoteControlPresenter/services/remoteConversationRunner.ts
  • src/main/presenter/remoteControlPresenter/telegram/telegramPoller.ts
  • src/main/presenter/remoteControlPresenter/types.ts
  • src/renderer/src/components/message/MessageBlockToolCall.vue
  • src/shared/lib/toolCallSummary.ts
  • test/main/presenter/remoteControlPresenter/feishuRuntime.test.ts
  • test/main/presenter/remoteControlPresenter/remoteBindingStore.test.ts
  • test/main/presenter/remoteControlPresenter/remoteBlockRenderer.test.ts
  • test/main/presenter/remoteControlPresenter/remoteConversationRunner.test.ts
  • test/main/presenter/remoteControlPresenter/telegramPoller.test.ts

Comment on lines +618 to 625
if (!existing) {
const messageIds: string[] = []
for (const chunk of nextChunks) {
const messageId = await this.deps.client.sendText(target, chunk)
if (messageId) {
messageIds.push(messageId)
}

await this.deps.client.updateText(contentMessageIds[index], nextChunks[index])
continue
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep Feishu chunk ids aligned with chunk indexes.

Lines 620-623, 645-649, and 672-675 compact away missing ids. If one sendText() call returns null and a later chunk succeeds, messageIds[index] no longer matches chunkFeishuText(normalized)[index], so the next updateText() can target the wrong message. Preserve positional holes or stop tracking the segment as editable when any chunk id is missing.

Also applies to: 643-649, 671-675

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/remoteControlPresenter/feishu/feishuRuntime.ts` around
lines 618 - 625, The messageId collection logic in the block using existing,
nextChunks and messageIds is compacting away missing ids which breaks alignment
with chunkFeishuText(normalized) and causes later updateText() calls to target
the wrong segment; inside the loop that calls this.deps.client.sendText(target,
chunk) (and in the similar loops at the other locations), preserve positional
holes by pushing a placeholder (e.g. null) into messageIds when sendText()
returns null (or alternatively, if you prefer the other behavior, mark the whole
segment as non-editable as soon as any sendText() returns null), and ensure
downstream code that reads messageIds expects and respects those null slots when
calling updateText() so indices remain aligned with
nextChunks/chunkFeishuText(normalized).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/presenter/remoteControlPresenter/telegram/telegramPoller.ts`:
- Around line 60-67: The TelegramRemoteDeliveryState currently types
segments[].messageIds as number[] which compacts away null placeholders; update
the contract and handling so messageIds preserves null slots (use Array<number |
null> or equivalent) and ensure code paths that read/compare against
chunkTelegramText(existing.lastText) do not strip or compact nulls—when
restoring or diffing segments in the Telegram poller (symbols:
TelegramRemoteDeliveryState, segments, messageIds, chunkTelegramText,
existing.lastText) keep the same length alignment by preserving null entries
exactly as Feishu does so index-based edits target the correct message
positions.
- Around line 497-509: In appendTerminalDeliverySegment, the terminal dedupe
only compares finalText against the very last segment; change it to scan
backward through segments to find the most recent segment where segment.kind ===
'answer' (ignore intervening 'process' segments) and compare that answer.text to
the normalized finalText; if they match, return segments unchanged, otherwise
append the terminal segment as before. Ensure you reference
RemoteDeliverySegment and the 'answer'/'process' kind values and preserve
existing behavior when sourceMessageId is null or normalized is empty.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 163ce6cf-a3e9-433b-b2cc-6ab71bb3458b

📥 Commits

Reviewing files that changed from the base of the PR and between f1833bc and 277a319.

📒 Files selected for processing (5)
  • src/main/presenter/remoteControlPresenter/feishu/feishuRuntime.ts
  • src/main/presenter/remoteControlPresenter/services/remoteBindingStore.ts
  • src/main/presenter/remoteControlPresenter/telegram/telegramPoller.ts
  • test/main/presenter/remoteControlPresenter/feishuRuntime.test.ts
  • test/main/presenter/remoteControlPresenter/telegramPoller.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/presenter/remoteControlPresenter/services/remoteBindingStore.ts
  • test/main/presenter/remoteControlPresenter/telegramPoller.test.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
test/main/presenter/remoteControlPresenter/telegramPoller.test.ts (2)

1050-1060: Minor: Redundant assertion can be simplified.

The assertion at lines 1050-1057 checks that sendMessage wasn't called with 'Final answer' plus a third argument, while lines 1058-1060 already verify exactly one call with 'Final answer'. The filter-based assertion is sufficient to catch duplicates.

Consider removing lines 1050-1057 if the intent is simply to prevent duplicate sends, or add a comment clarifying that the first assertion specifically guards against reply markup being attached to the final answer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/remoteControlPresenter/telegramPoller.test.ts` around
lines 1050 - 1060, The two assertions around client.sendMessage are redundant:
remove the negative assertion that checks sendMessage wasn't called with ({
chatId: 100, messageThreadId: 0 }, 'Final answer', expect.anything()) and keep
the existing filter-based assertion that client.sendMessage was called exactly
once with 'Final answer' (client.sendMessage.mock.calls filter), or if the
intent is to ensure no reply markup was attached, replace the negative assertion
with a clear comment stating that client.sendMessage should not include a third
arg for the final answer; update the test referencing client.sendMessage and
'Final answer' accordingly.

721-828: Consider adding positive assertions to strengthen coverage.

The test validates that null messageId holes don't cause incorrect edits (good defensive check), but only uses negative assertions. Consider adding a positive assertion to verify the expected behavior—whether new messages are sent or the existing state is preserved—to catch regressions where the implementation silently does nothing.

For example, after the waitFor block:

// Verify implementation handled the change (either sent new messages or preserved state)
expect(bindingStore.rememberRemoteDeliveryState).toHaveBeenCalled()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/remoteControlPresenter/telegramPoller.test.ts` around
lines 721 - 828, The test only asserts that an unwanted edit wasn't made; add a
positive assertion to ensure the change was actually handled — e.g., after the
vi.waitFor block assert that the bindingStore or client recorded/issued the
expected action (reference bindingStore.rememberRemoteDeliveryState and
client.editMessageText/client.sendMessage) so the test catches silent no-ops;
add a spy or Jest/Vi matcher on bindingStore.rememberRemoteDeliveryState (or
client.sendMessage) and assert it was called with the expected delivery/state
update after poller.start() completes, then keep the existing negative assertion
and poller.stop().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/main/presenter/remoteControlPresenter/telegramPoller.test.ts`:
- Around line 1050-1060: The two assertions around client.sendMessage are
redundant: remove the negative assertion that checks sendMessage wasn't called
with ({ chatId: 100, messageThreadId: 0 }, 'Final answer', expect.anything())
and keep the existing filter-based assertion that client.sendMessage was called
exactly once with 'Final answer' (client.sendMessage.mock.calls filter), or if
the intent is to ensure no reply markup was attached, replace the negative
assertion with a clear comment stating that client.sendMessage should not
include a third arg for the final answer; update the test referencing
client.sendMessage and 'Final answer' accordingly.
- Around line 721-828: The test only asserts that an unwanted edit wasn't made;
add a positive assertion to ensure the change was actually handled — e.g., after
the vi.waitFor block assert that the bindingStore or client recorded/issued the
expected action (reference bindingStore.rememberRemoteDeliveryState and
client.editMessageText/client.sendMessage) so the test catches silent no-ops;
add a spy or Jest/Vi matcher on bindingStore.rememberRemoteDeliveryState (or
client.sendMessage) and assert it was called with the expected delivery/state
update after poller.start() completes, then keep the existing negative assertion
and poller.stop().

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d8bec7b0-fc8f-4c10-b970-03bcaa3d968d

📥 Commits

Reviewing files that changed from the base of the PR and between 277a319 and ea96081.

📒 Files selected for processing (2)
  • src/main/presenter/remoteControlPresenter/telegram/telegramPoller.ts
  • test/main/presenter/remoteControlPresenter/telegramPoller.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/presenter/remoteControlPresenter/telegram/telegramPoller.ts

@zerob13 zerob13 merged commit 2eccf66 into dev Apr 8, 2026
3 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.

1 participant