chore: import upstream snapshot with attribution
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,861 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _ Provider = (*openCodeFormatProvider)(nil)
|
||||
|
||||
// A SQLite WAL begins with a 32-byte header. Bytes beyond the header are
|
||||
// transaction frames, so a larger WAL can contain source changes worth
|
||||
// syncing. Read-only SQLite connections may create an empty WAL and its SHM
|
||||
// index simply by opening a quiet WAL-mode database; those sidecars must not
|
||||
// make the watcher trigger itself.
|
||||
const sqliteWALHeaderSize = int64(32)
|
||||
|
||||
type openCodeFormatProviderFactory struct {
|
||||
def AgentDef
|
||||
spec openCodeProviderSpec
|
||||
}
|
||||
|
||||
func newOpenCodeProviderFactory(def AgentDef) ProviderFactory {
|
||||
return openCodeFormatProviderFactory{
|
||||
def: cloneAgentDef(def),
|
||||
spec: openCodeProviderSpecForAgent(AgentOpenCode),
|
||||
}
|
||||
}
|
||||
|
||||
func newKiloProviderFactory(def AgentDef) ProviderFactory {
|
||||
return openCodeFormatProviderFactory{
|
||||
def: cloneAgentDef(def),
|
||||
spec: openCodeProviderSpecForAgent(AgentKilo),
|
||||
}
|
||||
}
|
||||
|
||||
func newMiMoCodeProviderFactory(def AgentDef) ProviderFactory {
|
||||
return openCodeFormatProviderFactory{
|
||||
def: cloneAgentDef(def),
|
||||
spec: openCodeProviderSpecForAgent(AgentMiMoCode),
|
||||
}
|
||||
}
|
||||
|
||||
func newIcodemateProviderFactory(def AgentDef) ProviderFactory {
|
||||
return openCodeFormatProviderFactory{
|
||||
def: cloneAgentDef(def),
|
||||
spec: openCodeProviderSpecForAgent(AgentIcodemate),
|
||||
}
|
||||
}
|
||||
|
||||
func (f openCodeFormatProviderFactory) Definition() AgentDef {
|
||||
return cloneAgentDef(f.def)
|
||||
}
|
||||
|
||||
func (f openCodeFormatProviderFactory) Capabilities() Capabilities {
|
||||
return openCodeFormatProviderCapabilities()
|
||||
}
|
||||
|
||||
func (f openCodeFormatProviderFactory) NewProvider(cfg ProviderConfig) Provider {
|
||||
cfg = cfg.Clone()
|
||||
return &openCodeFormatProvider{
|
||||
ProviderBase: ProviderBase{
|
||||
Def: cloneAgentDef(f.def),
|
||||
Caps: openCodeFormatProviderCapabilities(),
|
||||
Config: cfg,
|
||||
},
|
||||
sources: newOpenCodeFormatSourceSet(cfg.Roots, f.spec),
|
||||
}
|
||||
}
|
||||
|
||||
type openCodeFormatProvider struct {
|
||||
ProviderBase
|
||||
sources openCodeFormatSourceSet
|
||||
}
|
||||
|
||||
func (p *openCodeFormatProvider) Discover(ctx context.Context) ([]SourceRef, error) {
|
||||
return p.sources.Discover(ctx)
|
||||
}
|
||||
|
||||
func (p *openCodeFormatProvider) WatchPlan(ctx context.Context) (WatchPlan, error) {
|
||||
return p.sources.WatchPlan(ctx)
|
||||
}
|
||||
|
||||
func (p *openCodeFormatProvider) SourcesForChangedPath(
|
||||
ctx context.Context,
|
||||
req ChangedPathRequest,
|
||||
) ([]SourceRef, error) {
|
||||
return p.sources.SourcesForChangedPath(ctx, req)
|
||||
}
|
||||
|
||||
func (p *openCodeFormatProvider) FindSource(
|
||||
ctx context.Context,
|
||||
req FindSourceRequest,
|
||||
) (SourceRef, bool, error) {
|
||||
req = ProviderFindRequestWithRawSessionID(p.Def, req)
|
||||
return p.sources.FindSource(ctx, req)
|
||||
}
|
||||
|
||||
func (p *openCodeFormatProvider) Fingerprint(
|
||||
ctx context.Context,
|
||||
source SourceRef,
|
||||
) (SourceFingerprint, error) {
|
||||
return p.sources.Fingerprint(ctx, source)
|
||||
}
|
||||
|
||||
func (p *openCodeFormatProvider) Parse(
|
||||
ctx context.Context,
|
||||
req ParseRequest,
|
||||
) (ParseOutcome, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return ParseOutcome{}, err
|
||||
}
|
||||
path, ok := p.sources.pathFromSource(req.Source)
|
||||
if !ok {
|
||||
return ParseOutcome{}, fmt.Errorf("%s source path unavailable", p.Def.Type)
|
||||
}
|
||||
|
||||
machine := firstNonEmptyJSONLString(req.Machine, p.Config.Machine)
|
||||
var (
|
||||
sess *ParsedSession
|
||||
msgs []ParsedMessage
|
||||
err error
|
||||
)
|
||||
if dbPath, sessionID, ok := p.sources.spec.parseVirtual(path); ok {
|
||||
sess, msgs, err = p.sources.spec.parseSQLite(dbPath, sessionID, machine)
|
||||
} else {
|
||||
sess, msgs, err = p.sources.spec.parseFile(path, machine)
|
||||
}
|
||||
if err != nil {
|
||||
return ParseOutcome{}, err
|
||||
}
|
||||
if sess == nil {
|
||||
return ParseOutcome{
|
||||
ResultSetComplete: true,
|
||||
SkipReason: SkipNoSession,
|
||||
}, nil
|
||||
}
|
||||
if req.Fingerprint.Hash != "" {
|
||||
sess.File.Hash = req.Fingerprint.Hash
|
||||
}
|
||||
return ParseOutcome{
|
||||
Results: []ParseResultOutcome{{
|
||||
Result: ParseResult{
|
||||
Session: *sess,
|
||||
Messages: msgs,
|
||||
},
|
||||
DataVersion: DataVersionCurrent,
|
||||
}},
|
||||
ResultSetComplete: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// openCodeProviderSpec parameterizes the one shared OpenCode-format
|
||||
// provider implementation for OpenCode and its Kilo and MiMoCode forks.
|
||||
// All three reuse the same discovery, source-lookup, fingerprinting,
|
||||
// and parsing code; they differ only in the per-agent SQLite filename,
|
||||
// the storage/<sessionSubdir> that holds session JSON, and the agent
|
||||
// label/ID prefix applied via relabel. Kilo and MiMoCode parse through
|
||||
// the OpenCode storage and SQLite readers, then relabel the result onto
|
||||
// their own agent and ID prefix.
|
||||
type openCodeProviderSpec struct {
|
||||
agent AgentType
|
||||
format openCodeFormat
|
||||
dbName string
|
||||
listSQLite func(string) ([]OpenCodeSessionMeta, error)
|
||||
sourceMtime func(string) (int64, error)
|
||||
relabel func(*ParsedSession)
|
||||
}
|
||||
|
||||
func openCodeProviderSpecForAgent(agent AgentType) openCodeProviderSpec {
|
||||
switch agent {
|
||||
case AgentOpenCode:
|
||||
return openCodeProviderSpec{
|
||||
agent: AgentOpenCode,
|
||||
format: openCodeFmt,
|
||||
dbName: openCodeFmt.dbName,
|
||||
listSQLite: ListOpenCodeSessionMeta,
|
||||
sourceMtime: OpenCodeSourceMtime,
|
||||
}
|
||||
case AgentKilo:
|
||||
return openCodeProviderSpec{
|
||||
agent: AgentKilo,
|
||||
format: kiloFmt,
|
||||
dbName: kiloFmt.dbName,
|
||||
listSQLite: ListKiloSessionMeta,
|
||||
sourceMtime: KiloSourceMtime,
|
||||
relabel: relabelOpenCodeSessionAsKilo,
|
||||
}
|
||||
case AgentMiMoCode:
|
||||
return openCodeProviderSpec{
|
||||
agent: AgentMiMoCode,
|
||||
format: mimoFmt,
|
||||
dbName: mimoFmt.dbName,
|
||||
listSQLite: ListMiMoCodeSessionMeta,
|
||||
sourceMtime: MiMoCodeSourceMtime,
|
||||
relabel: relabelOpenCodeSessionAsMiMoCode,
|
||||
}
|
||||
case AgentIcodemate:
|
||||
return openCodeProviderSpec{
|
||||
agent: AgentIcodemate,
|
||||
format: icodemateFmt,
|
||||
dbName: icodemateFmt.dbName,
|
||||
listSQLite: ListIcodemateSessionMeta,
|
||||
sourceMtime: IcodemateSourceMtime,
|
||||
relabel: relabelOpenCodeSessionAsIcodemate,
|
||||
}
|
||||
default:
|
||||
return openCodeProviderSpec{}
|
||||
}
|
||||
}
|
||||
|
||||
// resolve detects the OpenCode storage backend for a root.
|
||||
func (spec openCodeProviderSpec) resolve(root string) OpenCodeSource {
|
||||
return resolveOpenCodeFormatSource(spec.format, root)
|
||||
}
|
||||
|
||||
// discover lists file-backed storage session JSON files under a root.
|
||||
func (spec openCodeProviderSpec) discover(root string) []DiscoveredFile {
|
||||
return discoverOpenCodeFormatSessions(spec.format, root)
|
||||
}
|
||||
|
||||
// find locates a session source path (storage JSON or SQLite virtual
|
||||
// path) by raw session ID under a root.
|
||||
func (spec openCodeProviderSpec) find(root, sessionID string) string {
|
||||
return findOpenCodeFormatSourceFile(spec.format, root, sessionID)
|
||||
}
|
||||
|
||||
// watchRoots returns the directories that should be watched for live
|
||||
// updates under a configured root.
|
||||
func (spec openCodeProviderSpec) watchRoots(root string) []string {
|
||||
return resolveOpenCodeFormatWatchRoots(spec.format, root)
|
||||
}
|
||||
|
||||
// storageIDs returns the set of session IDs present as storage JSON
|
||||
// under a root, used to skip duplicate SQLite metas in hybrid roots.
|
||||
func (spec openCodeProviderSpec) storageIDs(root string) map[string]struct{} {
|
||||
return openCodeFormatStorageSessionIDs(spec.format, root)
|
||||
}
|
||||
|
||||
// parseVirtual splits an opencode-format SQLite virtual path
|
||||
// (<dbPath>#<sessionID>) when the DB base name matches this agent.
|
||||
func (spec openCodeProviderSpec) parseVirtual(
|
||||
sourcePath string,
|
||||
) (dbPath, sessionID string, ok bool) {
|
||||
return parseOpenCodeFormatVirtualPath(spec.dbName, sourcePath)
|
||||
}
|
||||
|
||||
// parseFile parses a file-backed storage session and relabels it onto
|
||||
// this agent's ID prefix when the agent is a fork of OpenCode.
|
||||
func (spec openCodeProviderSpec) parseFile(
|
||||
sessionPath, machine string,
|
||||
) (*ParsedSession, []ParsedMessage, error) {
|
||||
sess, msgs, err := parseOpenCodeStorageFile(sessionPath, machine)
|
||||
if err != nil || sess == nil {
|
||||
return sess, msgs, err
|
||||
}
|
||||
if spec.relabel != nil {
|
||||
spec.relabel(sess)
|
||||
}
|
||||
return sess, msgs, nil
|
||||
}
|
||||
|
||||
// parseSQLite parses a single SQLite-backed session and relabels it
|
||||
// onto this agent's ID prefix when the agent is a fork of OpenCode.
|
||||
func (spec openCodeProviderSpec) parseSQLite(
|
||||
dbPath, sessionID, machine string,
|
||||
) (*ParsedSession, []ParsedMessage, error) {
|
||||
sess, msgs, err := parseOpenCodeDBSession(dbPath, sessionID, machine)
|
||||
if err != nil || sess == nil {
|
||||
return sess, msgs, err
|
||||
}
|
||||
if spec.relabel != nil {
|
||||
spec.relabel(sess)
|
||||
}
|
||||
return sess, msgs, nil
|
||||
}
|
||||
|
||||
type openCodeFormatSource struct {
|
||||
Root string
|
||||
Path string
|
||||
// MTimeNS carries the session's time_updated (already listed during
|
||||
// SQLite discovery, scaled to nanoseconds) so Fingerprint does not
|
||||
// reopen the shared DB once per session. Zero means unknown and makes
|
||||
// Fingerprint fall back to querying the DB.
|
||||
MTimeNS int64
|
||||
}
|
||||
|
||||
type openCodeFormatSourceSet struct {
|
||||
roots []string
|
||||
spec openCodeProviderSpec
|
||||
}
|
||||
|
||||
func newOpenCodeFormatSourceSet(
|
||||
roots []string,
|
||||
spec openCodeProviderSpec,
|
||||
) openCodeFormatSourceSet {
|
||||
return openCodeFormatSourceSet{
|
||||
roots: cleanJSONLRoots(roots),
|
||||
spec: spec,
|
||||
}
|
||||
}
|
||||
|
||||
func (s openCodeFormatSourceSet) Discover(ctx context.Context) ([]SourceRef, error) {
|
||||
var sources []SourceRef
|
||||
seen := make(map[string]struct{})
|
||||
for _, root := range s.roots {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
src := s.spec.resolve(root)
|
||||
storageIDs := map[string]struct{}{}
|
||||
if src.Mode == OpenCodeSourceStorage {
|
||||
for _, file := range s.spec.discover(root) {
|
||||
source, ok := s.sourceRef(root, file.Path, false)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
source.ProjectHint = file.Project
|
||||
addJSONLSource(source, &sources, seen)
|
||||
}
|
||||
storageIDs = s.spec.storageIDs(root)
|
||||
}
|
||||
if src.DBPath == "" || !IsRegularFile(src.DBPath) {
|
||||
continue
|
||||
}
|
||||
dbSources, err := s.sqliteSources(ctx, root, src.DBPath, storageIDs)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The SQLite DB is optional alongside filesystem storage. A
|
||||
// corrupt or unreadable DB must not abort discovery of the valid
|
||||
// storage-backed sessions in this root; scope the failure to the DB
|
||||
// portion, matching the legacy independent discovery paths. A
|
||||
// SQLite-only root has nothing to fall back to, so keep failing.
|
||||
if src.Mode == OpenCodeSourceStorage {
|
||||
log.Printf("sync %s: skipping unreadable %s: %v",
|
||||
s.spec.agent, src.DBPath, err)
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
for _, source := range dbSources {
|
||||
addJSONLSource(source, &sources, seen)
|
||||
}
|
||||
}
|
||||
sortJSONLSources(sources)
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
func (s openCodeFormatSourceSet) WatchPlan(context.Context) (WatchPlan, error) {
|
||||
roots := make([]WatchRoot, 0, len(s.roots))
|
||||
for _, root := range s.roots {
|
||||
for _, watchRoot := range s.spec.watchRoots(root) {
|
||||
roots = append(roots, WatchRoot{
|
||||
Path: watchRoot,
|
||||
Recursive: true,
|
||||
IncludeGlobs: []string{
|
||||
"*.json",
|
||||
s.spec.dbName,
|
||||
s.spec.dbName + "-wal",
|
||||
},
|
||||
DebounceKey: string(s.spec.agent) + ":opencode:" + watchRoot,
|
||||
})
|
||||
}
|
||||
}
|
||||
return WatchPlan{Roots: roots}, nil
|
||||
}
|
||||
|
||||
func (s openCodeFormatSourceSet) SourcesForChangedPath(
|
||||
ctx context.Context,
|
||||
req ChangedPathRequest,
|
||||
) ([]SourceRef, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pathExists := true
|
||||
if _, err := os.Stat(req.Path); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
pathExists = false
|
||||
}
|
||||
for _, root := range s.roots {
|
||||
sources, ok, err := s.sourcesForChangedPathInRoot(
|
||||
ctx, root, req.Path, pathExists,
|
||||
)
|
||||
if err != nil || ok {
|
||||
return sources, err
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s openCodeFormatSourceSet) FindSource(
|
||||
ctx context.Context,
|
||||
req FindSourceRequest,
|
||||
) (SourceRef, bool, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return SourceRef{}, false, err
|
||||
}
|
||||
for _, path := range []string{req.StoredFilePath, req.FingerprintKey} {
|
||||
if path == "" {
|
||||
continue
|
||||
}
|
||||
for _, root := range s.roots {
|
||||
if source, ok := s.sourceRef(root, path, true); ok {
|
||||
return source, true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.RawSessionID == "" {
|
||||
return SourceRef{}, false, nil
|
||||
}
|
||||
for _, root := range s.roots {
|
||||
path := s.spec.find(root, req.RawSessionID)
|
||||
if path == "" {
|
||||
continue
|
||||
}
|
||||
if source, ok := s.sourceRef(root, path, false); ok {
|
||||
return source, true, nil
|
||||
}
|
||||
}
|
||||
return SourceRef{}, false, nil
|
||||
}
|
||||
|
||||
func (s openCodeFormatSourceSet) Fingerprint(
|
||||
ctx context.Context,
|
||||
source SourceRef,
|
||||
) (SourceFingerprint, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return SourceFingerprint{}, err
|
||||
}
|
||||
path, ok := s.pathFromSource(source)
|
||||
if !ok {
|
||||
return SourceFingerprint{}, fmt.Errorf("%s source path unavailable", s.spec.agent)
|
||||
}
|
||||
mtime := sourceCarriedMTimeNS(source)
|
||||
if mtime == 0 {
|
||||
var err error
|
||||
mtime, err = s.spec.sourceMtime(path)
|
||||
if err != nil {
|
||||
return SourceFingerprint{}, err
|
||||
}
|
||||
}
|
||||
fingerprint := SourceFingerprint{
|
||||
Key: firstNonEmptyJSONLString(source.FingerprintKey, source.Key, path),
|
||||
MTimeNS: mtime,
|
||||
}
|
||||
if dbPath, _, ok := s.spec.parseVirtual(path); ok {
|
||||
info, err := os.Stat(dbPath)
|
||||
if err != nil {
|
||||
return SourceFingerprint{}, fmt.Errorf("stat %s: %w", dbPath, err)
|
||||
}
|
||||
fingerprint.Size = info.Size()
|
||||
return fingerprint, nil
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return SourceFingerprint{}, fmt.Errorf("stat %s: %w", path, err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return SourceFingerprint{}, fmt.Errorf("stat %s: source is a directory", path)
|
||||
}
|
||||
fingerprint.Size = info.Size()
|
||||
// No content hash: no engine freshness gate consumes it for this
|
||||
// family (see providerFingerprintHashRequiredForFreshness), and the
|
||||
// authoritative storage fingerprint is computed by Parse and compared
|
||||
// post-parse by dropUnchangedSharedSQLiteResults. Computing it here
|
||||
// re-read and re-hashed every message and part file of the session on
|
||||
// every fingerprint call — the dominant cost of syncing streaming
|
||||
// storage sessions — for a value nothing read.
|
||||
return fingerprint, nil
|
||||
}
|
||||
|
||||
// sourceCarriedMTimeNS returns the discovery-listed session mtime carried on
|
||||
// a SQLite-backed source, or zero when the source was built without one
|
||||
// (storage sessions, FindSource lookups).
|
||||
func sourceCarriedMTimeNS(source SourceRef) int64 {
|
||||
switch src := source.Opaque.(type) {
|
||||
case openCodeFormatSource:
|
||||
return src.MTimeNS
|
||||
case *openCodeFormatSource:
|
||||
if src != nil {
|
||||
return src.MTimeNS
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s openCodeFormatSourceSet) pathFromSource(source SourceRef) (string, bool) {
|
||||
switch src := source.Opaque.(type) {
|
||||
case openCodeFormatSource:
|
||||
return src.Path, src.Path != ""
|
||||
case *openCodeFormatSource:
|
||||
if src != nil && src.Path != "" {
|
||||
return src.Path, true
|
||||
}
|
||||
}
|
||||
for _, candidate := range []string{
|
||||
source.DisplayPath,
|
||||
source.FingerprintKey,
|
||||
source.Key,
|
||||
} {
|
||||
for _, root := range s.roots {
|
||||
if ref, ok := s.sourceRef(root, candidate, false); ok {
|
||||
src := ref.Opaque.(openCodeFormatSource)
|
||||
return src.Path, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (s openCodeFormatSourceSet) sqliteSources(
|
||||
ctx context.Context,
|
||||
root string,
|
||||
dbPath string,
|
||||
storageIDs map[string]struct{},
|
||||
) ([]SourceRef, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metas, err := s.spec.listSQLite(dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sources := make([]SourceRef, 0, len(metas))
|
||||
for _, meta := range metas {
|
||||
if _, exists := storageIDs[meta.SessionID]; exists {
|
||||
continue
|
||||
}
|
||||
// meta was just read from this DB, so the session row is known to
|
||||
// exist. Build the SourceRef directly instead of routing through
|
||||
// sourceRef, which reopens the same SQLite DB once per row via
|
||||
// OpenCodeSQLiteSessionExists (O(n) opens for n sessions, and it would
|
||||
// silently drop a row whose redundant probe failed).
|
||||
source, ok := s.sqliteSourceRefFromMeta(root, meta)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
sources = append(sources, source)
|
||||
}
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
// sqliteSourceRefFromMeta builds a SourceRef for a session row already listed
|
||||
// from the SQLite DB at root. It validates the virtual path parses and that its
|
||||
// DB lives under root, but unlike sourceRef it skips the per-row
|
||||
// OpenCodeSQLiteSessionExists probe because the caller read the row from that
|
||||
// same DB moments earlier. The listed time_updated rides along so Fingerprint
|
||||
// does not reopen the DB per session.
|
||||
func (s openCodeFormatSourceSet) sqliteSourceRefFromMeta(
|
||||
root string,
|
||||
meta OpenCodeSessionMeta,
|
||||
) (SourceRef, bool) {
|
||||
root = filepath.Clean(root)
|
||||
path := filepath.Clean(meta.VirtualPath)
|
||||
dbPath, _, ok := s.spec.parseVirtual(path)
|
||||
if !ok {
|
||||
return SourceRef{}, false
|
||||
}
|
||||
if _, under := relUnder(root, dbPath); !under {
|
||||
return SourceRef{}, false
|
||||
}
|
||||
ref := s.newSourceRef(root, path, "")
|
||||
if src, ok := ref.Opaque.(openCodeFormatSource); ok {
|
||||
src.MTimeNS = meta.FileMtime
|
||||
ref.Opaque = src
|
||||
}
|
||||
return ref, true
|
||||
}
|
||||
|
||||
func (s openCodeFormatSourceSet) sourcesForChangedPathInRoot(
|
||||
ctx context.Context,
|
||||
root string,
|
||||
path string,
|
||||
pathExists bool,
|
||||
) ([]SourceRef, bool, error) {
|
||||
rel, ok := relUnder(root, path)
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
isSQLiteChange := true
|
||||
switch rel {
|
||||
case s.spec.dbName + "-shm":
|
||||
// SHM is only SQLite's WAL index. WAL frames (or the checkpointed main
|
||||
// database) carry the source changes, so SHM events are redundant.
|
||||
return nil, true, nil
|
||||
case s.spec.dbName + "-wal":
|
||||
// A read-only connection can create an empty WAL while inspecting a
|
||||
// quiet database. Ignore it, as well as WAL removal after a checkpoint;
|
||||
// the corresponding main-database write is watched separately.
|
||||
if !sqliteWALHasFrames(path) {
|
||||
return nil, true, nil
|
||||
}
|
||||
case s.spec.dbName:
|
||||
// The main database and WALs with transaction frames both fan out to
|
||||
// the logical sessions stored in this shared SQLite container.
|
||||
default:
|
||||
isSQLiteChange = false
|
||||
}
|
||||
|
||||
if isSQLiteChange {
|
||||
dbPath := filepath.Join(root, s.spec.dbName)
|
||||
if !IsRegularFile(dbPath) {
|
||||
return nil, true, nil
|
||||
}
|
||||
storageIDs := map[string]struct{}{}
|
||||
if s.spec.resolve(root).Mode == OpenCodeSourceStorage {
|
||||
storageIDs = s.spec.storageIDs(root)
|
||||
}
|
||||
sources, err := s.sqliteSources(ctx, root, dbPath, storageIDs)
|
||||
return sources, true, err
|
||||
}
|
||||
|
||||
src := s.spec.resolve(root)
|
||||
if src.Mode != OpenCodeSourceStorage {
|
||||
return nil, false, nil
|
||||
}
|
||||
parts := strings.Split(rel, string(filepath.Separator))
|
||||
sessionSubdir := filepath.Base(src.SessionRoot)
|
||||
switch {
|
||||
case pathExists &&
|
||||
len(parts) == 4 &&
|
||||
parts[0] == "storage" &&
|
||||
parts[1] == sessionSubdir &&
|
||||
strings.HasSuffix(parts[3], ".json"):
|
||||
source, ok := s.sourceRef(root, path, false)
|
||||
if !ok {
|
||||
return nil, true, nil
|
||||
}
|
||||
return []SourceRef{source}, true, nil
|
||||
case !pathExists &&
|
||||
len(parts) == 4 &&
|
||||
parts[0] == "storage" &&
|
||||
parts[1] == sessionSubdir &&
|
||||
strings.HasSuffix(parts[3], ".json"):
|
||||
source, ok := s.sourceRefFromStoragePath(root, path)
|
||||
if !ok {
|
||||
return nil, true, nil
|
||||
}
|
||||
return []SourceRef{source}, true, nil
|
||||
case len(parts) == 4 &&
|
||||
parts[0] == "storage" &&
|
||||
parts[1] == "message" &&
|
||||
strings.HasSuffix(parts[3], ".json"):
|
||||
source, ok := s.sourceForRawID(root, parts[2])
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
return []SourceRef{source}, true, nil
|
||||
case len(parts) == 4 &&
|
||||
parts[0] == "storage" &&
|
||||
parts[1] == "part" &&
|
||||
strings.HasSuffix(parts[3], ".json"):
|
||||
sessionID := ""
|
||||
if pathExists {
|
||||
sessionID = readOpenCodeProviderStorageSessionID(path)
|
||||
}
|
||||
if sessionID == "" {
|
||||
sessionID = findOpenCodeProviderStorageSessionIDByMessageID(root, parts[2])
|
||||
}
|
||||
if sessionID == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
source, ok := s.sourceForRawID(root, sessionID)
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
return []SourceRef{source}, true, nil
|
||||
case !pathExists &&
|
||||
len(parts) == 3 &&
|
||||
parts[0] == "storage" &&
|
||||
parts[1] == "message":
|
||||
source, ok := s.sourceForRawID(root, parts[2])
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
return []SourceRef{source}, true, nil
|
||||
case !pathExists &&
|
||||
len(parts) == 3 &&
|
||||
parts[0] == "storage" &&
|
||||
parts[1] == "part":
|
||||
sessionID := findOpenCodeProviderStorageSessionIDByMessageID(root, parts[2])
|
||||
if sessionID == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
source, ok := s.sourceForRawID(root, sessionID)
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
return []SourceRef{source}, true, nil
|
||||
}
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func sqliteWALHasFrames(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
// Only a missing WAL is a definitive no-op. Other stat failures fail
|
||||
// open so a real update is synced instead of silently dropped; at
|
||||
// worst that costs one redundant sync.
|
||||
return !errors.Is(err, fs.ErrNotExist)
|
||||
}
|
||||
if info == nil {
|
||||
return false
|
||||
}
|
||||
return info.Mode().IsRegular() && info.Size() > sqliteWALHeaderSize
|
||||
}
|
||||
|
||||
func (s openCodeFormatSourceSet) sourceForRawID(root, sessionID string) (SourceRef, bool) {
|
||||
path := s.spec.find(root, sessionID)
|
||||
if path == "" {
|
||||
return SourceRef{}, false
|
||||
}
|
||||
return s.sourceRef(root, path, false)
|
||||
}
|
||||
|
||||
func (s openCodeFormatSourceSet) sourceRef(
|
||||
root string,
|
||||
path string,
|
||||
promoteVirtual bool,
|
||||
) (SourceRef, bool) {
|
||||
root = filepath.Clean(root)
|
||||
path = filepath.Clean(path)
|
||||
if dbPath, sessionID, ok := s.spec.parseVirtual(path); ok {
|
||||
if _, under := relUnder(root, dbPath); !under {
|
||||
return SourceRef{}, false
|
||||
}
|
||||
if promoteVirtual {
|
||||
if selected := s.spec.find(root, sessionID); selected != "" &&
|
||||
selected != path {
|
||||
return s.sourceRef(root, selected, false)
|
||||
}
|
||||
}
|
||||
if !OpenCodeSQLiteSessionExists(dbPath, sessionID) {
|
||||
return SourceRef{}, false
|
||||
}
|
||||
return s.newSourceRef(root, path, ""), true
|
||||
}
|
||||
if !s.isStorageSessionPath(root, path, true) {
|
||||
return SourceRef{}, false
|
||||
}
|
||||
return s.sourceRefFromStoragePath(root, path)
|
||||
}
|
||||
|
||||
func (s openCodeFormatSourceSet) sourceRefFromStoragePath(
|
||||
root string,
|
||||
path string,
|
||||
) (SourceRef, bool) {
|
||||
if !s.isStorageSessionPath(root, path, false) {
|
||||
return SourceRef{}, false
|
||||
}
|
||||
return s.newSourceRef(root, path, openCodeSessionProject(path)), true
|
||||
}
|
||||
|
||||
func (s openCodeFormatSourceSet) newSourceRef(
|
||||
root string,
|
||||
path string,
|
||||
project string,
|
||||
) SourceRef {
|
||||
return SourceRef{
|
||||
Provider: s.spec.agent,
|
||||
Key: path,
|
||||
DisplayPath: path,
|
||||
FingerprintKey: path,
|
||||
ProjectHint: project,
|
||||
Opaque: openCodeFormatSource{
|
||||
Root: root,
|
||||
Path: path,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s openCodeFormatSourceSet) isStorageSessionPath(
|
||||
root string,
|
||||
path string,
|
||||
requireExisting bool,
|
||||
) bool {
|
||||
rel, ok := relUnder(root, path)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
src := s.spec.resolve(root)
|
||||
if src.Mode != OpenCodeSourceStorage {
|
||||
return false
|
||||
}
|
||||
parts := strings.Split(rel, string(filepath.Separator))
|
||||
return len(parts) == 4 &&
|
||||
parts[0] == "storage" &&
|
||||
parts[1] == filepath.Base(src.SessionRoot) &&
|
||||
strings.HasSuffix(parts[3], ".json") &&
|
||||
(!requireExisting || IsRegularFile(path))
|
||||
}
|
||||
|
||||
func readOpenCodeProviderStorageSessionID(path string) string {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
var data struct {
|
||||
SessionID string `json:"sessionID"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &data); err != nil {
|
||||
return ""
|
||||
}
|
||||
return data.SessionID
|
||||
}
|
||||
|
||||
func findOpenCodeProviderStorageSessionIDByMessageID(
|
||||
openCodeDir, messageID string,
|
||||
) string {
|
||||
messageRoot := filepath.Join(openCodeDir, "storage", "message")
|
||||
entries, err := os.ReadDir(messageRoot)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(messageRoot, entry.Name(), messageID+".json")
|
||||
if info, err := os.Stat(path); err == nil && !info.IsDir() {
|
||||
return entry.Name()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func openCodeFormatProviderCapabilities() Capabilities {
|
||||
return Capabilities{
|
||||
Source: SourceCapabilities{
|
||||
DiscoverSources: CapabilitySupported,
|
||||
WatchSources: CapabilitySupported,
|
||||
ClassifyChangedPath: CapabilitySupported,
|
||||
FindSource: CapabilitySupported,
|
||||
CompositeFingerprint: CapabilitySupported,
|
||||
IncrementalAppend: CapabilityNotApplicable,
|
||||
MultiSessionSource: CapabilityNotApplicable,
|
||||
PerSessionErrors: CapabilityNotApplicable,
|
||||
ExcludedSessions: CapabilityNotApplicable,
|
||||
ForceReplaceOnParse: CapabilityNotApplicable,
|
||||
},
|
||||
Content: ContentCapabilities{
|
||||
FirstMessage: CapabilitySupported,
|
||||
Cwd: CapabilitySupported,
|
||||
Relationships: CapabilitySupported,
|
||||
Thinking: CapabilitySupported,
|
||||
ToolCalls: CapabilitySupported,
|
||||
PerMessageTokenUsage: CapabilitySupported,
|
||||
Model: CapabilitySupported,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user