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

257 lines
9.7 KiB
Go

// ABOUTME: Per-session freshness gate for OpenCode-family file-backed
// ABOUTME: storage sessions, skipping re-parse when the tree is unchanged.
package sync
import (
"path/filepath"
"strings"
"go.kenn.io/agentsview/internal/parser"
)
// File-backed OpenCode-format sessions fan one session JSON plus its
// message and part files into a single source whose parse re-reads the
// whole tree. They are deliberately excluded from the mtime-keyed skip
// cache (see shouldCacheSkip): the cache keys on a composite max-mtime,
// which cannot see a child rewritten within one filesystem mtime granule
// or with a restored timestamp. The consequence was that every full sync
// and unwatched-root poll re-read and re-parsed every storage session of
// an idle archive, and every watcher event on a streaming session
// re-parsed its entire history.
//
// The gate restores a cheap answer for the unchanged case using
// parser.StatOpenCodeStorageSessionState, a per-file (name, size, mtimeNS)
// signature over exactly the files the parse would read. A session is
// skipped before fingerprinting only when its current signature equals one
// captured before a parse whose outcome the archive then absorbed: either
// every parsed result was dropped as already stored, or the results were
// confirmed fully written by the batch write path. The remaining blind
// spot — an in-place child rewrite preserving both size and mtime — is
// covered on the watcher path by invalidating trust for a session whenever
// a changed-path event resolves to it, so an event-signaled change always
// re-verifies by content. Trust is in-memory only: a restart re-verifies
// every session once, mirroring the shared-SQLite container gate.
// openCodeStorageSessionPath returns the discovered file's path when it is
// a file-backed OpenCode-format storage session JSON under a storage-mode
// root of its agent, or "" otherwise (SQLite containers and virtual rows
// included). The root-mode walk mirrors shouldCacheSkip, which excludes
// these same paths from the mtime skip cache.
func (e *Engine) openCodeStorageSessionPath(file parser.DiscoveredFile) string {
if !isOpenCodeFormatStorageAgent(file.Agent) {
return ""
}
if filepath.Base(file.Path) == openCodeFormatDBName(file.Agent) {
return ""
}
if isOpenCodeFormatSQLiteVirtualPath(file.Agent, file.Path) {
return ""
}
for _, dir := range e.agentDirs[file.Agent] {
if dir == "" {
continue
}
src := resolveOpenCodeFormatSource(file.Agent, dir)
if src.Mode != parser.OpenCodeSourceStorage {
continue
}
rel, ok := isUnder(dir, file.Path)
if !ok {
continue
}
rel = filepath.ToSlash(rel)
sessionPrefix := "storage/" + filepath.Base(src.SessionRoot) + "/"
if strings.HasPrefix(rel, sessionPrefix) {
return file.Path
}
return ""
}
return ""
}
// storageTrustSnapshot marks a point in a session's invalidation history:
// the trust epoch and the session's invalidation generation at capture
// time. A promotion carries the snapshot taken before its parse; if either
// counter has moved by promotion time, an invalidation landed after the
// capture and the promotion is discarded rather than resurrecting trust
// the invalidation just revoked.
type storageTrustSnapshot struct {
epoch uint64
gen uint64
}
// storageTrustSnapshotFor records the session's current invalidation
// coordinates. It must be taken before the stat signature is captured:
// an invalidation arriving after the snapshot then reliably blocks the
// promotion, even when the change it signals is invisible to the stat
// signature (a same-size, same-mtime child rewrite).
func (e *Engine) storageTrustSnapshotFor(path string) storageTrustSnapshot {
e.storageTrustMu.Lock()
defer e.storageTrustMu.Unlock()
return storageTrustSnapshot{
epoch: e.storageTrustEpoch,
gen: e.storageTrustGens[path],
}
}
// openCodeStorageSessionGateState captures the session's current stat
// signature for the gate, along with the invalidation snapshot that any
// later promotion of this capture must present. It returns ok=false when
// the file is not a gateable storage session, this run force-parses, or
// the signature cannot be captured; such sessions never skip and never
// promote.
func (e *Engine) openCodeStorageSessionGateState(
file parser.DiscoveredFile,
) (string, storageTrustSnapshot, bool) {
if e.forceParse || file.ForceParse {
return "", storageTrustSnapshot{}, false
}
sessionPath := e.openCodeStorageSessionPath(file)
if sessionPath == "" {
return "", storageTrustSnapshot{}, false
}
snap := e.storageTrustSnapshotFor(sessionPath)
state, ok := parser.StatOpenCodeStorageSessionState(sessionPath)
if !ok {
return "", storageTrustSnapshot{}, false
}
return state, snap, true
}
// openCodeStorageSessionFresh reports whether the session's captured stat
// signature matches the last verified one, in which case its parse inputs
// are unchanged and it skips before fingerprinting.
func (e *Engine) openCodeStorageSessionFresh(path, state string) bool {
if state == "" {
return false
}
e.storageTrustMu.Lock()
defer e.storageTrustMu.Unlock()
trusted, ok := e.trustedStorageSessions[path]
return ok && trusted == state
}
// promoteOpenCodeStorageSession records a stat signature that was captured
// before a parse whose outcome the archive has since absorbed (results
// dropped as already stored, or confirmed written). The
// capture-before-parse ordering makes overlapping writes safe: a write
// landing between capture and parse leaves the parsed content newer than
// the signature, so the next capture mismatches and re-verifies. The
// snapshot check covers the writes that ordering cannot: an invalidation
// landing between capture and promotion (a watcher event classified while
// a full sync was mid-pass) bumps the session's generation, so the stale
// promotion is dropped and the next pass re-verifies by content.
func (e *Engine) promoteOpenCodeStorageSession(
path, state string, snap storageTrustSnapshot,
) {
if path == "" || state == "" {
return
}
e.storageTrustMu.Lock()
defer e.storageTrustMu.Unlock()
if snap.epoch != e.storageTrustEpoch || snap.gen != e.storageTrustGens[path] {
return
}
if e.trustedStorageSessions == nil {
e.trustedStorageSessions = make(map[string]string)
}
e.trustedStorageSessions[path] = state
}
// stageOpenCodeStorageTrust decides what a finished parse means for the
// session's gate trust. A complete result set with no exclusions or
// deferred retries either re-verified the session (every result dropped as
// already stored — promote immediately) or produced content headed for the
// write batch (stage the state on the result; the write path promotes it
// once the batch is confirmed fully written). Anything else — retries,
// exclusions, policy-suppressed results — invalidates so the next pass
// re-verifies by content.
func (e *Engine) stageOpenCodeStorageTrust(
res *processResult,
path, state string,
snap storageTrustSnapshot,
parsedCount int,
resultSetComplete bool,
) {
clean := resultSetComplete &&
parsedCount > 0 &&
len(res.excludedSessionIDs) == 0 &&
res.retrySessionIDs == nil
if clean && len(res.results) == 0 {
e.promoteOpenCodeStorageSession(path, state, snap)
return
}
e.dropOpenCodeStorageTrust(path)
if clean {
res.storageTrustPath = path
res.storageTrustState = state
res.storageTrustSnap = snap
}
}
// promoteOpenCodeStorageTrustAfterWrite promotes the staged trust of every
// session in a written batch, but only when the batch verifiably persisted
// all of it: any failed, cwd-vetoed, or otherwise unwritten session leaves
// the whole batch unpromoted (write failures are not attributable to
// individual sessions), so those sessions re-verify on the next pass.
func (e *Engine) promoteOpenCodeStorageTrustAfterWrite(
batch []pendingWrite,
writtenSessions, failedWrites, cwdFiltered int,
) {
if failedWrites > 0 || cwdFiltered > 0 ||
writtenSessions != len(batch) {
return
}
for _, pw := range batch {
e.promoteOpenCodeStorageSession(
pw.storageTrustPath, pw.storageTrustState, pw.storageTrustSnap,
)
}
}
// dropOpenCodeStorageTrust removes the session's trusted signature without
// bumping its invalidation generation. Used by the pass that owns the
// session's staged promotion (results in flight to the write batch, or an
// unclean parse), so the drop does not veto that same pass's promotion.
func (e *Engine) dropOpenCodeStorageTrust(path string) {
if path == "" {
return
}
e.storageTrustMu.Lock()
defer e.storageTrustMu.Unlock()
delete(e.trustedStorageSessions, path)
}
// invalidateOpenCodeStorageSession drops trust for one session and bumps
// its invalidation generation. Called when a watcher changed-path event
// resolves to the session: the event says something changed even if the
// stat signature cannot see it, so the next pass must re-verify by
// content. The generation bump extends that guarantee across a
// concurrently running pass — a promotion captured before the event
// presents a stale snapshot and is discarded, so it cannot restore the
// trust this invalidation revoked.
func (e *Engine) invalidateOpenCodeStorageSession(path string) {
if path == "" {
return
}
e.storageTrustMu.Lock()
defer e.storageTrustMu.Unlock()
delete(e.trustedStorageSessions, path)
if e.storageTrustGens == nil {
e.storageTrustGens = make(map[string]uint64)
}
e.storageTrustGens[path]++
}
// clearTrustedOpenCodeStorageSessions drops every trusted session state
// and starts a new trust epoch, so promotions staged before the clear are
// discarded. Called by resync, which rebuilds the archive from scratch and
// must re-verify every session against it.
func (e *Engine) clearTrustedOpenCodeStorageSessions() {
e.storageTrustMu.Lock()
defer e.storageTrustMu.Unlock()
e.trustedStorageSessions = nil
e.storageTrustGens = nil
e.storageTrustEpoch++
}