Skip to content

Commit 3a576c4

Browse files
committed
feat(sync): fix a session's source machine at first ingestion
Resolve the machine for a discovered session from the configured filesystem root that produced it, and persist that label once. Every later write keeps the stored value: ordinary reparses, appends, watcher refresh, provider refresh, and trash handling all retain the database row's machine rather than adopting a newly configured label. During sync --full the archive store points at the original archive while e.db is the fresh replacement, so a full rebuild copies existing labels forward instead of relabeling. Editing a source label therefore affects only sessions discovered afterward. Because relabeling is no longer part of sync, this drops the machine-only update for trashed sessions, the all-machines baseline scan, and the second ownership baseline index keyed without machine. Watch reconciliation continues to use the baseline for the machine that originally admitted the session, so per-event work stays bounded by the changed batch. The previous approach chased reattribution through each new writer, trash path, snapshot path, and reconciliation edge, which kept widening the persistent-archive surface and reintroducing consistency bugs.
1 parent b3eb284 commit 3a576c4

20 files changed

Lines changed: 1104 additions & 113 deletions

cmd/agentsview/archive_query_backend.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ func (b localArchiveQueryBackend) SessionUsage(
250250
if known && !b.skipFreshData {
251251
engine := sync.NewEngine(b.database, sync.EngineConfig{
252252
AgentDirs: b.cfg.AgentDirs,
253+
SourceMachines: b.cfg.SourceMachines,
253254
IncludeCwdPrefixes: b.cfg.SyncIncludeCwdPrefixes,
254255
Machine: b.cfg.LocalMachineName,
255256
BlockedResultCategories: b.cfg.ResultContentBlockedCategories,

cmd/agentsview/archive_write_backend.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,7 @@ func (b *localArchiveWriteBackend) DuckDBPushWatch(
839839

840840
engine := syncpkg.NewEngine(b.database, syncpkg.EngineConfig{
841841
AgentDirs: b.appCfg.AgentDirs,
842+
SourceMachines: b.appCfg.SourceMachines,
842843
IncludeCwdPrefixes: b.appCfg.SyncIncludeCwdPrefixes,
843844
Machine: b.appCfg.LocalMachineName,
844845
BlockedResultCategories: b.appCfg.ResultContentBlockedCategories,

cmd/agentsview/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ func runServe(cfg config.Config, opts serveOptions) {
295295
var onStartupReconciled func(sync.SyncStats, error)
296296
engine = sync.NewEngine(database, sync.EngineConfig{
297297
AgentDirs: cfg.AgentDirs,
298+
SourceMachines: cfg.SourceMachines,
298299
IncludeCwdPrefixes: cfg.SyncIncludeCwdPrefixes,
299300
Machine: cfg.LocalMachineName,
300301
BlockedResultCategories: cfg.ResultContentBlockedCategories,

cmd/agentsview/parse_diff.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ func doParseDiff(cfg ParseDiffConfig) (failed bool) {
127127

128128
engine := sync.NewDiffEngine(database, sync.EngineConfig{
129129
AgentDirs: appCfg.AgentDirs,
130+
SourceMachines: appCfg.SourceMachines,
130131
IncludeCwdPrefixes: appCfg.SyncIncludeCwdPrefixes,
131132
Machine: appCfg.LocalMachineName,
132133
BlockedResultCategories: appCfg.ResultContentBlockedCategories,

cmd/agentsview/session_sync.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ func syncService(
7474
}
7575
engine := sync.NewEngine(d, sync.EngineConfig{
7676
AgentDirs: cfg.AgentDirs,
77+
SourceMachines: cfg.SourceMachines,
7778
IncludeCwdPrefixes: cfg.SyncIncludeCwdPrefixes,
7879
Machine: cfg.LocalMachineName,
7980
})

cmd/agentsview/sync.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,7 @@ func coordinateLocalSync(
884884

885885
engine := sync.NewEngine(database, sync.EngineConfig{
886886
AgentDirs: appCfg.AgentDirs,
887+
SourceMachines: appCfg.SourceMachines,
887888
IncludeCwdPrefixes: appCfg.SyncIncludeCwdPrefixes,
888889
Machine: appCfg.LocalMachineName,
889890
BlockedResultCategories: appCfg.ResultContentBlockedCategories,

cmd/agentsview/sync_worker.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ func openWorkerWriteDB(cfg config.Config) (*db.DB, *writeOwnerLock, error) {
360360
func workerEngineConfig(cfg config.Config) sync.EngineConfig {
361361
return sync.EngineConfig{
362362
AgentDirs: cfg.AgentDirs,
363+
SourceMachines: cfg.SourceMachines,
363364
IncludeCwdPrefixes: cfg.SyncIncludeCwdPrefixes,
364365
Machine: cfg.LocalMachineName,
365366
BlockedResultCategories: cfg.ResultContentBlockedCategories,

cmd/agentsview/sync_worker_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,28 @@ func TestSyncWorkerStartupModeSyncsAndEmitsTerminalResult(t *testing.T) {
9999
"public SyncStats fields must survive the NDJSON protocol")
100100
}
101101

102+
func TestSyncWorkerStartupUsesConfiguredSourceMachine(t *testing.T) {
103+
cfg := testConfigWithClaudeFixture(t)
104+
claudeRoot := cfg.AgentDirs[parser.AgentClaude][0]
105+
cfg.SourceMachines = map[parser.AgentType]map[string]string{
106+
parser.AgentClaude: {claudeRoot: "archivebox"},
107+
}
108+
109+
var out bytes.Buffer
110+
require.NoError(t, runSyncWorker(cfg, "startup", &out))
111+
assert.Equal(t, "ok", decodeSingleResult(t, &out).Status)
112+
113+
database, err := db.OpenReadOnly(cfg.DBPath)
114+
require.NoError(t, err)
115+
defer database.Close()
116+
page, err := database.ListSessions(context.Background(), db.SessionFilter{})
117+
require.NoError(t, err)
118+
require.Len(t, page.Sessions, 3)
119+
for _, sess := range page.Sessions {
120+
assert.Equal(t, "archivebox", sess.Machine)
121+
}
122+
}
123+
102124
func TestSyncWorkerReportsAbortAsFailure(t *testing.T) {
103125
cfg := testConfigWithClaudeFixture(t)
104126
ctx, cancel := context.WithCancel(context.Background())

cmd/agentsview/usage.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ func ensureFreshData(
312312
if database.NeedsResync() {
313313
engine := sync.NewEngine(database, sync.EngineConfig{
314314
AgentDirs: appCfg.AgentDirs,
315+
SourceMachines: appCfg.SourceMachines,
315316
IncludeCwdPrefixes: appCfg.SyncIncludeCwdPrefixes,
316317
Machine: appCfg.LocalMachineName,
317318
})
@@ -336,6 +337,7 @@ func ensureFreshData(
336337

337338
engine := sync.NewEngine(database, sync.EngineConfig{
338339
AgentDirs: appCfg.AgentDirs,
340+
SourceMachines: appCfg.SourceMachines,
339341
IncludeCwdPrefixes: appCfg.SyncIncludeCwdPrefixes,
340342
Machine: appCfg.LocalMachineName,
341343
})

internal/db/project_identity_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,63 @@ func TestProjectIdentityPublicationRevisionTracksSnapshotChanges(t *testing.T) {
8181
assert.Greater(t, afterDelete, afterInsert)
8282
}
8383

84+
func TestCopySessionMetadataPreservesRecordedMachineAttribution(
85+
t *testing.T,
86+
) {
87+
dir := t.TempDir()
88+
sourcePath := filepath.Join(dir, "source.db")
89+
source := testDBAtPath(t, sourcePath, "source")
90+
require.NoError(t, source.UpsertSession(Session{
91+
ID: "resynced-session", Project: "app",
92+
Machine: "oldbox", Agent: "claude",
93+
}))
94+
require.NoError(t, source.UpsertProjectIdentityObservation(
95+
t.Context(), export.ProjectIdentityObservation{
96+
SessionID: "resynced-session", Project: "app", Machine: "oldbox",
97+
RootPath: "/workspace/app", ObservedAt: time.Now().UTC(),
98+
},
99+
))
100+
require.NoError(t, source.Close())
101+
102+
destinationPath := filepath.Join(dir, "destination.db")
103+
destination := testDBAtPath(t, destinationPath, "destination")
104+
t.Cleanup(func() { _ = destination.Close() })
105+
require.NoError(t, destination.UpsertSession(Session{
106+
ID: "resynced-session", Project: "app",
107+
Machine: "newbox", Agent: "claude",
108+
}))
109+
require.NoError(t, destination.UpsertProjectIdentityObservation(
110+
t.Context(), export.ProjectIdentityObservation{
111+
SessionID: "resynced-session", Project: "app", Machine: "newbox",
112+
RootPath: "/workspace/app", ObservedAt: time.Now().UTC(),
113+
},
114+
))
115+
116+
require.NoError(t, destination.CopySessionMetadataFrom(sourcePath))
117+
118+
session, err := destination.GetSessionFull(
119+
t.Context(), "resynced-session",
120+
)
121+
require.NoError(t, err)
122+
require.NotNil(t, session)
123+
assert.Equal(t, "newbox", session.Machine)
124+
snapshots, err := destination.ListSessionProjectIdentitySnapshots(
125+
t.Context(),
126+
)
127+
require.NoError(t, err)
128+
require.Len(t, snapshots, 1)
129+
assert.Equal(t, "oldbox", snapshots[0].Machine)
130+
observations, err := destination.ListProjectIdentityObservations(
131+
t.Context(), []string{"app"},
132+
)
133+
require.NoError(t, err)
134+
require.Len(t, observations, 2)
135+
assert.ElementsMatch(t, []string{"newbox", "oldbox"}, []string{
136+
observations[0].Machine,
137+
observations[1].Machine,
138+
})
139+
}
140+
84141
func TestSessionProjectIdentitySnapshotPreservesFirstRootKeyUntilRemoteEvidence(
85142
t *testing.T,
86143
) {

0 commit comments

Comments
 (0)