Skip to content

fix: make order-compared TEXT timestamp columns sort by time, not by bytes - #153

Merged
George-RD merged 1 commit into
mainfrom
fix/canonical-sql-timestamp-ordering
Aug 7, 2026
Merged

fix: make order-compared TEXT timestamp columns sort by time, not by bytes#153
George-RD merged 1 commit into
mainfrom
fix/canonical-sql-timestamp-ordering

Conversation

@George-RD

@George-RD George-RD commented Aug 7, 2026

Copy link
Copy Markdown
Owner

The failure

The post-merge ./scripts/check.sh on main after #152 failed:

failure_surfacing::tests::notification_failure_is_truthful_and_retryable
  panicked at crates/openspine-kernel/src/failure_surfacing/tests.rs:71:10: due

claim_due_dead_letter(Timestamp::now()) returned None for a dead letter enqueued microseconds earlier.

Root cause

Several tables store an instant as an RFC 3339 TEXT column and then compare it with an inequality in SQL — next_attempt_at <= ?1, expires_at > ?5, created_at < ?1, occurred_at > ?2. SQLite compares TEXT bytewise, so those queries are only correct if the rendering is order-preserving.

jiff's Timestamp Display is not. It prints the smallest faithful fraction: trailing zeros trimmed, and no fractional part at all on a whole second (jiff-0.2.24/src/fmt/temporal/printer.rs:247, src/fmt/buffer.rs:527-538). Since 'Z' (0x5A) sorts after '.' (0x2E), time order inverts inside every second:

  "2026-08-07T04:22:57Z"  >  "2026-08-07T04:22:57.000123456Z"

The left instant is earlier, yet compares greater. The suite failure is the ~1-in-10⁶ case where the clock reads an exact whole second.

Two real defects, not just a flaky test

Stalled retry A dead letter whose next_attempt_at landed on an exact second was not claimable until the next second.
Fail-open ⚠️ A skill-context selection token expiring on an exact second read as still live for up to a second under expires_at > ?5.

Each now has a deterministic regression: a_whole_second_retry_deadline_is_claimable_one_nanosecond_later and a_token_expiring_on_an_exact_second_is_refused_one_nanosecond_later.

Fix

store::sql_time::sql_timestamp renders RFC 3339 with a fixed nine-digit fraction, so byte order and instant order coincide. Every write to, and comparison against, the order-compared columns goes through it:

notify_dead_letters.{enqueued_at, next_attempt_at, claimed_until} · task_grants.expires_at · skill_context_selections.expires_at · connector_restart_ledger.occurred_at · worker_dispatch.created_at

Versioned migration v4 normalizes existing rows: idempotent, skips NULLs, and only touches the legacy 20–29-character shape, so it can neither corrupt a value nor invent an instant.

Store::consume_skill_context_selection_and_append_audit now takes now: Timestamp instead of reading the clock internally, matching the existing claim_due_dead_letter(now). That is what makes the expiry boundary testable at all; its one caller already had gate()'s own now in scope, so the gate decision and the expiry check use the same instant.

Columns deliberately not converted — worker_dispatch.{updated_at, failed_at, recovery_claimed_at}, conversation_in_flight.claimed_at, digest_items.ts (ordered by integer seq) — are never inequality-compared. spend.rs's utc_day splits on T and is unaffected. Adding such a comparison later requires moving the column to the canonical renderer first.

Fixed-width rendering was chosen over migrating to INTEGER epoch nanoseconds because it preserves the stored shape, round-trips through the existing str::parse::<Timestamp>() readers unchanged, rewrites no column, and keeps the downgrade path trivial.

Recorded as D-154.

Verification

  • Mutation-verified: replacing sql_timestamp's body with timestamp.to_string() makes both behavioural regressions fail on their intended assertions, while the …is_live_one_nanosecond_earlier control still passes — so the tests bite on the real defect and are not trivially failing.
  • ./scripts/check.sh → exit 0 (fmt, clippy -D warnings, nextest 963 tests, file sizes, claims gate, omp-ceremony, openspec validate --all --strict 45/0).
  • New tests: 4 encoding-property tests in sql_time.rs, 3 in store/gate_support_tests.rs, 1 in failure_surfacing/tests.rs, 1 migration test covering all three legacy renderings plus idempotency.
  • Two independent reviewer subagents. The completeness lens returned REQUEST_CHANGES with two BLOCKERs — worker_dispatch.rs and worker_result_relay.rs each had a third notify_dead_letters writer still on Display, which would have re-introduced the exact stall against migrated rows. Both fixed, plus its SHOULD-FIX (document the four-digit-year precondition on the migration macro) and its nit. An earlier advisory also caught three missed occurred_at cutoffs in worker_supervision.rs. The scope/test-quality lens returned APPROVE.

CI is expected to fail instantly with the GitHub Actions billing/spending-limit error; that is not a code failure.


Summary by cubic

Make TEXT timestamp comparisons order-correct by rendering RFC 3339 with a fixed 9-digit fraction. This prevents stalled retries and a fail-open expiry at whole-second boundaries.

  • Bug Fixes

    • Added store::sql_time::sql_timestamp and used it for all inequality-compared columns (dead letters, task grants, skill selections, worker ledger, worker dispatch).
    • Fixed dead-letter claiming that could stall when next_attempt_at hit an exact second.
    • Fixed skill-context selection tokens to expire correctly at the exact second; consume_skill_context_selection_and_append_audit now takes now so checks use a consistent time.
  • Migration

    • Added v4 to normalize existing TEXT timestamps to the fixed-width form; idempotent, skips NULLs, and only rewrites legacy 20–29 char values.

Written for commit 377f471. Summary will update on new commits.

Review in cubic

…bytes

A post-merge scripts/check.sh on main failed at
failure_surfacing::tests::notification_failure_is_truthful_and_retryable's
.expect("due"). Root cause: several tables store an instant as an RFC 3339
TEXT column and compare it with an inequality in SQL, but jiff's Timestamp
Display is not an order-preserving encoding. It trims trailing fractional
zeros and omits the fraction entirely on a whole second, and 'Z' (0x5A) sorts
after '.' (0x2E), so time order inverts inside every second:

    "2026-08-07T04:22:57Z"  >  "2026-08-07T04:22:57.000123456Z"

Two live defects followed, each now covered by a deterministic regression:

- a dead letter whose next_attempt_at landed on an exact second was not
  claimable until the next second
  (a_whole_second_retry_deadline_is_claimable_one_nanosecond_later);
- fail-open: a skill-context selection token expiring on an exact second read
  as still live for up to a second under expires_at > ?
  (a_token_expiring_on_an_exact_second_is_refused_one_nanosecond_later).

store::sql_time::sql_timestamp renders a fixed nine-digit fraction, so byte
order and instant order coincide. Every write to, and comparison against, the
seven order-compared columns now goes through it. Versioned migration v4
normalizes existing rows; it is idempotent and skips NULLs and already-
canonical values.

consume_skill_context_selection_and_append_audit now takes now: Timestamp
rather than reading the clock internally, matching claim_due_dead_letter(now);
that is what makes the expiry boundary testable. Its one caller already had
the gate()'s own now in scope.

Recorded as D-154.

Mutation-verified: replacing sql_timestamp's body with timestamp.to_string()
fails both behavioural regressions on their intended assertions while the
...is_live_one_nanosecond_earlier control still passes.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@George-RD
George-RD merged commit 6f1e795 into main Aug 7, 2026
1 check passed
@George-RD
George-RD deleted the fix/canonical-sql-timestamp-ordering branch August 7, 2026 04:17
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d08bdd99-0b0c-4c43-a505-42acbb4dfab8

📥 Commits

Reviewing files that changed from the base of the PR and between 97541a6 and 377f471.

📒 Files selected for processing (47)
  • .raw/openspine-decision-log.md
  • crates/openspine-kernel/src/api/actions.rs
  • crates/openspine-kernel/src/failure_surfacing/tests.rs
  • crates/openspine-kernel/src/store/briefcase_support.rs
  • crates/openspine-kernel/src/store/budget_support.rs
  • crates/openspine-kernel/src/store/failure_surfacing.rs
  • crates/openspine-kernel/src/store/gate_support.rs
  • crates/openspine-kernel/src/store/gate_support_tests.rs
  • crates/openspine-kernel/src/store/migration_tests.rs
  • crates/openspine-kernel/src/store/migrations.rs
  • crates/openspine-kernel/src/store/mod.rs
  • crates/openspine-kernel/src/store/skill_read_queries.rs
  • crates/openspine-kernel/src/store/sql_time.rs
  • crates/openspine-kernel/src/store/task_dispatch.rs
  • crates/openspine-kernel/src/store/tests.rs
  • crates/openspine-kernel/src/store/worker_dispatch.rs
  • crates/openspine-kernel/src/store/worker_result_relay.rs
  • crates/openspine-kernel/src/store/worker_supervision.rs
  • graphify-out/.graphify_labels.json
  • graphify-out/2026-08-07/.graphify_labels.json
  • graphify-out/2026-08-07/GRAPH_REPORT.md
  • graphify-out/2026-08-07/graph.json
  • graphify-out/2026-08-07/manifest.json
  • graphify-out/GRAPH_REPORT.md
  • graphify-out/cache/ast/v0.9.10/0049f591c16c805d7a601c4645bf49f9ce9f1bc0377fab63424817ca8ad951a5.json
  • graphify-out/cache/ast/v0.9.10/119d60d25b5d6ebc8575ff7c7f15606c61a7b855d05b34d8b5da58712d5573e4.json
  • graphify-out/cache/ast/v0.9.10/231a4872d771ce6230e194eeda3a118dcac9a11038cf4d6c46dbafebc8bc090e.json
  • graphify-out/cache/ast/v0.9.10/2436e31d1ac4c55bd6fecaf25d997edf55e53edb42ae099dcdcb5a81c6e91d9b.json
  • graphify-out/cache/ast/v0.9.10/3be59f7f2c69e1bd5c181f02ef4aa3e1de8585677e497a3bc46164c51b464d5c.json
  • graphify-out/cache/ast/v0.9.10/3c1907b4758d9435f89eac44df5be4f4ec2cc4fda779c21cf95dd542754ba486.json
  • graphify-out/cache/ast/v0.9.10/590aa9fcc0f3dc8ddfce64ee0787bf5a7c07918ad85d0284a50c1b1f039ccb3a.json
  • graphify-out/cache/ast/v0.9.10/6ae14e112a4b1864904474225dbd9269d999886af1d0029f34d266852a97265e.json
  • graphify-out/cache/ast/v0.9.10/7eaabce624e3880bcdb7d73cb173efddab6e3cc2b9d386a273e110c9846507e1.json
  • graphify-out/cache/ast/v0.9.10/7ee8159829c022b85737c14d23750f8cbd6017312ae1afd309ad041f32ec647f.json
  • graphify-out/cache/ast/v0.9.10/886a1188b18ad036ede9d5107ea90658f301721cb891c073418f840ab26463ee.json
  • graphify-out/cache/ast/v0.9.10/a43baf0ea1b7ad8e43f7f58340f73ab87df8cb953092fe68a94954d70cebe65f.json
  • graphify-out/cache/ast/v0.9.10/a5549047cf52cdb8f1a1b4456d0a5edb6b5a37a94cfed8c0953209288e0e3284.json
  • graphify-out/cache/ast/v0.9.10/aa701388ae658bac45bacb97e5229c69d239b30b5d1425d855967b653c36db43.json
  • graphify-out/cache/ast/v0.9.10/ad907595c6f65d27922e440042c30dc39c6ab90913109764231dae21f53f8112.json
  • graphify-out/cache/ast/v0.9.10/b67acbed5afd7260bc18aa638f538e900512ca495d5d9b5d372a421f11aff360.json
  • graphify-out/cache/ast/v0.9.10/bbac00338d241f598452ee0dc360f4e5c51bd3daff1a28b45b8d750e1de5418b.json
  • graphify-out/cache/ast/v0.9.10/c9ba48a3d30d213ff35e7c2525c6dab3361dc81af8ff8c74c1457ab97271092a.json
  • graphify-out/cache/ast/v0.9.10/d2d1a44600bc6299e8f87e796d4cc71eccc4861517806aab47c61cdcff867272.json
  • graphify-out/cache/ast/v0.9.10/fe6d023623f4ebd261bfaedcf638fa5135fbab22d14c0e085765658a82d1a351.json
  • graphify-out/cache/stat-index.json
  • graphify-out/graph.json
  • graphify-out/manifest.json

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Improved timestamp ordering and comparisons across task grants, retries, worker processing, audits, and related records.
    • Fixed edge cases around exact-second expiration and retry deadlines.
    • Prevented expired or previously consumed skill tokens from being accepted.
  • Data Migration

    • Added a safe, repeatable migration that normalizes existing timestamps while preserving chronological ordering.
  • Tests

    • Added regression coverage for timestamp boundaries, token expiration, token reuse, and migration repeatability.

Walkthrough

The change adds canonical fixed-width RFC 3339 timestamp serialization for SQLite TEXT ordering, migrates legacy timestamp values to schema version 4, updates store call sites, and adds expiry, retry, and migration regression tests.

Changes

Timestamp normalization

Layer / File(s) Summary
Canonical formatter and store integration
crates/openspine-kernel/src/store/sql_time.rs, crates/openspine-kernel/src/store/*.rs
Adds sql_timestamp and uses it for persisted timestamps and comparison parameters across grants, workers, retries, failures, selections, and supervision.
Version 4 migration and validation
crates/openspine-kernel/src/store/migrations.rs, crates/openspine-kernel/src/store/migration_tests.rs
Adds idempotent normalization for affected timestamp columns and updates migration tests to version 4.
Expiry and retry boundary tests
crates/openspine-kernel/src/store/gate_support.rs, crates/openspine-kernel/src/store/gate_support_tests.rs, crates/openspine-kernel/src/api/actions.rs, crates/openspine-kernel/src/failure_surfacing/tests.rs
Injects the evaluation timestamp for skill-token expiry checks and tests exact-second expiry, replay prevention, and retry eligibility.
Decision and generated graph artifacts
.raw/openspine-decision-log.md, graphify-out/*
Records D-154 and regenerates graph labels, reports, manifests, and AST cache entries.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Poem

A rabbit hops through timestamps bright,
Nine neat fractions line up right.
Old rows learn a steady tune,
Tokens fade beneath the moon.
Retries wait, then safely run—
Canonical time for everyone.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/canonical-sql-timestamp-ordering

Comment @coderabbitai help to get the list of available commands.

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