Files
orca/src/shared/native-chat-merge.ts
T
f199a20c3a Preserve OpenCode reasoning and recorded patches in native history (#24790)
* Use bounded OpenCode context for vault session continuation

OpenCode database and synthetic row paths are not text transcripts. Use the
vault preview or captured pane context, preserving actual transcript paths
containing a hash and supporting both OpenCode lanes and Windows paths.

Adapted the intent of #11859 and extended it to actual installed v2 vault rows.

Co-authored-by: mrcha033 <mrcha033@users.noreply.github.com>

* Read real OpenCode sessions in terminal-backed native Chat

Reuse the bounded AI Vault SQLite worker for v1 and v2 session pages and live updates. Keep terminal input as the real execution path and pace OpenCode Stop through its two-Escape interrupt.

Co-authored-by: xodmd45-ctrl <xodmd45-ctrl@users.noreply.github.com>

* fix(opencode): publish approval cards for permission requests

* Send OpenCode native approval through its Enter selector

* Resolve mobile Chat readability for folder workspaces

* Bound OpenCode part batches and preserve v2 image attachments

* Prefer live migrated OpenCode sessions over legacy copies

* Consolidate mobile Chat eligibility test imports

* Consolidate OpenCode SQLite protocol type imports

* fix(native-chat): preserve OpenCode reasoning and patch parts

Separate genuine reasoning from answer blocks in both native SQLite schemas and retain recorded patches as completed patch tools. Keep each database row together at page boundaries so the existing raw-row cursors cannot drop half of a mixed row.

Adapted from @akhan157's OpenCode native history work in #13287 at bb661d10d716764fb472d824cd434678875b1947; retains the current bounded reader and account discovery instead of restoring the older capture and cursor implementation.

Verified against genuine private installed 2.0.16 and official 1.18.30 CLI ingestion.

Co-authored-by: Adnan Khan <adnank11427@gmail.com>

* fix(native-chat): keep split OpenCode rows intact on desktop and mobile

Preserve the native reader's bounded OpenCode row groups in paired reads and snapshot/replacement frames so a second presentation-count slice cannot drop reasoning while advancing the database cursor. Sort derived reasoning before its answer under the same provider timestamp while retaining journal order.

These two boundaries were reproduced with genuine installed 2.0.16 and official 1.18.30 sessions in a hidden desktop renderer and the current mobile view over an actual authenticated encrypted pairing.

Completes the semantic presentation from @akhan157's #13287 without importing its older clipping or cursor implementation.

Co-authored-by: Adnan Khan <adnank11427@gmail.com>

* fix(native-chat): keep reasoning and answers together in live windows

* Bound OpenCode transcript RPC pages and present omission notices

* Bound OpenCode transcript RPC pages and present omission notices

* Bound OpenCode transcript RPC pages and present omission notices

* Update native worker oversized-history notice contract

---------

Co-authored-by: mrcha033 <mrcha033@users.noreply.github.com>
Co-authored-by: xodmd45-ctrl <xodmd45-ctrl@users.noreply.github.com>
Co-authored-by: nwparker <nwparker@users.noreply.github.com>
2026-10-03 22:04:52 -07:00

123 lines
4.4 KiB
TypeScript

// Pure id-dedup and windowing for both desktop and mobile native-chat streams.
import {
nativeChatMessagesShareTranscriptRow,
NATIVE_CHAT_SOURCE_PRIORITY,
type NativeChatMessage,
type NativeChatSource
} from './native-chat-types'
export type NativeChatSourcePriority = Record<NativeChatSource, number>
/** Merge a batch of incoming messages into an existing ordered list, deduping by
* `id`. A re-emitted id replaces the existing entry in place only when the
* incoming source is at least as authoritative (higher-or-equal priority); new
* ids append in arrival order. First-seen order is preserved; never mutates the
* input; returns a new array (or the existing reference for an empty batch). */
export function mergeNativeChatMessagesWith(
existing: readonly NativeChatMessage[],
incoming: readonly NativeChatMessage[],
priority: NativeChatSourcePriority
): NativeChatMessage[] {
if (incoming.length === 0) {
return existing as NativeChatMessage[]
}
const merged = [...existing]
const indexById = new Map<string, number>()
merged.forEach((message, index) => indexById.set(message.id, index))
applyIncoming(merged, indexById, incoming, priority)
return merged
}
export function mergeNativeChatMessages(
existing: readonly NativeChatMessage[],
incoming: readonly NativeChatMessage[]
): NativeChatMessage[] {
return mergeNativeChatMessagesWith(existing, incoming, NATIVE_CHAT_SOURCE_PRIORITY)
}
/** Keep the recent tail without splitting a provider row. A non-positive limit means no cap. */
export function boundNativeChatWindow(
messages: readonly NativeChatMessage[],
limit: number
): NativeChatMessage[] {
if (limit <= 0 || messages.length <= limit) {
return messages as NativeChatMessage[]
}
let start = messages.length - limit
while (start > 0 && nativeChatMessagesShareTranscriptRow(messages[start - 1], messages[start])) {
start -= 1
}
return messages.slice(start)
}
/** Stateful id-dedup merger that caches the id→index map across appends so a
* streaming run pays O(incoming) per frame instead of O(existing+incoming).
* `replaceList` resets the cache for a new base (initial read / loadEarlier);
* `applyAppend` folds a live batch in. Output equals the pure
* `mergeNativeChatMessagesWith` for every input (locked by the oracle test). */
export type NativeChatMerger = {
list: NativeChatMessage[]
readonly indexById: Map<string, number>
readonly priority: NativeChatSourcePriority
}
export function createNativeChatMerger(
priority: NativeChatSourcePriority = NATIVE_CHAT_SOURCE_PRIORITY
): NativeChatMerger {
return { list: [], indexById: new Map(), priority }
}
/** Reset the merger to a new base list (replace, don't merge). Used on the
* initial read and loadEarlier re-reads, which return an ordered tail. */
export function replaceList(merger: NativeChatMerger, list: readonly NativeChatMessage[]): void {
merger.list = [...list]
merger.indexById.clear()
merger.list.forEach((message, index) => merger.indexById.set(message.id, index))
}
/** Fold a live batch into the merger, deduping by id with source precedence.
* Returns a new `list` reference (so React re-renders) and updates the cached
* index incrementally — O(incoming), never re-scanning the existing list. */
export function applyAppend(
merger: NativeChatMerger,
incoming: readonly NativeChatMessage[],
limit?: number
): NativeChatMessage[] {
if (incoming.length === 0) {
return merger.list
}
const next = [...merger.list]
applyIncoming(next, merger.indexById, incoming, merger.priority)
const bounded = limit === undefined ? next : boundNativeChatWindow(next, limit)
if (bounded !== next) {
// Why: trimming shifts every cached index, so rebuild at the window boundary.
replaceList(merger, bounded)
return merger.list
}
merger.list = next
return next
}
// Shared inner loop: one id-dedup + precedence rule for both the pure function
// and the stateful merger, so the two can never drift.
function applyIncoming(
list: NativeChatMessage[],
indexById: Map<string, number>,
incoming: readonly NativeChatMessage[],
priority: NativeChatSourcePriority
): void {
for (const message of incoming) {
const at = indexById.get(message.id)
if (at === undefined) {
indexById.set(message.id, list.length)
list.push(message)
continue
}
const current = list[at]!
if (priority[message.source] >= priority[current.source]) {
list[at] = message
}
}
}