feat(opencode): import local session history (#1677)

* feat(cli): list local OpenCode sessions over machine RPC

* feat(hub): import OpenCode sessions into HAPI

* feat(web): import OpenCode history from new-session picker

* fix(cli): derive OpenCode imported message ids from part ids

Random ids made every scan differ, so delta classification flagged
unchanged transcripts as diverged on re-import.

* fix(cli): surface unreadable OpenCode db as rpc error

A locked or unopenable opencode.db now fails the listing instead of
reporting an empty success; summaries normalize timestamps and the web
import toast counts only created or actually-appended sessions.

* fix(hub): fail closed on opencode re-import past live HAPI continuation

Imported entries must remain a strict prefix of the native transcript.
Insertions ahead of the imported boundary or new native messages after
the session continued in HAPI now fail closed instead of appending
duplicated or reordered history.

* fix(web,hub): apply selected launch config when importing OpenCode history

Pass the model, reasoning effort, and permission mode chosen in the
new-session picker through the import request and persist them on the
imported HAPI session via applySessionConfig, so reopen launches with
the visible selection instead of stale or default values.

* fix(hub,cli): preserve explicit launch resets and skip streaming parts on import

- Launch config keys are applied by presence, so explicit null/default
  selections from the picker must reach applySessionConfig to clear
  stale model, effort, or permission values on re-import. Requests that
  omit the fields entirely still leave persisted config untouched.
- Assistant text parts are mutable while a response streams. Mirror the
  live scanner gate and import only finished (time.end set), synthetic,
  or user text so an in-progress row can never be stored and later
  finalized as transcript_diverged.

* test(hub): fix launch-config reset assertions to satisfy strict types

* fix(cli): fail opencode transcript reads on query errors instead of partial snapshots

Swallowed SQLite errors could return a truncated message list that the
hub later rejects as transcript_diverged with no repair path. Full
transcript reads now propagate query failures; summary lastUserMessage
extraction stays resilient per message.

* test(cli): cover unreadable-part-table transcript read stays non-partial

* fix(hub): scope launch-config failures to the per-session import result

A thrown applySessionConfig call used to escape the bulk handler as an
unstructured 500 even though transcripts were already persisted. Catch
it per session and report it as a config_failed result so remaining
sessions in a bulk request still import.

Also cover the mid-transcript part-query failure with an injected
opener that rejects instead of returning a holey transcript.

* fix(cli): read opencode transcripts inside a deferred snapshot transaction

Messages and their parts were read in separate statements, so a
concurrent OpenCode write could leave a hole in the imported sequence
that positional re-import checks then reject forever. Wrap each full
transcript read in BEGIN DEFERRED/COMMIT with rollback on failure.

* fix(cli): follow machine path policy for OpenCode imports

* fix(web): use unified permission mode for OpenCode imports

* fix(opencode): harden session import consistency

* fix(hub): preserve metadata on rejected OpenCode imports

* fix(hub): refresh OpenCode import state under lock

* fix(hub): resolve OpenCode imports after locking

* fix(opencode): bind imported resume to source machine
This commit is contained in:
Junmo Kim
2026-10-28 12:14:18 +08:00
committed by GitHub
parent 3f4bd9afea
commit ae147e85db
21 changed files with 2229 additions and 11 deletions
+40
View File
@@ -287,6 +287,46 @@ export type PiLocalSessionWithMessages = z.infer<typeof PiLocalSessionWithMessag
export type ListPiSessionsRpcRequest = z.infer<typeof ListPiSessionsRpcRequestSchema>
export type ListPiSessionsRpcResponse = z.infer<typeof ListPiSessionsRpcResponseSchema>
export const OpencodeImportedMessageContentSchema = CodexImportedMessageSchema
export const OpencodeImportedMessageSchema = z.object({
localId: z.string().min(1),
createdAt: z.number(),
content: OpencodeImportedMessageContentSchema
})
export const OpencodeLocalSessionSummarySchema = z.object({
id: z.string().min(1),
title: z.string(),
lastUserMessage: z.string().nullable().optional(),
cwd: z.string().nullable().optional(),
file: z.string().min(1),
modifiedAt: z.number()
})
export const OpencodeLocalSessionWithMessagesSchema = OpencodeLocalSessionSummarySchema.extend({
messages: z.array(OpencodeImportedMessageSchema)
})
export const MAX_OPENCODE_SESSION_IMPORTS = 200
export const ListOpencodeSessionsRpcRequestSchema = z.object({
cwd: z.string().nullable().optional(),
sessionIds: z.array(z.string().min(1)).max(MAX_OPENCODE_SESSION_IMPORTS).optional()
})
export const ListOpencodeSessionsRpcResponseSchema = z.union([
z.object({ success: z.literal(true), sessions: z.array(z.union([OpencodeLocalSessionWithMessagesSchema, OpencodeLocalSessionSummarySchema])) }),
z.object({ success: z.literal(false), error: z.string() })
])
export type OpencodeImportedMessageContent = z.infer<typeof OpencodeImportedMessageContentSchema>
export type OpencodeImportedMessage = z.infer<typeof OpencodeImportedMessageSchema>
export type OpencodeLocalSessionSummary = z.infer<typeof OpencodeLocalSessionSummarySchema>
export type OpencodeLocalSessionWithMessages = z.infer<typeof OpencodeLocalSessionWithMessagesSchema>
export type ListOpencodeSessionsRpcRequest = z.infer<typeof ListOpencodeSessionsRpcRequestSchema>
export type ListOpencodeSessionsRpcResponse = z.infer<typeof ListOpencodeSessionsRpcResponseSchema>
export const SessionCollaborationModeRequestSchema = z.object({
mode: CodexCollaborationModeSchema
})
+1
View File
@@ -44,6 +44,7 @@ export const RPC_METHODS = {
ListPiSessions: 'listPiSessions',
ListOpencodeModels: 'listOpencodeModels',
ListOpencodeModelVariants: 'listOpencodeModelVariants',
ListOpencodeSessions: 'listOpencodeSessions',
ListOpencodeModelsForCwd: 'listOpencodeModelsForCwd',
ListGrokModelsForCwd: 'listGrokModelsForCwd',
ListGrokModels: 'listGrokModels',