Skip to content

port(upstream#1958): hash migration no longer reports success it never achieved - #42

Closed
adminopenclaw8-sketch wants to merge 2 commits into
masterfrom
codex/port-upstream-1958-hash-migration-false-success
Closed

adminopenclaw8-sketch wants to merge 2 commits into
masterfrom
codex/port-upstream-1958-hash-migration-false-success

Conversation

@adminopenclaw8-sketch

@adminopenclaw8-sketch adminopenclaw8-sketch commented Sep 13, 2026 •

Copy link
Copy Markdown
Collaborator

Split out of #25 (commit 758aabeb there). This branch holds exactly one upstream change so it can be reviewed, tested and reverted on its own.

Upstream

Problem

migrateContentHashesAsync set hashMigrationComplete in an unconditional defer. On the server's read-only DB handle (Kpa-clawbot#1283) every batch fails at begin/prepare/commit and continues, so the loop always reached the defer and /api/stats answered hashMigrationComplete: true although nothing was migrated.

Change

Counts failed batches (including a recovered panic). If any failed, the flag stays false and one [hash-migrate] INCOMPLETE … line is logged.

Adaptation to this fork

None. The cherry-pick applied without conflicts and the changed lines are identical to upstream.

Notes for review

Visible change on this fork: our server opens SQLite mode=ro, so after merge /api/stats will report hashMigrationComplete: false and the INCOMPLETE line appears at startup. That is the truthful value. In this tree the field is only exposed through /api/stats (routes.go); nothing under public/ reads it.

Dependencies and merge order

Verification

Local run of the same commands as CI's “Go Build & Test” job (server tests with -race), on this branch and on master fda24ca5 under the same conditions (same machine, run one after another):

Check master fda24ca5 this branch verdict
go-server-build-vet PASS PASS
go-server-test-race PASS PASS
channel-lib-test PASS PASS
decrypt-cli-build-test PASS PASS
dockerfile-copy-invariants FAIL FAIL not runnable locally: script needs bash ≥4 (declare -A), macOS has 3.2; identical on master
staging-disk-monitor PASS PASS
css-vars-lint PASS PASS

Baseline failures (fail identically on master; not introduced or changed here): see rows marked baseline failure, unchanged.

Browser validation (local, fixture DB, no staging/production): Not applicable (no frontend change).

Not run:

  • Playwright E2E suites (no local Playwright install); CI's E2E job will also be skipped, see below.
  • eslint (not installed locally; CI installs it on the fly).
  • Frontend JS suites (no frontend change).
  • Browser tests against staging/production (deliberately none).

Expected GitHub CI: “Go Build & Test” is expected to fail on TestPruneOldNeighborMetrics, which already fails on master (see #25's run). Downstream jobs (Playwright, image build) are therefore skipped. “Deploy Staging” and all GHCR publish steps only run on push to master and cannot run for this PR.
Two further ingestor tests have failed intermittently in this split's CI on branches whose cmd/ingestor tree is byte-identical to master (#27, #28), so they can also appear here without being caused by this change:

GitHub CI result: run 34751729111 on c272f6e3. Go Build & Test: failure; all downstream jobs incl. Deploy Staging skipped. Failed tests:

  • TestPruneOldNeighborMetrics: fails on master, documented baseline

🤖 Generated with Claude Code

efiten and others added 2 commits September 13, 2026 10:42
…ever achieved (Kpa-clawbot#1958)

Part 2 of Kpa-clawbot#1856. **Part 1 is deliberately not fixed here** and the issue
stays open for it; reasoning at the end.

## The bug

`migrateContentHashesAsync` set `store.hashMigrationComplete` in a
deferred func that ran unconditionally. Every DB failure inside the loop
takes a `continue` (begin tx, prepare, commit), so the loop always
reaches that defer, **including when not a single batch was written**.

That is not hypothetical. The server has held a `mode=ro` handle since
Kpa-clawbot#1283, so `Begin`, `Prepare` and `Commit` all fail, every batch is
skipped, and `/api/stats` then answers `hashMigrationComplete: true`
after migrating nothing. The migration is started unconditionally on
every boot at `main.go:546`.

## The fix

The three failure paths now count, and the defer only claims completion
when the count is zero. When it is not, it logs once, naming the
read-only handle as the expected cause and pointing at this issue, so an
operator can tell "no work to do" apart from "could not do the work".

**Nothing waits on the flag.** The only reader is `routes.go:828`, which
reports it in `/api/stats`. Leaving it false on failure blocks nothing;
it just stops the endpoint from lying.

The in-memory index is untouched on failure. That was already true,
because the index update runs only after a successful commit, and the
test now asserts it so memory and disk cannot drift apart.

## Verification

The regression test **fails on unmodified master**:

```
hash_migrate_test.go:115: hashMigrationComplete must stay false when no batch
could be written; reporting true here is what Kpa-clawbot#1856 called self-reported success
```

It closes the DB handle to make writes fail. That is deterministic and
exercises the identical path as a read-only handle (`Begin` errors,
batch skipped); the in-memory test DB cannot be reopened read-only.

The existing happy-path test still passes, so the flag still turns true
on a real migration. `gofmt` clean, `go vet` clean, `cmd/server` suite
ok in 59.7s.

## Why part 1 is not in here

`handlePostPacket` writes to the same read-only handle and therefore
always answers 500. I checked the error path before assuming it was
misleading: it already returns `"transmission insert: attempt to write a
readonly database"`, so the message is accurate. The endpoint is not
confusing, it is simply dead.

The issue asks maintainers directly: *"is this endpoint still wanted? If
ingestion is MQTT-only now, deleting it is simpler than routing it
through a handoff."* That is a product decision, not a fix, and
inventing a middle answer would only add code without settling it. Worth
noting the repository already has a precedent for the handoff shape: the
server writes `request-<id>.json` and the ingestor consumes it
(`cmd/ingestor/prune_geofilter.go`).

Two things a decision should account for: the endpoint is documented in
`openapi.go:69` and guarded by `requireAPIKey`, and
`routes_test.go:4850` asserts it writes an observation row using the v3
schema, which passes only because the test DB is read-write.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 56d6d4c)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings the branch up to master 834c8da so CI runs on current master.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dborup

dborup commented Sep 20, 2026

Copy link
Copy Markdown
Owner

Parked: this does not fix the case it targets

Independent review found, and I then reproduced independently, that the premise in the code comment is not what SQLite does. The comment says that on the read-only handle the server has held since Kpa-clawbot#1283 "begin, prepare and commit all fail, every batch is skipped". Probing a real mode=ro handle with the driver this repo uses:

Begin()   err = <nil>                                    => SUCCEEDS
Prepare() err = <nil>                                    => SUCCEEDS
Exec()    err = attempt to write a readonly database (8) => FAILS
Commit()  err = <nil>                                    => SUCCEEDS
on-disk value afterwards: unchanged

That is expected: the DSN sets no _txlock, so BEGIN is deferred and takes no write lock, sqlite3_prepare compiles without writing, and a transaction that wrote nothing commits cleanly. Only stmt.Exec fails — and that is the one error path this PR does not count.

Consequence: all three new failedBatches++ sites are unreachable in the deployment the PR names, so /api/stats still answers hashMigrationComplete: true after migrating nothing. Run end to end against a read-only handle, the patched function still logs "Migrated 1 content hashes" and reports complete, with the old hash still on disk.

The test does not catch this because it closes the handle rather than opening it read-only. Close() makes Begin() return sql: database is closed — a database/sql-level error raised before the driver is consulted — so it exercises a different path from SQLITE_READONLY. The test comment asserts the two are "the identical failure path"; they diverge at the first call. Mutation testing agrees: removing the prepare or commit counter leaves the test green, and only the begin counter is covered.

Two further things surfaced, both pre-existing but in the same function and relevant to any real fix:

For what it is worth: hashMigrationComplete has no consumer beyond the /api/stats field — the contract comment claiming it "gates content-hash-dependent code paths (dedup correctness on the write side)" describes a gate that does not exist, and cannot, since the server has no write side. So flipping the flag carries no behaviour risk; the value is purely in reporting honestly.

Why parked rather than patched

The minimal correction (count stmt.Exec) makes every production start log an error-shaped INCOMPLETE line forever, for a condition that is expected and cannot be fixed in this process. That is a design decision, not a mechanical fix, and the alternatives differ in kind:

  1. Detect the read-only handle up front (it is known at open time) and skip the migration with an INFO line.
  2. Count stmt.Exec and accept a permanent per-boot warning.
  3. Move the migration to the ingestor, which this PR's own log message names as where it belongs — that would resolve the misclassification and the invariant-test gap at the same time.

The branch is synced with current master and otherwise ready; it needs a decision on which of those to take.

@dborup

dborup commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Closing as superseded: the content-hash migration now runs as a one-time ingestor migration (#222), so this port of upstream#1958 no longer applies. The branch is also ~690 commits behind and conflicts.

@dborup dborup closed this Oct 7, 2026
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.

3 participants