Skip to content

Delphi: comment embeddings as one row per comment (migration) - #3035

Merged
colinmegill merged 5 commits into
edgefrom
embeddings-table
Oct 13, 2026
Merged

colinmegill merged 5 commits into
edgefrom
embeddings-table

Conversation

@colinmegill

@colinmegill colinmegill commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

DRAFT — SCHEMA CHANGE, out of the merge queue. Not for merge until the schema discussion has happened; merging it means the migration applies at the next deploy.

Observable changes

  • Product behaviour: none. No server route, daemon path, Python writer or importer calls any of the new objects. Every Delphi result is stored and read exactly as before.
  • Schema (additive): migration 000038_create_delphi_comment_embeddings.sql adds:
    • tables delphi_embedding_models (seeded with one row: sentence-transformers/all-MiniLM-L6-v2, 384 dims), delphi_comment_embeddings and delphi_embedding_sets;
    • five executor-callable functions (pd_embedding_put, pd_embedding_close, pd_embedding_set, pd_embedding_served, pd_embedding_rows), the owner-only pd_embedding_prune (the only path that deletes rows or sets: every row and set of one conversation, optionally of one model, or of a retired model across the environment; refused while a job of an affected conversation is unfinished), and three internal ones. Embedding rows keep the run that first wrote them as provenance only (no foreign key), so they never keep a result batch from being pruned.
      It changes no existing table, function, view or grant. It is declared additive in compat.txt, so a release without it runs beside it.
  • Deploy day: creating the foreign keys takes SHARE ROW EXCLUSIVE on conversations and delphi_result_batches until commit. Writes to those two tables wait for milliseconds; reads do not. An open writer transaction makes the migration wait lock_timeout (5 s), fail with no history row, and the deploy retries.
  • Numbering: 000032–000037 are reserved for other drafts. 000038 references none of their objects, so it applies correctly before or after them.

What it is

Comment embeddings are stored one row per (environment, conversation, model, comment) as real[], instead of one codec text value per run. Today that text value is bounded at 64 MiB, which about 5,950 comments of 384 floats already exceed.

  • Write-once rows. A comment's text does not change, so a run adds only the comments it has not seen. Rows are immutable. The same comment with different text is refused.
  • Embedding set. A run publishes an embedding set: every row for the conversation and model up to a sequence mark. The set is closed before the run's result batch is sealed, so the existing served pointer publishes it together with the run's other results. A closed set never changes; later runs only add rows with higher sequence numbers.
  • Reads. pd_embedding_rows returns a set in comment order, for COPY (SELECT …) TO STDOUT (FORMAT binary).
  • Why real[] rather than pgvector vector(384). The dev, CI and fresh-database images (postgres:17-alpine, postgres:17.11) have no vector extension. The same bytes convert later with ALTER TABLE … ALTER COLUMN embedding TYPE vector USING embedding::vector.
  • Reversal. down/000038_drop_delphi_comment_embeddings.sql drops everything 000038 added. It locks both tables, then refuses while any embedding is stored; rolling back what is served is a pointer change, not a drop.

Also in this PR:

  • docs/migrations.md gains the declaration row.
  • build-migration-report.py and report/first-deploy.sql are regenerated.
  • forward.py counts 000038 among the tolerated additive migrations.

Evidence (run locally on a work box, Postgres 17)

  • Migration-runner job steps: runner fmt, clippy, tests and build; API build; build-migration-report.py --check; prove.py (25), indexes.py (6), adoption.py (3), selection.py (5), forward.py (11), lexer.py (10); fresh-image.sh. All pass.
  • Function checks:
    • put, retry and close are idempotent;
    • text conflict, wrong dimensions, NaN, duplicate comment, unknown model and a stale lease epoch are refused;
    • the executor cannot read the tables directly;
    • close refuses outside READ COMMITTED, after the batch is sealed, and when a put follows a close;
    • a second run's set grows while the first set stays the same;
    • the set digest matches an independent recomputation;
    • artifact → upstream set resolution and the served lookup work;
    • UPDATE and DELETE are refused even for the owner, except DELETE inside pd_embedding_prune, and that permission ends when the call returns;
    • the prune's conversation and model modes remove exactly their scope; both refuse while a job of an affected conversation is unfinished; any other mode is refused; deleting a result batch that still has a set fails loudly;
    • pruning superseded sets of a conversation that stays is deliberately not part of this migration.
    • the reversal script refuses when a put is in flight.
  • Size and speed at 100,000 comments:
    • 205 MB including indexes (1,668 B per row);
    • about 0.65 s per 4,096-row put (one call per transaction);
    • close about 0.9 s;
    • binary COPY of the whole set about 1.2 s (314 MB stream).

Not in this PR: the Rust daemon, Python writer and importer changes that would use these objects, behind a flag that defaults to off.

🤖 Generated with Claude Code

https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

colinmegill and others added 5 commits October 12, 2026 00:48
…draft)

