Native chat: a message is never marked not delivered after a crash, and reopening a chat stops reading the transcript (#30580)

* Native chat: never mark a message not delivered after a crash; reopening a chat stops reading the transcript

After a crash or restart, attach read a window of the agent's saved log and marked any
unconfirmed message missing from it as rejected "not delivered". Absence from a log Orca
does not own is not proof: the window's anchor moves every turn (STA-7416) and its text
filters drop tagged prompts (STA-7453), so messages the agent received were marked
undelivered and retyping them duplicated the question. The read also ran on every attach
(STA-7434).

Delete the inference: the provider history window and its adapter hooks (Claude window and
branch proof, OpenCode stored-messages reader), the submission matcher, and the attach step.
A send the crash boundary leaves unconfirmed stays an ordinary sent message; the interrupted
turn still ends through the existing settlement. Explicit live refusals are unchanged.

A queued card whose hand-off a restart left unconfirmed no longer counts as able to return
to the queue, so it stops keeping an idle chat loaded.

* test(native-chat): update restart fixture calls after history removal

* Hide rewind targets without a saved native boundary

* Run rewind boundary regression across host and renderer

* Keep incoming main package-manager lock unchanged

* Test recovered chat consumer outcomes without delivery inference

* Assign the complete cursor in recovered phone regression fixture

* Supply presentation capture in rewind boundary fixture
This commit is contained in:
Brennan Benson
2026-10-24 00:27:01 -07:00
committed by GitHub
parent c2241d44af
commit 6059a5f7fa
50 changed files with 539 additions and 3728 deletions
@@ -1,111 +0,0 @@
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import Database from '../sqlite/sync-database'
import { readOpenCodeTranscriptPage } from '../native-chat/transcript-opencode-sqlite-query'
import {
openCodeStoredUserMessagesReader,
type OpenCodeTranscriptPageReader
} from './opencode-acp-stored-messages'
const roots: string[] = []
afterEach(() => {
for (const root of roots.splice(0)) {
rmSync(root, { recursive: true, force: true })
}
})
/** An `opencode.db` under a fresh XDG data home, built by `schema`; returns that data home. */
function dataHomeWith(schema: string): string {
const root = mkdtempSync(join(tmpdir(), 'orca-opencode-stored-'))
roots.push(root)
mkdirSync(join(root, 'opencode'))
const db = new Database(join(root, 'opencode', 'opencode.db'))
db.exec(schema)
db.close()
return root
}
/** The bounded reader in-process, standing in for its worker. */
const readInProcess: OpenCodeTranscriptPageReader = async (args) => readOpenCodeTranscriptPage(args)
function readFrom(dataHome: string) {
return openCodeStoredUserMessagesReader(readInProcess)({
env: { HOME: dataHome, USERPROFILE: dataHome, XDG_DATA_HOME: dataHome },
providerSessionId: 'ses_1',
signal: new AbortController().signal
})
}
const OPENCODE_2_TABLES = `CREATE TABLE session_v2 (id TEXT PRIMARY KEY);
CREATE TABLE session_message (id TEXT PRIMARY KEY, session_id TEXT, type TEXT, seq INTEGER,
data TEXT, time_created INTEGER, time_updated INTEGER);`
describe('OpenCode stored user messages', () => {
it("reads the database under the chat's own home, and only its user messages", async () => {
const readPage = vi.fn<OpenCodeTranscriptPageReader>(async () => ({
items: [
{
rowid: 1,
fingerprint: 'a',
message: { id: 'msg_1', role: 'user', blocks: [], timestamp: 5, source: 'transcript' }
},
{
rowid: 2,
fingerprint: 'b',
message: {
id: 'msg_2',
role: 'assistant',
blocks: [],
timestamp: 6,
source: 'transcript'
}
}
],
hasMore: false,
beforeMessageRowId: 1
}))
const home = join('/', 'child-home')
const read = openCodeStoredUserMessagesReader(readPage)
await expect(
read({
env: { HOME: home, USERPROFILE: home },
providerSessionId: 'ses_1',
signal: new AbortController().signal
})
).resolves.toEqual([{ id: 'msg_1', blocks: [], createdAt: 5 }])
expect(readPage).toHaveBeenCalledWith(
expect.objectContaining({
dbPath: join(home, '.local', 'share', 'opencode', 'opencode.db'),
sessionId: 'ses_1'
}),
expect.anything()
)
})
it('reads a 2.x session from its live tables, not the frozen 1.x copy an upgrade left', async () => {
const dataHome = dataHomeWith(`${OPENCODE_2_TABLES}
CREATE TABLE session (id TEXT PRIMARY KEY);
CREATE TABLE message (id TEXT, session_id TEXT, time_created INTEGER, time_updated INTEGER,
data TEXT);
CREATE TABLE part (id TEXT, message_id TEXT, session_id TEXT, time_updated INTEGER, data TEXT);
INSERT INTO session VALUES ('ses_1');
INSERT INTO message VALUES ('old', 'ses_1', 1, 1, '{"role":"user"}');
INSERT INTO part VALUES ('p', 'old', 'ses_1', 1, '{"type":"text","text":"frozen"}');
INSERT INTO session_v2 VALUES ('ses_1');
INSERT INTO session_message VALUES
('u1', 'ses_1', 'user', 1, '{"text":"live prompt"}', 50, 50),
('a1', 'ses_1', 'assistant', 2, '{"content":[{"type":"text","text":"reply"}]}', 60, 60);`)
await expect(readFrom(dataHome)).resolves.toEqual([
{ id: 'opencode:u1', blocks: [{ type: 'text', text: 'live prompt' }], createdAt: 50 }
])
})
it('finds nothing in a 2.x database whose message table it does not know', async () => {
const dataHome = dataHomeWith(`CREATE TABLE session_v2 (id TEXT PRIMARY KEY);
CREATE TABLE session_message (id TEXT PRIMARY KEY, session_id TEXT, body TEXT);
INSERT INTO session_v2 VALUES ('ses_1');`)
await expect(readFrom(dataHome)).resolves.toBeNull()
})
})
@@ -1,38 +0,0 @@
import { homedir } from 'node:os'
import { OPENCODE_TRANSCRIPT_MAX_WINDOW } from '../../shared/opencode-transcript-page-limit'
import type { AcpStoredUserMessagesReader } from '../acp/acp-recovery-history'
import { resolveOpenCodeDatabasePath } from './opencode-data-directory'
import type { readOpenCodeTranscriptPageViaWorker } from '../ai-vault/session-scanner-opencode-sqlite-worker-spawn'
export type OpenCodeTranscriptPageReader = typeof readOpenCodeTranscriptPageViaWorker
/** The latest user messages of an OpenCode session, read from the database its pinned account
* selects, through the existing bounded reader: 2.x's `session_v2` rows first, else 1.x's. */
export function openCodeStoredUserMessagesReader(
readPage?: OpenCodeTranscriptPageReader
): AcpStoredUserMessagesReader {
return async ({ env, providerSessionId, signal }) => {
// The child's own home, which its account may name, not this process's.
const home = (process.platform === 'win32' ? env.USERPROFILE : env.HOME) || homedir()
const dbPath = resolveOpenCodeDatabasePath(env, home)
if (!dbPath) {
return null
}
const read =
readPage ??
(await import('../ai-vault/session-scanner-opencode-sqlite-worker-spawn'))
.readOpenCodeTranscriptPageViaWorker
const page = await read(
{ dbPath, sessionId: providerSessionId, limit: OPENCODE_TRANSCRIPT_MAX_WINDOW },
signal
)
if (!page) {
return null
}
return page.items.flatMap(({ message }) =>
message.role === 'user' && message.timestamp !== null
? [{ id: message.id, blocks: message.blocks, createdAt: message.timestamp }]
: []
)
}
}