Skip to content

fix(ship): preserve checkpoints across device changes - #57

Draft
ronheichman wants to merge 2 commits into
perplexityai:mainfrom
ronheichman:psi/portable-ship-checkpoints
Draft

ronheichman wants to merge 2 commits into
perplexityai:mainfrom
ronheichman:psi/portable-ship-checkpoints

Conversation

@ronheichman

@ronheichman ronheichman commented Sep 15, 2026

Copy link
Copy Markdown

Why

Version 1 checkpoints include a platform identity with device and file-object components.

A durable volume remount can change only the device component. Ship then replays acknowledged records.

Legacy positive-offset checkpoints can lack a content guard. Device-portable rotation matching without a guard skipped new prefixes in the regression.

What

  • Device-portable matching requires a stable file-object component and a matching checkpoint guard.
  • Exact identities still support legacy checkpoints without guards.
  • Rotated candidates now use the same guard-aware matching as active inputs.
  • Drained rotation IDs compare object, size, and bounded content components independently from the device.
  • Rotation search does not treat matching guarded bytes alone as file identity.
  • Ambiguous copied rotations replay from their beginning.
  • The replacement test retains the original inode while creating its replacement.

The checkpoint schema remains version 1. Existing state files need no rewrite.

Destination changes, truncation, replacement, changed guards, and ordinary inode reuse still trigger safe replay.

No HTTP batch splitting behavior changes in this pull request.

An adversarial inode collision with identical guarded bytes can still match. The bounded guard is not proof of whole-file identity.

Delivery remains at least once. Receivers must tolerate duplicates after ambiguous outcomes.

Deployment context is inferred from the public v0.2.0 source pin. No deployed binary was inspected directly.

Original red baseline

This command ran with the original tests against unchanged f0778c09dc48281aa93a3887d05096c0a1f3f9f7.

go test ./cmd/numbat -run 'TestShip(CheckpointSurvivesDeviceChange|RotatedCheckpointSurvivesDeviceChange|PortableCheckpointRejectsReusedInodeContent|DrainedRotationSurvivesDeviceChange|RotationGuardDoesNotReplaceFileIdentity|LegacyCheckpointKeepsExactFileIdentity)$' -count=1 -v

=== RUN   TestShipCheckpointSurvivesDeviceChange
    ship_test.go:171: acknowledged record deliveries=2, want 1
--- FAIL: TestShipCheckpointSurvivesDeviceChange (0.02s)
=== RUN   TestShipRotatedCheckpointSurvivesDeviceChange
--- PASS: TestShipRotatedCheckpointSurvivesDeviceChange (0.01s)
=== RUN   TestShipPortableCheckpointRejectsReusedInodeContent
--- PASS: TestShipPortableCheckpointRejectsReusedInodeContent (0.02s)
=== RUN   TestShipDrainedRotationSurvivesDeviceChange
    ship_test.go:317: drained rotation replayed 1 times
--- FAIL: TestShipDrainedRotationSurvivesDeviceChange (0.01s)
=== RUN   TestShipRotationGuardDoesNotReplaceFileIdentity
    ship_test.go:363: replacement prefix deliveries=0, want 1
--- FAIL: TestShipRotationGuardDoesNotReplaceFileIdentity (0.02s)
=== RUN   TestShipLegacyCheckpointKeepsExactFileIdentity
--- PASS: TestShipLegacyCheckpointKeepsExactFileIdentity (0.01s)
FAIL
FAIL	github.com/perplexityai/numbat/cmd/numbat	0.101s
FAIL

Original green

This original focused run included replacement, truncation, destination, and inode-reuse coverage.

go test ./cmd/numbat -run 'TestShip(CheckpointSurvivesDeviceChange|RotatedCheckpointSurvivesDeviceChange|PortableCheckpointRejectsReusedInodeContent|DrainedRotationSurvivesDeviceChange|RotationGuardDoesNotReplaceFileIdentity|LegacyCheckpointKeepsExactFileIdentity|DetectsRotation|CheckpointFailsSafe|DrainedIdentityDistinguishesReusedFileID)$' -count=1 -v

=== RUN   TestShipDrainedIdentityDistinguishesReusedFileID
--- PASS: TestShipDrainedIdentityDistinguishesReusedFileID (0.00s)
=== RUN   TestShipCheckpointSurvivesDeviceChange
--- PASS: TestShipCheckpointSurvivesDeviceChange (0.02s)
=== RUN   TestShipRotatedCheckpointSurvivesDeviceChange
--- PASS: TestShipRotatedCheckpointSurvivesDeviceChange (0.01s)
=== RUN   TestShipPortableCheckpointRejectsReusedInodeContent
--- PASS: TestShipPortableCheckpointRejectsReusedInodeContent (0.02s)
=== RUN   TestShipDrainedRotationSurvivesDeviceChange
--- PASS: TestShipDrainedRotationSurvivesDeviceChange (0.01s)
=== RUN   TestShipRotationGuardDoesNotReplaceFileIdentity
--- PASS: TestShipRotationGuardDoesNotReplaceFileIdentity (0.03s)
=== RUN   TestShipLegacyCheckpointKeepsExactFileIdentity
--- PASS: TestShipLegacyCheckpointKeepsExactFileIdentity (0.01s)
=== RUN   TestShipDetectsRotation
=== RUN   TestShipDetectsRotation/truncate_shorter
=== RUN   TestShipDetectsRotation/truncate_and_regrow_larger
=== RUN   TestShipDetectsRotation/replace_with_larger_file
=== RUN   TestShipDetectsRotation/copytruncate_with_retained_file
--- PASS: TestShipDetectsRotation (0.07s)
    --- PASS: TestShipDetectsRotation/truncate_shorter (0.02s)
    --- PASS: TestShipDetectsRotation/truncate_and_regrow_larger (0.01s)
    --- PASS: TestShipDetectsRotation/replace_with_larger_file (0.01s)
    --- PASS: TestShipDetectsRotation/copytruncate_with_retained_file (0.02s)
=== RUN   TestShipCheckpointFailsSafe
--- PASS: TestShipCheckpointFailsSafe (0.00s)
PASS
ok  	github.com/perplexityai/numbat/cmd/numbat	0.180s

Follow-up red

The new restart and rotation regression ran three times against e5cd748427b4fe366b3535a2365cad1eb3e6057d.

go test ./cmd/numbat -run '^TestShipLegacyRotatedCheckpointRequiresExactFileIdentity$' -count=3 -v

=== RUN   TestShipLegacyRotatedCheckpointRequiresExactFileIdentity
    ship_test.go:474: record rotated-1 deliveries=0, want 1
--- FAIL: TestShipLegacyRotatedCheckpointRequiresExactFileIdentity (0.09s)
=== RUN   TestShipLegacyRotatedCheckpointRequiresExactFileIdentity
    ship_test.go:474: record rotated-1 deliveries=0, want 1
--- FAIL: TestShipLegacyRotatedCheckpointRequiresExactFileIdentity (0.01s)
=== RUN   TestShipLegacyRotatedCheckpointRequiresExactFileIdentity
    ship_test.go:474: record rotated-1 deliveries=0, want 1
--- FAIL: TestShipLegacyRotatedCheckpointRequiresExactFileIdentity (0.02s)
FAIL
FAIL	github.com/perplexityai/numbat/cmd/numbat	0.123s
FAIL

Follow-up green

The same regression ran three times after the fix.

go test ./cmd/numbat -run '^TestShipLegacyRotatedCheckpointRequiresExactFileIdentity$' -count=3 -v

=== RUN   TestShipLegacyRotatedCheckpointRequiresExactFileIdentity
--- PASS: TestShipLegacyRotatedCheckpointRequiresExactFileIdentity (0.04s)
=== RUN   TestShipLegacyRotatedCheckpointRequiresExactFileIdentity
--- PASS: TestShipLegacyRotatedCheckpointRequiresExactFileIdentity (0.02s)
=== RUN   TestShipLegacyRotatedCheckpointRequiresExactFileIdentity
--- PASS: TestShipLegacyRotatedCheckpointRequiresExactFileIdentity (0.02s)
PASS
ok  	github.com/perplexityai/numbat/cmd/numbat	0.080s

The final focused checkpoint suite passed at f0b3c0eb7c37a2602e7ecf438e5e00c167187bac.

Full verification

All Linux CI commands passed locally with Go 1.26.6 at the final SHA.

go mod tidy
git diff --exit-code -- go.mod go.sum
go generate ./rules
git diff --exit-code -- rules/internal/checked
test -z "$(git ls-files --others --exclude-standard -- rules/internal/checked)"
test -z "$(gofmt -l .)"
go vet ./...
go test -race ./...
go build -buildvcs=false ./cmd/numbat
go test -coverprofile=/tmp/numbat-pr57-coverage.out ./...
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 run
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 fmt --diff
go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
GOOS=windows GOARCH=amd64 go test -c -o /tmp/numbat-pr57-windows.test.exe ./cmd/numbat
GOOS=darwin GOARCH=amd64 go test -c -o /tmp/numbat-pr57-darwin.test ./cmd/numbat

Coverage was 84.6 percent. Every configured fuzz target passed for 30 seconds using these commands.

for t in FuzzClaudeExtract FuzzCodexExtract FuzzGeminiExtract FuzzPiExtract \
         FuzzKimiCodeExtract FuzzGeminiSessionExtract FuzzCursorExtract \
         FuzzWindsurfExtract FuzzOpenCodeExtract FuzzOpenClawExtract \
         FuzzCodexCodeModeExecCommand
do
  go test -run='^$' -fuzz="^${t}$" -fuzztime=30s ./internal/extract/
done

for t in FuzzString FuzzJSONProducesValidJSON
do
  go test -run='^$' -fuzz="^${t}$" -fuzztime=30s ./internal/redact/
done

go test -run='^$' -fuzz='^FuzzAnalyzeShellCommands$' -fuzztime=30s ./internal/rule/
go test -run='^$' -fuzz='^FuzzOTLPLogs$' -fuzztime=30s ./internal/otel/

Hosted checks are absent. The final CI run completed with action_required and zero jobs.

Native Windows and macOS execution remains unverified. Both test binaries cross-compile locally, which does not verify native execution.

Combined patch verification

Temporary integration used upstream main f0778c09dc48281aa93a3887d05096c0a1f3f9f7, HTTP 413 head 70074dd34ead15466ed6974fc475d67b01bae068, and this head f0b3c0eb7c37a2602e7ecf438e5e00c167187bac.

Both patches applied cleanly. The combined focused regressions and go test -race ./... passed.

Built with Codex

Match durable file objects across device changes only when the existing content guard also matches.

Keep exact legacy identities valid and replay ambiguous rotated copies from their beginning.

Built with Codex
Require exact file identity for legacy unguarded rotated checkpoints.

Keep cross-device matching available when a content guard validates the offset.

Built with Codex
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