Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

590 lines
15 KiB
Go

package parser
import (
"context"
"crypto/sha256"
"database/sql"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
func newDevinProviderFactory(def AgentDef) ProviderFactory {
return devinProviderFactory{def: cloneAgentDef(def)}
}
type devinProviderFactory struct {
def AgentDef
}
func (f devinProviderFactory) Definition() AgentDef {
return cloneAgentDef(f.def)
}
func (f devinProviderFactory) Capabilities() Capabilities {
return devinProviderCapabilities()
}
func (f devinProviderFactory) NewProvider(cfg ProviderConfig) Provider {
cfg = cfg.Clone()
return &devinProvider{
ProviderBase: ProviderBase{
Def: cloneAgentDef(f.def),
Caps: devinProviderCapabilities(),
Config: cfg,
},
sources: newDevinSourceSet(cfg.Roots),
}
}
type devinProvider struct {
ProviderBase
sources devinSourceSet
}
func (p *devinProvider) Discover(ctx context.Context) ([]SourceRef, error) {
return p.sources.Discover(ctx)
}
func (p *devinProvider) WatchPlan(ctx context.Context) (WatchPlan, error) {
return p.sources.WatchPlan(ctx)
}
func (p *devinProvider) SourcesForChangedPath(
ctx context.Context,
req ChangedPathRequest,
) ([]SourceRef, error) {
return p.sources.SourcesForChangedPath(ctx, req)
}
func (p *devinProvider) FindSource(
ctx context.Context,
req FindSourceRequest,
) (SourceRef, bool, error) {
req = ProviderFindRequestWithRawSessionID(p.Def, req)
return p.sources.FindSource(ctx, req)
}
func (p *devinProvider) Fingerprint(
ctx context.Context,
source SourceRef,
) (SourceFingerprint, error) {
return p.sources.Fingerprint(ctx, source)
}
func (p *devinProvider) Parse(
ctx context.Context,
req ParseRequest,
) (ParseOutcome, error) {
if err := ctx.Err(); err != nil {
return ParseOutcome{}, err
}
src, ok := p.sources.sourceFromRef(req.Source)
if !ok {
return ParseOutcome{}, fmt.Errorf("%s source path unavailable", AgentDevin)
}
if _, err := os.Stat(src.DBPath); err != nil {
if os.IsNotExist(err) {
return ParseOutcome{
ResultSetComplete: true,
SkipReason: SkipNoSession,
}, nil
}
return ParseOutcome{}, fmt.Errorf("stat %s: %w", src.DBPath, err)
}
machine := firstNonEmptyJSONLString(req.Machine, p.Config.Machine)
sess, msgs, err := parseDevinSession(src.DBPath, src.SessionID, machine)
if errors.Is(err, sql.ErrNoRows) {
return ParseOutcome{
ResultSetComplete: true,
ForceReplace: true,
SkipReason: SkipNoSession,
}, nil
}
var transcriptErr *devinTranscriptError
if errors.As(err, &transcriptErr) {
return ParseOutcome{}, transcriptErr
}
if err != nil {
return ParseOutcome{}, err
}
if sess == nil {
return ParseOutcome{
ResultSetComplete: true,
ForceReplace: 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,
ForceReplace: true,
}, nil
}
type devinSource struct {
Root string
DBPath string
SessionID string
}
type devinSourceSet struct {
roots []string
}
func newDevinSourceSet(roots []string) devinSourceSet {
return devinSourceSet{roots: cleanJSONLRoots(roots)}
}
func (s devinSourceSet) 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
}
dbPath := devinDBPath(root)
if dbPath == "" {
continue
}
metas, err := ListDevinSessionMeta(dbPath)
if err != nil {
return nil, err
}
for _, meta := range metas {
addJSONLSource(s.newSourceRefWithMTime(root, dbPath, meta.RawSessionID, meta.FileMtime), &sources, seen)
}
}
sortJSONLSources(sources)
return sources, nil
}
func (s devinSourceSet) WatchPlan(context.Context) (WatchPlan, error) {
roots := make([]WatchRoot, 0, len(s.roots)*2)
for _, root := range s.roots {
cliRoot := filepath.Join(root, "cli")
roots = append(roots,
WatchRoot{
Path: cliRoot,
Recursive: false,
IncludeGlobs: []string{devinDBFilename, devinDBFilename + "-*"},
DebounceKey: string(AgentDevin) + ":db:" + root,
},
WatchRoot{
Path: filepath.Join(cliRoot, "transcripts"),
Recursive: false,
IncludeGlobs: []string{"*.json"},
DebounceKey: string(AgentDevin) + ":transcripts:" + root,
},
)
}
return WatchPlan{Roots: roots}, nil
}
func (s devinSourceSet) SourcesForChangedPath(
ctx context.Context,
req ChangedPathRequest,
) ([]SourceRef, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
for _, root := range s.roots {
if req.WatchRoot != "" && !samePath(req.WatchRoot, filepath.Join(root, "cli")) && !samePath(req.WatchRoot, filepath.Join(root, "cli", "transcripts")) {
continue
}
if ref, ok := s.sourceRef(root, req.Path, true); ok {
return []SourceRef{ref}, nil
}
if dbPath, ok := s.dbPathForEvent(root, req.Path); ok {
metas, err := ListDevinSessionMeta(dbPath)
if err != nil {
return nil, err
}
sources := make([]SourceRef, 0, len(metas))
seen := make(map[string]struct{}, len(metas))
for _, meta := range metas {
addJSONLSource(s.newSourceRefWithMTime(root, dbPath, meta.RawSessionID, meta.FileMtime), &sources, seen)
}
for _, path := range req.StoredSourcePaths {
ref, ok := s.sourceRef(root, path, true)
if !ok {
continue
}
src := ref.Opaque.(devinSource)
if !samePath(src.DBPath, dbPath) {
continue
}
addJSONLSource(ref, &sources, seen)
}
sortJSONLSources(sources)
return sources, nil
}
if sessionID, ok := s.transcriptSessionIDForEvent(root, req.Path); ok {
if ref, ok, err := s.findByRawSessionID(root, sessionID, false); err != nil {
return nil, err
} else if ok {
return []SourceRef{ref}, nil
}
for _, path := range req.StoredSourcePaths {
ref, ok := s.sourceRef(root, path, true)
if !ok {
continue
}
src := ref.Opaque.(devinSource)
if src.SessionID == sessionID {
return []SourceRef{ref}, nil
}
}
}
}
return nil, nil
}
func (s devinSourceSet) 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 {
ref, ok := s.sourceRef(root, path, true)
if !ok {
continue
}
src := ref.Opaque.(devinSource)
if req.RawSessionID != "" && src.SessionID != req.RawSessionID {
continue
}
if req.RequireFreshSource {
fresh, err := s.sourceExists(src)
if err != nil {
return SourceRef{}, false, err
}
if !fresh {
continue
}
}
return ref, true, nil
}
}
if req.RawSessionID == "" {
return SourceRef{}, false, nil
}
for _, root := range s.roots {
ref, ok, err := s.findByRawSessionID(root, req.RawSessionID, req.RequireFreshSource)
if err != nil {
return SourceRef{}, false, err
}
if ok {
return ref, true, nil
}
}
return SourceRef{}, false, nil
}
func (s devinSourceSet) Fingerprint(
ctx context.Context,
source SourceRef,
) (SourceFingerprint, error) {
if err := ctx.Err(); err != nil {
return SourceFingerprint{}, err
}
src, ok := s.sourceFromRef(source)
if !ok {
return SourceFingerprint{}, fmt.Errorf("%s source path unavailable", AgentDevin)
}
key := firstNonEmptyJSONLString(source.FingerprintKey, source.Key, src.virtualPath())
dbInfo, err := os.Stat(src.DBPath)
if err != nil {
if os.IsNotExist(err) {
return SourceFingerprint{Key: key}, nil
}
return SourceFingerprint{}, fmt.Errorf("stat %s: %w", src.DBPath, err)
}
meta, err := getDevinSessionMeta(src.DBPath, src.SessionID)
if err != nil {
return SourceFingerprint{}, err
}
if meta == nil {
return SourceFingerprint{Key: key}, nil
}
fingerprint := SourceFingerprint{Key: key}
transcriptPath := filepath.Join(filepath.Dir(src.DBPath), "transcripts", src.SessionID+".json")
transcriptInfo, err := os.Stat(transcriptPath)
if err != nil && !os.IsNotExist(err) {
return SourceFingerprint{}, newDevinTranscriptError("stat", err)
}
fingerprint.MTimeNS = maxDevinFingerprintMTime(meta, dbInfo, transcriptInfo)
hash, err := devinFingerprintHash(meta, src.DBPath, transcriptPath, transcriptInfo)
if err != nil {
return SourceFingerprint{}, err
}
fingerprint.Hash = hash
if transcriptInfo != nil {
fingerprint.Size = transcriptInfo.Size()
}
return fingerprint, nil
}
func maxDevinFingerprintMTime(
meta *DevinSessionMeta,
dbInfo os.FileInfo,
transcriptInfo os.FileInfo,
) int64 {
var mtime int64
if meta != nil {
mtime = meta.FileMtime
if created := meta.CreatedAt.UnixNano(); created > mtime {
mtime = created
}
if lastActivity := meta.LastActivity.UnixNano(); lastActivity > mtime {
mtime = lastActivity
}
}
if dbInfo != nil {
if dbMTime := dbInfo.ModTime().UnixNano(); dbMTime > mtime {
mtime = dbMTime
}
}
if transcriptInfo != nil {
if transcriptMTime := transcriptInfo.ModTime().UnixNano(); transcriptMTime > mtime {
mtime = transcriptMTime
}
}
return mtime
}
func devinFingerprintHash(
meta *DevinSessionMeta,
dbPath string,
transcriptPath string,
transcriptInfo os.FileInfo,
) (string, error) {
h := sha256.New()
if _, err := fmt.Fprintf(
h,
"session\x00%s\x00created\x00%d\x00last_activity\x00%d\x00updated\x00%d\x00title\x00%s\x00cwd\x00%s\x00model\x00%s\x00",
meta.RawSessionID,
meta.CreatedAt.UnixMilli(),
meta.LastActivity.UnixMilli(),
meta.UpdatedAt.UnixMilli(),
meta.Title,
meta.CWD,
meta.Model,
); err != nil {
return "", err
}
if transcriptInfo == nil {
if err := devinAppendMessageNodesFingerprint(h, dbPath, meta.RawSessionID); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
if _, err := fmt.Fprintf(
h,
"transcript_size\x00%d\x00transcript_mtime\x00%d\x00",
transcriptInfo.Size(),
transcriptInfo.ModTime().UnixNano(),
); err != nil {
return "", err
}
f, err := os.Open(transcriptPath)
if err != nil {
return "", newDevinTranscriptError("open", err)
}
defer f.Close()
if _, err := io.Copy(h, f); err != nil {
return "", newDevinTranscriptError("hash", err)
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
func devinAppendMessageNodesFingerprint(h io.Writer, dbPath, rawSessionID string) error {
nodes, err := listDevinMessageNodes(dbPath, rawSessionID)
if err != nil {
return err
}
var maxCreatedAtMS int64
for _, node := range nodes {
if node.CreatedAtMS > maxCreatedAtMS {
maxCreatedAtMS = node.CreatedAtMS
}
}
if _, err := fmt.Fprintf(h, "message_nodes\x00count\x00%d\x00max_created\x00%d\x00", len(nodes), maxCreatedAtMS); err != nil {
return err
}
for _, node := range nodes {
if _, err := fmt.Fprintf(
h,
"row\x00%d\x00node\x00%d\x00parent_valid\x00%t\x00parent\x00%d\x00created\x00%d\x00chat_len\x00%d\x00",
node.RowID,
node.NodeID,
node.ParentNodeID.Valid,
node.ParentNodeID.Int64,
node.CreatedAtMS,
len(node.ChatMessage),
); err != nil {
return err
}
if _, err := io.WriteString(h, node.ChatMessage); err != nil {
return err
}
if _, err := io.WriteString(h, "\x00"); err != nil {
return err
}
}
return nil
}
func (s devinSourceSet) sourceFromRef(source SourceRef) (devinSource, bool) {
switch src := source.Opaque.(type) {
case devinSource:
return src, src.DBPath != "" && src.SessionID != ""
case *devinSource:
if src != nil && src.DBPath != "" && src.SessionID != "" {
return *src, true
}
}
for _, candidate := range []string{source.DisplayPath, source.FingerprintKey, source.Key} {
for _, root := range s.roots {
if ref, ok := s.sourceRef(root, candidate, true); ok {
return ref.Opaque.(devinSource), true
}
}
}
return devinSource{}, false
}
func (s devinSourceSet) sourceRef(root, path string, allowMissing bool) (SourceRef, bool) {
root = filepath.Clean(root)
path = filepath.Clean(path)
dbPath, sessionID, ok := ParseVirtualSourcePathForBase(path, devinDBFilename)
if !ok || sessionID == "" {
return SourceRef{}, false
}
if !samePath(dbPath, filepath.Join(root, "cli", devinDBFilename)) {
return SourceRef{}, false
}
if !allowMissing && !IsRegularFile(dbPath) {
return SourceRef{}, false
}
return s.newSourceRef(root, dbPath, sessionID), true
}
func (s devinSourceSet) dbPathForEvent(root, path string) (string, bool) {
root = filepath.Clean(root)
path = filepath.Clean(path)
cliRoot := filepath.Join(root, "cli")
rel, ok := relUnder(cliRoot, path)
if !ok || strings.Contains(rel, string(filepath.Separator)) {
return "", false
}
if rel == devinDBFilename || rel == devinDBFilename+"-wal" || rel == devinDBFilename+"-shm" {
return filepath.Join(cliRoot, devinDBFilename), true
}
return "", false
}
func (s devinSourceSet) transcriptSessionIDForEvent(root, path string) (string, bool) {
root = filepath.Clean(root)
path = filepath.Clean(path)
rel, ok := relUnder(filepath.Join(root, "cli", "transcripts"), path)
if !ok || strings.Contains(rel, string(filepath.Separator)) || filepath.Ext(rel) != ".json" {
return "", false
}
return strings.TrimSuffix(rel, ".json"), true
}
func (s devinSourceSet) sourceExists(src devinSource) (bool, error) {
if !IsRegularFile(src.DBPath) {
return false, nil
}
meta, err := getDevinSessionMeta(src.DBPath, src.SessionID)
if err != nil {
return false, err
}
return meta != nil, nil
}
func (s devinSourceSet) findByRawSessionID(root, rawID string, requireFresh bool) (SourceRef, bool, error) {
if root == "" || rawID == "" {
return SourceRef{}, false, nil
}
dbPath := devinDBPath(root)
if dbPath == "" {
return SourceRef{}, false, nil
}
meta, err := getDevinSessionMeta(dbPath, rawID)
if err != nil {
return SourceRef{}, false, err
}
if meta == nil {
return SourceRef{}, false, nil
}
ref := s.newSourceRefWithMTime(root, dbPath, rawID, meta.FileMtime)
if requireFresh {
fresh, err := s.sourceExists(ref.Opaque.(devinSource))
if err != nil {
return SourceRef{}, false, err
}
if !fresh {
return SourceRef{}, false, nil
}
}
return ref, true, nil
}
func (s devinSourceSet) newSourceRef(root, dbPath, sessionID string) SourceRef {
return s.newSourceRefWithMTime(root, dbPath, sessionID, 0)
}
func (s devinSourceSet) newSourceRefWithMTime(root, dbPath, sessionID string, mtimeNS int64) SourceRef {
virtualPath := VirtualSourcePath(dbPath, sessionID)
return SourceRef{
Provider: AgentDevin,
Key: virtualPath,
DisplayPath: virtualPath,
FingerprintKey: virtualPath,
DiscoveryMTimeNS: mtimeNS,
Opaque: devinSource{
Root: root,
DBPath: dbPath,
SessionID: sessionID,
},
}
}
func (s devinSource) virtualPath() string {
return VirtualSourcePath(s.DBPath, s.SessionID)
}
func devinProviderCapabilities() Capabilities {
return Capabilities{
Source: dbBackedSourceCapabilities(CapabilityNotApplicable),
Content: ContentCapabilities{
FirstMessage: CapabilitySupported,
SessionName: CapabilitySupported,
Cwd: CapabilitySupported,
Thinking: CapabilitySupported,
ToolCalls: CapabilitySupported,
ToolResults: CapabilitySupported,
PerMessageTokenUsage: CapabilitySupported,
Model: CapabilitySupported,
},
}
}