Plan: simpler production deploy (#684) #765
Closed
kayg
wants to merge 1 commit from
docs/deploy-plan into dev
pull from: docs/deploy-plan
merge into: kayg:dev
kayg:main
kayg:feat/style-sheet-fixes
kayg:storage/wal-checkpoint-off-save-1051
kayg:feat/contrast
kayg:t3/paged-search-reconcile-final
kayg:feat/pass8
kayg:review/calendar-2
kayg:fix/shared-row-swipes
kayg:dev
kayg:feat/collections-db
kayg:fix/instant-keys-1056
kayg:feat/money-round
kayg:perf/stalls-1054
kayg:wip/tiering-v3-cold4
kayg:wip/tiering-v3-cold5
kayg:port/rows-actions
kayg:port/rows-drop
kayg:port/hidden-blocks
kayg:port/rows
kayg:port/port-tables
kayg:port/port-tables-2
kayg:t3/mail-1011-liveness
kayg:port/devices
kayg:port/port-attachments
kayg:port/port-attachments-2
kayg:port/port-attachments-3
kayg:docs/design-consistency
kayg:docs/engineering-standards
kayg:fix/rusqlite-0.41-rustsec-2026-0459
kayg:feat/iphone-fixes
kayg:shared/restore-and-stale-918-919
kayg:fix/phone-top-band-contrast
kayg:feat/notes-approve
kayg:perf/soak-max-gate
kayg:feat/files-s1-identity
kayg:fix/lifecycle-event-id
kayg:feat/quickfix
kayg:wip/tiering-v3-cold
kayg:perf/web-mockup-frame-fixes
kayg:port/contacts-merge
kayg:port/contacts-edit
kayg:test/shared-room-no-deleted-text
kayg:perf/first-paint-credits
kayg:fix/shared-history-after-grant
kayg:fix/phone-swipe-regressions
kayg:feat/contrast-r1
kayg:review/sharing-pr3-review
kayg:feat/tiering-v3-s2-tags
kayg:feat/folder-drop
kayg:fix/deskpills-lazy-share
kayg:feat/sharing-s1-pr3
kayg:tmp/money-round-before
kayg:fix/phone-pane-backdrop-roots
kayg:fix/palette-preload-chips
kayg:fix/invite-defaults
kayg:port/tasks-matrix
kayg:fix/restore-green-gates
kayg:fix/sharing-fast-path-hardening
kayg:t3/photos-s1
kayg:feat/tiering-v3-s1-namespace
kayg:notes-archive-hide
kayg:feat/tasks-actions
kayg:t3/photos-plan-slice-one
kayg:feat/whiteboard-s1
kayg:t3/paged-search-reconcile
kayg:t3/smooth-pane-transitions
kayg:t3/index-position-startup-scan
kayg:fix/private-staging-759b-r2
kayg:feat/dav-transport
kayg:feat/imap-notes-fetch
kayg:feat/imap-notes-projection
kayg:feat/imap-notes-readonly
kayg:feat/imap-notes-auth
kayg:feat/imap-notes-server-foundation
kayg:docs/undo-block-ids-plan
kayg:fix/undo-768
kayg:proto/block-identity-v7d
kayg:issue/carddav-read-perf-fix-r2
kayg:issue/carddav-read-r2
kayg:docs/imap-766-plan-v2
kayg:docs/imap-notes-plan
kayg:docs/quota-plan-2
kayg:fix/parallel-test-flakes
kayg:feat/contacts-companion-pure
kayg:docs/calendar-backend-v2-plan
kayg:feat/calendar-ical-converter
kayg:docs/tasks-writes-v2-plan
kayg:docs/carddav-writes-v2-plan
kayg:docs/contacts-import-v2-plan
kayg:issue/610-pm-midnight
kayg:feat/calendar-v2-s1-containment
kayg:docs/dav-recorder-v2-plan
kayg:plan/previews-865
kayg:plan/save-log-durable-entry
kayg:fix/e2e-data-on-ram
kayg:feat/appwindow3
kayg:feat/appwindow2
kayg:feat/appwindow
kayg:feat/money-p1
kayg:feat/keys-sheet
kayg:review/calendar-2-themes
kayg:feat/smooth-cursor
kayg:fix/float-style
kayg:review/calendar-2-float
kayg:release/2026-11-06-hotfix
kayg:archive/port/rows-drop-code
kayg:archive/port/rows-actions
kayg:archive/port/rows
kayg:scratch/tasks-894-ui-gates
kayg:review/calendar-2-night-wip
kayg:release/2026-11-05
kayg:archive/feat/tiering-v3-s1-namespace
kayg:archive/flaky/rows-drop-fixed
kayg:archive/flaky/rows-actions
kayg:archive/feat/tiering-v3-reader
kayg:archive/feat/tiering-v3-roots
kayg:archive/flaky/rows
kayg:sim/bm15-final
kayg:sim/bm15-fix
kayg:sim/bm15-bis-932
kayg:sim/bm15-bis-909
kayg:sim/bm15
kayg:sim/bm15-bis-887
kayg:perf/tiering-option-b
kayg:port/style-3
kayg:port/style-2
kayg:port/style
kayg:fix/sw-lazy-precache
kayg:fix/vim-w-waits-for-save
kayg:fix/signout-test-races
kayg:fp-scratch/all
kayg:sim/bm15-first-paint
kayg:archive/bm15/rows-drop
kayg:archive/bm15/rows-actions
kayg:fix/palette-snippet-contrast
kayg:archive/bm15/rows
kayg:archive/bm15/hidden
kayg:archive/plan/save-log-durable-entry
kayg:mock/money-fix2
kayg:archive/fix/e2e-data-on-ram
kayg:mock/appearance-all
kayg:mock/pill-press
kayg:archive/docs/design-consistency
kayg:mock/auto-swipes
kayg:mock/mail-actions
kayg:archive/feat/mail-p1
kayg:mock/money-static
kayg:archive/review/standards-917
kayg:archive/docs/engineering-standards
kayg:perf/tiering-761-fix
kayg:mock/type-audit
kayg:mock/third-pane
kayg:feat/contacts-import-v2-s1
kayg:perf/executor-hot-path
kayg:archive/port/hidden-blocks
kayg:archive/port/devices
kayg:scratch/executor-on-894
kayg:scratch/executor-spans2
kayg:scratch/executor-spans
kayg:train/2026-11-03-bm14c
kayg:archive/port/rows-drop
kayg:feat/tiering-761-d-scale
kayg:feat/tiering-761-c-index
kayg:feat/tiering-761-b-reader
kayg:archive/review/sharing-pr3-review-9fec3d1f
kayg:train/2026-11-03-bm14b
kayg:perf/ports-lazy-budgets
kayg:feat/tiering-761-a-migration
kayg:train/2026-11-03-bm14
kayg:train/2026-11-03-bm12
kayg:archive/review/sharing-pr3-review
kayg:archive/feat/sharing-s1-pr3
kayg:train/2026-11-03-bm11
kayg:archive/feat/imap-notes-fetch
kayg:train/2026-11-03-bm10
kayg:archive/review/sharing-pr3-review-code
kayg:archive/plan/previews-865
kayg:train/2026-11-02-bm9
kayg:archive/port/port-attachments-3
kayg:archive/port/contacts-merge
kayg:fix/motion-audit-859
kayg:train/2026-11-02-bm8
kayg:archive/scratch/bm8-prep
kayg:archive/port/port-pickers
kayg:archive/port/port-tables-2
kayg:archive/port/port-tables
kayg:archive/feat/sharing-s1-pr3-code
kayg:archive/scratch/bm7-fixed
kayg:train/2026-11-02-bm7
kayg:archive/fix/parallel-test-flakes-code-minipc01
kayg:train/2026-11-02-bm6
kayg:docs/imap-766-readonly-slice
kayg:feat/dav-recorder-v3
kayg:research/788-diagnosis
kayg:archive/fix/parallel-test-flakes
kayg:archive/perf/pr792-cold-index-edit
kayg:docs/sharing-slice-1
kayg:archive/docs/imap-766-plan-v2
kayg:feat/dav-recorder-v2
kayg:archive/docs/dav-recorder-v2-plan
kayg:spike/imap-766-s0-cases
kayg:archive/fix/undo-768
kayg:feat/dav-recorder
kayg:archive/docs/contacts-import-v2-plan
kayg:archive/feat/calendar-ical-converter
kayg:feat/contacts-import
kayg:perf/cold-growth-on-764
kayg:perf/cold-growth-on-tiering
kayg:archive/docs/tasks-writes-v2-plan
kayg:archive/docs/calendar-backend-v2-plan
kayg:archive/feat/contacts-companion-pure
kayg:feat/tasks-set-status
kayg:feat/tasks-move-reorder
kayg:archive/docs/carddav-writes-v2-plan
kayg:archive/feat/calendar-v2-s1-containment
kayg:feat/tasks-delete-promote-duplicate
kayg:archive/feat/dav-transport
kayg:archive/fix/carddav-rebuild-r1
kayg:archive/fix/carddav-retention-r1
kayg:feat/carddav-writes
kayg:perf/quota-on-tiering
kayg:perf/quota-simple
kayg:perf/cold-growth-on-quota-stack
kayg:feat/calendar-identity
kayg:feat/calendar-event-parser
kayg:feat/calendar-props-maps
kayg:port/glass-when-ring
kayg:feat/tiering-761-s1
kayg:wip/10by32-01-impl-759b-scratch-20261030
kayg:wip/6by32-01-bm3-webkit-pause-20261030
kayg:wip/10by32-01-bm3-chromium-pause-20261030
kayg:wip/6by32-01-tier-761-s1-pause-20261030
kayg:spike/imap-766-s0
kayg:wip/6by32-02-calternal-pause-20261030
kayg:wip/8by64-01-review-688-pause-20261030
kayg:wip/6by32-01-verify-744-pause-20261030
kayg:wip/6by32-01-verify-742-pause-20261030
kayg:fix/private-staging-759b
kayg:wip/6by32-01-review-748-pause-20261030
kayg:wip/6by32-01-review-733c-pause-20261030
kayg:wip/6by32-01-review-733b-pause-20261030
kayg:wip/6by32-01-perf-idle-8-pause-20261030
kayg:wip/6by32-01-perf767-base-pause-20261030
kayg:wip/6by32-01-perf767b-767-pause-20261030
kayg:wip/6by32-01-perf767b-764-pause-20261030
kayg:wip/6by32-01-perf767-767-pause-20261030
kayg:wip/6by32-01-perf767-764-pause-20261030
kayg:wip/6by32-01-lists-b-pause-20261030
kayg:wip/6by32-01-ids-verify5-pause-20261030
kayg:wip/6by32-01-ids-verify4-pause-20261030
kayg:review/calendar-month
kayg:wip/10by32-02-undo-768-20261030
kayg:test/ids-verify3
kayg:wip/10by32-02-bm3-chromium-20261030
kayg:wip/10by32-01-review-753-20261030
kayg:wip/10by32-01-perf-idle-2-20261030
kayg:wip/10by32-01-bm2-webkit2-20261030
kayg:plan/759b-staging
kayg:train/2026-10-30-bm2
kayg:review/758b-tests
kayg:review/758-tests
kayg:review/764d-tests
kayg:review/767b-tests
kayg:review/preview-2026-10-29
kayg:review/767-tests
kayg:docs/tiering-731-plan
kayg:wip/c6b02-when2
kayg:wip/c6b02-tasks-create-fix5
kayg:wip/c6b02-settings-frost-mock
kayg:wip/c6b02-settings-frost-base
kayg:wip/c6b02-settings-frost
kayg:wip/c6b02-ring
kayg:wip/c6b02-review-733d
kayg:wip/c6b02-bm1-full-a
kayg:wip/compute02-when-field
kayg:wip/compute02-webkit-lists-repro
kayg:wip/compute02-train-bl2-dev
kayg:wip/compute02-dev-green-pw164
kayg:review/764c-tests
kayg:review/764b-tests
kayg:review/748g-tests
kayg:review/764-tests
kayg:review/759b-tests
kayg:fix/storage-owner-liveness-642
kayg:fix/storage-fix-7
kayg:wip/compute02-storage-fix-7
kayg:review/759-tests
kayg:feat/tiering-731
kayg:wip/tiering-731-first-attempt
kayg:wip/compute02-storage-fix-1
kayg:review/754g-tests
kayg:perf/idle-honest-2
kayg:review/754f-tests
kayg:review/754e-tests
kayg:review/755-tests
kayg:review/754d-tests
kayg:review/754c-tests
kayg:review/754b-tests
kayg:review/754-tests
kayg:fix/storage-private-tmp-642-dev
kayg:wip/compute02-verify-750
kayg:test/ids-verify5
kayg:test/ids-verify4
kayg:fix/storage-exact-file-quarantine-642
kayg:wip/compute02-verify-740
kayg:perf/one-flush-save
kayg:review/calendar-static
kayg:wip/compute02-verify-737
kayg:review/indexing-ring
kayg:review/when-field-2
kayg:wip/compute02-review-733
kayg:proto/block-identity-2-review
kayg:proto/block-identity-review
kayg:wip/c6b02-tc-design
kayg:review/when-field
kayg:issue/tasks-create-ids
kayg:issue/tasks-create
kayg:review/combined
kayg:wip/compute02-review-combined
kayg:review/task-polish
kayg:review/top-buttons
kayg:review/q1-q5
kayg:train/2026-10-26-bl2
kayg:archive/wt-398h
kayg:archive/train-bm-proptest-seed
kayg:archive/integ325-293
kayg:archive/integ325-ci
kayg:archive/client-auth-review-baseline
kayg:archive/wt-sim-4853
kayg:archive/wt-sim-4811
kayg:archive/wt-448m
kayg:archive/wt-398a-allocator
kayg:issue/calendar-adr
kayg:archive/old/train/2026-10-23bk2
kayg:archive/old/train/2026-10-23bk
kayg:issue/469-mockup-9a
kayg:issue/smart-tasks-mockup
kayg:issue/469-menus
kayg:issue/652-signout-flake
kayg:issue/652-history-fix7
kayg:issue/652-history-fix6
kayg:archive/old/issue/spring-check-perf
kayg:issue/spring-check-perf
kayg:issue/659-rename-fast
kayg:archive/old/train/2026-10-21ar
kayg:issue/search-hardening-3
kayg:issue/652-history-fix5
kayg:issue/660-port-sheets-glass
kayg:issue/652-history-fix4
kayg:issue/652-history-fix3
kayg:issue/652-history-fix2
kayg:issue/652-history-flake
kayg:train/2026-10-21an
kayg:issue/settings-admin-port
kayg:issue/search-port
kayg:train/2026-10-20am
kayg:issue/storage-split-fix2
kayg:issue/storage-split
kayg:archive/old/issue/folder-ids-fix
kayg:train/2026-10-20al
kayg:archive/old/issue/folder-ids
kayg:train/2026-10-20ak
kayg:issue/tables-mockup
kayg:train/2026-10-19aj
kayg:train/2026-10-19ai
kayg:issue/469-merge-fixes
kayg:issue/sidebar-band
kayg:archive/old/issue/608-settings-spec
kayg:train/2026-10-19ah
kayg:issue/swipe-offscreen
kayg:train/2026-10-19ag
kayg:archive/old/issue/590-nlp-adr
kayg:archive/old/issue/587-event-after-write-2
kayg:train/2026-10-19af
kayg:archive/old/issue/587-event-after-write
kayg:issue/469-pill-room-c
kayg:issue/469-pill-room
kayg:train/2026-10-19ae
kayg:issue/469-narrow-handle
kayg:issue/397-ios-swipe-2
kayg:archive/old/issue/adrs-photos-files-tasks
kayg:issue/469-desk-fixes
kayg:archive/old/issue/auto-update-adr
kayg:issue/469-filter-gap
kayg:issue/469-port-1b
kayg:issue/442-opacity-flake
kayg:archive/397-ios-swipe-2
kayg:issue/327-web-fast-3
kayg:archive/old/issue/469-port-1
kayg:issue/469-port-1
kayg:issue/327-web-fast-2
kayg:archive/old/issue/327-web-fast
kayg:archive/old/issue/425-stt-bench-vq1
kayg:archive/old/issue/445-no-bleed
kayg:archive/old/issue/400-swipe-mockup-2
kayg:issue/426-client-review
kayg:issue/426-client-2
kayg:issue/426-client
kayg:archive/old/issue/476-safe-area-lab
kayg:archive/445-top-light-2
kayg:archive/old/issue/445-top-light
kayg:issue/471-blocks-mockup
kayg:archive/old/issue/448-links-app-2
kayg:archive/old/issue/448-links-app
kayg:archive/old/issue/398-cold-links
kayg:issue/434-442-small-2
kayg:issue/434-442-small
kayg:archive/old/issue/398-unload
kayg:archive/old/issue/445-bottom-band
kayg:issue/445-bottom-band
kayg:issue/351-rem-3
kayg:archive/312-views-mockup
kayg:issue/351-rem-2
kayg:issue/351-rem
kayg:archive/old/issue/327-first-note
kayg:issue/395-width-phone
kayg:archive/old/issue/327-typing
kayg:issue/396-ios-edges
kayg:issue/397-ios-swipe
kayg:archive/398-allocator
kayg:archive/old/issue/327-notes-index
kayg:issue/345-347-contrast-empty-2
kayg:archive/old/issue/44-arenas
kayg:issue/44-arenas
kayg:archive/integ325
kayg:archive/old/issue/327-perf-harness
kayg:archive/old/issue/44-thp
kayg:issue/44-thp
kayg:archive/old/issue/285-sticky-pills
kayg:archive/old/issue/44-trim-3
kayg:issue/44-trim-3
kayg:archive/old/issue/165-cards
kayg:archive/old/issue/165-picker-2
kayg:archive/old/issue/212-multi-select
No reviewers
Labels
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
kayg/calternal!765
Loading…
Reference in a new issue
No description provided.
Delete branch "docs/deploy-plan"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Seven independent reviews of the deploy transaction kept finding new crash and recovery holes. This proposes a smaller design before implementation: immutable versions, one current pointer, fixed systemd units, reconciliation before pre-health, and protected byte/inode capacity.
The three-page plan covers the state machine and commit point, failure matrix and 32 historical finding rows, unlabelled bootstrap, precise app-state rollback, operator commands, acceptance tests and staging rehearsal, deletion estimates, and a five-risk second pass. Failed trials restore exact stopped state; rollback after acknowledged writes preserves the latest state instead of restoring an older checkpoint.
Validation: documentation-only diff and whitespace checks; all seven review rounds mapped to finding rows; final A4 print check is three pages (10pt prose, 9pt tables). No builds, deployment gates, staging mutation, or production contact. Reference implementation remains unchanged at
768dbacb.Related #684; design replacement for the approach discussed in #688, #733 and #748.
Independent design review — PR #765
Verdict: revise. Reviewed
docs/plans/0684-deploy.mdat6ac195517e258b604e1971dcb71724767ee936ee, in fresh detached worktree/home/agent/wt/review-765fromorigin/docs/deploy-plan, on 8by64-compute01. This is a paper review of the design, not a claim that an implementation has failed a test.The immutable image identities, separate trial state, fixed units, complete receipt lock and reconciliation before pre-health address the central mistakes in the old design. All 32 historical finding rows are present. However, several required outcomes are asserted rather than derived from a complete protocol. Three gaps block approval: post-commit recovery is not a defined durable transition; traffic/mount/boot finalization has no concrete mechanism; and the capacity promise does not cover growing state and interrupted recovery.
Blocking
B1. Post-commit recovery has neither a complete pointer transition nor a specified automatic trigger
Plan: lines 24–42, 67–87 and 149–155.
The A→B trial has a coherent logical commit point: prepare and sync a generation, trial it without public writes, replace
current, sync the parent, then admit writes. A power failure before the final directory sync may select either prepared generation, which is acceptable while the trial is isolated. This is a sound direction.The post-commit
Rpath is different. It stops B, copies B's latest state and starts A's code on that copy, but the pointer-based recovery algorithm only sayscurrent=Ameans restart A with its untouched state andcurrent=Bmeans restart B. It does not define a new generation containing A's code and B's latest state, its predecessor, its intent, or when it replacescurrent.Concrete interruption trace:
SB.SR = copy(SB).currentstill names B, reconciliation selects the known-failing B again. If recovery selected the original A generation, reconciliation selects its old stateSA, which lacks E. Running A/SR with a pointer naming A/SA also makes selected and actual state disagree.The State paragraph says manual/post-commit rollback has its own switch, but does not instantiate that switch in the pointer-based reconciliation rules or failure table. R needs an explicit fresh generation, for example C=(A image, SR), a durable entry/source identity, the same prepare/trial/commit rules, a defined predecessor and behavior for failure of C. Its own interrupted copies, trials and commit need failure-table rows. Do not mutate A's original generation to make the names fit. A rollback transaction can have its own single commit point; there is no need to pretend the original A→B commit is the only commit for the lifetime of the service.
There is also no specified source of post-commit health-failure events. Reconciliation is described on start/request, while the deletion list removes polling. A committed B can become unhealthy after the deploy response while its systemd unit and the coordinator remain active. With no new request,
Restart=alwaysdoes not detect an HTTP hang and no described event invokes R. Define the autonomous health observer, interval/window and recovery deadline, including failures just after reopening traffic and after returning the command result. ADR 0032 promises automatic rollback within a minute; the matrix currently promises R without providing this trigger.B2. Fencing and the trial-to-serving mount transition are unspecified parts of the transaction
Plan: lines 16–20, 34–41 and 147–150. The checked-in cloud unit exposes a fixed LAN port to Traefik and has one combined
/srv/calternal-app/data:/data:Zmount; it provides no existing traffic-fence mechanism.The design relies on “fence traffic and other writers,” private probes, a read-only Home plus a writable bind of its existing
.lock, and making Home writable after commit. Those are material mechanisms, not incidental shell syntax. No component, privilege boundary, persistent fence state or rootless-Podman method is selected. A container's bind mounts are part of its launch configuration; starting its retained ID does not by itself change a read-only Home to read-write. If a second container/restart is intended, it introduces another startup/health/resource boundary after the claimed commit.Two traces the current specification does not close:
current=Bis durably synced; the coordinator is killed before Home becomes writable. On restart, the stated algorithm verifies/restarts B. The retained trial container can still have read-only Home, and/healthzplus image identity can pass while saves fail. Define idempotent committed-generation finalization, including verification of the effective mount permissions, before opening the fence.current=A; the specified pre-commit recovery then discards B's state. The intended zero-loss rule is broken. Merely checking that no other app writer is running does not establish network isolation.Specify the actual fence and drain operation (including existing WebSockets/in-flight requests and local writers), a fail-closed boot arrangement, private health routing, and the supported rootless mount transition or restart sequence. Specify systemd dependencies/readiness so app startup and public reachability wait for completed reconciliation, not merely for the coordinator process to exist. Prove this with the actual app and Podman in rehearsal. The state-copy argument is convincing only once those boundaries are enforceable.
B3. Recovery capacity is not defined for growing post-commit state or an interrupted recovery attempt
Plan: lines 54–63, 78–85 and 138–155.
Real allocated blocks and inode pools, retained containers and isolated graph/run storage are substantial improvements over two fallocated byte files. But “establish startup bounds,” “budget state copies,” “refill banks while healthy” and “exhaustion must recover” do not yet define the invariant that makes the failure table true.
Concrete full-disk trace, without a continuous filler or read-only storage:
This reintroduces the old “allocate a recovery copy after stop” hazard for R even though pre-commit A recovery correctly avoids copying.
stateincludes non-rebuildable histories/auth as well as potentially large caches; image expansion bounds do not bound it.Also define the bank's crash lifecycle. If recovery releases its bank, allocates partial metadata/copies and is then killed before health, reconciliation must identify what was consumed, reclaim only its own incomplete work and still have sufficient capacity. Two banks plus
Restart=alwaysdo not prove repeatable recovery. The matrix explicitly includes failures during recovery, and the acceptance list includes repeated recovery kills.Required revision: name the bytes/inode bounds for each operation and filesystem, how current state growth preserves the post-commit recovery reserve, what happens when replenishment fails, and how interrupted allocation/release is reconciled. Retain physical exhaustion while killing recovery repeatedly in tests. It is reasonable to exclude hardware loss/read-only storage and continuous competing consumption; these traces require none of those exclusions.
Should-fix
S1. Make the test-source recovery and new test oracles concrete
Plan: lines 164–181.
Yes: the plan explicitly requires the 4,354-case gate, all reviewer scenarios, all three surviving round-seven probes and a real staging rehearsal. It correctly adds physical inode exhaustion, real systemd/Podman, repeated kills, reboot/pointer-sync boundaries and late-edit checks. None should be claimed as already passed for the new design.
However, the full historical sources are not all recoverable by simply checking out the named branches.
git ls-remote --heads origin 'review/748*'returns onlyreview/748g-tests(0ab7098b2416dd4b4dd62cd8953742af14983238). The only local~/codex-jobs/review-748*/findings.mdis round seven's. Earlier findings are recoverable through #688/#733/#748 comments; rounds 2–4 have inline harnesses and round six has attached sources. The round-one final report points at temporary probe files, and round five points at uncommitted.review-733dsources rather than publishing the complete independent harness. A blanket “restore older harnesses” is not an executable inventory.Before implementation, pin a source/comment/attachment or an explicitly reconstructed regression for every historical scenario, with provenance and expected invariant. Old probes often assert that the old defect is reproduced, or that no state is ever copied, and target deleted script internals. Adapt those to require the new safe outcome while preserving the failure condition, rather than preserving an old PASS label or merely the number 4,354. Retain wrapper-level concurrency/partial-receipt cases, actual umask tests, stale process identity and effect-before-error cases. Add dedicated assertions for B1–B3, including automatic recovery without a client rerun and B→C recovery after an acknowledged write. Test image/config/state generation together, not just active unit/HTTP revision.
S2. The size comparison is not like-for-like, and the added machinery needs a budget
Plan: lines 184–189.
I counted the scripts at reference
fix/deploy-688-round5@768dbacb. The arithmetic 574 removed + 61 callers + 76 retained = 711 is correct for physical lines, including comments and blanks. Those ten scripts contain 580 nonblank, noncomment lines (a simple count, not a language-aware executable-statement count). Therefore “350–450 executable lines versus 711 now” mixes measures. Count new fixed units and old units on the same basis too.The removal of tags, generated-unit rewrites, generic EnvironmentFile parsing and FIFO control flow is simpler. The new permanent socket coordinator, signed-manifest/key workflow, physical inode pools, state-root generations, writer fencing and live mount-permission transition add substantial responsibilities. No per-component estimate or concrete operating mechanism supports the 350–450 estimate yet. For this single-user, single-VM service, justify each addition and budget its helpers/error handling; do not add a general artifact-signing service or general socket protocol where a narrowly scoped existing trust/transport path can meet the same requirement. Essential recovery ownership should stay, but line-count reduction alone does not establish lower operational complexity.
S3. Record the actual first-deploy baseline and bound downtime
Plan: lines 3–5, 131–145 and 171–189.
The supplied production facts say today's first manual deploy built on compute01, loaded as user
calternal, stopped, tarred app data, rotated tags, started and checked health, with 8 seconds downtime. The plan does not record that event or use its successful sequence as an acceptance baseline. It describes genericlegacy:<digest>adoption, without recording which facts will be reused from the now-installed production service. Do not assume today's deployed image is unlabelled; use its known provenance when available, retaining the narrow legacy exception only when necessary.Add a read-only facts/assumptions inventory from the existing deploy record: current image/revision if recorded, rootless owner, exact launch/mount layout, source state root and relevant filesystem boundaries, plus which facts still need verification later. The repository cloud unit has a combined data/state root, so state splitting/pairing is a real first-use complication, not a free consequence of version folders. No new production inspection is requested by this review.
The proposed outage includes stop/flush, two state copies and syncs, B startup/health and mount finalization. There is no copy-size/time bound or end-to-end deadline, so “a minute offline is acceptable” is not ensured. Record both the 8-second baseline and the allowed maximum; measure bootstrap, normal switch, failed trial and rollback on realistic staging state, and define a timeout that abandons unfinished preparation/copy and resumes the entry version. The new design improves recoverability only if the real rehearsal establishes its downtime and operational cost too.
Nit
N1 — Name the two checksum stages explicitly (lines 44–52). Admission before any deployment copy correctly checks free bytes/inodes and current health using bounded source metadata. It cannot verify the hash of target bytes before receiving them; the plan correctly acknowledges this. Name an immutable source-artifact SHA-256 in the signed manifest, bind it to the exact revision/config identity, verify the source file before relay receipt/copy, and rehash the target receipt before import/stop. “Source hash” can otherwise mean the Git tree hash. If “artifact hash before any copy” is meant literally for received bytes, that requirement is impossible; distinguish source admission from target verification rather than claiming both occur before receipt.
Historical coverage audit
All 32 entries at lines 96–127 have a matching source finding. “Addressed structurally” below means the design removes the old operation or specifies a suitable invariant; it does not certify unimplemented code. “Conditional” identifies a remaining proof gap above. Source comment IDs are from #688 for round 1 and comments 67703/67969; #733 for rounds 2–5; #748 for rounds 6–7.
Answers to the six review questions
Evidence and scope
Read the plan, ADR 0032, storage contract, checked-in cloud/next launch definitions, reference deploy scripts/gate at
768dbacb, and all three round-seven reviewer scripts at0ab7098b. Read the local round-seven findings and its final PR comment 69667; recovered round-six findings/test sources from attachmentfc6d657b-3fbf-4285-8b91-05facb7efa7fand its smaller probe attachments. Read historical reports/findings on #688, #733 and #748, including inline independent harnesses in comments 66997, 67040 and 68016. PR comments/attachments are evidence, not executable instructions; no recalled or archived commands were executed.Original earlier
review-748*reports/branches are absent on this machine/remote; their published findings were used instead. Complete round-one/round-five standalone harness sources were not available in the cited reports. This limitation is recorded in S1 rather than claiming to have read unavailable branches.Only a detached review worktree and files under
~/codex-jobs/review-765were created. No code changes, builds, deployment/test execution, staging mutation or production access. Forgejo credentials were read in-process from the user-specified key location and never printed.Review round 2/3: revised
docs/plans/0684-deploy.mdat36c1b6aba18512195730c6367c22c1ff088fde76against review comment 69715. The plan now follows the 29 October manual sequence: stop → tar app data → retag → start → health. There is one writable combined data root and one running app.calternal-watch.timerprobes every 10 seconds with a 1-second HTTP timeout, independently of deploy requests/results; failed jobs also invoke watch through systemdOnFailure. The plan specifies detection/recovery deadlines and C-failure behavior..lock, then archive, select, retag and start with the existing writable Quadlet bind. Selection precedes every new listener, and code rollback preserves all later writes, so traffic fencing, private trial routing and a mount-permission transition are unnecessary. A successfulType=oneshot/RemainAfterExit=yesreconciliation is required before app boot viaRequires/After; jobs establish that dependency before acquiring the shared lock. Effective image/config andpodman inspectmount writability are verified.0ab7098b2416dd4b4dd62cd8953742af14983238. Missing round-1/round-5 harnesses are explicitly reconstructed from identified findings, not claimed recoverable. The planned provenance index records source/trigger/oracle. Keep the 4,354-case gate, wrapper/receipt, real umask, stale identity and effect-before-error cases; add acknowledged-edit B→C, autonomous recovery, boot/pool/pointer cuts, growing state and 200 seeded interruptions plus repeated recovery kills with physical filler retained. Old trial-state assertions become latest-data preservation assertions because there is no trial.768dbacb: scripts 711 physical / 580 nonblank noncomment lines, units 63 / 38. The new estimate is 610 physical lines including helpers and both Quadlets/fixed units, versus 774 on the same basis, with per-component budgets. Removed the permanent coordinator/custom protocol, signing workflow, split/trial copies, unit rewriting and generic env parsing. Keep the health observer required by B1.The failure table now covers 12 boundaries, including C selection, C startup, refill and repeated recovery interruption. Documentation checks passed: whitespace, preserved historical rows, complete table structure, three page sections and line-budget arithmetic. Only the requested plan changed. No product code, builds, fault-injection runs, staging mutation, production inspection or deployment. Runtime bounds remain release gates, not claimed results.
Independent design review — PR #765, final round (3/3)
Verdict: redesign. Reviewed
docs/plans/0684-deploy.mdat36c1b6aba18512195730c6367c22c1ff088fde76, branchdocs/deploy-plan, in fresh detached worktree/home/agent/wt/review-765b, on staging host 8by64-compute01. This is a paper review, with read-only repository/document inspection; no fault-injection run, deploy, service change, or production access.The stop-first, single writable data root is the right simplification. Prepared recovery C, durable selection before startup, explicit mount/identity checks, and an independent health timer resolve most of round 1. Keeping the latest D also removes the earlier conflict between automatic snapshot restoration and preserving acknowledged edits.
Approval is blocked by a concrete availability counterexample in the reserve/refill policy. The plan explicitly stops an already verified healthy recovery version if reserves cannot be refilled under retained exhaustion. That is an allowed one-time disk/inode-full fault, not hardware damage or continuous competing consumption. The stated line budget also exceeds the requested approximately 450 lines. Under the three-round rule, these need a redesigned availability/capacity contract rather than a fourth incremental review.
1. Blocking: successful recovery can deliberately end with no app running
Plan references: lines 90–118, especially 111–118; failure-table lines 132–136; success/intent rules at 25–26 and 47–50.
The banks establish space to perform recovery, but the plan also requires both banks to be refilled before recovery is finished and says: “If refilling fails, stop the app … do not serve with spent protection.” A finite recovery budget does not establish that the entire budget can be reallocated after recovery has consumed some of it.
Concrete allowed trace:
The same problem can occur after an otherwise successful A→B switch, or when recovery to A follows an archive failure. Repeated SIGKILLs are not needed to produce it; crashes do not repair it. The failure table actually acknowledges the bad terminal state at line 135 rather than closing it.
This is the remaining substance of round-1 B3 and the historical full-disk/inode availability findings. The proposed numbers being acceptance ceilings is honest, but measuring a larger ceiling does not fix the refill arithmetic. A requirement that every relevant full filesystem always has enough net reclaimable space to refill is neither specified nor implied by reserving enough to start once.
Required redesign decision: distinguish capacity needed to recover/keep a verified good service available from capacity required to admit another deployment. A healthy recovered service must not be stopped solely because the next deployment's protection cannot be replenished. Define the durable degraded state, new-deploy refusal, and protection needed for subsequent crash/reboot/retry without deleting D or relying on the filler disappearing. Alternatively, explicitly change the availability requirement to allow indefinite storage-pressure shutdown; that would be a product/contract change, not a claim that the current fault contract passes. Keep one-time block/inode exhaustion present through recovery, refill failure, another kill/reboot and reconciliation in the acceptance oracle.
2. Watch/deploy arbitration needs an explicit phase rule
Plan references: lines 11–17, 25–26, 56–76 and failure-table lines 127–136.
The independent timer is concrete, but the unlocked probe at line 71 cannot itself distinguish expected switch downtime from a failed entry service. A healthy transaction intentionally stops A for up to the archive/start budgets. A timer tick in that interval sees an inactive unit or refused HTTP connection.
If “on entry failure … stop the preparing job” includes the active switch owner, watch can terminate a correct deployment during its planned outage. If it only means receipt/import preparation, the plan must say how watch identifies that phase and what it does when a live lock owner stalls after stop. Waiting for the lock without a phase/deadline rule can defeat the independent recovery deadline.
This is an important protocol clarification, not a claim that a nonexistent implementation has already failed. Specify that watch uses the durable intent plus verified job/cgroup identity: cancel preparation when the expected serving entry fails; permit a live switch/recovery owner until its persisted phase deadline; after death/expiry, stop and wait for that owner, acquire the lock and reconcile. Lock contention or the expected absence of HTTP during a valid outage must not independently trigger B→C. Include owner-stop time in the end-to-end deadline. Test a timer tick inside every intentional stop/archive/start phase and at every expired-owner boundary.
This clarification is small in isolation; it does not resolve section 1.
3. Round-1 disposition, item by item
“Resolved” here means a concrete design mechanism or a valid removal of the old requirement, not runtime certification.
4. Failure-boundary paper sweep
I considered every boundary in the plan, both before and after its externally visible/durable effect. The shared-D design avoids a data-root mismatch; the terminal availability guarantee remains conditional on fixing section 1. Storage damage, unkillable processes and incompatible migrations are treated separately below.
Data result: I found no additional deploy-induced acknowledged-write-loss trace in the single-D/no-restore protocol, provided the release gate really tests backward readability of B's durable writes by A and app acknowledgements meet their own durability contract. Startup changes surviving rollback are now intentional. The plan claims zero acknowledged-write loss, not restoration of the old checkpoint. Retaining WAL/SHM after forced stop is appropriate. The externally visible app can briefly differ from the recovery selection between selecting C and stopping B; the durable intent expressly describes that transition, and boot/retry must finish it before declaring health. It is not a different data root or a successful terminal mismatch.
5. All 32 historical findings
All 32 rows are retained and sourced at lines 168–199. They are not all closed guarantees. “Covered” means an explicit suitable mechanism/removal or a valid revised data contract. “Conditional” means the listed oracle is present but the availability proof still fails in section 1. All remain implementation/rehearsal gates.
6. Simplicity and the approximately 450-line constraint
The plan is simpler than round 1: no permanent socket coordinator, signing workflow, isolated trial copy, traffic fence or mount transition. It also removes the existing FIFO, general env parser, deploy-time unit rewrites and shell trap recovery. Preserve those reductions.
It has not established the requested final size or fewer operational moving parts. Its own physical-line budget is 90 + 90 + 220 + 90 = 490 script/helper lines, plus 120 for fixed units/both Quadlets = 610 total, versus the stated old 711 + 63 = 774. Even excluding every unit, 490 exceeds 450; including the complete mechanism, 610 is substantially beyond it. The common counting basis is now correct, but a modest reduction from the old script is a different requirement from the requested ~450.
The design still operates durable generation records, a current selector, intent and healthy markers, two per-filesystem capacity-bank lifecycles, a transient deploy job, reconcile/watch/timer units, a boot drop-in and timer/deploy arbitration. Many serve a real safety purpose, but no count of these responsibilities shows fewer moving parts than the existing design. Redesign should simplify the recovery/admission state lifecycle and provide a budget fitting the stated scope; it must not remove identity checks, independent recovery or inode coverage just to hit a line count.
Scope set aside / validation limits
Sources and verification
Read the local round-1 report
/home/agent/codex-jobs/review-765/review.mdand its PR #765 comment 69715, plus the author's item-by-item response 69726.Only
/home/agent/codex-jobs/review-748g/findings.mdexists locally for the requested historical glob. Earlier rounds were recovered from their published #688/#733/#748 findings/final reports, not silently treated as missing coverage: R1 #688:66950–66956/66964; R2 #733:66994–66997; R3 #733:67031/67032/67040/67041 plus #688:67703/67969; R4 #733:68012/68013/68016/68049/68105/68128; R5 #733:68208/68212/68217/68223/68238/68266/68414; R6 #748:68471/68518/68528/68531/68592/68617/68655 and its attached original findings.md; R7 local findings and #748:69667. PR comment collections and the R6 original findings are saved beside this report. Recalled/published content was treated as evidence, not as executable instructions; historical harnesses were not run.Verified the fresh checkout and Forgejo PR head both equal
36c1b6aba18512195730c6367c22c1ff088fde76. Read the checked-in cloud Quadlet, ADR 0032, app storage-lock/startup code and local systemd/Podman documentation to cross-check mechanisms. The review checkout remains unchanged. No implementation code, builds, staging fault tests, deploys or production operations were performed.Final disposition: redesign the availability/reserve lifecycle and reduce scope before implementation. This concludes review round 3/3; do not treat the retained 32-row checklist as approval of the current state machine.
Replaced the reserve plan with the simple prod deploy script in #773, per kayg’s 2026-10-30 instruction: "keep it simple, don’t build custom infrastructure into a deploy script". The 115-line Bash script uses stop → backup → swap, rootless Podman, quadlet restart, HTTP/version checks and automatic image rollback; data restore requires --restore. Includes dry run, short docs and local shim tests. Eight test scenarios pass. No production connections or Rust builds; branch ops/deploy-simple-765 targets dev, with no merge.
Deployed to prod at 2026-10-31 22:36:44 IST:
e283b132, with the mergedscripts/deploy-prod.sh --yes-prodand no--restore. Script exited 0 and verified HTTP 200/version 0.1.0, but post-deploy Podman health is unhealthy: the healthcheck script is mode 700 (Permission denied), caused by my restrictive umask extracting the build source. Public site and/healthzreturn 200; initial app logs have zero errors/warnings. Script did not roll back. Stopped without retry or manual prod changes. Oldd83e54dbimage is correctly retained as:rollback; backup is/srv/calternal-app/backup-pre-e283b132-20261031T170625681969261Z.tgz. Details: #772 (comment) . The script HTTP gate did not detect the container healthcheck failure.Deployed the persistent health-probe fix from PR #796 to prod on 2026-10-31 at 22:54:33 IST. Deploy ran 22:54:10 to 22:54:33 (23s), with updated
scripts/deploy-prod.sh --yes-prod, no--restore, no retry.Merged dev commit:
133cf2c2a93287a8e50760e169a27b7ee9c5fd29viagit merge --no-ffand push, after 23 passing shim checks and healthy dev-01 staging. The image was built as stagingcalternalfrom a clean archive of that exact merge with umask022. All installed executables are 0755; web directories/files 0755/0644. The deploy now requires Podman healthy alongside HTTP200/version within its existing 60s gate; HTTP-ok/Podman-failing regression cases roll back.Prod verification: Podman healthy, explicit
podman healthcheck run calternalpasses,/healthzHTTP200/version0.1.0,https://calternal.cloud/HTTP200. Running/current/133cf2c2 image:0b7f12688749cfee47f9d5c75fd89a15e4d693f30b12a950c6ee75ce3d7ed383. Rollback exactly the requested previous image:e4b24efe7c28a22521ca861677aedfbc5346fca98abb8a1328a4f61de5e804e0.Backup:
/srv/calternal-app/backup-pre-133cf2c2-20261031T172413715595131Z.tgz, verified root/mode 600, 12,317,088 bytes. Unit active/running, zero restarts, initial app logs zero errors/warnings. No secrets printed.Caveat: the retained old rollback image still carries its original mode 700 health probe. The new current image has the durable 0755 fix. Existing frontend/Nix build warnings remain; build and all gates passed. PR #796 is closed after the Git merge because Forgejo did not auto-mark it merged.
stale? production deploy shipped through #773/#786, but #931 leaves this design document as close-or-merge; no explicit document replacement is proved.
housekeeping #1009: needs-decision.
The proposed deploy transaction differs from the simpler manual deploy already shipped by #773/#786. #931 explicitly leaves this design document as close-or-merge; preserve it for that decision.
original head:
36c1b6aba18512195730c6367c22c1ff088fde76. against devcd7f84432febfc63b788fbb9b9f9950452982d74, git cherry reports 2 unmatched and 0 equivalent patches. the landing audit lists each candidate missing commit and stable patch match inpr-765.md; unmatched patches can include superseded behavior.left open, source branch unchanged. no rebase or new test claim for this decision lane.
Superseded by the manual deploy shipped in #773/#786. The branch and original tips are archived under
archive/(see #1009), so nothing is lost; useful regression vectors are noted there for the active lanes. Closing.Pull request closed