How Multi-Target PostgreSQL Sync Manages Named Targets in agentsview

The multi-target PostgreSQL sync in agentsview isolates each destination's sync state by prefixing SQLite keys with a scope identifier derived from the SyncStateTarget name, enabling independent watermarks for multiple PostgreSQL databases.

agentsview supports pushing session data from its local SQLite store to one or more PostgreSQL databases. The multi-target PostgreSQL sync implementation achieves this through a named target system that lives in internal/postgres/sync.go, where each target maintains its own progress tracking without interference.

Where the Named Target Logic Lives

The core implementation spans three files in the repository:

  • internal/postgres/sync.go – Defines SyncOptions, the Sync struct, and the pushSyncStateScope helper that builds scope identifiers (lines 45–55, 191–221). Also implements scopedSyncStateStore (lines 93–121) which prefixes all sync-state keys with the target name.
  • internal/postgres/sync_test.go – Validates that different targets keep independent watermarks and confirms legacy migration behavior.
  • cmd/agentsview/pg.go – The CLI entry point where the --target flag maps directly to SyncOptions.SyncStateTarget.

How Named Targets Become Scoped Keys

When instantiating a Sync via New(), the caller supplies a SyncOptions.SyncStateTarget string (e.g., "prod" or "dev"). The system converts this into a scope identifier:

// Inside New() in internal/postgres/sync.go
syncStateScope := pushSyncStateScope(
    opts.SyncStateTarget,
    opts.Projects,
    opts.ExcludeProjects,
)

The pushSyncStateScope function (lines 191–221) builds the scope using these rules:

  1. If no project filters are applied, the scope is simply the target name.
  2. If project include/exclude filters exist, a deterministic fingerprint of the filter set is appended using the format target:project-filter:<fingerprint>.

This syncStateScope string is passed to newScopedSyncStateStore, which wraps the underlying SQLite SyncStateStore.

ScopedSyncStateStore: Per-Target Isolation

The scopedSyncStateStore struct (lines 19–45) ensures complete isolation between targets by prefixing every key with the scope:

func (s *scopedSyncStateStore) scopedKey(key string) string {
    if s.scope == "" {
        return key
    }
    return key + ":" + s.scope   // e.g., "last_push_at:prod"
}

This means logical keys like last_push_at and last_push_target_fingerprint are stored separately for each target. When ReadLastPushAt executes (lines 500–528), it computes the same scope via pushSyncStateScope and retrieves the appropriately-scoped value. Similarly, Sync.Status (lines 401–424) displays the watermark for the selected target, allowing the UI to show distinct "last push" timestamps per destination.

Legacy State Migration

If a user previously ran agentsview pg push without the --target flag, sync state was stored under bare keys (e.g., last_push_at). When SyncOptions.MigrateLegacySyncState is true, the first push for a new target triggers the ensureMigration logic (lines 46–95):

  1. Read existing legacy values from unscoped keys.
  2. Copy them into the newly scoped key (last_push_at:<target>).
  3. Clear the legacy keys to prevent reuse.

This ensures zero data loss when upgrading to the multi-target system.

Practical Example: Pushing to Multiple Targets

The following example demonstrates pushing to distinct production and development PostgreSQL targets from the same local database:

// Push to production target
prodSync, err := postgres.New(
    pgURLProd, "public", localDB,
    "my-machine", false,
    postgres.SyncOptions{
        SyncStateTarget: "prod",            // <-- named target
        Projects:        []string{"myapp"},
    })
if err != nil { log.Fatal(err) }
defer prodSync.Close()

if err := prodSync.Push(ctx, nil); err != nil { log.Fatal(err) }

// Push to development target (different name, same local DB)
devSync, _ := postgres.New(
    pgURLDev, "public", localDB,
    "my-machine", false,
    postgres.SyncOptions{
        SyncStateTarget: "dev",             // <-- another named target
    })
defer devSync.Close()
devSync.Push(ctx, nil)                    // uses its own watermark

Both instances share the same local SQLite database, but their watermarks are stored under distinct keys:

  • last_push_at:prod – the production push watermark.
  • last_push_at:dev – the development push watermark.

Each target reads its own watermark on subsequent runs, ensuring incremental sync without duplicate data.

Summary

  • Named targets are defined via SyncOptions.SyncStateTarget and transformed into scope identifiers by pushSyncStateScope.
  • scopedSyncStateStore prefixes every sync-state key with the target scope, providing isolated watermarks per destination.
  • Legacy migration automatically copies unscoped state to scoped keys when MigrateLegacySyncState is enabled, ensuring backward compatibility.
  • The architecture allows a single agentsview instance to push to multiple PostgreSQL destinations while tracking progress independently for each.

Frequently Asked Questions

How does agentsview prevent sync state collisions between multiple PostgreSQL targets?

agentsview prevents collisions by using scopedSyncStateStore to prefix every sync-state key with the target name. When you specify SyncStateTarget: "prod", the watermark is stored as last_push_at:prod rather than the global last_push_at, ensuring each target maintains its own independent progress tracking.

What happens if I add a target name to an existing agentsview installation that previously used no target?

If SyncOptions.MigrateLegacySyncState is set to true, the system runs ensureMigration (lines 46–95) on the first push. This copies any existing unscoped values (like last_push_at) into the new scoped key (last_push_at:<target>) and clears the legacy keys, preventing data loss while transitioning to the multi-target system.

Can I use the same local SQLite database with different project filters for different targets?

Yes. The pushSyncStateScope function incorporates a deterministic fingerprint of your project filters into the scope identifier. If you push "prod" with Projects: []string{"myapp"} and "prod" with Projects: []string{"api"}, agentsview treats these as distinct scopes (prod:project-filter:<fingerprint>), maintaining separate watermarks for each filtered view.

Where does the --target CLI flag map to in the source code?

In cmd/agentsview/pg.go, the --target flag value is passed directly to postgres.SyncOptions.SyncStateTarget when constructing the Sync instance. This field then flows through to pushSyncStateScope in internal/postgres/sync.go to generate the scoped key prefix.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →