Skip to content

Commit 1278767

Browse files
Technophobe01claude
andcommitted
pg: add --from-now to start a new push target at the join point
A target's first push has no watermark, so it backfills the entire local archive. Pushing into a database shared with other people therefore discloses every session on the machine, including unrelated local work, and there was no way to ask for "only what happens from here on". Add SyncOptions.PushFromNow, exposed as `pg push --from-now`, which seeds the watermark with the push cutoff when a target has no history yet. It is applied after the existing reset checks rather than before: those treat a watermark with no matching target fingerprint or PG-side push marker as corrupt local state and clear it, so a watermark seeded earlier (or from outside the process) is wiped and the push falls back to a full backfill. It is also ignored for an explicitly requested full push and for a target that already has history, so it can only bound a first push, never open a gap in an established one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 6a0e546 commit 1278767

6 files changed

Lines changed: 88 additions & 2 deletions

File tree

cmd/agentsview/archive_write_backend.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,7 @@ func (b *localArchiveWriteBackend) PGPush(
623623
ps, err := postgres.New(
624624
target.PG.URL, target.PG.Schema, b.database,
625625
target.PG.MachineName, target.PG.AllowInsecure,
626-
target.syncOptions(projects, excludeProjects, vectorSource),
626+
target.syncOptions(projects, excludeProjects, vectorSource, cfg.FromNow),
627627
)
628628
if err != nil {
629629
return postgres.PushResult{}, err
@@ -870,7 +870,7 @@ func (b *localArchiveWriteBackend) PGPushWatch(
870870
s, cErr := postgres.New(
871871
target.PG.URL, target.PG.Schema, b.database,
872872
target.PG.MachineName, target.PG.AllowInsecure,
873-
target.syncOptions(projects, exclude, vectorSource),
873+
target.syncOptions(projects, exclude, vectorSource, cfg.FromNow),
874874
)
875875
if cErr != nil {
876876
return nil, cErr

cmd/agentsview/cli.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,7 @@ func newPGPushCommand() *cobra.Command {
652652
cmd.Flags().DurationVar(&cfg.Debounce, "debounce", defaultWatchDebounce, "Coalesce window after a change before pushing (--watch only)")
653653
cmd.Flags().DurationVar(&cfg.Interval, "interval", defaultWatchInterval, "Periodic floor push interval (--watch only)")
654654
cmd.Flags().BoolVar(&cfg.NoVectors, "no-vectors", false, "Skip pushing semantic-search vectors")
655+
cmd.Flags().BoolVar(&cfg.FromNow, "from-now", false, "On a target's FIRST push, start from now instead of backfilling local history (ignored with --full)")
655656
return cmd
656657
}
657658

cmd/agentsview/pg.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ type PGPushConfig struct {
2929
Debounce time.Duration
3030
Interval time.Duration
3131
NoVectors bool
32+
// FromNow starts a target that has no watermark yet at the current time
33+
// instead of backfilling the whole local archive. Ignored with --full and
34+
// on a target that already has push history.
35+
FromNow bool
3236
// ScopeVectorsToChangedSessions is set internally by the watch
3337
// loop for change-triggered pushes; it has no CLI flag.
3438
ScopeVectorsToChangedSessions bool
@@ -66,13 +70,15 @@ func (s pgTargetSelection) label() string {
6670
func (s pgTargetSelection) syncOptions(
6771
projects, excludeProjects []string,
6872
vectorSource postgres.VectorPushSource,
73+
pushFromNow bool,
6974
) postgres.SyncOptions {
7075
return postgres.SyncOptions{
7176
Projects: projects,
7277
ExcludeProjects: excludeProjects,
7378
SyncStateTarget: s.SyncStateTarget,
7479
MigrateLegacySyncState: s.MigrateLegacySyncState,
7580
VectorSource: vectorSource,
81+
PushFromNow: pushFromNow,
7682
}
7783
}
7884

internal/postgres/push.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ func (s *Sync) PushWithOptions(
134134
) (PushResult, error) {
135135
full := opts.Full
136136
start := time.Now()
137+
// The caller's own --full intent, kept because `full` is reassigned below
138+
// when reset detection forces a rebuild; PushFromNow must never override an
139+
// explicitly requested full push.
140+
requestedFull := full
137141
var result PushResult
138142
state := s.effectiveSyncState()
139143
aliasBackfillState := s.aliasBackfillSyncStateOrDefault()
@@ -302,6 +306,24 @@ func (s *Sync) PushWithOptions(
302306
}
303307
cutoff := time.Now().UTC().Format(LocalSyncTimestampLayout)
304308

309+
// Start a brand-new target at "now" instead of backfilling the archive.
310+
// Applied HERE, after every reset check above has run: those checks key on
311+
// lastPush being non-empty and treat a watermark with no matching target
312+
// fingerprint or PG-side marker as corrupt local state, so a watermark
313+
// seeded any earlier (or from outside this process) would be wiped and the
314+
// push would fall back to a full backfill. finalizePushState records cutoff
315+
// at the end, so every later push for the target is normally incremental.
316+
if boundary, applied := pushFromNowBoundary(
317+
s.pushFromNow, requestedFull, lastPush, cutoff,
318+
); applied {
319+
log.Printf(
320+
"pgsync: first push for this target starts at %s "+
321+
"(from-now); local history before it is not uploaded",
322+
cutoff,
323+
)
324+
lastPush = boundary
325+
}
326+
305327
// Candidate selection shares ListSessionsForMirrorWindow with the
306328
// DuckDB mirror push: sync_marker >= lastPush, inclusive below and
307329
// deliberately unbounded above. An upper bound at cutoff would let a
@@ -1353,6 +1375,20 @@ func persistPushTargetFingerprint(
13531375
return nil
13541376
}
13551377

1378+
// pushFromNowBoundary decides the lower bound of a from-now push. It applies
1379+
// only to a target with NO watermark yet, and never overrides an explicitly
1380+
// requested full push: narrowing an established target would silently create a
1381+
// gap in what the hub has, whereas bounding a target's very first push just
1382+
// means its history starts at the join point.
1383+
func pushFromNowBoundary(
1384+
enabled, requestedFull bool, lastPush, cutoff string,
1385+
) (string, bool) {
1386+
if !enabled || requestedFull || lastPush != "" {
1387+
return lastPush, false
1388+
}
1389+
return cutoff, true
1390+
}
1391+
13561392
func pushTargetState(
13571393
lastPush, boundaryState,
13581394
storedTargetFingerprint, currentTargetFingerprint string,

internal/postgres/push_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1822,3 +1822,34 @@ func TestShouldSkipSessionMessagesInBatchedPush(t *testing.T) {
18221822
sessionID, 2, unchangedFP, true, baseComparisons,
18231823
), "full mode should not skip by fingerprint check")
18241824
}
1825+
1826+
// A from-now push must bound only a target's FIRST push, and must never
1827+
// silently narrow an established target or override an explicit full push.
1828+
func TestPushFromNowBoundary(t *testing.T) {
1829+
const cutoff = "2026-07-24 12:00:00"
1830+
const established = "2026-07-01 00:00:00"
1831+
cases := []struct {
1832+
name string
1833+
enabled bool
1834+
requestedFull bool
1835+
lastPush string
1836+
want string
1837+
wantApplied bool
1838+
}{
1839+
{"fresh target starts at now", true, false, "", cutoff, true},
1840+
{"disabled backfills as before", false, false, "", "", false},
1841+
{"explicit full push wins", true, true, "", "", false},
1842+
{"established target is untouched", true, false, established, established, false},
1843+
{"established target with full push is untouched", true, true, established, established, false},
1844+
{"disabled and established is untouched", false, false, established, established, false},
1845+
}
1846+
for _, tc := range cases {
1847+
t.Run(tc.name, func(t *testing.T) {
1848+
got, applied := pushFromNowBoundary(
1849+
tc.enabled, tc.requestedFull, tc.lastPush, cutoff,
1850+
)
1851+
assert.Equal(t, tc.want, got)
1852+
assert.Equal(t, tc.wantApplied, applied)
1853+
})
1854+
}
1855+
}

internal/postgres/sync.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ type Sync struct {
173173
// vectorSource, when set, supplies the local vectors.db active generation
174174
// pushed as a phase at the end of Push. Nil disables the phase.
175175
vectorSource VectorPushSource
176+
177+
// pushFromNow starts a fresh target at the current time rather than
178+
// backfilling the local archive. See SyncOptions.PushFromNow.
179+
pushFromNow bool
176180
// afterVectorApply is a full/scoped post-apply test hook.
177181
afterVectorApply func()
178182
// beforeVectorWitnessRecord is a generation-wide pre-witness test hook.
@@ -219,6 +223,13 @@ type SyncOptions struct {
219223
// VectorSource, when non-nil, enables the vector push phase, replicating
220224
// the local vectors.db active generation into PG. Nil skips the phase.
221225
VectorSource VectorPushSource
226+
// PushFromNow starts a target's history at the moment of its FIRST push
227+
// instead of backfilling the whole local archive. It applies only when the
228+
// target has no watermark yet and the caller did not ask for a full push,
229+
// so it cannot silently narrow an established target. Intended for pushing
230+
// into a SHARED database, where uploading a machine's entire history would
231+
// disclose unrelated local work to everyone with read access.
232+
PushFromNow bool
222233
}
223234

224235
// New creates a Sync instance and verifies the PG connection.
@@ -297,6 +308,7 @@ func New(
297308
projects: opts.Projects,
298309
excludeProjects: opts.ExcludeProjects,
299310
vectorSource: opts.VectorSource,
311+
pushFromNow: opts.PushFromNow,
300312
}, nil
301313
}
302314

0 commit comments

Comments
 (0)