feat: adaptive binary subdivision with remaining-based state, per-stream error resilience, and pure progress reducers#307
Closed
feat: adaptive binary subdivision with remaining-based state, per-stream error resilience, and pure progress reducers#307
Conversation
742660a to
46cf03f
Compare
tonyxiao
added a commit
that referenced
this pull request
Apr 17, 2026
#296 Merge the "massive progress fix" (PR #296) onto the n-ary search pagination branch (PR #307), reconciling the two feature sets: Protocol: - Simplify TraceStreamStatus.status to ['started', 'complete'] - Errors are orthogonal to lifecycle — a stream can be complete with errors - range_complete is now a separate field on TraceStreamStatus, not a status value - Add CatalogMessage to SyncOutput, TraceGlobalProgress with cumulative stats Engine: - Progress tracker emits catalog upfront, stream_status + global_progress pairs on transitions, and tracks completed_ranges via mergeRanges - New sync-progress-state.ts reducer and sync-ui.tsx Ink/React CLI component - CLI gains --progress flag for interactive terminal progress display Service: - Error classification distinguishes globalPermanent vs streamPermanent - Only global permanent errors (bad API key, invalid config) park the workflow - Stream-scoped permanent errors skip the stream on resume Source: - Remove label from HttpRetryOptions - Use isRetryableHttpError for failure type classification - Emit complete status after non-global errors (errors orthogonal to lifecycle) - Add 'Unrecognized request URL' to skippable error patterns Made-with: Cursor Committed-By-Agent: cursor Made-with: Cursor Committed-By-Agent: cursor
6 tasks
…on tests Verifies that when a stream has newer_than_field set, the destination skips upserts where the incoming record is older than the existing row, and allows upserts when the incoming record is newer. Also fixes docker port parsing (IPv4/IPv6 multi-line output). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
…progress tracking
- Extract app.ts helpers (formatEof, logApiStream, etc.) into api/helpers.ts
- Remove backfill CLI command and lib/backfill.ts (sync-only CLI)
- Remove sync-errors.ts error classification — eof.run_progress.derived.status
already contains failure info
- Simplify pipeline-sync activity to just drain and return { eof }
- Fix progress tracking: separate run_progress (cumulative) from
request_progress (per-call), seed with catalog stream names
- Extract stateReducer into own file with tests, fold progress into state
naturally instead of bolting on at eof
- Simplify pipeline-lifecycle workflow: remove permanent error state machine,
replace ReconcileState enum with backfilling + backfillCount
- Update test-all-accounts.sh for new CLI entry point and --base-url arg
- Pipeline schema progress field now uses ProgressPayload (not EofPayload)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Committed-By-Agent: claude
Local test script with account-specific env vars, not for CI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Formats ProgressPayload into a human-readable string with emojis, elapsed time, throughput, and per-stream status. Will be used by the CLI sync command to replace raw NDJSON output. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Explicitly annotate the generator return type so TypeScript doesn't widen the yield type from takeLimits' T | EofMessage union. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
- Replace emoji with monospace-friendly icons (○ ◐ ● ⊘ ✗) - formatProgress accepts optional prev param to show per-stream deltas (e.g. "+50") since last emission Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
⚪ not_started, 🟡 started, 🟢 completed, ⏭️ skipped, 🔴 errored Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
- Error message now appended to header line instead of separate line - Header shows total row delta (+N) when prev progress is provided - Added 10-stream test case for realistic output visibility Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
- Connection error message appears on the errored stream's line (falls back to header when multiple streams errored) - Checkpoint count shows delta (+N) when prev is provided Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Replaces raw NDJSON stdout with human-readable progress on stderr. Shows run_progress on each progress emission and at eof, with deltas from the previous emission. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
- Use progress.derived.records_per_second and states_per_second directly instead of recomputing from elapsed_ms - Collapse not_started streams into "⚪ N streams pending" - Only show streams with activity (started/completed/errored/skipped) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
…progress/ - Show all stream names (no collapsing not_started) - Completed streams always show record count (even 0) - Only show "Sync failed" when no streams are still active - Move format.ts into progress/ folder Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
…rted - deriveStatus only reports 'failed' when no streams are still active - Rates are 0 when elapsed_ms is 0 (no more divide-by-near-zero) - formatProgress collapses not_started streams into "⚪ N remaining" - Added 'succeeded' status label in format Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Header shows "Syncing N streams", collapsed line shows "N not started: names" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
…ames Header shows count per status: [3 🟢 2 🟡 5 ⚪] Collapsed not-started line just lists names. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
[3 done, 2 active, 5 queued] instead of [3 🟢 2 🟡 5 ⚪] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
…ader line Shows "Syncing N streams" and appends breakdown (3 completed, 2 started, etc.) to the header line. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
progressReducer now throws if msg._ts is missing — the reducer stays pure. The engine stamps _ts on every message before it reaches the reducers, fixing the 0.0s elapsed bug. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com>
Use the connector's JSON Schema (via z.fromJSONSchema, already cached on the resolver) in strict mode so that typos like --postgres.connection_strin are caught with a clear error listing valid keys. Also adds --reset-state to pipelines get. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
ConfiguredStream.time_range.gte and .lt are now optional. The engine only injects lt (from time_ceiling) and never fabricates gte. The source fills missing bounds from account metadata before processing. Also adds time_range to PipelineConfig.streams so users can specify bounded syncs. User-provided bounds are never overridden by the engine. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Connector shorthand flags (e.g. --postgres.url) now merge on top of the stored pipeline config instead of replacing it entirely. This fixes sync losing fields like api_key when only overriding one property. Extracted fetchAndMergeOverrides helper shared by both get and sync so the fetch → merge → validate-via-OAS-schema flow isn't duplicated. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Print the error message and exit instead of letting it bubble up as an unhandled exception (which caused citty and Node to both print it). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com>
Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com>
Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com>
Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com>
v3 runs on Node.js 20 which is deprecated and will be removed from GitHub Actions runners on 2026-09-16. v4 supports Node.js 24. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
- Create `packages/logger/src/bin/pretty.ts` — stdin NDJSON pretty printer with consistent `type:` labels for all protocol message types - Move progress formatting (ProgressView, formatProgress) from `apps/engine/src/lib/progress/format.tsx` into `packages/logger/src/format/progress.tsx`, exported as `@stripe/sync-logger/progress` - Add `_ts` field to all protocol log payloads from the logger - Add ink, react as logger package dependencies - Update all consumers to import from `@stripe/sync-logger/progress` Usage: cat sync_run.log | tsx packages/logger/src/bin/pretty.ts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Type labels now match protocol exactly (stream_status:, source_state:, connection_status:, etc.) with no fixed-width padding. Time ranges trimmed to second precision. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Progress uses ProgressHeader component (compact, 2 lines). EOF renders ProgressView inside an Ink Box with rounded border, colored by status (red/green/yellow). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
withHttpRetry uses exponential backoff (up to 31s) but had no abort signal, so retries blocked past the chunk time limit causing hard deadline hits. Race listFn against the abort signal in withRateLimit so retries are abandoned immediately on pipeline teardown. Also handle AbortError in the stream catch block: log a warning about potential cross-chunk retry loops instead of emitting stream_status:error, letting the stream retry on the next chunk. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Remove max_concurrent_streams config — concurrent streams is now min(rate_limit, catalog.streams.length). With rate_limit=50 and 74 streams, all 50 run in parallel instead of the old default of 5. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
withAbortOnReturn was using plain Error('iterator returned') as the
abort reason, which callers couldn't reliably detect. Changed to
DOMException with name 'AbortError' so the entire abort chain uses
a consistent error type. Also swallow the losing Promise.race
rejection in withRateLimit to prevent unhandled promise crashes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Committed-By-Agent: claude
…config node-postgres parses connectionString last via Object.assign, so sslmode in the URL always overwrites any ssl key set on the config object. Strip SSL params from the connection string and translate sslmode to Node.js TLS options only when the caller hasn't already set an explicit ssl key. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
The SSL param stripping logic (stripSslParams + sslConfigFromConnectionString) addresses a real node-postgres issue where sslmode in the URL overwrites the ssl config key. However, this proxy + SSL + node-postgres interaction has been repeatedly tricky and needs thorough testing across RDS, local Docker, and tunneled connections before enabling. Keeping the code commented out as a reference for when we're ready to re-enable with proper test coverage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
…unction Move SSL connection string handling into a named, exported function instead of inline commented-out code. Not called yet — needs testing across RDS, Docker, and tunneled connections. When enabled, should be applied in the pool factory (all connections), not just proxied ones. Adds tests for sslmode=require, sslmode=verify-full, and explicit ssl override scenarios. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
… var normalizePgSslConfig is now called inside withPgConnectProxy so all callers (destination-postgres, state-postgres) get it consistently. Gated by PG_NORMALIZE_SSL=1 — set it to test, unset to disable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the fixed-segment backfill algorithm with an adaptive binary subdivision strategy that recursively splits time ranges until each completes in a single API page. This is a ground-up rewrite of the pagination, state, progress, and error-handling layers across the stack (155 commits, 150 files, +11k/-6.4k lines).
Core changes
createdtimestamp. Ranges are processed with bounded concurrency untilremaining: [].BackfillState/SegmentStatereplaced by{ remaining: RemainingRange[] }. Done-ness isremaining: []. No error status stored in state. Range reconciliation handles catalogtime_rangechanges between runs.TraceMessagedeleted; replaced by top-levelStreamStatusMessage(discriminated union: start/running/complete/range_complete/error/skip) andProgressMessage. EOF simplified to{ has_more, ending_state, run_progress, request_progress }.(ProgressPayload, Message) → ProgressPayloadreducer with per-stream record/state counts, completed time ranges, and derived rates.newerThanColumnupsert option prevents out-of-order writes.upsertWithStatsreturns created/updated/deleted/skipped counts.stream_status: errorand are skipped for the rest of the run instead of aborting the entire sync.pipeline-workflowreplaced bypipeline-lifecycle(setup → backfill → live → teardown) + childpipeline-backfill(loops paginated runs withcontinueAsNew).max_concurrent_streams(default 5) withmax_requests_per_secondderived from API key mode (live=20, test=10).Supporting changes
Retry-Afterheader respected on 429saccount_createdresolved alongsideaccount_idin setupMotivation
Syncs were failing and we couldn't tell why. The old algorithm was a black box — when a sync stalled or errored, there was no way to see which stream failed, where in its time range it got stuck, or whether the issue was transient. The
TraceMessagesystem was an afterthought bolted onto the side; it didn't carry enough structure to answer "what went wrong and where."This PR rewrites both the core algorithm (for performance and correctness) and the protocol (for observability):
Why the algorithm had to change
error_statusfield in state. On the next run, the segment would retry from scratch with no indication of what happened.What we replaced it with
remainingwith their cursor — retries resume exactly where they left off.remaining: []= done — State is trivially inspectable. You can look at any stream's state and immediately see how much work is left and where.StreamStatusMessage— A structured, top-level message type with discriminated variants (start,running,complete,range_complete,error,skip). Every lifecycle transition is observable.range_completecarries the exact{ gte, lt }that finished.ProgressPayload— Per-stream record counts, state counts, completed time ranges, and derived rates. Emitted on every meaningful event. The Ink-based CLI renders this as a live dashboard.stream_status: errorand is skipped. The other 49 streams continue. The error message appears in the progress display in real time.The net result: when a sync fails now, you can see exactly which stream, which time range, what error, and how far it got — without touching logs.
Package-by-package changes
packages/protocol— The foundation everything else builds onTraceMessageand all subtypes deleted entirelyStreamStatusMessage(discriminated union:start | running | complete | range_complete | error | skip) andProgressMessageSyncStateredesigned: old 3-section model (source/destination/engine) → newsource: SourceState,destination: Record<string, unknown>,sync_run: SyncRunState(carriessync_run_id,time_ceiling,progress)EofPayload: oldreasonenum → new{ has_more, ending_state, run_progress, request_progress }ConfiguredStreamgainssupports_time_rangeandtime_range: { gte, lt }Streamgainsnewer_than_fieldandsoft_delete_fieldutils/binary-subdivision.ts: puresubdivideRanges()+ asyncstreamingSubdivide()with bounded concurrencyutils/async-iterable.ts:mergeAsync()added;split()/channel()removedcoerceSyncState()→parseSyncState()(validates against connector spec schema)createSourceMessageFactory()andcreateEngineMessageFactory()typed envelope constructorspackages/source-stripe— The biggest behavioral changecompactState,expandState,probeAndBuildSegments,segmentCountFromDensity,buildSegments,splitRange,sequentialBackfillStream,paginateSegment,getFailureType,errorToTracereconcileRanges()— handles catalogtime_rangechanges between runs (trims/drops/adds ranges)paginateRange()— fetches one page from aRemainingRange, returns after one page for created-filter streams so subdivision can happeniterateStream()— per-stream driver: loops remaining ranges, dispatches viamergeAsync, callsnextStep()to subdivide after each roundSegmentState/BackfillState→RemainingRange(ISO gte/lt + cursor).remaining: []= done.trace: error→stream_status: error; checks structuredStripeApiRequestErrorbodyaccount-metadata.ts,retry.ts(getRetryAfterMs())rate_limitauto-derived from key mode (live=20, test=10),max_concurrent_streamsconfigurableapps/engine— Sync loop + progress rewritepipeline_syncrewritten: No moresplit()forking. Destination is sole consumer (pull-based backpressure). Engine iterates destination output through two pure reducers:stateReducerandprogressReducer.state-reducer.ts: Pure(SyncState, StateEvent) → SyncStateprogress/module:reducer.ts— pure(ProgressPayload, Message) → ProgressPayloadranges.ts—mergeRanges()for coalescing time rangesformat.tsx— Ink-based terminal displaywithTimeRanges()— injectstime_range.ltfromtime_ceilinginto catalogresolvePipeline()— consolidates connector resolution, spec, catalog, state normalizationtakeLimits: EOF is{ has_more: boolean }withLoggedStream,engineLogContext, oldlib/backfill.ts, oldlib/progress.ts,cli/backfill.tscli/sync.tsx(Ink-based),--plainflagpackages/util-postgres— Upsert overhaulkeyColumns→primaryKeyColumns,noDiffColumns→volatileColumns,mustMatchColumns→guardColumnsnewerThanColumn:WHERE EXCLUDED.col > tbl.col— prevents stale writesupsertWithStats(): Returns created/updated/deleted/skipped counts viaRETURNING (xmax = 0)packages/destination-postgres— Error resilience + stale-write integrationupsertManyacceptsnewerThanField, forwarded to new upsert optionstream_status: error+ skipped (not aborted)catchwithtraceerror removedpackages/openapi— Minorretry-afteradded toDEBUG_HEADERSpackage.jsonfiles expanded for.tssource resolutionapps/service— Workflow architecture overhaulpipeline-workflow.ts→pipeline-lifecycle.ts(setup → backfill → live → teardown) +pipeline-backfill.ts(child workflow, loops untilhas_more=false,continueAsNewafter 200 iterations)RunResult,mergeStateMessage,classifySyncErrorsdeleted.pipelineSyncreturns{ eof: EofPayload }.sync-errors.tsdeleted entirely — engine handles error classificationpausedSignalfor pause/resumeSyncStateremoved from OpenAPI,progress→ProgressPayloade2e/— Test adaptationseofshape (has_more+run_progress/request_progress)BackfillStatetoremaining[]Root / Config / CI
.github/workflows/prod-e2e-test.yml— hourly Sigma reconciliationinjectWorkspacePackages: truefrompnpm-workspace.yamlbench-subdivision.sh,check-sync-efficiency.ts,reconcile-sigma-vs-postgres.ts,test-all-accounts.shTest plan
pnpm build— all packages compile cleanlypnpm lint+pnpm format— no violationsscripts/test-all-accounts.sh— full sync against 9 Stripe accounts (QA + prod, small → large)scripts/test-all-accounts.sh --verify— Sigma reconciliation confirms every synced row matches Stripe's source of truth.github/workflows/prod-e2e-test.yml🤖 Generated with Claude Code
File-by-file breakdown
apps/engine (+3,915 / -2,571 · net +1,344)
Rewritten sync loop with pure state and progress reducers, new Ink-based terminal UI, and split CLI binaries. The old
split()-based forking, backfill scheduling, and text progress are deleted.src/__generated__/openapi.jsonsrc/lib/progress/reducer.test.tssrc/lib/engine.test.tssrc/cli/sync.tsxsrc/lib/progress/format.tsxsrc/lib/engine.tssrc/lib/progress/reducer.tssrc/lib/state-reducer.test.tssrc/api/helpers.tssrc/lib/progress/format.test.tsxsrc/lib/state-reducer.tssrc/cli/command.tssrc/cli/subprocess.tssrc/cli/source-config-cache.test.tssrc/__tests__/bin-serve.test.tssrc/api/server.tssrc/cli/source-config-cache.tssrc/api/index.test.tssrc/lib/pipeline.test.tssrc/lib/progress/ranges.test.tssrc/lib/pipeline.tssrc/lib/progress/ranges.tssrc/bin/serve.tssrc/api/app.test.tssrc/__tests__/docker.test.tssrc/__generated__/openapi.d.tssrc/api/app.tspackage.jsonsrc/bin/sync-engine.tssrc/lib/source-test.tssrc/lib/remote-engine.test.tssrc/bin/bootstrap.tssrc/lib/source-exec.tssrc/lib/progress/index.tssrc/index.tssrc/api/index.tstsconfig.jsonjsx: react-jsxfor Ink componentssrc/lib/createSchemas.tssrc/lib/destination-test.tssrc/__tests__/openapi.test.tssrc/lib/progress.test.tssrc/lib/progress.tssrc/cli/sync.tssrc/cli/backfill.tssrc/lib/backfill.test.tssrc/lib/backfill.tssrc/serve-command.tssrc/cli/index.tsdocs (+1,971 / -89 · net +1,882)
New algorithm design docs, sync lifecycle guides, state-flow diagrams, plans, and a debugging guide. Old state-flow diagrams relocated.
engine-refactor/sync-lifecycle.mdengine-refactor/sync-lifecycle-source-stripe.mdarchitecture/binary-subdivision.mdengine/pipeline-handle-events.mdengine-refactor/sync-lifecycle-start-end-message.mdplans/2026-04-18-engine-binary-split.mdengine-refactor/state-flow.pumlplans/2026-04-19-structured-request-logging.mdarchitecture/binary-subdivision.pumlguides/debugging-sync-cli.mdguides/cli-spec.mdarchitecture/packages.mdslides/demo.mdengine-refactor/state-flow.pngengine-refactor/state-flow.svgarchitecture/binary-subdivision.svgservice/entities.svgengine/sync-engine-types.tsslides/step5-engine.shengine/state-flow.pumlengine/state-flow.pngengine/state-flow.svgpackages/source-stripe (+1,550 / -1,454 · net +96)
Full rewrite of pagination from fixed segments to n-ary subdivision. Nearly net-zero — it's a true replacement, not additive. New reconcileRanges, paginateRange, iterateStream. Error handling via stream_status instead of trace.
src/index.test.tssrc/src-list-api.tssrc/src-list-api.test.tssrc/index.tssrc/process-event.tssrc/account-metadata.tssrc/src-events-api.tssrc/resourceRegistry.tssrc/spec.tssrc/retry.tssrc/catalog.tssrc/client.tssrc/__tests__/eventsPolling.integration.test.tspackage.jsonsrc/rate-limiter.tssrc/transport.test.tsscripts (+1,160 / -5 · net +1,155)
New operational tooling: multi-account test harness, Sigma reconciliation, efficiency analysis, and subdivision benchmarking.
reconcile-sigma-vs-postgres.tscheck-sync-efficiency.tstest-all-accounts.shbench-subdivision.shgenerate-diagrams.shmitmweb-env.shopen-docs.shpackages/protocol (+1,139 / -743 · net +396)
TraceMessage deleted. Replaced by StreamStatusMessage + ProgressMessage. New SyncRunState, simplified EOF. Binary subdivision algorithm and mergeAsync added. split/channel removed.
src/utils/binary-subdivision.test.tssrc/protocol.tssrc/utils/binary-subdivision.tssrc/helpers.tssrc/utils/async-iterable.tssrc/index.tssrc/__tests__/cli.test.tssrc/cli.tspackage.jsonsrc/utils/async-iterable.test.tssrc/__tests__/control.test.tssrc/__tests__/state.test.tsapps/service (+986 / -1,109 · net -123)
Actually shrank. Monolithic workflow split into lifecycle + child backfill. Error classification deleted (engine handles it). Activities simplified to return
{ eof }.src/__generated__/openapi.jsonsrc/__generated__/openapi.d.tssrc/temporal/workflows/pipeline-lifecycle.tssrc/__tests__/workflow.test.tssrc/temporal/workflows/pipeline-backfill.tssrc/api/app.test.tssrc/api/app.tssrc/temporal/activities/pipeline-sync.ts{ eof }instead of RunResultsrc/temporal/activities/_shared.tssrc/temporal/workflows/_shared.tssrc/temporal/workflows/index.tssrc/lib/createSchemas.tstsconfig.jsonsrc/temporal/activities/pipeline-teardown.tssrc/index.tssrc/temporal/workflows/pipeline-workflow.tssrc/temporal/activities/index.tssrc/temporal/sync-errors.tspackages/util-postgres (+623 / -99 · net +524)
Upsert overhaul: renamed options for clarity, new
newerThanColumnfor stale-write prevention, newupsertWithStatswith row-level classification.src/upsert.test.tssrc/upsert.tssrc/index.tspackage.jsonsrc/httpConnectStream.test.tse2e (+179 / -170 · net +9)
All assertions updated for new eof/progress shape and remaining-based state. Nearly net-zero — same coverage, new format.
test-server-sync.test.tstest-sync-engine.test.tstest-server-all-api.test.tsconnector-loading.test.shtest-e2e-network.test.tstest-disconnect.test.tsheader-size-docker.test.tsstripe-to-postgres.test.ts.github (+162 / -0 · net +162)
workflows/prod-e2e-test.ymlpackages/destination-postgres (+138 / -48 · net +90)
Per-stream error resilience (failed streams skipped, not fatal) and stale-write prevention via newerThanField passthrough.
src/index.test.tssrc/index.tspackage.jsonpackages/openapi (+12 / -2 · net +10)
listFnResolver.tspackage.jsonpackages/destination-google-sheets (+12 / -15 · net -3)
src/index.tspackage.jsonpackages/ts-cli (+35 / -15 · net +20)
src/openapi/command.tssrc/ndjson.tssrc/__tests__/json-content-header.test.tssrc/openapi/parse.tssrc/openapi/command.test.tssrc/env-proxy.test.tssrc/openapi/types.tspackage.jsonpackages/state-postgres (+2 / -2 · net 0)
package.jsonapps/supabase (+6 / -3 · net +3)
src/__tests__/bundle.test.tssrc/index.tspackage.jsonRoot config
pnpm-lock.yaml.gitignoreAGENTS.mdDockerfilepnpm-workspace.yamlinjectWorkspacePackages: true(breaks hardlinks)package.jsoneslint.config.mjsdemo
stripe-to-postgres.shstripe-to-google-sheets.shstripe-to-postgres-live.sh