Files
wehub-resource-sync f99010fae1
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
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

286 lines
8.3 KiB
Go

package parser
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// Shelley stores every conversation in one shared SQLite database
// (shelley.db). It is a multi-session container provider: discovery surfaces
// the database as a single source and Parse fans it out into one session per
// conversation, addressed by "<db>::<conversationID>" virtual paths. All
// behavior is wired into the shared multi-session-container base via options.
func newShelleyProviderFactory(def AgentDef) ProviderFactory {
return NewMultiSessionProviderFactory(
def,
shelleyProviderCapabilities(),
func(cfg ProviderConfig) multiSessionContainerSourceSet {
return NewMultiSessionContainerSourceSet(
AgentShelley,
cfg.Roots,
WithContainerDiscovery(shelleyDiscoverContainers),
WithWatchRoots(shelleyWatchRoots),
WithChangedPathClassifier(shelleyClassifyPath),
WithMemberLookup(shelleyFindMember),
WithFingerprint(shelleyFingerprintSource),
WithContainerParse(shelleyParseContainer),
WithMemberParse(shelleyParseMember),
// Special case: confirm a stored conversation still exists for
// RequireFreshSource lookups.
WithMemberPresence(shelleyMemberPresent),
)
},
)
}
func shelleyDiscoverContainers(root string) []string {
if dbPath := shelleyDBPath(root); dbPath != "" {
return []string{dbPath}
}
return nil
}
func shelleyWatchRoots(roots []string) []WatchRoot {
out := make([]WatchRoot, 0, len(roots))
for _, root := range roots {
out = append(out, WatchRoot{
Path: root,
Recursive: false,
IncludeGlobs: []string{shelleyDBName, shelleyDBName + "-*"},
DebounceKey: string(AgentShelley) + ":db:" + root,
})
}
return out
}
// shelleyClassifyPath maps a stored or changed path to its database container
// and conversation. allowMissing relaxes the regular-file requirement so a
// database delete (or its WAL/SHM sibling) still classifies for changed-path
// tombstones, reproducing the legacy strict sourceRef / lenient
// sourceRefForChangedPath split.
func shelleyClassifyPath(
root, path string, allowMissing bool,
) (multiSessionMatch, bool) {
root = filepath.Clean(root)
path = filepath.Clean(path)
requireRegular := !allowMissing
if dbPath, conversationID, ok := parseShelleyVirtualPath(path); ok {
if !shelleyDBUnderRoot(root, dbPath, requireRegular) {
return multiSessionMatch{}, false
}
return multiSessionMatch{
Path: path,
Container: dbPath,
MemberID: conversationID,
}, true
}
if shelleyDBUnderRoot(root, path, requireRegular) {
return multiSessionMatch{Path: path, Container: path}, true
}
if allowMissing {
if dbPath, ok := shelleyDBPathForEvent(root, path); ok {
return multiSessionMatch{Path: dbPath, Container: dbPath}, true
}
}
return multiSessionMatch{}, false
}
// shelleyFindMember resolves a raw conversation ID to its virtual source path
// inside the shared database. The ID is validated only to reject path-like
// input; all conversations live in one DB.
func shelleyFindMember(root, rawID string) (multiSessionMatch, bool) {
if root == "" || !IsValidSessionID(rawID) {
return multiSessionMatch{}, false
}
dbPath := shelleyDBPath(root)
if dbPath == "" || !ShelleyConversationExists(dbPath, rawID) {
return multiSessionMatch{}, false
}
return multiSessionMatch{
Path: ShelleyVirtualPath(dbPath, rawID),
Container: dbPath,
MemberID: rawID,
}, true
}
func shelleyFingerprintSource(src multiSessionSource) (SourceFingerprint, error) {
info, err := os.Stat(src.Container)
if err != nil {
if os.IsNotExist(err) {
return SourceFingerprint{}, nil
}
return SourceFingerprint{}, fmt.Errorf("stat %s: %w", src.Container, err)
}
fingerprint := SourceFingerprint{
Size: info.Size(),
MTimeNS: info.ModTime().UnixNano(),
}
if src.MemberID == "" {
if compositeMtime, err := sqliteDBCompositeMtime(src.Container); err == nil {
fingerprint.MTimeNS = compositeMtime
}
fingerprint.Hash, err = hashJSONLSourceFile(src.Container)
if err != nil {
return SourceFingerprint{}, err
}
return fingerprint, nil
}
conn, err := OpenShelleyDB(src.Container)
if err != nil {
return SourceFingerprint{}, err
}
defer conn.Close()
metas, err := ListShelleyConversationMetas(conn, src.Container)
if err != nil {
return SourceFingerprint{}, err
}
for _, meta := range metas {
if meta.RawID != src.MemberID {
continue
}
fingerprint.MTimeNS = meta.FileMtime
fingerprint.Hash = meta.Fingerprint
return fingerprint, nil
}
// The conversation row is gone but the database file is still present.
// Return a keyed-empty fingerprint without error (matching the db-backed
// and Kiro tombstone behavior) so the engine proceeds to Parse rather than
// aborting on the fingerprint. Parse then force-replaces the deleted
// conversation out of the archive; erroring here would strand the stale
// session because the engine fingerprints before parsing.
return SourceFingerprint{}, nil
}
func shelleyMemberPresent(src multiSessionSource) bool {
if src.MemberID == "" {
return IsRegularFile(src.Container)
}
return ShelleyConversationExists(src.Container, src.MemberID)
}
func shelleyParseMember(
src multiSessionSource, req ParseRequest,
) (*ParseResult, error) {
dbInfo, err := os.Stat(src.Container)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("stat %s: %w", src.Container, err)
}
if !IsValidSessionID(src.MemberID) {
return nil, fmt.Errorf("invalid Shelley session ID: %s", src.MemberID)
}
conn, err := OpenShelleyDB(src.Container)
if err != nil {
return nil, err
}
defer conn.Close()
return parseShelleyConversationFromDB(
conn, src.Container, src.MemberID, req.Machine, dbInfo,
)
}
func shelleyParseContainer(
src multiSessionSource, req ParseRequest,
) ([]ParseResult, error) {
dbInfo, err := os.Stat(src.Container)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("stat %s: %w", src.Container, err)
}
conn, err := OpenShelleyDB(src.Container)
if err != nil {
return nil, err
}
defer conn.Close()
metas, err := ListShelleyConversationMetas(conn, src.Container)
if err != nil {
return nil, err
}
results := make([]ParseResult, 0, len(metas))
for _, meta := range metas {
result, err := parseShelleyConversationFromDB(
conn, src.Container, meta.RawID, req.Machine, dbInfo,
)
if err != nil {
return nil, err
}
if result == nil {
continue
}
results = append(results, *result)
}
return results, nil
}
// shelleyDBPath resolves the shared shelley.db under root, returning "" when
// the root holds no Shelley database.
func shelleyDBPath(root string) string {
if root == "" {
return ""
}
path := filepath.Join(root, shelleyDBName)
if !IsRegularFile(path) {
return ""
}
return path
}
func shelleyDBUnderRoot(root, dbPath string, requireRegular bool) bool {
root = filepath.Clean(root)
dbPath = filepath.Clean(dbPath)
rel, ok := relUnder(root, dbPath)
if !ok || filepath.ToSlash(rel) != shelleyDBName {
return false
}
return !requireRegular || IsRegularFile(dbPath)
}
func shelleyDBPathForEvent(root, path string) (string, bool) {
root = filepath.Clean(root)
path = filepath.Clean(path)
rel, ok := relUnder(root, path)
if !ok {
return "", false
}
if filepath.ToSlash(rel) == shelleyDBName ||
(filepath.Dir(rel) == "." &&
strings.HasPrefix(filepath.Base(rel), shelleyDBName+"-")) {
return filepath.Join(root, shelleyDBName), true
}
return "", false
}
// parseShelleyVirtualPath splits a Shelley virtual source path into its
// physical shelley.db path and raw conversation ID. The container basename
// must be shelley.db and the conversation ID must be non-empty.
func parseShelleyVirtualPath(path string) (string, string, bool) {
return ParseVirtualSourcePathForBase(path, shelleyDBName)
}
func shelleyProviderCapabilities() Capabilities {
return Capabilities{
Source: multiSessionContainerSourceCapabilities(
CapabilitySupported,
CapabilitySupported,
),
Content: ContentCapabilities{
FirstMessage: CapabilitySupported,
SessionName: CapabilitySupported,
Cwd: CapabilitySupported,
Relationships: CapabilitySupported,
Thinking: CapabilitySupported,
ToolCalls: CapabilitySupported,
ToolResults: CapabilitySupported,
PerMessageTokenUsage: CapabilitySupported,
Model: CapabilitySupported,
},
}
}