TUI: extract ProjectMailStore lifecycle ownership#649
Open
TZZheng wants to merge 1 commit into
Open
Conversation
This was referenced Jul 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
MailModelinto one root-ownedProjectMailStoreMailModelgeneration before root state or UI state can be publishedPart of #645. This PR does not close the issue. It is a reviewable lifecycle/ownership change; it does not add the broader Agent rail or thread experience.
Exact reviewed candidate:
76ab358434ada1f15ee43dfb2acd4816a3026c5ad0167c35d696654899678e5a5498df04ebab6492git diff --binary HEAD^ HEAD):ea3214317980cef2d801a9e6f63c4ba6e32a2bbff655f3d6c42a6c5adc30392drefactor(tui): extract project mail store lifecycleThe source worktree was clean when these coordinates and the review-report hashes were recomputed. Drafting this body performed no push, PR operation, issue operation, or merge action; merge is not authorized by this artifact.
Behavior and scope
One project-lifetime owner
Appnow owns the activeProjectMailStorebesideMailModel, plus an optional suspended home store during a cross-project visit. For one project/human-directory identity, replacing targetMailModels reuses the same store. The store owns:fs.MailCacheProjectMailSnapshotsequence and versionMailModelremains the target/session/presentation consumer. It retains the accepted snapshot reference, boundedSessionCachereconstruction and persistence state, history paging/count state, compose input, and rendering state. It no longer owns a project mailbox cache, scanner, refresh slot, or project-mail polling loop. Its 250 mspulseTickis presentation animation, not mailbox polling. The separate app-level auto-refresh ticker still serves the kanban and is explicitly not a mailbox scan path.Identity and generation gates
Every installed
MailModelreceives a monotonically increasing target generation. A physical refresh captures the store ID, canonical project identity, activation, source version, and launching Mail generation. Acceptance proceeds in this order:MailModel.Update, and a location update be scheduled.An unrelated stale result cannot release the active slot. An exact completion from a replaced Mail generation releases its old slot but publishes no root cache, snapshot, UI state, or location side effect.
MailModelalso retains its own generation checks for accepted session rebuilds, persistence continuations, older-page loads, counts, and UI-local async results.Initial versus steady arbitration
A steady refresh updates the detached mailbox cache and target/status state. An authoritative initial additionally rebuilds the bounded session cache and is the only refresh that clears
initialLoading.The one store-local slot distinguishes three cases:
initialRefreshPendingand starts after that exact slot completes;refreshGenerationproves A's rebuild cannot satisfy B.Same-generation duplicate initial requests still coalesce. When old work completes,
App.Updatestarts the pending initial from the live current Mail model, so a second replacement cannot resurrect an older generation. This is correctness arbitration, not a general work queue.Physical scan serialization and updater ownership
Bubble Tea commands already launched cannot be cancelled. A data-less process-wide mutex therefore wraps the complete physical
MailCache.Refreshplus initial session-rebuild body. A visited-project scan may wait for an already-running suspended-home scan, but the scanner bodies cannot overlap. The mutex owns no project data, snapshot, tick, or updater state.ProjectMailStoreis also the sole production owner that injectsfs.UpdateHumanLocation. The old startup launcher intui/main.goand the recipe-substitution launcher are removed. Location work is created only after a current-generation accepted refresh, then re-checks a shared atomic active/activation/version token immediately before the filesystem side effect. Delayed work becomes a no-op after suspension or after a newer accepted store version.View, tick, and visit lifecycle
tea.Everycommand has already fired; a late tick cannot re-arm itself.App; it does not pause a live Mail chain.Test-first evidence
The committed 16-test lifecycle contract grew through real behavioral RED stages. Each recorded test package compiled and reached the intended assertion; the failures below were not syntax, import, toolchain, or dependency failures.
1. Original ownership and lifecycle RED on the exact base
red.md(SHA-256d1f43f831c0fcd441cef196e67181fe821be8ae205571740794660e8117ef3e2) records:It exited
1on five intended gaps:MailModelstill owned the project cache; late ticks re-armed after short Mail/view cycles and repeated markdown-close delivery; a same-generation wrong-project refresh was accepted; andApphad no explicit active/suspended store lifecycle.2. Parent edge and visit contracts before their corrections
parent-edge-red.mda71dab5f15d0b6f8fafd5e695c96ac49c378ffd5dfeb4ea6f8ae47def82a4770parent-visit-red.mdb2b74b01d8e13bea40f5052c38fdbf646c041c812a6ebebe6da31a86f387393aparent-final-red.mdf2418323336b59ad4b1f5d9191b01633486c7520135912c696f4689eacfdeb9cChronology note:
parent-visit-red.mdtruthfully records the exact base with the earlier refresh-coalescing correction still uncommitted; it is not represented as a pristine-base run.3. First independent-review BLOCK and test-first correction
At old candidate
10c63ed74ba74441e17572e0c0bf3c331fa6dc19, four tests were added before production/anatomy repair.review-block-red.md(SHA-256fad18cbe86be73fc97fe685cb4bba0409f216d1f17a3a8e8a3c31dd656b6444f) records:The run exited
1at the intended assertions: old-generation work installed root version/snapshot state, home and visited scanners reachedmax active=2, delayed location work ran after activation/version supersession, andtui/main.goremained a second location updater. The correction introduced generation-before-publication, the process-wide physical gate, runtime side-effect revalidation, sole updater ownership, and matching anatomy repairs.4. Second independent-review BLOCK and final correction
At old candidate
4ac8d26d558ecdff9c89464df4bc618e75d437d9, the first correction still tracked only whether in-flight work was initial, not which generation launched it. The test-only RED addedTestSupersededInitialRefreshQueuesReplacementInitialbefore production/documentation changes.review-second-block-red.md(SHA-2563c3ea8b60bfdcacd32e6c77424960e7b400b960f11f5a0bbf2d9b4f22b3f239a) records:It exited
1at the exact intended assertion:The minimum fix added
refreshGeneration, used it in initial arbitration, and cleared it with the exact physical slot. That prevents a replacement generation from remaining ininitialLoadingindefinitely.Historical PASS reports on either old HEAD are superseded and do not count toward the final gate.
Validation
Parent validation is bound to exact final HEAD
76ab358434ada1f15ee43dfb2acd4816a3026c5a, parentd0167c35d696654899678e5a5498df04ebab6492, and binary patch SHA-256ea3214317980cef2d801a9e6f63c4ba6e32a2bbff655f3d6c42a6c5adc30392d. The final parent report isreview-second-block-fix-validation.md, SHA-2564df98427a3daf31e7fc9135c0a7556fd39ea7824fbb29cf161ce9470ba4c1093.ProjectMailStore/ visit contracts1.417s-race3.150sgo test ./internal/tui -count=18.273sgo test ./... -count=1internal/headless85.910s,internal/tui11.884sgo vet ./...gofmt -lover every changed Go filegit diff --check HEAD^ HEADThe final incremental old-initial/new-initial correction did not rerun the complete repository-wide race suite. It did rerun all 16 adjacent lifecycle contracts under
-race, and they are green. The earlier causal record remainsrace-attribution.md, SHA-2569397a59ed9bed00615ff3992df0d13af545e34d94e76793862a1c66e18157064: historical full-race candidate runs emitted no race-detector report but had portal PID-startup timeouts (reproduced on exact clean base) and one load-sensitive clear timeout (not reproduced by focused candidate/base comparison). Those runs are documented, not relabeled as PASS.This distinguishes three facts: the parent exact-head non-race repository suite passed; the changed lifecycle contract union passed under race; the historical unfiltered repository race command is not claimed green.
Independent exact-head review
Only the fresh reports below count. Both independently bound themselves to the final HEAD, parent, patch hash, 24-file scope, and clean start/end state, and both end exactly
VERDICT: PASS.Claude — PASS
Report:
reviews/claude-direct-76ab3584.mdSHA-256:
ad62930586fa358ee3821365965adb5c8337b971c10314da7708f0ae1e797d25Claude re-derived the ownership and generation ordering, ran all 16 contracts normally and with
-race, passed unfilteredinternal/tui, vet, formatting, diff, and anatomy checks, and found no exact-head defect. Its fullgo test ./... -count=1environment observed six failures in the root,internal/inventory, andinternal/migrate; the same six failures reproduced identically on a clean exact-base checkout in that environment, outside this diff. They are not treated as product failures, and Claude did not claim universal full-suite green.Codex — PASS
Report:
reviews/codex-direct-76ab3584.mdSHA-256:
ee9b9edbcf08fb0bc7daa61fdc195cc76388b52cabe9e82031cb26138dd46af9Codex hand-traced the final arbitration, physical gate, updater token, Mail/UI boundary, visit lifecycle, and anatomy; its static diff/format/anatomy/ownership/binding checks passed and it found no exact-head defect. Its read-only sandbox denied every Go test/vet attempt before compilation because the configured external Go build cache could not create its shard directory (
operation not permitted). That is an explicitly reported review-environment limitation, not a candidate failure; executable coverage comes from the exact-head parent and Claude runs above.The older review rounds are useful defect history only. Their PASS verdicts are non-transferable after each correction.
Cleanup and bridge ledger
MailModelno longer contains the projectfs.MailCache, project refresh slot, mailbox polling chain, or location-updater owner.installMailModeland preserves the matching store; mismatched active stores are suspended before replacement.UpdateHumanLocationlaunchers are removed. Production has one store-owned injection after generation-gated acceptance.tui/internal/tui/ANATOMY.mddocuments the new owner, generation/publication order, physical gate, visit lifecycle, updater token, writes, and ephemeral state. Shiftedmain.gocitations intui/ANATOMY.mdandtui/internal/config/ANATOMY.mdwere also corrected.visitReturnStatepreserves App-owned back-navigation and UI model state until issue TUI: add a persistent per-project Agent switcher with isolated one-to-one chats #645's planned PR 7 installs boundedThreadState. PR 7 owns its removal; it must not be deferred to PR 8. The bridge owns no mailbox cache, scanner, refresh slot, updater, or tick, although the retainedMailModelmay reference accepted snapshot/session-cache presentation state. There is no second store adapter or hidden mailbox owner.Risks and non-goals
Appvalues; it does not make project data process-global. Project cache/snapshot/tick ownership stays in each store.Rollback
Revert the exact commit as one unit to restore the former
MailModel-owned mailbox refresh/tick lifecycle and the prior visit behavior. There is no data migration to reverse. Do not selectively revert only the sole-updater removals or only the generation gate: that would recreate duplicate side effects or permit stale root publication without restoring a coherent former owner. After rollback, rerun the TUI suite and re-auditUpdateHumanLocation,MailCache.Refresh, and anatomy ownership as a single boundary.