Adds delphi_embedding_models, delphi_comment_embeddings (real[] per
conversation, model and comment, write-once) and delphi_embedding_sets
(a run's closed set: every row up to a sequence mark), with fenced
pd_embedding_put / pd_embedding_close and the read functions
pd_embedding_set, pd_embedding_served and pd_embedding_rows. Additive:
nothing existing is redefined and no current code calls the new objects.
A reversal script refuses while any embedding is stored.

Declared additive in compat.txt, listed in release.txt and
docs/migrations.md; migration report regenerated; forward.py counts it
among the tolerated additive migrations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
The reversal of 000038 checked for stored embeddings without a table lock,
so a put still in flight could commit after the check and be dropped with
the tables. It now takes ACCESS EXCLUSIVE on both embedding tables before
the check: an in-flight put either commits first and the script refuses,
or waits and fails once the tables are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…d prune path

Embedding rows outlive the run that first wrote them, so batch_id is now a
plain provenance column with no foreign key: rows no longer keep a result
batch from being pruned. Sets keep their foreign key to their batch, so a
batch prune must remove the batch's sets first and fails loudly otherwise.

Rows and sets still refuse UPDATE, and refuse DELETE except inside the new
owner-only pd_embedding_prune, which:
- superseded: per model, keeps the sets the served pointers resolve to, any
  newer set, the newest older one for rollback by pointer, and any set an
  unfinished job still consumes; deletes other sets, then rows of runs that
  never closed a set when no job for the conversation is unfinished;
- conversation: deletes every row and set of a conversation;
- model: deletes every row and set of a retired model.
It takes the same per-conversation, per-model lock as put and close, and
refuses the conversation and model modes while a job is unfinished.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
… retired models only

pd_embedding_prune no longer has a mode that deletes superseded sets of a
conversation that stays. Deciding which sets a later pointer, a rollback or
an unfinished graph can still resolve to needs a rule per report scope over
fully finished graphs; that is left for a later migration. A run's set is a
small metadata row, since the embedding rows themselves are stored once per
comment and shared.

The function keeps two modes, conversation (optionally one model) and
model, both refused while a job of an affected conversation is unfinished.
Its signature drops the keep argument; the reversal script follows it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
@github-actions

Copy link
Copy Markdown

Delphi Coverage Report

File Stmts Miss Cover
polismath/init.py 2 0 100%
polismath/benchmarks/bench_pca.py 128 107 16%
polismath/benchmarks/bench_repness.py 81 65 20%
polismath/benchmarks/bench_update_votes.py 38 28 26%
polismath/benchmarks/benchmark_utils.py 34 0 100%
polismath/components/init.py 1 0 100%
polismath/components/config.py 165 70 58%
polismath/conversation/init.py 2 0 100%
polismath/conversation/conversation.py 1291 264 80%
polismath/conversation/group_vote_counts.py 38 2 95%
polismath/conversation/manager.py 131 42 68%
polismath/database/init.py 1 0 100%
polismath/database/dynamodb.py 392 176 55%
polismath/database/postgres.py 414 95 77%
polismath/delphi_storage/init.py 1 0 100%
polismath/delphi_storage/codec.py 295 22 93%
polismath/delphi_storage/golden_corpus.py 60 9 85%
polismath/delphi_storage/import_report.py 51 3 94%
polismath/delphi_storage/legacy_import.py 171 16 91%
polismath/delphi_storage/postgres.py 49 22 55%
polismath/delphi_storage/production_import.py 419 96 77%
polismath/delphi_storage/resource.py 179 32 82%
polismath/delphi_storage/scan_pacing.py 77 0 100%
polismath/delphi_storage/writer.py 158 17 89%
polismath/empty_output.py 29 2 93%
polismath/engine_adapter.py 317 265 16%
polismath/job_child/init.py 331 50 85%
polismath/job_child/census.py 101 8 92%
polismath/pca_kmeans_rep/init.py 5 0 100%
polismath/pca_kmeans_rep/clusters.py 257 38 85%
polismath/pca_kmeans_rep/corr.py 98 17 83%
polismath/pca_kmeans_rep/group_k_smoother.py 28 0 100%
polismath/pca_kmeans_rep/legacy_kmeans.py 197 8 96%
polismath/pca_kmeans_rep/pca.py 162 19 88%
polismath/pca_kmeans_rep/repness.py 226 5 98%
polismath/poller/init.py 4 0 100%
polismath/poller/admission.py 318 15 95%
polismath/poller/backfill.py 936 32 97%
polismath/poller/capacity.py 485 27 94%
polismath/poller/capacity_queue.py 248 13 95%
polismath/poller/coordinator_bridge.py 317 317 0%
polismath/poller/math_writer.py 113 3 97%
polismath/poller/promotion.py 112 4 96%
polismath/poller/readiness.py 368 13 96%
polismath/poller/rebuild_child.py 133 5 96%
polismath/poller/service.py 665 52 92%
polismath/poller/worker_pool.py 140 0 100%
polismath/queue/executor.py 223 111 50%
polismath/regression/init.py 4 0 100%
polismath/regression/clojure_comparer.py 188 20 89%
polismath/regression/comparer.py 891 640 28%
polismath/regression/datasets.py 135 27 80%
polismath/regression/recorder.py 36 27 25%
polismath/regression/utils.py 130 85 35%
polismath/replay/init.py 2 0 100%
polismath/replay/attribution_capture.py 49 0 100%
polismath/replay/axis_continuity.py 347 24 93%
polismath/replay/certify.py 999 59 94%
polismath/replay/crosslang.py 128 2 98%
polismath/replay/decision_ties.py 55 8 85%
polismath/replay/diagnostics.py 50 7 86%
polismath/replay/driver.py 73 1 99%
polismath/replay/dsn_admission.py 36 1 97%
polismath/replay/equiv_query.py 10 0 100%
polismath/replay/event_ingress.py 95 1 99%
polismath/replay/fixture_bundle.py 815 62 92%
polismath/replay/fixture_config.py 266 11 96%
polismath/replay/fixture_extract.py 285 12 96%
polismath/replay/fixture_generate.py 103 1 99%
polismath/replay/fixture_samples.py 157 14 91%
polismath/replay/fixture_selection.py 119 0 100%
polismath/replay/fixture_survey.py 147 11 93%
polismath/replay/legacy_pca.py 29 17 41%
polismath/replay/polarity.py 319 25 92%
polismath/replay/poller_equiv.py 776 99 87%
polismath/replay/prodclone.py 193 2 99%
polismath/replay/real_data.py 146 10 93%
polismath/replay/schedule.py 205 10 95%
polismath/replay/served_math.py 159 4 97%
polismath/replay/shard_bench.py 189 65 66%
polismath/replay/stagecompare.py 890 161 82%
polismath/replay/stages.py 256 40 84%
polismath/replay/stepcompare.py 86 1 99%
polismath/replay/store.py 120 11 91%
polismath/replay/tie_capture.py 107 19 82%
polismath/replay/types.py 71 6 92%
polismath/run_math_pipeline.py 284 93 67%
polismath/tools/engine_rebuild.py 230 11 95%
polismath/types.py 22 0 100%
polismath/utils/cli_flags.py 15 0 100%
polismath/utils/clj_hash.py 44 0 100%
polismath/utils/env_flags.py 13 0 100%
polismath/utils/general.py 63 39 38%
polismath/utils/output_profile.py 66 2 97%
polismath/utils/serialization.py 10 5 50%
polismath/utils/vote_convention.py 159 5 97%
umap_narrative/501_calculate_comment_extremity.py 113 38 66%
umap_narrative/502_calculate_priorities.py 136 103 24%
umap_narrative/700_datamapplot_for_layer.py 502 502 0%
umap_narrative/701_static_datamapplot_for_layer.py 310 310 0%
umap_narrative/702_consensus_divisive_datamapplot.py 435 275 37%
umap_narrative/801_narrative_report_batch.py 666 455 32%
umap_narrative/803_check_batch_status.py 268 148 45%
umap_narrative/llm_factory_constructor/init.py 2 0 100%
umap_narrative/llm_factory_constructor/model_provider.py 234 107 54%
umap_narrative/narrative_data.py 178 65 63%
umap_narrative/numerical_stages.py 54 25 54%
umap_narrative/polismath_commentgraph/init.py 1 0 100%
umap_narrative/polismath_commentgraph/cli.py 268 268 0%
umap_narrative/polismath_commentgraph/core/init.py 3 3 0%
umap_narrative/polismath_commentgraph/core/clustering.py 108 108 0%
umap_narrative/polismath_commentgraph/core/embedding.py 104 104 0%
umap_narrative/polismath_commentgraph/lambda_handler.py 219 219 0%
umap_narrative/polismath_commentgraph/schemas/init.py 2 0 100%
umap_narrative/polismath_commentgraph/schemas/dynamo_models.py 160 9 94%
umap_narrative/polismath_commentgraph/tests/conftest.py 17 17 0%
umap_narrative/polismath_commentgraph/tests/test_clustering.py 74 74 0%
umap_narrative/polismath_commentgraph/tests/test_embedding.py 55 55 0%
umap_narrative/polismath_commentgraph/tests/test_storage.py 87 87 0%
umap_narrative/polismath_commentgraph/utils/init.py 9 7 22%
umap_narrative/polismath_commentgraph/utils/converter.py 283 237 16%
umap_narrative/polismath_commentgraph/utils/group_data.py 332 289 13%
umap_narrative/polismath_commentgraph/utils/storage.py 581 466 20%
umap_narrative/reset_conversation.py 162 53 67%
umap_narrative/run_pipeline.py 337 207 39%
umap_narrative/topic_naming.py 148 11 93%
Total 25411 7950 69%

@colinmegill
colinmegill marked this pull request as ready for review October 13, 2026 05:14
@colinmegill
colinmegill merged commit 51cceb4 into edge Oct 13, 2026
33 of 34 checks passed
@colinmegill
colinmegill deleted the embeddings-table branch October 13, 2026 05:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant