Skip to content

DRAFT — polis-jobs: the built-in queue sweep (off unless POLIS_JOBS_SWEEP=1; needs 000026, P-083) - #2981

Draft
colinmegill wants to merge 37 commits into
edgefrom
queue/daemon-sweep
Draft

colinmegill wants to merge 37 commits into
edgefrom
queue/daemon-sweep

Conversation

@colinmegill

@colinmegill colinmegill commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Observable changes: with POLIS_JOBS_SWEEP=1 (default 0, off) and migration 000026 applied, an idle polis-jobs worker deletes expired queue history once a day per queue env and prints one polis_jobs.sweep/1 line per sweep (counts of jobs, attempts, log rows, bindings, runs and ledger rows deleted; pages; what stopped it). With the flag on and 000026 absent it prints polis_jobs sweep off: … once and carries on. Nothing changes with the flag off. Draft: it stands on the 000026 schema ruling (#2978). Not a schema change itself.

Corrections from the connections map

Observable changes (updated): with POLIS_JOBS_SWEEP=1 and the corrected 000026, a sweep deletes nothing as shipped: every retention rule is a dry run and the polis_jobs.sweep/1 line reports dry_run: true and would counts (would_job_succeeded, would_logs_failed, ...). Once an operator turns a rule on (an UPDATE of polis_queue_retention_policy, no migration), rows are first tombstoned (restorable) and purged by a later sweep after the grace; the line adds jobs_tombstoned, logs_tombstoned, bindings_tombstoned and tombstones_restored to the deleted counts. The sweep never deletes the dead jobs an open dead-job breaker counts (#729).

Merge order: #2944, then #2978, then this PR (each merged into the next, never rebased).

  • queue-rs/src/jobs/sweep.rs: sweep_line builds the line; unit tests a_dry_run_line_carries_the_would_counts_and_deletes_nothing, a_deleting_line_has_no_would_counts.
  • queue-rs/tests/jobs_integration.rs: the_built_in_sweep_dry_runs_then_tombstones_then_purges (real daemon, real 000025+000026 template): the shipped dry run deletes nothing; the policy UPDATE; the next day's sweep tombstones; after the grace the purge takes the older rebuild with its attempt and logs and keeps the latest with its manifest row; three ledger rows; not_due.
  • README: retention by reachability and the breaker.

Gates on mm2 at 85848fe1b: queue-rs fmt, clippy -D warnings (both feature sets), unit (43, including the two new line tests) and jobs_integration (25, including the rewritten sweep test) against PostgreSQL 17; down tests 000023, 000024, 000026; the composed chain proof (delphi/tests/poller/chain_proof.py: route, run, finalize, promote, cancel, SIGTERM, SIGKILL, cancel running, three deaths -> poisoned, new commit admits) on 000000..000026 with this branch's daemon: every step held. #2978's gates cover the schema.

Original description (superseded where the corrections above say so)

What it is

Cost-reduction plan P-083, PR 2 ("built-in queue_sweep"). The sweep runs inside the daemon: no child process, no DSN handed out. After the claim pass, if it found nothing to start (or the worker is at its concurrency), the main loop asks pq_sweep(env, sweep_id, page, max_pages) for one bounded page; the next page on the next pass. That is the plan's lane-2 rule (a sweep never goes ahead of a job) without a queue row. The SQL decides the rest: cadence (one sweep per 24 h per env, not_due otherwise), a single sweeper (busy), abandoned sweeps, every retention bound and page size (#2978 has the table). POLIS_JOBS_SWEEP_MAX_PAGES (100, 1..1000) bounds one sweep; POLIS_JOBS_SWEEP_CHECK_SECONDS (900, 1..86400) is how often an idle worker asks whether one is due. The finished sweep's counts also sit in polis_queue_sweeps, which pq_queue_usage reads for sweep_age_ms.

Where the plan did not survive contact with the code

P-083 makes the sweep a queue job (stage queue_sweep, lane 2, keyed by date, rescheduling itself). Every claimable job belongs to a conversation on the merged schema (polis_queue_runs.zid, delphi_jobs.zid NOT NULL with foreign keys; pq_claim joins delphi_jobs), so the sweep is built in instead and its schedule lives in SQL. Details in #2978.

Gates (mm2)

  • queue-rs: cargo fmt --check, clippy -D warnings both feature sets, unit (the sweep settings), jobs_integration against postgres:17 including two new tests: the_built_in_sweep_removes_expired_history_and_keeps_the_latest (two finished rebuilds aged 31 days: the older one goes with its attempts and logs, the latest stays with only its manifest row, one sweep/1 line, the ledger row, then not_due) and the_sweep_turns_itself_off_without_000026 (on /3: one line, the worker keeps running and finishes a job): pass.
  • Down tests 000019, 000023, 000024, 000026: pass (test_000022.sh fails on edge too, unrelated).
  • Vote convention test_gate and sign_lint: pass.
  • Composed chain proof on the chain through 000026: every step held.

Stacked on #2978 (branch queue/retention-000026); its commits show here until it lands.

🤖 Generated with Claude Code

https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

colinmegill and others added 24 commits October 3, 2026 04:23
… functions, views, ledger) — P-078 PR-A

Schema change, ruling R-A. No data change: the seed row (version 0,
agree_value -1) states today's storage convention, so every reader that
consults it computes what it computes today.

000023_vote_convention.sql adds, in one transaction: the vote_convention
singleton (updated in place) with its append-only history and a monotonic
guard; the schema_migrations ledger with 000000-000022 backfilled as
pre-ledger and this file's own row (sha256 of the file without its
ledger-marker line); vote_convention_current() (STABLE, SECURITY DEFINER);
vote_semantic()/vote_storage() (IMMUTABLE, STRICT); vote_insert() (SECURITY
DEFINER, lock_timeout 2 s as a function SET clause, FOR SHARE, expected-version
check, the 000006 rule maintains votes_latest_unique); the votes_semantic and
votes_latest_unique_semantic views; grants to the 000021 coordinator roles
when they exist, recorded in the ledger row. vote_insert is revoked from
PUBLIC. Named SQLSTATEs P0780-P0784, P0789.

down/000023_drop_vote_convention.sql drops exactly those objects and refuses
(P0789) once the convention has moved or a later migration is in the ledger.
down/test_000023_down.sh runs 11 cases on a disposable PostgreSQL 17.
docs/vote-convention.md: grants, restore-detection rule, runbook, and how
the server and engine readers bind to the row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
… view grants, ledger checksum CI

Review fixes, batched into one change of 000023's bytes:
- vote_insert refuses (P0791) when the convention row is missing instead of
  storing NULL votes; DELETE/TRUNCATE on vote_convention refuse (P0790);
  TRUNCATE on the history refuses (P0781); a row put back after a forced
  removal must continue the history (the guard now also runs on INSERT);
  the guard stamps changed_at/changed_by.
- The coordinator observer no longer gets SELECT on the two semantic views
  (they read votes with the owner's rights).
- Catalog comments and the ledger note carry no internal ids.
- server/postgres/check_ledger_checksums.py and a CI workflow recompute the
  self-checksum of every migration from 000023 on; the runbook gains the
  operator pre-apply check.
- docs: the un-flip's precondition (every writer through vote_insert) and
  the proposed table locks for its preamble; 55P03 scope; READ COMMITTED;
  pre-ledger rows are asserted, not observed; down-file precondition.
- Tests (15): a writer blocked by the flip then writes +1 under version 1;
  restore detection against the held un-flip file when present; the
  permanence cases; the checker.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
… a convention it was not built for

Failing first. Pins, before the change that makes them pass:

- server: a startup check that reads public.vote_convention_current() and
  refuses no table, no row, an unknown contract and the other sign, naming
  the exact declare command; the import worker refuses before consuming.
- engine: the same decision for the math poller, the math pipeline stage
  and the UMAP stage; the stage fakes answer the two startup queries.
- coordinator: the same decision as a pure function; the schema census
  grows by the three tables migration 000023 adds (625 columns).
- migration 000023: the row is written only on an empty database; a
  database holding votes is left undeclared with the DECLARE_NEEDED
  notice; the declare operation at both signs and its refusals; the shell
  entry point's status line; the down file from seeded, undeclared and
  declared databases; the restore rule knows "undeclared".
- the sign lint treats the migration, its reversal, the operation and
  this test as the database's own chokepoint.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…o start against a convention it was not built for

Migration 000023 (P-078 PR-A, ruling R-A 2026-10-05) gives the database one
row, public.vote_convention, naming which stored vote value means "agree".

- The migration writes the row ONLY on an empty database (version 0,
  agree -1, operation seed-empty). A database that already holds votes is
  left undeclared, with NOTICE DECLARE_NEEDED naming the one command.
- The operator declares an existing database once:
  make vote-convention-declare AGREE=-1 (server/bin/vote-convention-declare.sh,
  server/postgres/operations/vote_convention_declare.sql). AGREE=+1 writes
  (1, +1) as the first state for a deployment that reversed its own signs.
  make vote-convention-status reports GUARD_NEEDED / DECLARE_NEEDED / GUARDED.
- Every reader of votes checks the row before its first vote read and
  refuses to start on no table, no row, an unknown contract, or a sign
  other than the one it was built for: the server (index.ts), the import
  worker, the math poller, the math pipeline stage, the UMAP stage and the
  commentgraph CLI, the Delphi job poller, and the coordinator (at startup
  and at every source snapshot). The messages name the command and the
  guide (docs/vote-convention-upgrade.md).
- The row and its history record the operation that wrote them and the
  operation file's ledger checksum; vote_convention_current() carries the
  contract version; the monotonic guard admits (0, -1) and a declared
  (1, +1) as first states; the down file reverts an undeclared database too.
- The ledger checker covers operation files; the coordinator's typed rows
  and census include the three new tables; the characterization harness
  leaves the permanent tables alone; the queue crate's fixture accepts
  later server migrations.
- Docs: docs/vote-convention-upgrade.md (what other deployments see and
  run), docs/vote-convention.md, docs/migrations.md, docs/upgrading.md,
  CHANGELOG, README.

No vote value changes. No read, write, export or API response changes.
Applying the migration to production is a separate, explicit step by the
owner, before the release that reads the row is deployed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
Hosted CI's vote-sign lint flagged server/bin/vote-convention-declare.sh:
it names the sign it writes (AGREE=-1 / +1) and exists only to run the
declare operation, which is already registered as the database chokepoint.
The local lint run had preceded `git add`, and the lint scans tracked files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…job table on edge; 000024 is reserved)

The migration, its reversal, the down test and its compose file move from
000023 to 000025, with every reference, pin and the two ledger self-checksums
(the migration's and the declare operation's) regenerated. The chain neither
requires nor records 000024, which another change holds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…n-wt

# Conflicts:
#	queue-rs/tests/jobs_integration.rs
…rapper, and the lock the migration takes

Corrections from the operating-surface review of #2944:

- The migration ledger records what the catalog shows for every earlier
  file (000000..000023: 'verified' when the file's signature object is
  present, 'unverified' when it is not, the probe named in the note)
  instead of asserting them as applied. The migration refuses (P0780)
  without the vote tables from 000000 and 000006 and needs nothing later;
  000024 is neither required nor recorded. The down file keys its
  later-migration refusal on ledger-era rows (64-hex checksums).
- The header and the guide say what the file locks: AccessShareLock on
  votes and votes_latest_unique until COMMIT (the empty-table checks),
  compatible with ordinary traffic, rolled back after the 5 s lock budget
  behind an ACCESS EXCLUSIVE holder; nothing on the vote tables is
  rewritten.
- server/postgres/bin/apply-migration.sh covers 000025: seal from
  down/000025-files.sha256, PostgreSQL 17, the login owns the vote tables,
  the vote tables exist, no convention object exists yet, no old
  transaction, free disk; the budgets; a post-check of the ledger row and
  the convention state (DECLARE_NEEDED / GUARDED). Docs route the apply
  through it.
- run_delphi.py runs polismath/check_vote_convention.py as its first
  stage, before the reset that removes previous results, so a job
  launched directly or by an older poller refuses with nothing changed.
  The commentgraph CLI's test-postgres exits 1 on a refusal and prints the
  operator message; the docs claim only that command.
- The guide says the checks are made at startup (and at the job
  boundary), that the import worker and the Python processes check
  DATABASE_URL only, and that this release does not serve a +1 database.
- The down test covers the chain 000000..000023 then 000025, the verified
  ledger, a copy without the dormant 000019/000021/000023, the
  prerequisite refusal and the wrapper (preflight-only, an old
  transaction, an ACCESS EXCLUSIVE holder, the apply, a second run, the
  seal). The census pins 000023 and the renumbered 000025.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
… and the renumbered 000025

The census test enumerates the migrations directory and binds every file's
bytes, so it had been red since 000023_create_delphi_foundation.sql merged
without a census. Measured on PostgreSQL 17.11 with the full chain applied
to a fresh database: eight new relations (delphi_jobs, delphi_job_aliases,
delphi_job_inputs, delphi_current, delphi_job_guards,
delphi_provider_requests, polis_queue_logs, delphi_foundation_install), the
four columns 000023 adds to the 000019 tables, and the constraints and
indexes they change. 95 relation types, 694 columns.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…e standard library; the commentgraph CLI builds a real PostgresConfig

Found by the composed-image rehearsal: run by file path from run_delphi.py,
polismath/check_vote_convention.py had its own package directory first on
sys.path, where polismath/types.py shadowed the standard library's types and
every run crashed before the check. The script drops that entry before its
imports and run_delphi.py sets PYTHONPATH before the stage. The commentgraph
CLI's test-postgres passed a dict where PostgresClient needs a PostgresConfig
(pre-existing on edge), so its refusal path was unreachable in the image.
The guide states the exit codes and which lines show at the default log level.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
… first, with its own refusal rows

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
Edge brought 000024 (the large worker class, polis-queue/3). The queue
integration test keeps the prefix check, now 000000-000024; files after it
(the vote convention, 000025) are in no template.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
… the census measures it

Edge merged 000024 (the large worker class, polis-queue/3), which this
branch had treated as held elsewhere. Corrections, nothing else:

- 000025's ledger gains a row for 000024_create_polis_queue_large_class,
  probed by public.polis_queue_large_class_install (verified when present,
  unverified with the probe named when not, as for every earlier file).
  Comments, docs/migrations.md, docs/vote-convention.md and the checker's
  docstring say 000000..000024. Resealed: the ledger self-checksum and
  down/000025-files.sha256.
- test_000025_down.py: the chain is 000000..000024 (no 000020); the pol.is
  shaped copy leaves out 000019, 000021, 000023 and 000024; the wrapper case
  expects 24 verified earlier files.
- The coordinator schema census, measured again on PostgreSQL 17.11 with
  000000..000025 applied to a fresh database: the only differences are
  000024's (polis_queue_large_class_install; the widened kind, stage,
  worker_class and contract checks; pq_stage_large; the /3 default).
  96 relations, 697 columns; 000025 pointers moved up one line.
- The apply wrapper's header and docs/vote-census.md (which named the vote
  convention migration by its old number) are updated. The wrapper still
  covers 000019, 000023 and 000025; 000024 has no apply policy there and is
  left to its own change.

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

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

The required campaign compares the Rust test names it runs with
coordinator-rs/ci/inventory-v2.json exactly. tests/vote_convention.rs added
seven tests that the inventory did not list, so the campaign's candidate gate
failed on 'unexpected' names (on this branch since those tests were added).
The identities are appended; no existing entry changes.

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

The startup check exited 1 on any failure to read the convention, including
ECONNREFUSED while postgres was still starting. Under compose (the e2e stack,
make start) the server then restarted with a new address, and nginx, which
resolves `server` once, answered 502 for every request: the hosted E2E run
failed that way (server log: "failed to read the database's vote convention
connect ECONNREFUSED", then a restart; every spec's cy.visit got 502).

The read is now retried every 2 s for about a minute when the database does
not answer, as the server tolerated before the check existed. A refusal (no
table, no row, the other sign, an unknown contract) still exits at once.

Checked on the build box with the e2e compose stack: postgres held back 20 s,
the server logs "could not read the database's vote convention (attempt 1 of
30); retrying in 2 s: connect ECONNREFUSED ...", then "vote convention:
version 0, agree stored as -1" and "Server started"; restarts 0; nginx GET /
and /api/v3/participationInit 200. One forced server restart on the
unchanged stack reproduces the 502 (new container address, nginx upstream
unreachable).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
Edge brought the math poller's large class as a queue child (#2973) and the
per-class CDK worker groups (#2974). One conflict in delphi/scripts/math_poller.py:
_build_service keeps the vote convention check before it builds the service,
with edge's simplified constructor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
server/postgres/bin/apply-migration.sh refused 000024 ("has no apply
policy here"), so an existing database had no checked path from
polis-queue/2 to /3. The wrapper now accepts 000024:

- seal: down/000024-files.sha256
- rights: superuser or SET membership in polis_queue_owner (as 000023)
- chain: 000023 installed and polis_queue_install reads polis-queue/2
  (a second run is refused here, since the install then reads /3)
- rows: every queue and job table is empty (000024 alters the queue
  tables' CHECK constraints, ACCESS EXCLUSIVE until COMMIT)
- lock line: names the four queue tables; no lock on conversations
- post-check: polis_queue_install reads polis-queue/3

The usage text now prints the whole header (it was cut at line 60).

test_000024_down.sh gains check (w): refused without 000023 and with a
queued job (catalog unchanged both times); --preflight-only passes and
changes nothing; the apply exits 0 with post-check /3 and the same
schema dump as a plain psql apply; a second run is refused at preflight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
… schema change)

pq_sweep deletes one bounded page of expired queue history per call
(P-083's bounds), recorded in a new polis_queue_sweeps ledger;
pq_class_parked lists a class's parked jobs with their unproven attempts
(P-086 restart-safe rediscovery); pq_queue_usage reports the queue's bytes
and the last finished sweep; pq_class_depth gains oldest_eligible_at and
answers polis-queue/4. Three indexes serve the sweep's scans. The contract
version stays polis-queue/3.

Sealed down script and its test (forward, backward byte-identical,
refusals, the chain unwinding through 000024 and 000023). The poller's
depth decoder accepts the /4 reply; the server and queue-rs tests learn
the new functions and the jobs_v4 template.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…rows are reported, not refused

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…needs migration 000026)

The daemon runs retention itself, page by page through pq_sweep, only when
its claim pass has nothing to start (cost-reduction plan P-083: the sweep
never goes ahead of a job). The SQL decides cadence (once per 24 h per
env), the single sweeper and every bound; each finished sweep is one
polis_jobs.sweep/1 line with its counts. POLIS_JOBS_SWEEP_MAX_PAGES (100)
and POLIS_JOBS_SWEEP_CHECK_SECONDS (900) bound it. On a database without
000026 the sweep says so once and turns itself off.

Stacked on the 000026 draft: a schema-dependent change, draft too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
colinmegill and others added 5 commits October 6, 2026 16:20
…row; the large-class child checks it

The row is the only truth for the stored vote sign: STORAGE_AGREE_VALUE never
overrides it, and a build for the other sign refuses. Seven campaign tests had
reversed the votes with a raw UPDATE and launched the coordinator at +1 while
the row still said (0, -1), so this PR's coordinator refused them.

- conftest: declare_convention() moves the row to the next version with the
  other sign (the transition its triggers admit, recorded in the history);
  reverse_votes() stores every vote with the other sign and leaves the row.
- Polarity through database ingress (three fixtures), the rebuild schedule
  and the tie key: votes reversed AND the row declared +1, then the +1 run;
  the -1 build is shown to refuse (#mismatch). The "negative" control is the
  opposite votes under the same declaration (votes reversed again, +1 run),
  instead of the same votes read under the wrong sign.
- The node reader's deliberately different third namespace is the same
  conversation with every vote reversed (row unchanged), not a run under the
  other sign.
- math_poller --job (the large-class rebuild child): the same startup check
  as the poller, before it builds the service that reads votes; a refusal
  exits 2 (EXIT_JOB_ENV_INVALID) with the operator message. Test:
  tests/poller/test_rebuild_child_convention.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
Edge gave the apply wrapper a policy for 000024 (#2975). One conflict in
server/postgres/bin/apply-migration.sh: the wrapper now covers 000019, 000023,
000024 and 000025, each with its own seal, rights, chain, rows, lock line and
post-check, unchanged from the two sides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
# Conflicts:
#	server/postgres/bin/apply-migration.sh
…eft 'latest' to the job ids' order)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
colinmegill and others added 8 commits October 6, 2026 17:36
… (a grep that exits early made pipefail flaky)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
#2978 is now built on #2944 and merges after it: 000026 records itself in
the migration ledger 000025 creates, so the combined apply wrapper lands
here, carrying #2944's 000025 entry and adding 000026's.

Conflicts, resolved:
- server/postgres/bin/apply-migration.sh: #2944's wrapper (000019, 000023,
  000024, 000025) plus the 000026 entry, with the corrections that follow
  in the next commit (every file of the seal, the ledger self-checksum,
  000026's rights, chain on 000025's ledger row, post-check on the ledger).
- queue-rs/tests/jobs_integration.rs: the 000000-000024 chain check from
  #2944; the jobs_v4 template applies 000025 then 000026.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…breaker, and retains by reachability

Corrections from the connections map (cost-reduction notes; findings
#716, #721, #728, decisions #729 and #758). Schema change, sealed; nothing
is applied anywhere by this commit.

000026_create_polis_queue_retention.sql
- Ledger: its last statement inserts its own public.schema_migrations row
  (self-checksum, as every file from 000025 on must); its guard refuses
  without 000025's row ("migration ledger missing") and when the ledger
  already records 000026 or later. The queue owner never touches the
  ledger: the guard and the insert run as the applying login.
- Dead-job breaker (decision #729), replacing 000024's latch read from
  surviving rows: polis_queue_breakers holds a durable per-(env, zid,
  product) count kept by a trigger (pq_breaker_record) as jobs end. Three
  consecutive dead jobs under one image open it; pd_enqueue answers
  `poisoned` (same reply, naming the latest dead job) for 24 hours, then
  admits ONE probe; the probe succeeding closes it, dying re-opens it for
  24 hours, a cancelled probe frees the slot; a new image resets it. Seeded
  at apply from the queue's history.
- Retention by reachability (decision #758), replacing fixed-days-only
  deletion: pq_retention_reached marks every job something that matters
  reaches (unfinished, pinned, alias, current pointer, held guard, a head's
  run, unproven exit, open provider request, dead under an open breaker, or
  an input or parent of a reached job, transitively). Rules are data in
  polis_queue_retention_policy (kind, keep_days, keep_last, action), so a
  retention change is an UPDATE. Every kind ships as dry_run: pq_sweep
  reports would_<kind> counts and deletes nothing. A kind set to delete
  goes in two phases: tombstone (polis_queue_tombstones; restorable, and
  restored automatically when the row is reached or kept by policy again),
  then purge after the tombstone grace. Bounded per page. The sweep keeps
  every dead job its open breaker counts.
- The down drops all of it, restores 000024's pd_enqueue from the recorded
  baseline, removes 000026's ledger row, refuses once a purge has deleted
  anything (dry runs and tombstones are reversible) and refuses while the
  ledger records a later migration. 000025's down already refuses while
  000026's row is there; 000024's refuses the /4 catalog. Resealed.

test_000026_down.sh (run on mm2, PostgreSQL 17): dry run, policy change by
UPDATE, tombstone/restore/purge, a pinned job's inputs survive, the
breaker's open / poisoned / sweep-keeps / 24 h probe / probe death / probe
success / new image / cancelled probe, the seed from 000024-era history,
the down refusals and order, the chain 000019..000026 up/down/up byte for
byte one file at a time, the wrapper, and the populated chain through the
wrapper with each step's lock held by another session (each apply waits,
times out, changes nothing; 000024-000026 apply beside a conversations
writer).

Census: measured again on PostgreSQL 17.11 with 000000..000026 applied to
a fresh database; the only differences are 000026's (five tables, three
indexes). 101 relations, 729 columns. queue-rs foundation test names the
new objects. docs/migrations.md and docs/queue-substrate.md give the order
and the contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
Brings the corrected 000026: its ledger row, the durable dead-job breaker
and retention by reachability (policy as data, dry run by default,
tombstone then purge), with #2944's vote convention and the combined apply
wrapper underneath. No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…es; the integration test follows retention by reachability

000026 now retains by reachability with the policy as data, every kind a
dry run as shipped, and deletion in two phases (decision #758). The
daemon's `polis_jobs.sweep/1` line keeps its deleted counts and adds
`jobs_tombstoned`, `logs_tombstoned`, `bindings_tombstoned`,
`tombstones_restored`, `dry_run` and `would` (the would_<kind> counts the
SQL reports for kinds still dry run). Unit tests for both line shapes.

jobs_integration: the sweep test now proves the shipped dry run (the line
says it would delete the older rebuild; nothing goes), an UPDATE of
polis_queue_retention_policy turning deletion on (no migration), the next
day's sweep tombstoning without deleting, and the purge after the grace
(the older rebuild goes with its attempt and logs; the latest stays with
its manifest row; three ledger rows; then not_due). README updated, with
the breaker that replaces 000024's latch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…nt, chain scope, sign lint)

- coordinator-rs census: polis_queue_tombstones.ref is `r#ref` (a Rust
  keyword), as `r#type` and `r#mod` already are.
- 000026: pq_breaker_record and pq_retention_reached pin TimeZone=UTC like
  every other function the migration adds (queue-substrate test 39 holds
  every new pq_ function to both pins); its function count is 35 with
  000026. Resealed (ledger self-checksum, down/000026-files.sha256, census
  hash); no line moved.
- test_000025_down.py: its chain case covers the files through 000025;
  000026 on is proven by test_000026_down.sh.
- test_000026_down.sh: no `head -1` (the vote-sign lint reads it as a
  minus-one literal); 0 new sites.

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

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

Delphi Coverage Report

File Stmts Miss Cover
init.py 2 0 100%
benchmarks/bench_pca.py 128 107 16%
benchmarks/bench_repness.py 81 65 20%
benchmarks/bench_update_votes.py 38 28 26%
benchmarks/benchmark_utils.py 34 0 100%
check_vote_convention.py 33 8 76%
components/init.py 1 0 100%
components/config.py 165 70 58%
conversation/init.py 2 0 100%
conversation/conversation.py 1291 264 80%
conversation/group_vote_counts.py 38 2 95%
conversation/manager.py 131 42 68%
database/init.py 1 0 100%
database/dynamodb.py 395 178 55%
database/postgres.py 414 95 77%
delphi_storage/init.py 1 0 100%
delphi_storage/codec.py 295 23 92%
delphi_storage/golden_corpus.py 60 9 85%
empty_output.py 29 2 93%
engine_adapter.py 317 265 16%
job_child/init.py 325 49 85%
job_child/census.py 101 13 87%
pca_kmeans_rep/init.py 5 0 100%
pca_kmeans_rep/clusters.py 257 38 85%
pca_kmeans_rep/corr.py 98 17 83%
pca_kmeans_rep/group_k_smoother.py 28 0 100%
pca_kmeans_rep/legacy_kmeans.py 197 7 96%
pca_kmeans_rep/pca.py 162 19 88%
pca_kmeans_rep/repness.py 226 5 98%
poller/init.py 4 0 100%
poller/admission.py 318 15 95%
poller/backfill.py 936 32 97%
poller/capacity.py 485 27 94%
poller/capacity_queue.py 255 13 95%
poller/coordinator_bridge.py 317 317 0%
poller/math_writer.py 113 3 97%
poller/promotion.py 112 4 96%
poller/readiness.py 368 13 96%
poller/rebuild_child.py 133 6 95%
poller/service.py 665 52 92%
poller/worker_pool.py 140 0 100%
queue/executor.py 223 111 50%
regression/init.py 4 0 100%
regression/clojure_comparer.py 188 20 89%
regression/comparer.py 891 640 28%
regression/datasets.py 135 27 80%
regression/recorder.py 36 27 25%
regression/utils.py 130 85 35%
replay/init.py 2 0 100%
replay/attribution_capture.py 49 0 100%
replay/axis_continuity.py 347 24 93%
replay/certify.py 999 59 94%
replay/crosslang.py 128 2 98%
replay/decision_ties.py 55 8 85%
replay/diagnostics.py 50 7 86%
replay/driver.py 73 1 99%
replay/dsn_admission.py 36 1 97%
replay/equiv_query.py 10 0 100%
replay/event_ingress.py 95 1 99%
replay/fixture_bundle.py 815 62 92%
replay/fixture_config.py 266 11 96%
replay/fixture_extract.py 285 12 96%
replay/fixture_generate.py 103 1 99%
replay/fixture_samples.py 157 14 91%
replay/fixture_selection.py 119 0 100%
replay/fixture_survey.py 147 11 93%
replay/legacy_pca.py 29 17 41%
replay/polarity.py 319 25 92%
replay/poller_equiv.py 776 99 87%
replay/prodclone.py 193 2 99%
replay/real_data.py 146 10 93%
replay/schedule.py 205 10 95%
replay/served_math.py 159 4 97%
replay/shard_bench.py 189 65 66%
replay/stagecompare.py 890 161 82%
replay/stages.py 256 40 84%
replay/stepcompare.py 86 1 99%
replay/store.py 120 11 91%
replay/tie_capture.py 107 19 82%
replay/types.py 71 6 92%
run_math_pipeline.py 294 93 68%
tools/engine_rebuild.py 230 74 68%
types.py 22 0 100%
umap_narrative/501_calculate_comment_extremity.py 113 38 66%
umap_narrative/502_calculate_priorities.py 135 135 0%
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 434 275 37%
umap_narrative/801_narrative_report_batch.py 837 620 26%
umap_narrative/803_check_batch_status.py 267 148 45%
umap_narrative/llm_factory_constructor/init.py 2 2 0%
umap_narrative/llm_factory_constructor/model_provider.py 234 234 0%
umap_narrative/polismath_commentgraph/init.py 1 0 100%
umap_narrative/polismath_commentgraph/cli.py 280 280 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 3 0 100%
umap_narrative/polismath_commentgraph/utils/converter.py 283 237 16%
umap_narrative/polismath_commentgraph/utils/group_data.py 331 289 13%
umap_narrative/polismath_commentgraph/utils/storage.py 585 470 20%
umap_narrative/reset_conversation.py 159 50 69%
umap_narrative/run_pipeline.py 385 234 39%
umap_narrative/topic_naming.py 148 148 0%
utils/cli_flags.py 15 0 100%
utils/clj_hash.py 44 0 100%
utils/env_flags.py 13 0 100%
utils/general.py 63 39 38%
utils/output_profile.py 66 2 97%
utils/serialization.py 10 5 50%
utils/vote_convention.py 159 5 97%
utils/vote_convention_boot.py 72 4 94%
Total 24416 8252 66%

This branch has not been deployed

No deployments
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