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

376 lines
8.5 KiB
Go

package sync
import (
"context"
"os"
"path"
"strings"
"go.kenn.io/agentsview/internal/db"
"go.kenn.io/agentsview/internal/parser"
)
var (
fetchS3Object = parser.FetchS3Object
statS3Object = parser.StatS3Object
statClaudeS3Session = parser.StatClaudeS3Session
statCodexS3Session = parser.StatCodexS3Session
)
func s3SourceFileInfo(file parser.DiscoveredFile) (os.FileInfo, error) {
size := file.SourceSize
mtime := file.SourceMtime
if mtime == 0 {
obj, err := statS3SourceObject(file)
if err != nil {
return nil, err
}
size = obj.Size
mtime = obj.LastModified.UnixNano()
}
return fakeSnapshotInfo{
fName: path.Base(file.Path),
fSize: size,
fMtime: mtime,
}, nil
}
func s3SourceFingerprint(file parser.DiscoveredFile) string {
if file.SourceFingerprint != "" {
return file.SourceFingerprint
}
if file.SourceMtime != 0 {
return ""
}
obj, err := statS3SourceObject(file)
if err != nil {
return ""
}
return obj.Fingerprint
}
func statS3SourceObject(file parser.DiscoveredFile) (parser.S3Object, error) {
stat := statS3Object
switch file.Agent {
case parser.AgentClaude:
stat = statClaudeS3Session
case parser.AgentCodex:
stat = statCodexS3Session
}
return stat(file.Path)
}
func s3DiscoveredSessionID(file parser.DiscoveredFile) string {
switch file.Agent {
case parser.AgentClaude:
id := claudeSessionIDFromPath(file.Path)
if id == "" {
return ""
}
return applyIDPrefixToID(s3SessionIDPrefix(file.Machine), id)
case parser.AgentCodex:
uuid := parser.CodexSessionUUIDFromFilename(path.Base(file.Path))
if uuid == "" {
return ""
}
return applyIDPrefixToID(
s3SessionIDPrefix(file.Machine), "codex:"+uuid,
)
default:
return ""
}
}
func (e *Engine) s3SourceMetadataChanged(file parser.DiscoveredFile) bool {
if file.SourceMtime == 0 {
return false
}
return e.s3SourceMetadataChangedFromInfo(
file, file.SourceSize, file.SourceMtime, file.SourceFingerprint,
)
}
func (e *Engine) s3SourceMetadataChangedFromInfo(
file parser.DiscoveredFile, size, mtime int64, sourceFingerprint string,
) bool {
sessionID := s3DiscoveredSessionID(file)
if sessionID == "" {
return false
}
storedPath := e.db.GetSessionFilePath(sessionID)
if storedPath == "" || storedPath != file.Path {
return true
}
storedSize, storedMtime, ok := e.db.GetSessionFileInfo(sessionID)
if !ok {
return true
}
if storedSize != size || storedMtime != mtime {
return true
}
if sourceFingerprint != "" {
storedHash, ok := e.db.GetSessionFileHash(sessionID)
if !ok || storedHash != sourceFingerprint {
return true
}
}
return false
}
type s3CodexIndexSnapshot struct {
mtime int64
statOK bool
missing bool
err error
titles map[string]string
titlesLoaded bool
}
func (e *Engine) resetS3CodexIndexCache() {
e.s3CodexIndexMu.Lock()
e.s3CodexIndexCache = make(map[string]s3CodexIndexSnapshot)
e.s3CodexIndexMu.Unlock()
}
func (e *Engine) s3CodexIndexSnapshot(
indexURI string, needTitles bool,
) s3CodexIndexSnapshot {
e.s3CodexIndexMu.Lock()
defer e.s3CodexIndexMu.Unlock()
if e.s3CodexIndexCache == nil {
e.s3CodexIndexCache = make(map[string]s3CodexIndexSnapshot)
}
snapshot, ok := e.s3CodexIndexCache[indexURI]
if !ok {
obj, err := statS3Object(indexURI)
if err != nil {
if isMissingS3Object(err) {
snapshot = s3CodexIndexSnapshot{
statOK: true,
missing: true,
titlesLoaded: true,
}
} else {
snapshot.err = err
}
e.s3CodexIndexCache[indexURI] = snapshot
return snapshot
}
snapshot = s3CodexIndexSnapshot{
mtime: obj.LastModified.UnixNano(),
statOK: true,
}
e.s3CodexIndexCache[indexURI] = snapshot
}
if needTitles && snapshot.statOK && !snapshot.missing &&
!snapshot.titlesLoaded && snapshot.err == nil {
titles, err := fetchS3CodexSessionIndexTitles(indexURI)
snapshot.titlesLoaded = err == nil
if err == nil {
snapshot.titles = titles
} else if isMissingS3Object(err) {
snapshot.missing = true
snapshot.titlesLoaded = true
snapshot.titles = nil
} else {
snapshot.err = err
}
e.s3CodexIndexCache[indexURI] = snapshot
}
return snapshot
}
func (e *Engine) s3CodexIndexNeedsRefreshSince(
file parser.DiscoveredFile,
cutoffNs int64,
) bool {
uuid := parser.CodexSessionUUIDFromFilename(path.Base(file.Path))
if uuid == "" {
return false
}
indexURI, ok := parser.CodexS3SessionIndexURI(file.Path)
if !ok {
return false
}
snapshot := e.s3CodexIndexSnapshot(indexURI, false)
if snapshot.err != nil {
return true
}
if !snapshot.statOK {
return false
}
if snapshot.missing {
return e.s3CodexStoredNameDiffers(file, uuid, "")
}
if snapshot.mtime < cutoffNs {
return false
}
snapshot = e.s3CodexIndexSnapshot(indexURI, true)
if snapshot.err != nil {
return true
}
if snapshot.missing {
return e.s3CodexStoredNameDiffers(file, uuid, "")
}
if !snapshot.titlesLoaded {
return false
}
title := snapshot.titles[uuid]
return e.s3CodexStoredNameDiffers(file, uuid, title)
}
func (e *Engine) s3CodexIndexSessionNameChanged(
file parser.DiscoveredFile, uuid string,
) (bool, error) {
indexURI, ok := parser.CodexS3SessionIndexURI(file.Path)
if !ok {
return false, nil
}
snapshot := e.s3CodexIndexSnapshot(indexURI, true)
if snapshot.err != nil {
return false, snapshot.err
}
if snapshot.missing {
return e.s3CodexStoredNameDiffers(file, uuid, ""), nil
}
if !snapshot.statOK || !snapshot.titlesLoaded {
return false, nil
}
title := snapshot.titles[uuid]
return e.s3CodexStoredNameDiffers(file, uuid, title), nil
}
func (e *Engine) s3CodexStoredNameDiffers(
file parser.DiscoveredFile, uuid, indexTitle string,
) bool {
sessionID := applyIDPrefixToID(
s3SessionIDPrefix(file.Machine), "codex:"+uuid,
)
return e.codexStoredNameDiffersBySessionID(
sessionID, indexTitle, false,
)
}
func fetchS3CodexSessionIndexTitles(
indexURI string,
) (map[string]string, error) {
rc, err := fetchS3Object(indexURI)
if err != nil {
return nil, err
}
defer rc.Close()
return parser.ParseCodexSessionIndexTitles(rc)
}
func isS3SourcePath(path string) bool {
return strings.HasPrefix(path, "s3://")
}
func (e *Engine) shouldSkipFileWithPrefix(
prefix, sessionID string, info os.FileInfo, sourceFingerprint ...string,
) bool {
if e.forceParse {
return false
}
fullID := applyIDPrefixToID(prefix, sessionID)
storedSize, storedMtime, ok := e.db.GetSessionFileInfo(
fullID,
)
if !ok {
return false
}
if storedSize != info.Size() ||
storedMtime != info.ModTime().UnixNano() {
return false
}
if len(sourceFingerprint) > 0 && sourceFingerprint[0] != "" {
storedHash, ok := e.db.GetSessionFileHash(fullID)
if !ok || storedHash != sourceFingerprint[0] {
return false
}
}
if e.db.GetSessionDataVersion(fullID) <
db.CurrentDataVersion() {
return false
}
return true
}
func s3MachineFromRoot(root string) string {
segs := strings.Split(strings.TrimPrefix(root, "s3://"), "/")
for i := len(segs) - 2; i > 1; i-- {
if segs[i] == "raw" && isS3AgentRootSegment(segs[i+1]) {
return segs[i-1]
}
}
return ""
}
func isS3AgentRootSegment(seg string) bool {
return seg == "claude" || seg == "codex"
}
func s3RelFromRoot(root, uri string) (string, bool) {
prefix := strings.TrimSuffix(root, "/")
if !strings.HasPrefix(uri, prefix+"/") {
return "", false
}
return strings.TrimPrefix(uri, prefix+"/"), true
}
func (e *Engine) hydrateS3DiscoveredFile(
ctx context.Context, sessionID string, file *parser.DiscoveredFile,
) {
if !isS3SourcePath(file.Path) {
return
}
if sess, _ := e.db.GetSession(ctx, sessionID); sess != nil {
if sess.Project != "" &&
!parser.NeedsProjectReparse(sess.Project) {
file.Project = sess.Project
}
}
for _, root := range e.agentDirs[file.Agent] {
if !isS3SourcePath(root) {
continue
}
rel, ok := s3RelFromRoot(root, file.Path)
if !ok {
continue
}
if file.Machine == "" {
file.Machine = s3MachineFromRoot(root)
}
if file.Project == "" && file.Agent == parser.AgentClaude {
if first, _, ok := strings.Cut(rel, "/"); ok {
file.Project = first
}
}
break
}
if file.Machine == "" {
if host, _ := parser.StripHostPrefix(sessionID); host != "" {
file.Machine = host
}
}
if file.SourceMtime == 0 {
stat := statS3Object
switch file.Agent {
case parser.AgentClaude:
stat = statClaudeS3Session
case parser.AgentCodex:
stat = statCodexS3Session
}
if obj, err := stat(file.Path); err == nil {
file.SourceSize = obj.Size
file.SourceMtime = obj.LastModified.UnixNano()
file.SourceFingerprint = obj.Fingerprint
}
}
}