Skip to content

Commit 5a12c42

Browse files
authored
fix(duckdb): preserve source machine attribution (#1302)
DuckDB mirrors now preserve each session's source-machine label, falling back to the publishing machine only for legacy empty or `local` values. The resolved label also participates in the session fingerprint, so attribution changes re-push the row and match PostgreSQL's existing behavior. Mirror status, residency checks, and curation replacement now operate across every session in the single source archive instead of filtering by the last publishing machine. These dropped machine filters are the most behaviorally significant part of the change: the publisher remains mirror metadata, while resident sessions may carry labels from multiple filesystem sources. This bumps the disposable mirror schema from 6 to 7. Existing mirrors rebuild once into a fresh validated file and atomically swap on upgrade, which corrects older rows that could sit outside the incremental cutoff. The change was extracted from #1170, where it was bundled with separate session-source work, so that PR can shed this pre-existing DuckDB/PostgreSQL parity fix and its schema-wide rebuild. Co-authored-by: Matthew Jacobs <mjacobs@users.noreply.github.com>
1 parent a421fe8 commit 5a12c42

7 files changed

Lines changed: 232 additions & 70 deletions

File tree

internal/duckdb/connect.go

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -154,20 +154,10 @@ func readLocalMirrorStatus(
154154
)
155155
}
156156

157-
// readMachineStatus reads the target's push metadata and machine-scoped row
158-
// counts. It tolerates a mirror with no sync_metadata rows yet, or missing
159-
// tables entirely (a fresh, foreign, or pre-v3 file): both degrade to zero
160-
// values instead of erroring, since status must not crash on an old mirror.
161-
//
162-
// The row counts are filtered by the TARGET's own recorded LastPushMachine
163-
// when it is set, not by the caller's configured machine name: a remote
164-
// Quack client's configured machine name is normally its own hostname,
165-
// which almost never matches the hostname of whatever machine actually
166-
// pushed the mirror it is reading, so filtering by the configured name
167-
// there would report zero rows even though the display line above already
168-
// shows a real LastPushMachine and non-zero mirror content. The configured
169-
// machine name is used only as the fallback for a mirror with no recorded
170-
// push yet (LastPushMachine == ""), where it is the best available guess.
157+
// readMachineStatus reads the target's push metadata and total mirror row
158+
// counts. Sessions retain their per-source machine attribution, so status
159+
// cannot use LastPushMachine as a row filter. Missing metadata or tables
160+
// degrade to zero values so status remains usable for fresh and old mirrors.
171161
func readMachineStatus(
172162
ctx context.Context,
173163
duck *sql.DB,
@@ -186,14 +176,8 @@ func readMachineStatus(
186176
status.DataVersion = meta.DataVersion
187177
status.Scope = meta.Scope
188178

189-
countMachine := meta.LastPushMachine
190-
if countMachine == "" {
191-
countMachine = machine
192-
}
193-
194179
if err := queryDuckDBRowContext(ctx, duck, connectionKind, quack,
195-
`SELECT COUNT(*) FROM sessions WHERE machine = ?`,
196-
countMachine,
180+
`SELECT COUNT(*) FROM sessions`,
197181
).Scan(&status.DuckDBSessions); err != nil {
198182
if isMissingDuckDBTable(err) {
199183
return status, nil
@@ -204,9 +188,8 @@ func readMachineStatus(
204188
`SELECT COUNT(*)
205189
FROM messages
206190
WHERE session_id IN (
207-
SELECT id FROM sessions WHERE machine = ?
191+
SELECT id FROM sessions
208192
)`,
209-
countMachine,
210193
).Scan(&status.DuckDBMessages); err != nil {
211194
if isMissingDuckDBTable(err) {
212195
return status, nil

internal/duckdb/probe.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -278,15 +278,10 @@ func (p MirrorProbe) NeedsRebuild(scope string, sourceDataVersion int) bool {
278278
// rebuilds: identity-less mirrors only come from earlier builds of this
279279
// unreleased branch, so they simply rebuild once and record the id.
280280
//
281-
// The machine-change check exists because mirror rows are machine-stamped
282-
// (see the sessions.machine column and duckSessionFingerprintFields): an
283-
// incremental push only rewrites sessions whose LOCAL content changed
284-
// within the current mirror window, so a session that has not changed
285-
// since the mirror's last push stays permanently labeled with the OLD
286-
// machine name even after the push metadata's LastPushMachine flips to the
287-
// new one — silently stranding it under a machine filter (see
288-
// readMachineStatus) that will never again select it. A full rebuild
289-
// re-pushes every session under the new machine name instead.
281+
// The machine-change check remains necessary for legacy sessions whose local
282+
// machine is empty or "local": mirroring substitutes the current push machine
283+
// for those values, so a full rebuild must restamp every such row when that
284+
// configured name changes. Explicit per-source machine labels remain unchanged.
290285
//
291286
// localDeletionRevision is the caller's local.SessionDeletionPublicationRevision
292287
// read, and localDatabaseID the caller's local.GetDatabaseID read; both are

internal/duckdb/push.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -600,7 +600,7 @@ func (snap curationSnapshot) fingerprint() (string, error) {
600600
// All work is bounded by curation size, not mirror size: mirror membership
601601
// is validated for exactly the snapshot's session IDs (one batched lookup,
602602
// see mirrorResidentSessionIDs), pin notes are preserved, and the delete
603-
// side stays the machine-scoped clear of both tables, so removed
603+
// side clears both tables for sessions in this source archive, so removed
604604
// stars/pins disappear without enumerating them.
605605
func (s *Sync) replaceCuration(
606606
ctx context.Context, snap curationSnapshot,
@@ -641,8 +641,8 @@ func (s *Sync) replaceCuration(
641641
if err := s.execMutation(ctx, tx, `
642642
DELETE FROM `+table+`
643643
WHERE session_id IN (
644-
SELECT id FROM sessions WHERE machine = ?
645-
)`, s.machine); err != nil {
644+
SELECT id FROM sessions
645+
)`); err != nil {
646646
return fmt.Errorf("clearing duckdb %s: %w", table, err)
647647
}
648648
}
@@ -899,7 +899,9 @@ func (s *Sync) upsertSession(
899899
secrets_rules_version = excluded.secrets_rules_version,
900900
agentsview_push_fingerprint = excluded.agentsview_push_fingerprint`
901901

902-
args := sessionInsertArgs(sess, s.machine, fingerprint)
902+
args := sessionInsertArgs(
903+
sess, mirroredSessionMachine(sess, s.machine), fingerprint,
904+
)
903905
if err := s.execMutation(ctx, exec, query, args...); err != nil {
904906
return fmt.Errorf("writing duckdb session %s: %w", sess.ID, err)
905907
}
@@ -944,6 +946,13 @@ func sessionInsertArgs(sess db.Session, machine, fingerprint string) []any {
944946
}
945947
}
946948

949+
func mirroredSessionMachine(sess db.Session, pushMachine string) string {
950+
if sess.Machine == "" || sess.Machine == "local" {
951+
return pushMachine
952+
}
953+
return sess.Machine
954+
}
955+
947956
func insertMessages(
948957
ctx context.Context, exec duckMutationExecutor, msgs []db.Message,
949958
) error {

internal/duckdb/rebuild.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func ensureMirrorWorkDir(path string) (string, error) {
8181
// rebuildMirror builds a fresh DuckDB mirror file from scratch in a
8282
// temporary file inside the mirror's work directory, then atomically swaps
8383
// it over path. It is
84-
// the only way a schema v6 mirror is created or repaired: unlike Sync.Push,
84+
// the only way a schema v7 mirror is created or repaired: unlike Sync.Push,
8585
// it never touches an existing mirror file in place, so a rebuild that
8686
// fails at any point leaves the previous mirror (if any) fully intact.
8787
func rebuildMirror(

internal/duckdb/schema.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import (
1111
)
1212

1313
// SchemaVersion is the version of the DuckDB mirror schema created by
14-
// createSchema. Mirror schema v6 is create-only: there are no in-place
14+
// createSchema. Mirror schema v7 is create-only: there are no in-place
1515
// migrations between versions. A version mismatch means the mirror file
1616
// must be rebuilt with 'agentsview duckdb push --full'.
17-
const SchemaVersion = 6
17+
const SchemaVersion = 7
1818

1919
const schemaVersionMetadataKey = "agentsview_schema_version"
2020

internal/duckdb/sync.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,7 @@ func (s *Sync) readMirrorFingerprintBatch(
597597
}
598598

599599
// mirrorResidentSessionIDs reports which of ids currently exist in the
600-
// mirror under this Sync's machine name. IDs are deduplicated and queried
600+
// mirror for this source archive. IDs are deduplicated and queried
601601
// in batches of 500, so cost tracks the caller's ID list (a candidate
602602
// window, a tombstone delta, the curation set), never total mirror size.
603603
func (s *Sync) mirrorResidentSessionIDs(
@@ -627,14 +627,13 @@ func (s *Sync) readMirrorResidentBatch(
627627
ctx context.Context, batch []string, out map[string]bool,
628628
) error {
629629
placeholders := make([]string, len(batch))
630-
args := make([]any, 0, len(batch)+1)
631-
args = append(args, s.machine)
630+
args := make([]any, 0, len(batch))
632631
for i, id := range batch {
633632
placeholders[i] = "?"
634633
args = append(args, id)
635634
}
636635
rows, err := s.duck.QueryContext(ctx,
637-
`SELECT id FROM sessions WHERE machine = ? AND id IN (`+
636+
`SELECT id FROM sessions WHERE id IN (`+
638637
strings.Join(placeholders, ",")+`)`, args...,
639638
)
640639
if err != nil {
@@ -938,7 +937,9 @@ func (s *Sync) sessionFingerprints(
938937
SecretFindings []db.SecretFinding
939938
Pins []db.PinnedMessage
940939
}{
941-
SessionFields: duckSessionFingerprintFields(sess, s.machine),
940+
SessionFields: duckSessionFingerprintFields(
941+
sess, mirroredSessionMachine(sess, s.machine),
942+
),
942943
Messages: msgs,
943944
Usage: usage[sess.ID],
944945
ToolCalls: toolCalls,

0 commit comments

Comments
 (0)