The user delegated the release decision after asking for a review first. The review is the part worth recording, because most of what it found could not have been caught by anything already in place.
- The release-blocking defect was in a file nothing validates.
release.ymlpublishes with--notes-file RELEASE_NOTES.md, and that file described v0.4.0 from its title down. Tagging would have shipped v0.4.0's notes as v0.4.1's. Nothing would have objected: it is not compiled, not linted, not tested, and CI never reads it. The general shape — an artifact a workflow consumes but no check covers — is worth looking for directly, since by construction no suite will point at it. - A property was confirmed rather than assumed: the overlap fix cannot deadlock.
Waiting for a reservation another operation holds invites the question, and the answer
is structural.
start_walk_memberacquires one reservation and drops it when its own body ends, beforerestore_profilesadvances; so a walk never holds A while waiting for B. Hold-and-wait is absent, and without it deadlock is impossible regardless of how many groups overlap or in what order. This is a consequence of the per-member granularity chosen earlier overreserve_many, which would have created exactly the multi-resource hold that makes deadlock possible. - The honest limit: three-way contention on one member can exceed the 25-second handoff budget, and the third waiter fails with "Another lifecycle operation is still in progress for this profile". Recorded as a known degradation rather than fixed. An unbounded wait would trade a legible failure for a hang, and raising the number only moves the boundary.
- The UI asymmetry is intentional and was re-derived, not inherited. Restore and a
group action exclude each other; two groups do not. The reason is that
LaunchGroupSection.test.tsxasserts a second group's Start stays enabled — the same shipped assertion that redirected the original fix from the frontend to the backend — and with the backend waiting, both arrangements are safe. Both guards are courtesy.
The checksum verification is the methodological point, and it argues against my own
local test. Simulating the workflow's command in Git Bash proves nothing here:
sha256sum on this platform defaults to binary mode and writes HASH *name, while the
publish job runs on ubuntu-latest where GNU coreutils writes HASH name. Under the
local format the old sed 's# \./# #' does not even match, so the broken command passes
locally — a local reproduction would have reported the defect as absent.
Two things replace it, and both are stronger. The published v0.4.0 SHA256SUMS.txt
was read byte by byte: three spaces after the hash, where GNU -c requires exactly two.
That is the defect measured on the artifact users downloaded, not inferred. And the fixed
workflow now runs sha256sum -c against the file it just wrote, in the same job on the
same runner, so a wrong format fails the release rather than shipping. When a local
simulation and self-verification-in-target-environment disagree about what has been
proven, the second one is the claim to make.
Both halves of that argument now have their answer from the published artifact. Run
33479967618 went green on all seven jobs and published the release; the five archives and
SHA256SUMS.txt were then downloaded from the release page, and a plain
sha256sum -c SHA256SUMS.txt returned OK on every line with exit 0. od -c on the
first line shows two spaces. So the fix is confirmed where it matters, on a real download,
with no workaround — the thing the v0.4.0 file could not do.
The published notes said the download was "about a third smaller — roughly 8.6 MB against 13 MB". Measured on the two published exes:
| build | exe bytes |
|---|---|
v0.4.0, CI, no desktop [profile.release] |
13,212,672 |
| v0.4.1, CI, with it | 11,978,752 |
| v0.4.1, this machine, with it | 8,557,568 |
The real win is 9%, about 91 KB off the compressed zip — not a third. The wrong number
came from pairing the third row with the first: a local post-fix build against a CI pre-fix
build. Both releases were built by rustc 1.98.0 on windows-latest; local is rustc 1.96.0.
That local-vs-CI baseline gap (25,912,797 against 13,212,672, 1.96x) was already recorded
in this file as measured and unexplained — and then read straight across anyway. The rule
worth keeping: a before/after measurement is only a measurement when both halves come from
the same builder. An unexplained gap between two environments is precisely the signal not
to mix numbers from them.
The correction also sharpened what the profile does here. On both published exes the .pdb
path and its RSDS signature are still present, so strip = true is nearly inert on MSVC —
debug info was never in the exe to begin with. The reduction is lto and
codegen-units = 1, corroborated by cargo-registry path literals dropping from 368 to 186.
The earlier note that "LTO did the bulk" was right for the wrong reason: it was right
because strip had nothing to remove, not because debug info was absent from a release
build. Keep the profile — 9% and a denser binary on every user's download is worth a
duplicated four-line block — but do not describe it as a third.
README.md, CHANGELOG.md, and RELEASE_NOTES.md were corrected, and the published
release notes were edited in place. The zips inside the release still embed the earlier
README.md and CHANGELOG.md text: replacing published binaries to fix one sentence would
churn the artifacts users verify by checksum for no functional gain, so that was
deliberately not done.
The user asked for a shorter, Chinese-first README. It went from 285 lines / 21,279
characters to 177 / 9,084 — the old file alternated an English paragraph with its Chinese
translation throughout, so most of the cut is the removed duplication rather than removed
information. English survives as one blockquote paragraph up top; CHANGELOG.md and the
release notes stay English.
Every safety notice was checked back in by grep, not by memory: 升级不可逆, 令牌 (the
archive may capture credentials), 未签名 / SmartScreen, WebView2, 只读 (elevated mode
disables actions), PID revalidation, .env is not read, and the port trust order. All four
internal anchor links were dropped after confirming nothing outside the file referenced
them.
The rewrite did drop one real user-visible behavior — the last development root is
re-scanned once at startup — and it was caught on a re-read rather than by any check. It is
restored in 7963b9e, verified against App.tsx:289-293. A summary that loses a behavior
reads as clean prose, so the only defense is re-reading the cut against the source.
cfcb260 — a docs-only commit — failed the Windows desktop job on
manual_start_stays_starting_until_managed_expected_port_is_ready. It asserted at
commands.rs:2759 that the fixture's port was not yet connectable while the profile read
Starting; TcpStream::connect succeeded instead. The fixture sleeps 5 s before it
listens, so a runner slow enough to spend 5 s between the status check and the connect
attempt inverts the assertion. That commit touched only README.md, so a Rust failure
there cannot be a regression from it, and 7963b9e — also docs-only — passed all five jobs
on the same runner image.
Recorded as environment-dependent, which the repository has precedent for
(external_termination_with_verified_identity_stops_tree_and_releases_port, 2026-08-18).
Deliberately not "fixed": widening the timing window means weakening the assertion, and the
contract it tests — status stays Starting until the expected port is listening and owned
by the profile's tree — is the whole point of wait_for_profile_ready. A test that is
occasionally wrong about timing is worth more than one that no longer checks the ordering.
A read-through of the shipped v0.4.0 code, with the user's standing approval to fix what it found. One real defect, one regression the fix caused and its cause, and one fix that was written, measured against an existing test, and then withdrawn.
-
A restore and a whole-group action could run at once, and the guards were asymmetric in a way that hid it.
runGroupActioncheckedrestoreInFlight, so clicking a group during a restore was refused — but the button stayed live, so the click just did nothing. The other direction was worse:restoreLastRunSetchecked only its own latch, and the restore button ignoredbusyGroups, so a restore started during a group action and both walked overlapping profiles. Nothing corrupts — the backend reserves each profile — but whichever arrived second failed with an "already starting" the user did nothing to cause. Fixed at both layers, latch and button, because either alone leaves the other door open. -
Adding the profile label to the restore message broke two tests, and the cause is worth keeping.
profileLabelclosed oversnapshot, so putting it inrestoreLastRunSet's dependency list made that callback change identity on every one-second poll — and the tray subscription effect depends on that identity, so it tore down and re-subscribed once a second. The two tests that failed (coalesces repeated tray restore requests while restore is runningand the toolbar one) were pinning exactly that. Fixed by reading the snapshot through alatestSnapshotref so the callback is referentially stable. The rule this leaves behind: a callback that feeds an effect's dependency array must not close over the polled snapshot. -
The group-versus-group overlap was first attacked in the frontend, and that attempt failing pointed at the real layer. Two groups may share a member, and starting both at once made the second fail on the shared one. A guard that disabled the second group's Start worked, but
LaunchGroupSection.test.tsx'slocks a group's whole row while one of its actions is in flightasserts at its last line that a different group's Start stays enabled, and that fixture'sMorning stack[db, web]andEverything down[web, astro]shareweb. The guard and a deliberate, shipped assertion were in direct conflict, so the guard came out rather than the assertion.Read again, that conflict was information rather than an obstacle. The assertion is right: a user with two overlapping groups should be able to start both, and a UI that greys out the second is answering a question the UI cannot answer — it does not know whether the shared member is about to be ready. What was wrong was the backend refusing. The fix is therefore in
commands.rs, and it makes the assertion true rather than working around it.Three candidate semantics for reaching a profile another operation holds:
Semantics Verdict Fail (what shipped) Rejected. The situation is ordinary, and the report reads as a fault the user caused. Skip the member and continue Rejected, and this is the subtle one. It breaks the ordering promise: the group's whole point is that webdoes not start untildbis ready, and skippingdbstartswebagainst a database that is still coming up.Wait for it, then take the ordinary path Chosen. Ordering holds, and the reserved start's AlreadyRunningearly return makes a member the other operation finished cost one event.Waiting also composes with the two directions that are not a start racing a start: a member another operation stopped is then started, which is what this group asked for; and a group stop that waits out a start re-checks
processes.infoafterwards, because the operation it waited out may have been the very stop that member needed.The wait is polled rather than signalled. A reservation is a
HashSetentry, and adding a condition variable to serve a wait that resolves in seconds would put a second synchronization mechanism inside the lock that guards every lifecycle operation. A shutdown is returned immediately instead of waited out, since nothing starts during one.The budget is
PROFILE_READY_TIMEOUT_SECS + 5, derived rather than written as its own number: the operation being waited on may spend its entire readiness budget, so an equal budget would let the waiter report a failure for a start that had not failed yet.try_reservewas added toProcessManagerfor this, becauseAppErroris a flat string with no discriminator — without it, the caller would have to match on"Another lifecycle operation is already in progress"to tell a held profile from a refusal, and a message edit would silently change control flow.reserveis expressed through it, so no existing caller or message changed. Both tests were mutation-checked against the old behavior. No existing assertion was touched at any point.The frontend guards from the withdrawn pass stayed, re-justified: they stop the user queueing work already underway. Correctness in the backend, courtesy in the UI.
-
The release workflow's checksum step was defective, and the fix removed the step rather than correcting it.
release.ymlransha256sum ./*.zip ./*.tar.gz | sed 's# \./# #'.sha256sumalready emitsHASH␣␣./name, so substituting␣./with␣␣produced three spaces, one more thansha256sum -caccepts — every line of the published file failed to open on a byte-perfect download. The narrow fix wassed 's#\./##', but the better one is to stop needing a fix-up at all:sha256sum -- *.zip *.tar.gzemits the accepted shape directly, and--protects a filename starting with-exactly as the./prefix did. Asha256sum -c SHA256SUMS.txtnow runs before the publish step, so the file RunCove tells people to verify with is proven to work in the same job that writes it. That check, not the substitution's absence, is what stops this class of defect from shipping again.The
./prefix was not pointless — it was guarding against a leading-dash filename — which is why this is worth recording: the defect was a correct instinct implemented with the wrong tool, not carelessness.RELEASE_NOTES.mdalso now prints the actual commands, including--ignore-missing, because a plain-ccounts the four archives a Windows-only user did not download as failures. -
Verified clean, so these are not suspects:
launch-group.tshandles the empty group (everyover nothing is vacuously true, and it returnsidle);PRAGMA foreign_keysis ON in bothStorageconstructors, so theON DELETE CASCADEthat justified real tables over settings JSON actually fires;save_launch_group's early returns drop the transaction, which rolls it back; the eightRunStatusReasonvariants are all covered byrun-status.ts;zhCnMessages: Record<MessageKey, Message>makes the compiler enforce bilingual parity, whichtypecheckpassing proves — 450 keys each side, no key on one side only; and noTODO,FIXME, ortodo!remains in any production source file. -
Every new test was checked against a reverted fix, not just for passing. Each fails without its guard and passes with it. A test that is green either way pins nothing.
-
The one step reserved for the user was rehearsed on a copy, and the rehearsal changed what should be said about it. The
1 → 2 → 3migration of the real database was the outstanding risk, so the live file was copied and the copy handed toStorage::open— the exact calllib.rs:240makes at startup, reached through a temporary test instorage.rsthat was deleted afterwards. The copy migrated touser_version = 3with all three added tables present and every reader (settings,list_projects,list_launch_groups,list_sessions) working; the live file was re-read afterwards and was still at 1 at that point. It is at 3 now — the user launched the installed app later the same day and the real migration ran; see the entry at the end of this section.Rehearsing on a copy rather than reasoning about it is the point: the migration tests use synthetic fixtures, and a fixture cannot tell you whether this file migrates. What it found is that the live database holds 0 projects, 0 profiles, 0 associations, 4 run sessions, and 2 settings rows, last written 2026-08-11. So the honest description is "irreversible, rehearsed successfully, with essentially no user data at stake" — not "risky". The earlier framing was accurate about irreversibility and silent about volume, which made it read as more dangerous than it is. The backup stays anyway, because the argument for it never depended on the row count.
-
Then the same rehearsal was run through the shipped binary, which is a different claim. A library call proves
Storage::openmigrates; it does not prove the application starts. So an isolated build was made — identifiercom.abysswhale.runcove.verify0901, andINSTANCE_MUTEX_NAMEchanged with it, because that constant is hardcoded rather than derived from the identifier and an isolated build would otherwise contend with a production instance for the same mutex — its data directory was seeded with another copy of the liveuser_version = 1file, and it was launched. It came up with a window titledRunCoveand migrated the seeded database to 3 during startup. Both patched files were restored withgit checkout --.Worth stating plainly because it was not true before today: this is the first evidence in the project that the built application boots. Everything else — 252 desktop tests, 211 frontend tests, 7 e2e — exercises units or a mocked frontend. Cite the two separately; a green suite has never implied a binary that runs.
The isolated-build pattern is the reusable part: patch the identifier and the mutex name, seed the isolated data directory with a copy, launch, then
git checkout --the two files. It answers "does it work on the user's data" without ever opening the user's data. -
The launch-group feature had no test of its own happy path, and that gap survived the release. Asked whether v0.4.0 could be called stable, the honest answer required checking rather than recalling — and the check found that of the three group tests in
commands.rs, one injects a closure, one exercises the empty-group refusal, and one exercises a port conflict. The e2e suite drivesmock-data.ts. The isolated launch above booted the binary but clicked nothing. So "press Start and the stack comes up" had never been executed by anything, in a release whose single headline feature is launch groups.a_whole_group_starts_real_processes_in_order_and_stops_them_togethercloses it: two realnode.exemembers, each binding its own expected port, asserted on launch order, both processes running, a second start reusing the same PIDs, and a reverse-order stop that leaves nothing behind.The port assertions are the load-bearing part, and the reason is worth keeping. Reading
processes.infoalone would pass on a walk that never waited for readiness —infois populated at spawn, not at readiness. Refusing a freshTcpListener::bindon both ports after the walk returns is what makes the ordering promise observable, because it can only hold if every member is still up when the last one finishes. Mutation-checked by walking the stop forward instead of in reverse.The general lesson, since this is the second time this shape has come up: a feature can have several tests and still have none on the path a user takes. Count the paths, not the tests. Both times the missing one was the happy path, because failure paths are easier to write.
-
The desktop executable was the one shipped binary built without the project's release settings, and a size measurement is what exposed it. Installing the app locally put a 25,912,797-byte exe next to the 13,212,672-byte one published with v0.4.0. A gap that size between the same source at the same version is not build noise, so it was worth chasing rather than shrugging at.
The chase found a real defect, but not the one that explains the trigger — keep those two apart, because collapsing them is the easy mistake. What the size difference prompted was a look at the build configuration; what the look found was that the desktop package never received the release profile at all. That is a genuine defect worth fixing on its own terms, and it is not an account of why one profile-less build was 25.9 MB and another was 13.2 MB. That second question is still open, further down.
The defect is a structural assumption that was never true here. The root
Cargo.tomlcarries[profile.release] strip = true, lto = true, codegen-units = 1, and profile settings apply to a whole workspace — but the root manifest has no[workspace]section, soapps/desktop/src-tauriis an unrelated package rather than a member. When cargo's build root is the desktop manifest, the root's profile is not read at all. Cargo does not warn: ignoring a profile in a non-root manifest is normal, and there is nothing here for it to call suspicious. So the CLI binaries got strip and LTO and the desktop application — the artifact users actually download — got neither, which is the exact inverse of what writing those settings once was meant to achieve.Fixed by repeating the block in
apps/desktop/src-tauri/Cargo.tomlwith a comment saying why it is duplicated, so the next reader does not "clean up" the duplication. Creating a real workspace would remove the duplication instead, and was rejected for this change: it moves both packages' target directories and lockfiles and touches the release workflow, all to tidy six lines. The exe is now 8,557,568 bytes — 0.33x the local build before the change and 0.65x the published v0.4.0.The published artifact was checked directly rather than inferred from the local size. The exe extracted from
runcove-desktop-windows-x86_64-portable.zip— the verified v0.4.0 download kept atD:\tmp\runcove-v040-dl\, whose checksum is recorded in this file — holds oneruncove_desktop.pdbreference and 632 source-path strings; the new build holds zero PDB references and 288. The extraction itself was scratch and has been deleted; re-extract from that zip to re-run the check. Astripremoves the debug directory that names the PDB, so that pair of readings is the evidence the shipped binary was built without it.Two corrections to the obvious reading of those numbers, both of which the first write-up got wrong. First, most of the shrink is LTO, not
strip: the release profile defaults todebug = false, so there was little debuginfo to remove, andlto = truewithcodegen-units = 1did the bulk of the work. Attributing 17 MB tostripwould send the next reader looking in the wrong place. Second, 288 source paths survive in the stripped build because panic messages embedfile!()as ordinary.rdatastring data —striptakes the symbol table and debug directory, not string literals, so do not describe it as removing path traces.One thing is measured and unexplained, and is left that way on purpose. The local build before the change was 25,912,797 bytes while CI's published build was 13,212,672, and neither had the profile. Nothing found accounts for a 1.96x gap between them: no
RUSTFLAGS, no.cargo/config.tomlbuild overrides in the repository, and the only global cargo config sets a registry mirror. The pre-change binary has been overwritten, so the question is no longer testable here. It does not affect the fix — the shipped binary demonstrably lacked the settings and demonstrably works with them — but a guess written down as a cause would be worse than the gap.The build-time cost was overstated on first measurement and the correction is the useful part. The build right after the change took 3m22s, which invited "LTO doubles the build" — but that build recompiled every dependency under a profile none of them had been built with. The next full release build took 1m53s against roughly 1m34s before, so the recurring cost is about twenty seconds and the large number is paid once per profile change. Measure a build-time regression on the second build, not the first.
-
The real migration happened, run by the user, and it matched both rehearsals exactly. The installed app started at 13:56:51 on 2026-09-01 and the live database read
user_version = 3at 13:56:52 — ten tables, the three added ones present, and the row counts unchanged at 4 run sessions and 2 settings. The backup is untouched at version 1, last written 2026-08-11.Two things this settles, and one it creates. It settles that the rehearsals were worth running: a library-level
Storage::openon a copy and then the real binary on a seeded copy both predicted this outcome, and the prediction held on the file that mattered. It also settles that the step belonged to the user — it was handed over unlaunched, and they took it. What it creates is that the one-way property is now real rather than theoretical: this file is no longer openable by v0.3.0 or earlier, so the backup is the only way back and costs whatever has been written since. Every note that says the live database "is still at 1" is describing the state before 13:56:51.cargo testcannot verify this change, which is the part worth carrying forward: tests build under thetestprofile, so a green suite says nothing about whether LTO and strip produced a working release binary. The isolated-build recipe from the migration rehearsal answered it instead — identifier and mutex patched tolto0901, data directory seeded with auser_version = 1copy, launched. It came up titledRunCove, spawned six WebView2 processes including a live renderer, sampled 1,415 distinct colours across its 1295×800 client area, and migrated the seeded database to 3 with the 4 sessions and 2 settings intact. The colour count is the assertion that matters: a binary whose embedded frontend assets had been damaged would still migrate the database and still show a window, just an empty one.
P3 had two halves. One was done, one was declined after measuring it, and the second is the decision worth keeping.
- Deleted the three stale remote branches.
feat/launch-groups(9e6ea53),codex/release-v0.3.0(4ca80a4), andcodex/runcove-v0.2.0(0a14cea) were all ancestors ofmain, so deleting the refs lost no commit and rewrote no history; each can be recreated exactly withgit push origin <sha>:refs/heads/<name>. Two of the three named a tool in the branch name and were publicly visible on the repository page, which is a better reason to remove them than housekeeping was.originnow holdsmainalone. - Did not move
V0.2.1_PLAN.mdandV0.3.0_PLAN.mdout of the tracked tree, which the plan called for.notes.mdandHANDOFF.mdciteV0.3.0_PLAN.mdabout forty times and most of those citations carry line numbers — they are how a decision record points at the design it came from. Untracking the file would leave every clone holding forty references to a file it does not have, which is a direct cost to the one thing this repository's documents are for: letting another reviewer follow a decision to its evidence. The alternative of moving onlyV0.2.1_PLAN.md(three citations, none by line) trades the pair's consistency for one filename. The original motive for this half of P3 was the 软著 application's AI-trace scrubbing, and that workstream was dropped on 2026-08-30, so what remained was tidiness — not enough to pay that price. Reopen it only together with a plan that rewrites the citations.
-
The release preparation went on the feature branch, not on
mainafter the merge. Version numbers, both Cargo lockfiles,package-lock.json, the changelog, the release notes, and the README all landed inf90b8a6onfeat/launch-groupsso that PR CI would run against them. The reason is asymmetric:release.yml's CLI job builds withcargo build --lockedandci.ymldoes not, so a lockfile that disagrees with a manifest is invisible until the tag already exists — and by then the tag is the thing that would have to be moved.ci.ymldoes runnpm ci, so npm drift would have failed the PR either way. Both packages were also checked locally withcargo check --lockedbefore the push. -
Three third-party crates sit at
0.3.0in the desktop lockfile —dtor,fallible-iterator,urlpattern— so a blind search-and-replace of the old version string would have corrupted it. Both lockfiles were regenerated withcargo checkinstead of edited, and ours were identified by reading the surrounding[[package]] name =lines. -
The release note's upgrade path was widened after the first release-prep commit (
9e6ea53). It said the database migrates "from schema version 2 to version 3", which is the step v0.4.0 adds but not the path a v0.2.1 user takes — theirs goes through version 2 on the way. The consequence sentence was already right for everyone; the path was not, and an irreversible schema step is the wrong place to be right for only the most recent release. One extra CI cycle was cheaper than a release note that misdescribes a one-way upgrade. -
README.md's## v0.3.0 Scopebecame## v0.4.0 Scoperather than gaining a sibling. The launch-group plan said to keep the v0.3.0 section as a historical record and add a new one, but that was written when the README still described v0.3.0 as the current release. Two Scope sections would have carried the same seven-item exclusion list twice in a user-facing file, and the per-release detail already lives inCHANGELOG.md, so the duplicate was dropped and the exclusions kept. -
The published archives were verified twice, by two methods, and the fallback is worth keeping. For most of release day
release-assets.githubusercontent.comrefused every connection from this machine —gh release downloadandcurl -Lboth reset, ~60 attempts, whilegithub.comandapi.github.comworked — so the v0.3.0 method of downloading all five archives and runningsha256sum -cwas unavailable. The substitute: thesha256sumoutput printed by thePublish GitHub releasejob, which is the content of the publishedSHA256SUMS.txt, diffed against thedigestfield the API reports for each stored asset. Those are produced independently — one on the runner before upload, one by GitHub from the bytes it stored — and all five matched, which proves the published bytes are what CI built without proving what a downloader elsewhere receives. Later the same day the asset host recovered, and the real check ran: all six assets downloaded,sha256sum -cOKon all five archives, agreeing with both the digests and the sums below. The desktop zip holdsruncove-desktop.exeatFileVersion 0.4.0, aREADME.mdbyte-identical togit show v0.4.0:README.md, aCHANGELOG.mdheaded[0.4.0] - 2026-08-31, andLICENSE. Keep the digest method written down: it is the only verification available when the asset CDN is unreachable, and it very nearly was the whole record for this release. -
sha256sum -c SHA256SUMS.txtfails on the published file, and the bytes are not the reason. All five lines reportNo such file or directory.release.ymlbuilds the file withsha256sum ./*.zip ./*.tar.gz | sed 's# \./# #', and sincesha256sumalready emits<hash><space><space>./<name>, replacing" ./"with two spaces yields three spaces where the format allows exactly two — sosha256sumtakes the leading space as part of the filename and looks for" runcove-…". Every archive is intact, and no document gives a wrong command:RELEASE_NOTES.md:66says only "verify the archive againstSHA256SUMS.txt" andREADME.mdnever mentions the file. The defect is that the obvious way to carry out that instruction fails on a good download, which is worse than cosmetic because fiveFAILEDlines read as a corrupt or tampered archive. Normalizing works today —sed -E 's/^([0-9a-f]{64})[[:space:]]+/\1 /' SHA256SUMS.txt | sha256sum -c -— and the real fix issed 's#\./##'in the workflow, which is out of bounds here and cannot be applied to an already-published asset regardless. Carried as a v0.5.0 item together with the release-note wording, not patched now. -
The checksums themselves, recorded so the claim above can be re-checked without trusting it. These are the sums the workflow computed on the runner and published as
SHA256SUMS.txt, and they equal GitHub's stored-object digest for every asset. Re-verify by downloading an archive and hashing it, or by reading.assets[].digestfromgh api repos/AbyssWhalen/RunCove/releases/tags/v0.4.0.853d269b36d8065db4b173611a9a79b791b123487524509e9299be06243da96c runcove-cli-linux-x86_64.tar.gz fd4e23586fa922a4f54af0b529a58b5b702c081bd6b8f0c8c978add075c67ec4 runcove-cli-macos-aarch64.tar.gz 1691e2d03c020cd2424835396538236593a802ac61c6fec85319e4f4a604ecc7 runcove-cli-macos-x86_64.tar.gz febb3c20847c0ab685b603eb642af8e00df7f91b1e516aec413321796dc3f73e runcove-cli-windows-x86_64.zip 8707794802c3cf5c0563809376a22be2c24078d14ffd934334b125968fb4051f runcove-desktop-windows-x86_64-portable.zip -
The network to GitHub was unreliable all session and retrying was the right first move, not diagnosing. Pushes failed with
Recv failure: Connection was resetandFailed to connect to github.com port 443and then succeeded unchanged — one on the third attempt, one on the sixth. No proxy is configured and none was added. A retry loop written asif git push ... | tail -2is useless, because the pipeline's exit status istail's; capture the output and test$?instead. -
The merge commit body carries no AI marker.
bd2b777, the v0.3.0 merge, contains[codex];0d6b934does not, and the subject follows the sameRelease RunCove vX.Y.Z (#N)shape so the two releases still read as a series. The two[Qoder]markers and that[codex]one stay where they are — rewriting published history would break the tags and the release associations.
- The defect, measured before it was fixed. Five fields in
ProjectModal.tsxwrapped their<input>in a<label>that also renders that field's validation error, and the accessible name of an input labelled by a wrapping<label>is the label's whole text content. So the moment an error appeared,ProgrambecameProgram This field is required.and the field answered to a name nobody would look for. The error was already wired througharia-describedby, so it was also being announced twice. Confirmed as red first: the new test failed atgetByLabelText("Project name")with all five fields invalid, then passed after the fix, with no other assertion touched. - Why the existing suite could not see it.
ProjectModal.test.tsx's validation test queries every field before it submits and then holds the element references (:125-129, asserting at:135-136), so it never asks for a field by name while an error is on screen. That is the exact window the defect lives in — worth remembering as a shape, not just as one bug: a query taken before the state change cannot observe a name that only breaks after it. - The fix is
id+aria-labelledbyon the caption, matchingLaunchGroupModal.tsx, which pins the name to the caption regardless of what else the label renders. Five source sites, three of them inside the profile loop so their ids carry the index:project-name-label,project-path-label, andprofile-${index}-{name,program,cwd}-label. Five, not the "~13" an earlier note estimated — that number counted runtime instances of the looped fields, and the source sites are what get edited. - Deliberately unchanged: the argument and expected-port inputs, whose labels are
<div>s and whose inputs already carry explicitaria-labels, and the root-import checkbox, which needs itsaria-labelprecisely because its label holds a paragraph of project metadata.
- P1 and P2 cannot be split by commit.
models.rs,commands.rs,App.tsx, andmessages.tseach carry both the run-statusreasonwork and the launch-group work (measured interleave:commands.rsgroup 46 / reason 16,messages.tsgroup 66 / reason 18,models.rsgroup 12 / reason 30,App.tsxgroup 30 / reason 8). Splitting them needs hunk-level staging, interactivegit add -pis unavailable in this environment, and the halves would not build. So the granularity chosen was every commit builds on its own:f8a2447the matrix fix,fc56693all code,6b80f61all documentation,842efb9the accessible-name fix. The feature commit's body records why the run-status fix rides along, so the decision is auditable fromgit logalone. git pushtoorigin/feat/launch-groupsfailed once withRecv failure: Connection was resetand succeeded on the immediate retry, with no proxy configured. Retry before diagnosing; nothing was half-pushed.- The
gh pr createbody went through a file rather than a heredoc: a heredoc carrying the body tripped bash's parser (unexpected EOF while looking for matching '). Write the body to a file outside the repository and pass--body-file.
- Decision: the runtime mechanism was already there, so the feature is persistence plus a
thin command layer.
restore_profilesis already an ordered, fail-fast launcher andwait_for_profile_readyalready waits for the expected ports to listen under this profile's own process tree.start_launch_grouptherefore loads the group and callsrestore_profiles, and no new state machine, no new event, and no new poll exist. Three behaviors come free rather than being re-implemented: a member that is already running returnsAlreadyRunningand counts as started, which makes a group start idempotent and turns Start into "fill the gaps"; the first failure stops the walk and keeps what started; and therelatedPortpayload that drivesView occupantarrives unchanged. - Decision: two tables, not a field in the settings JSON, and
ON DELETE CASCADEis the entire reason. Deleting a profile has to remove it from every group that listed it. In the JSON that means every read filters dangling ids forever and one missed filter is a crash; in SQLite it is one clause the database enforces. The price is a schema version bump, which the user authorized on 2026-08-31 for exactly this. Positions may then have holes, which costs nothing because every read isORDER BY positionand no code treats a position as an index. - Decision: stop walks in reverse and does not stop the rest. Start fails fast because
continuing past a failed dependency starts things that cannot work; stop cannot borrow
that reasoning, because interrupting a stop leaves more processes running than
finishing it. Each member goes through the same
stop_profile_inner, a failure lands infailures, and the walk continues — the same choiceprocesses.rs'sstop_all_with_intentalready made. - Decision: a group has no stored state. Running / partial / idle is derived in
components/launch-group.tsfrom member statuses the snapshot already carries, so the backend gained no status column and no group event, and the derived answer cannot drift from the members. "Up" meansrunningorstarting;conflictis not up, which is the repository's existing rule and not a new one. - Decision: groups may cross projects. Members reference
launch_profiles(id)directly. A database in one project and a web app in another is the case that motivates the feature, so forbidding it would remove the point. - Decision: reservations stay per member.
restore_profilealready reserves one profile at a time andreserve_manywas not used, sostart_profile_inner_reserved's contract did not change. The cost is that a user can still act on an individual profile while a group is starting; restore shipped and was verified with exactly that cost. - Decision: empty groups are refused at validation.
validate_launch_grouprequires a trimmed non-empty name, at least one member, no duplicate members, and every referenced profile to exist. A group with no members has a Start button that means nothing. - Decision:
group.*message keys, not the planneddialog.group*. The plan proposed putting the modal's strings underdialog., which would have split one feature's vocabulary across two prefixes for no gain. Everything the feature says now lives undergroup., in bothenMessagesandzhCnMessages;zhCnMessagesis typedRecord<MessageKey, Message>, so a missing or extra key failstypecheckrather than shipping a blank label. - Decision:
busyGroupsis aMap<string, GroupAction>, not aSet. The button has to say which action is in flight — "Starting…" and "Stopping…" are different labels on different buttons — so membership alone is not enough information. The per-profile busy set is then derived:effectiveBusyProfileIdsunions the realbusyProfileIdswith every member of every busy group, so a group action disables its members' individual buttons without a second source of truth that could get out of step. - A real defect was found in passing and fixed:
save_projectrewoundupdated_at. The insert readVALUES (?1, ?2, ?3, ?4, ?4), bindingcreated_atinto both timestamp columns, so every new project'supdated_atwas its creation time and the column was wrong from the first write. It is?5withnowbound separately (storage.rs:63-66). Unrelated to launch groups; found while reading the upsert thatsave_launch_groupwas modeled on. LaunchGroupModal's inputs have programmatic names;ProjectModal's still do not. The modal's name field usesid="group-name-label"plusaria-labelledby, because a label that is only visually adjacent leaves the input nameless to a screen reader.ProjectModal.tsxhas the same pattern at roughly thirteen sites and was left unfixed on purpose: it is a pre-existing defect in a file this feature does not touch, and folding a thirteen-site accessibility change into this diff would make both harder to review. The new.field-errorrule instyles.cssdoes incidentally styleProjectModal's existing error text, which is a cosmetic improvement, not the fix.- The e2e overflow assertion had to be rewritten, and the reason generalizes:
scrollWidthcannot measure overflow anywhere in this app. Fourresponsive.spec.tsfailures were my own instrument, not the layout. EveryIconButtonrenders its tooltip as an absolutely positioned::afterpseudo-element that is invisible until hover and up to 220px wide;scrollWidthincludes it, so any row containing an icon button reports overflow it does not have.expectRowFitsViewport(responsive.spec.ts:41) measures the right edge of the row's real buttons against the viewport instead. No assertion was weakened — the new one fails on real overflow and the old one failed on a tooltip. Note also that.restore-sequenceis a horizontal scroll container by design, so overflow inside it is correct behavior. - No
V0.4.0_PLAN.mdwas created.V0.2.1_PLAN.mdandV0.3.0_PLAN.mdare precedent, but P3's goal is to get agent-facing documents out of the repository, so adding a third would push against the direction already chosen. The plan stayed in the session plan file; the durable record is this section plus theHANDOFF.mdcheckpoint. - Verification, run before any number was written into a document. Root crate: fmt ok,
clippy clean under
-D warnings,38 passed; 0 failedacross five targets — unchanged, since the root crate has no part in this feature. Desktop crate inapps/desktop/src-tauri: fmt ok, clippy clean in 56.30s,cargo test --all-targets250 passed; 0 failed; 1 ignored(from 240), including the environment-dependentexternal_termination_with_verified_identity_stops_tree_and_releases_port, which passed through itsOkpath so its P1-3 guard is still unexercised. Frontend inapps/desktop: lint and typecheck clean,npm test -- --run26 passed (26)files /208 passed (208)tests in 21.70s (from 23 / 171),npm run buildJS 335.03 kB / CSS 38.32 kB,npm run e2e7 passed (18.8s)(from 6),npm run tauri buildexit 0 —1m 37sfirst, then56son the rebuild that restored the production identifier. - Migration evidence, on a throwaway bundle identifier, with the real database never
opened. The desktop tests build their fixtures in memory, so only this exercises an
install path. A pinned version 2 database —
V1_SCHEMA+V1_FIXTURE+V2_ADDITIONcopied out ofstorage.rs, plus onecompletearchive row,user_version=2, eight tables — was staged into%LOCALAPPDATA%\com.abysswhale.runcove.demo0831\, opened once by a build whose identifier and instance mutex both carrieddemo0831, and afterwards read back:user_version=3, ten tables, both new tables matching the pinned DDL includingCOLLATE NOCASE,integrity_checkok,foreign_key_checkok, and every fixture row intact down to the restore set's order. A group whose members were deliberately out of profile sort order then survived a second 14-second session unchanged, with nothing on stderr.%LOCALAPPDATA%\com.abysswhale.runcove\runcove.sqlite3is stilluser_version=1, last written 2026-08-11, read from its SQLite header without opening the file. INSTANCE_MUTEX_NAMEis hardcoded, so changing the bundle identifier alone does not isolate a build.lib.rs:42is a literal andsingle_instance.rs:61derives the wake event as{name}.Wake, so a demo build that changed only the identifier would still share the guard with production: launching it would wake the running RunCove and start nothing, and the experiment would measure a process that never ran. Both were changed for the demo build and both were reverted;git grep demo0831finds nothing.- A verification trap that silently produced a false pass. The first attempt seeded the
database with Microsoft Store Python, whose writes under
%LOCALAPPDATA%are redirected into%LOCALAPPDATA%\Packages\PythonSoftwareFoundation.Python.3.13_*\LocalCache\Local\. The app found no database, created a fresh version 3 one, and every reading came back "correct" while the version 2 → 3 upgrade had never run. Two files with the same path existed and Python and PowerShell each saw a different one. Rule for next time: stage anything under%LOCALAPPDATA%with PowerShell, and let Python work on a copy underD:\tmp. - Three post-run differences in the isolated database are correct behavior, not data
loss. The session left
runningby the previous kill becameinterrupted, which is startup reconciliation;archiveRunLogsappeared in the settings JSON, which is the#[serde(default)]round-trip; andlanguagePreferencewent fromzh-CNtosystembecauseApp.tsx:389-393deliberately lets WebViewlocalStoragewin over the database when they disagree, and an earlier run in that throwaway WebView profile had storedsystem. On a real install the two agree, so this is an artifact of reusing one WebView profile across differently-seeded databases. - The one-way upgrade is now stated in
README.mdin both languages, together with the recommendation to back up%LOCALAPPDATA%\com.abysswhale.runcove\before running amainbuild for the first time. A failed upgrade rolls back and stays at version 2; a successful one cannot be undone, and v0.3.0 then refuses the database as newer than it supports.
- Verification, run from the repository root unless noted. Root crate: fmt ok, clippy
clean under
-D warnings,cargo test --all-targets38 passed; 0 failedacross five targets (12 + 0 + 0 + 10 + 16). Desktop crate inapps/desktop/src-tauri: fmt ok, clippy clean,240 passed; 0 failed; 1 ignored. Frontend inapps/desktop: lint and typecheck clean,npm test -- --run23 passed (23)files /171 passed (171)tests,npm run build1607 modules in 1.73s,npm run e2e6 passed (18.5s),npm run tauri buildexit 0 with the release profile finishing in1m 55s. tauri buildproducing no installer is the configured behavior, not a failure.tauri.conf.jsonsetsbundle.active: false, so the command buildstarget/release/runcove-desktop.exeand stops; packaging belongs to the release workflow. That exe was rebuilt and not launched — it carries the production identifier.- A defect in
AGENTS.md's matrix was found while following it. It listed the threecargocommands once, at the root. The root package is not a workspace (Cargo.tomlhas no[workspace]), so those commands never reachruncove-desktop: following the recipe literally skipped 240 tests plus that crate's fmt and clippy.AGENTS.mdnow carries both Rust blocks and states the reason, so the omission cannot be read as intentional. The enforced gate was never weaker —ci.yml:110-119already runs all three commands inapps/desktop/src-tauri— so this was a documentation defect in the local completion gate, not a hole in CI. - Decision: the next version is
0.3.1, and it should not be released on its own. The delta sincev0.3.0is a single user-visible bug fix (RunCove's own lifecycle sentences showing in English under a Chinese interface) with no feature and no break; the IPCreasonfield is additive and optional, and 0.x still gets a patch bump for that. Holding it: a release costs a tag, a CI run, and published artifacts, and0.4.0is already reserved for the P2 feature, which this fix can ride with.CHANGELOG.md's[Unreleased]section accumulates either way, so nothing is lost if the user decides to ship the patch sooner. - No version file was edited. A bump would touch root
Cargo.toml,apps/desktop/src-tauri/Cargo.toml,tauri.conf.json,apps/desktop/package.json, andpackage-lock.json(lines 3 and 9), plus the two cargo lockfiles — all four manifests currently read0.3.0. A bump is only meaningful with a release, and release is unauthorized, so the number is a recommendation and nothing on disk asserts it.
- Decision: a runtime guard on the refusal, not
#[ignore].external_termination_with_verified_identity_stops_tree_and_releases_port(commands.rs:1830) is the only test that performs a real termination, and it fails intermittently on this machine with WindowsAccess deniedwhile passing on CI. It now treats a refusal fromtaskkillas an environment refusal — reported througheprintln!, thenreturn— and keeps failing on every error RunCove decides for itself. Test code only; no production behavior changed. - Why not
#[ignore], which is what the other live test uses.live_imports_detect_conflicts_without_touching_existing_processes(commands.rs:1884) needs live services configured by hand and can never run unattended. This one builds its own fixture and works on CI, so#[ignore]would remove the only check of the successful path everywhere — CI does not run-- --ignored— to quiet one machine. That is coverage loss disguised as a guard. - Why the predicate matches RunCove's wrapper and not the OS reason.
termination_refused_by_environment(commands.rs:1822) tests for the prefix"Could not terminate process tree:"(commands.rs:1050).taskkillprints in the system language and its stderr reaches that message throughfrom_utf8_lossy, so on a non-English Windows — this one is Windows 11 Home China — matchingAccess is deniedwould be either a translation miss or mojibake. The prefix is narrow by enumeration: every otherErrinterminate_external_windowsis RunCove's own verdict (changed identity, changed executable, managed process, missingtaskkill.exe, refusing itself, an unreadable handle) and none of them begins with it. - Why
taskkill's own failure is environmental rather than a product defect. Its/Twalk reportsAccess is denied.for a child that exited between enumeration and termination, or for one security software stands in front of, and it returns a failing status even when the root is already gone. RunCove reporting that failure is correct — judging success by "the root looks dead" instead would risk leaving children behind, so the process-safety model stays as it is and the guard belongs in the test. - The guard is unexercised. The flake did not reproduce: 10 consecutive dedicated
runs plus a parallel and a single-threaded full suite all took the
Okpath. Say it that way rather than claiming the flake is fixed — the nextAccess deniedon any machine is the first real exercise, and it will show as a pass with a--nocaptureline. - Verification: desktop crate
240 passed; 0 failed; 1 ignored, identical in parallel and single-threaded;fmt --checkandclippy --all-targets --all-features -D warningsclean. Test count unchanged, since a body changed and no test was added.
- Decision: close P2-1 as a disclosed limitation. When the closing flush itself
fails,
line_countcan name a line the file does not hold. The reasoning now lives atreturn_file(archive.rs:2618-2636), where the defect is, so nobody re-derives it. No production behavior and no test changed — this item was a decision, not an implementation. - Why it is tolerable. Nothing decides on
line_count: the quota and eviction readbyte_sizeand the timestamps, andbyte_sizeis already re-measured from the file on exactly this path (both close sites,archive.rs:2728and:2955). The viewer already treats the row's counters as possibly stale and pages from the file itself (models.rs:415-417). The error is always an over-count, never a silent loss, and the same close already labels the rowpartial/write-error, so the user is told the archive is broken before they read any number on it. - Why "count at flush boundaries" — the plan's own option — does not work. A flush can also go out partially. Saying which buffered lines survived needs every buffered line's byte length, kept for every open session on every write, to describe a disk that has already failed. This was already the code's stated reason and it still holds.
- Why recounting the file does not work either — the finding that decided it. The
close already measures that file, so a recount looks nearly free. But the two counts
would then disagree about identical bytes:
Sweep::count_lines(archive.rs:1311) usestext.lines().count(), which counts a trailing fragment as a line, while a short write here charges the fragment todropped_linesand reportsline_count0 — pinned by"a fragment is not a line"(archive.rs:4988). The same file would read one line longer after a crash than after a close. Exactness therefore means unifying that definition across the sweep, the writer, and their tests: re-opening a settled contract for a display-only number. Two smaller costs point the same way —read_to_stringfails on a flush truncated mid-UTF-8 and would fall back to the same over-count, and the read is up to 10 MiB per session at close time, includingclose_allat shutdown, on a disk that has just failed. - What the decision obliges. The limitation stays disclosed.
CHANGELOG.mdgained an[Unreleased]section that carries it forward and records P1-1's fix; the published[0.3.0]entry is left exactly as it shipped, including its now-superseded English- message limitation, because a released section is a historical record.
- The frontend translates, the backend does not. RunCove's own lifecycle
sentences were being composed in Rust and shown verbatim in a Chinese window.
The fix sends
RunStatusReason— an internally-tagged enum whosekindis kebab case — on bothRunStatusEventandRunLogEvent, and translates it in the existing i18n catalog. The alternative, passing the window's language down to the backend, was rejected: it would put copy in two places and make every emit site language-aware. - Additive, not a replacement.
reasonis optional and skipped when absent;messageandlinekeep the English sentence fromRunStatusReason::describe. So an unrecognizedkind— a newer backend, or a payload nothing validated — falls back to readable English instead of rendering nothing. This is whytypes.tsdeclareskind: stringrather than a union of literals. - Scope is RunCove's own sentences only. The eight converted strings are the six
watch_childexit arms andcommands.rs's"Stop requested"and"Profile is already running".AppErrortext — the port-conflict message and theerror.to_string()start failures — is untouched: it has no fixed enumeration, it is already framed byt("error.lifecycleDetail", …), and the conflict wording also travels as the command'sErr. Localizing it is a separate, much larger job. - Archived lifecycle lines stay English. The archive stores
line, andencode_recordwrites exactly three keys ({t,s,l}) at schema version 2. Adding a reason to the file would be a format change for a cosmetic gain, so the archive viewer shows what was written. The live drawer shows the localized form because it has the event. logKeystill hashesline. The dedupe that merges history with live output must compare the stable wire text, not a rendering that changes with the language.- Tests were written on the defect path only, per the new policy: the bilingual
mapping with its two fallbacks, a zh-CN notice, a zh-CN failure alert, a zh-CN log
line whose clipboard copy matches the screen, and — in Rust — the kebab-case
kindand the omitted-field shape, because a rename there would quietly restore English. - Verification: root crate
38 passedacross six test targets; desktop crate240 passed; 0 failed; 1 ignored;fmt --checkandclippy --all-targets --all-features -D warningsclean in both crates; frontendlint,typecheck,test -- --run(23 files / 171 tests),build,e2e(6) green.npm run tauri buildis left to P1-4's full matrix.external_termination_with_verified_identity_stops_tree_and_releases_portpassed in this run, so P1-3's guard is still needed — the failure is intermittent on this machine, not gone.
- The software copyright registration goal is abandoned. Reported policy since 2026-03 is that 中国版权保护中心 refuses applications built on AI-generated code, with 失信名单 / 个人征信 consequences, and that 2026 review adds AI-material screening and code-similarity comparison. RunCove's code is substantially agent-written and its public repository documents that, so the application path is closed. Removing AI traces to make it pass was declined and is not a task anyone should revive. The official text could not be fetched from this environment, so this is consistent secondary reporting; confirm at ccopyright.com.cn if the question ever matters again.
- The AI-trace audit that preceded the decision is worth keeping because it is
measured: production and test source carry zero AI-vendor references — every
git grephit underapps/desktop/srcandapps/desktop/e2eis the user's ownD:\CodexProject\personal-projectspath in fixtures. The references live inHANDOFF.md(21),notes.md(12),V0.3.0_PLAN.md(1), and in two commit bodies carrying🤖 Generated with [Qoder]plus[codex]in thebd2b777merge body. - Testing process changed: the slice-by-slice red-to-green method used for the v0.3.0 archive is retired. Tests follow real defect paths and regression risk from now on. The rule against weakening, retargeting, or deleting an assertion to reach green is unchanged.
- The next plan is in
HANDOFF.md, not here: P1 defect closure, P2 one v0.4.0 feature awaiting the user's pick, P3 authorization-gated housekeeping.
- PR #3 was merged and the public
v0.3.0release was published successfully.main,origin/main, and the annotated tag resolve tobd2b7776d56ddf750ffe97a3d8219168fbb04069. - Release workflow
32500425361passed all validation, cross-platform CLI, Windows desktop, packaging, checksum, and publish jobs. The five downloaded binary archives matched the publishedSHA256SUMS.txtentries by SHA-256. No workflow file was changed. - The post-restart repository check is clean. Local scratch verification lives
outside the repository in
D:\tmp; no build output or real application database was introduced into the tree. - Local verification caveats remain recorded rather than hidden: the managed
session saw Windows
Access deniedin the real-process termination test andspawn EPERMwhen trying to start Node children. GitHub's clean Windows job passed the complete release matrix, including six E2E flows andtauri build. - Decision: freeze v0.3.0 for exploratory use. The next change should be a new user-approved product scope, not a cleanup refactor or soft-copyright padding.
- The user explicitly authorized commit, push, tag, and public release after the
isolated local demo. Release readiness is defined as no known P0/P1, the complete
local matrix green, GitHub CI green on the release branch, and the existing tag
workflow producing all expected archives plus
SHA256SUMS.txt. - The release keeps two disclosed P2 limitations: a closing flush failure can over-count which buffered line reached disk while byte size is reconciled, and backend-composed stop/exit messages remain English under the Chinese interface. Neither affects the normal archive path, process safety, or local-only boundary.
- Tool-specific
CLAUDE.mdremains local.AGENTS.mdis restored to its published form and no CI, release-workflow,.env, real database, other project, or existing process is modified as part of the release preparation. - The final release commit is intentionally one cohesive 43-path change: the frozen
36-path implementation/demo candidate plus seven manifest, lockfile, and public
release-document paths. Generated build output and local
CLAUDE.mdare excluded. - The official npm audit caught high-severity advisory
GHSA-2v37-7h3g-55p8in the transitive development dependencynanoid3.3.17. A package-lock-only update to 3.3.18 followed bynpm cireduced the official-registry audit to zero findings; no direct dependency was added. The configured mirror's audit endpoint returns 404, so release evidence usesregistry.npmjs.orgexplicitly. - The fresh local release run is honest but not wholly green in this managed Codex
session. Root Rust (38), frontend lint/typecheck/Vitest (157), archive (99), and
archive-service (11) checks pass. The desktop run is 237 pass / 1 fail / 1 ignored,
with only the known real-process termination test failing because Windows returned
Access denied. Node child-process creation is denied withspawn EPERM, preventing a fresh Playwright and Vite/Tauri rerun even though both passed on the frozen tree the previous day. Decision: do not change product code or tests for environment behavior; require every unchanged GitHub PR check to pass before merge and tag. - PR #3 exposed a toolchain-drift lint, not a behavior failure: Rust 1.98 rejects the
test-only expression
format!("{uuid}")asclippy::useless_format, while the local stable toolchain accepted it. Replace it withuuid.to_string(); test inputs and assertions are unchanged. The same CI run already proved npm audit, frontend build, and all six Playwright workflows on the clean Windows runner.
- Scope: documentation and verification only —
README.md,HANDOFF.md,notes.md, and one pointer bullet inCLAUDE.md. No production or test source file changed, so every decision recorded below this section still stands as written. Untouched: CI, the release workflow, tags,.env, every real database, unrelated projects, and existing developer processes. No commit, no push. - Decision: the README states the archive as a
main-only, off-by-default exception rather than restating v0.2.1's promise. The contradiction was not one sentence but a premise — v0.2.1's public text says session output is never written to disk, and that is still true of the published zip. Six places now name the boundary explicitly: the log-boundary bullet, the Desktop App and Help bullets, the new Run Log Archive section, Architecture's index row and one-way schema step, Privacy And Process Safety, and the v0.2.1 Scope exclusion. Wording rule for anything written next: the published v0.2.1 build has no archive at all; onmainit exists, stays off until the user turns it on, and only affects runs started after that. - Decision: both P2s stay open under the freeze, and neither is a silent risk.
line_countover-counting after a failed closing flush is documented at its cause (archive.rs:2624-2630) and can only ever overstate what was written, while the byte side self-corrects from the disk. The English stop and exit messages (processes.rs:562,:580) are a localization gap in strings the backend composes, not a behavior defect. Fixing either would mean new production code, which the freeze excludes. - Finding, not a defect: a per-session
beginfailure cannot reach the drawer's warning.unavailable_reasonshort-circuits toNonewhile the writer exists, so that warning describes this run's initialization and nothing else; a session that fails to open is reported through the transientArchiveReporterchannel and surfaces as a 未归档 badge. That is the honest reading ofenabledversusavailable(models.rs:324-335), and it means the initialization-failure notice is reproducible only by obstructing the directory before launch. Recorded rather than changed: making a per-session failure sticky in the drawer is a product decision, not a wrap-up item. - Method: the initialization-failure proof deleted nothing, twice over. The archive directory was renamed aside and a 15-byte file left in its place; undoing it moved the placeholder out to a scratch path instead of removing it, then renamed the directory back. All four archives were then verified byte-identical by SHA256 (4 files / 682,845 bytes). Keep this shape for any future destructive-looking proof: rename and move, never delete, and re-verify by hash rather than by size.
- Verification at the freeze, all green: root crate fmt and clippy clean with 38 tests;
desktop crate fmt and clippy clean with
238 passed; 0 failed; 1 ignoredout of 239;npm run lint,npm run typecheck,npm test -- --runat 22 files / 157 tests,npm run build,npm run e2eat 6 passed, andnpm run tauri build. The manual demo checklist was re-run end to end on the frozen build; its measurements — paging to 「已到归档开头」 at 2,261 records, the 150,054-byte delete credit measured on disk, the instant toggle-off finalization with zero"s":"system"records — and the criterion-7 nuance are in the top section ofHANDOFF.md. - Carried forward:
apps/desktop/src-tauri/target/release/runcove-desktop.exeis a production-identifier build after that lasttauri buildand must not be launched — the demo exe isD:\tmp\runcove-v030-demo\RunCove-demo.exe. Twocommands.rsport/child-timing tests have each failed once on some machine, so no cross-machine all-green baseline may be claimed.commit,push,tag, CI, and Release remain unauthorized.
- Scope:
apps/desktop/src-tauri/src/—archive_service.rs,commands.rs,lib.rs,models.rs,processes.rs,state.rs,storage.rs— plus the frontend (App.tsx,api.ts,types.ts,mock-data.ts,styles.css,components/{LogDrawer,RunLogArchiveDrawer,RunHistory*,OverviewView},components/archive.ts,i18n/messages.ts) and the docs. No schema change: the version 2 migration was already in place andSCHEMA_VERSIONis untouched. Untouched: CI, the release workflow, tags,.env, and every real database. No commit, no push. - Decision:
update_countersis thinned, and nothing else is.ThrottledArchiveIndex(archive_service.rs:103) lets a session's first refresh through and then requires 4 s or 1 MiB, whichever comes first. A pump batch can be a single line, so untouched this turns a chatty child process into one SQLite write per printed line. What makes it safe is that the row a reader finally sees is written byArchiveIndex::close; all that is thinned is how fast a running row catches up with its file. It lives in the service rather than inarchive.rson purpose — the writer's contract is that it refreshes after every batch and its tests assert exactly that, while "often enough for a user watching a row" is an application decision. A clock that has gone backwards is deliberately not thinned. - Decision: shutdown is bounded by the queue's caps, not by a clock.
shutdownstops the pump signal, then closes every open archive asInterrupted. One pump drains everything queued and what can be queued is capped byQueueBounds, so there is no timeout to tune and no file half-written by one.close_open_archivespumps first so the bytes it can account for pass through the quota — a close does not consult it — and a failed pump is reported without stopping the closes, because an unwritable disk is exactly when the rows matter. - Decision: the setting is persisted before it is applied.
persist_run_log_archiving(commands.rs:538) writesAppSettingsand only then callsset_enabled, so a database that refuses the write leaves the runtime and the stored value agreeing instead of archiving with nothing to remember it by. - Decision: a read never crosses IPC whole.
read_run_log_archiveisasync+run_blocking, seeks to the end, and walks backwards under both a record cap and a byte cap (MIN_PAGE_RECORDS = 1,DEFAULT_PAGE_RECORDS = 500,MAX_PAGE_RECORDS = 2_000,archive.rs:79-85). The viewer's cursor is the previous page'spage_start_offset, so "load earlier" is a bounded backwards walk rather than a re-read of the file. - Decision: turning the setting on never backfills. A session's archive is opened at
launch and only there, so
set_enabled(true)mid-run leaves the running session unarchived — proven on screen, not merely intended. Turning it off closes what is open aspartial/user-disabledwhile the process keeps streaming to the in-memory drawer. - Two closes, two different reload problems. The exit path emits
run-archive-closed(processes.rs:675) because the reload the exit event itself triggers still seeswriting— the close finishes its writes after the lock that event is emitted under is released. The toggle path emits nothing at all (close_open_archivesannounces no rows), soApp.tsx'stoggleRunLogArchivingawaits a history reload aftersetRunLogArchivingresolves; that is deterministic only because the command isasync+run_blocking, which makes every affected archive final on disk before the promise settles. Both were caught on screen. The second guard was checked for teeth by breaking it and watchingApp.history.test.tsxfail, then restoring it. finalizingis a frontend inference, not a wire status.components/archive.tsderives it from awritingrow whose session already hasendedAt, which is the one state the backend cannot name — the row is still open while the writer is closing it.canViewArchiveexcludesnone/removed;canDeleteArchiveallowscomplete/partial/unknown.- Verification: root crate fmt and clippy clean with 38 tests; desktop crate fmt and
clippy clean with
238 passed; 0 failed; 1 ignoredout of 239 forcargo test --all-targets(99archive, 31storage, 26commands, 17state, 13processes, 12discovery, 11archive_service, 7 eachimport_observation/models/tests, 3 eachlanguage/single_instance, 2privileges, 1error);npm run lint,npm run typecheck,npm test -- --runat 22 files / 157 tests (29 of them the two new archive files),npm run build,npm run e2eat 6 passed, andnpm run tauri build. All green. - Local acceptance: all seven criteria proven on an isolated demo build
(
com.abysswhale.runcove.demo0819), with no production RunCove running, no real database opened, and no existing developer process touched. Evidence and the reproduction path are in the top section ofHANDOFF.md. - The initialization-failure case was proven without deleting anything.
run-log-archiveswas renamed aside and a 15-byte file left in its place, so the failure was real (os error 183) while every archived byte stayed on disk. Port scanning, project launch, and the in-memory drawer were all unaffected; the placeholder never grew, and both archives measured byte-identical afterwards. - Residual limitations, recorded and not blocking the demo. P2:
line_countcan over-count a session whose closing flush failed — a small record is counted whenwrite_allreturns and a partial flush cannot be attributed per line; the byte side self-corrects from the disk and the normal path is unaffected. P2: the process-exit toast reads English ("Process exited normally") under a Chinese UI, because that string is composed in the backend. Measurement trap: on NTFS,Get-ChildItemreportsLength = 0for an archive whose writer handle is open — the directory entry is stale until the handle closes, so an open archive must be measured through an opened handle, which is what RunCove's own reader does.
- Scope:
apps/desktop/src-tauri/src/archive.rsonly —ArchiveWriter::close(archive.rs:2567) andclose_all(:2694), the four private helpers they are built from (begin_close:2729,write_taken:2773,write_residual_gap:2806,append:2837), theClosingSessionstruct (:1655), theworsenedfold (:1680), and three behavior-preserving refactors (take_session:1417,pending_write:1625,next_write:2093). Notodo!remains in the module. Untouched: theStorage-backedArchiveIndex, the commands, the frontend, CI, the release workflow, tags,.env, and any real database. No commit, no push.lib.rsunchanged, so the feature still has no runtime caller. - Decision: the close holds
pump_lock, and that is not optional. The pump borrows the session's handle out of its slot for the length of a write. A close that did not serialize against it could reachbegin_closemid-write, findslot.file == None, and silently write nothing — every record it had accepted lost with acompleterow over an incomplete file. The lock order ispump_lock → open → queue → total. - Decision: the boundary marks and extracts in the same critical section. Marking
Closingtellsenqueueto refuse; taking the session's records tells the close what it owes. Doing them separately leaves a window in which a record is neither refused nor written — stranded in the queue, blockingfinish_session, holding room. Sobegin_closedoes both under the open lock and the queue lock, and does all the file work after releasing them. - Decision: a refusal is inert, and that is the whole of the double-close rule. A
slot that is missing,
Opening, orClosingis refused with a per-state message and nothing else happens: no file, no counter, noindexcall. So a second close cannot overwrite the first one's row, andclose_allwith nothing open isOkand equally inert. - Decision: the first write failure ends the session, and everything behind it is
charged. A handle that has refused bytes will refuse them again, so retrying inside
a close would only stall it. The remaining records are
discarded — charged to the row's drop counters — rather than dropped silently, because a line that left a capture thread has to end up in a counter somewhere. - Decision: the residual gap belongs to this close and to no other. The trailing
run of losses is written as one
LogStream::Systemline whose text isgap_line(gap)and whose timestamp isended_at, then counted intoline_countand charged to the quota.writer_closestill writes none: it is closing because the disk refused a write or the cap refused a byte, and such a file must not be asked for one more line. The distinction is the file's state, not a change of policy. - Decision: a close does not consult the quota. Its records were accepted while the
session was open, and its linearization point is already behind it, so a refusal would
have nowhere to go but the drop counters of a session that did nothing wrong. Worse,
room_forcan fail retryably through eviction, and a close cannot retry. The overshoot is bounded — one session's queued bytes plus one gap line — and every byte is still charged, so the nextpumpand the next sweep both see the truth. - Decision: the verdict accumulates.
worsenedfoldsQueueOverflowwhen the drop counters are non-zero andWriteErrorwhen the file refused or could not be synced, on top of the caller'sreason;Completeiff nothing folded.most_severeis a total order, so the result is fold-order independent — a cleanclose_all(UserDisabled)still reportsuser-disabled, and a session that also lost lines reports the worse of the two. - Decision: the slot is removed before the row is written. An index failure then
returns
Errover durable bytes and awritingrow — precisely what the startup sweep repairs topartial/interrupted— instead of leaving a session that is open, unclosable, and undeletable. The user-visible consequence is that a failed close is retryable by restarting, never by closing again. - Decision:
finish_session'sErris defaulted rather than propagated, and the comment says why. It refuses only while the session still has queued records, and the boundary took every one of them, so it is unreachable; returning there would leave the slotClosingforever. - One test added,
a_close_writes_the_trailing_gap_no_later_record_could_carry. The 14 red tests never reach the residual gap — every existing gap test has a following record that carries it — so the acceptance criterion "residual gap correct" had nothing behind it. The new test drops two records with nothing after them and pins the file's last line as the gap, itstas the close'sended_at,line_count == 3,byte_size == text.len(), the row's drop counters,partial/queue-overflow, and an empty queue. It uses only existing helpers: no new seam, and no production change was needed to make it pass. - Residual limitation, carried at P2 by the user's instruction. A small record is
buffered, counted into
line_countwhenwrite_allreturns, and can still be lost if the closing flush fails; the byte side self-corrects from the disk, so the error is only ever an over-count on a failed-disk path. No design was expanded for it this round, the normal path is unaffected, and it is documented onreturn_file. - Verification in
apps/desktop/src-tauri: each of the 14 C tests alone (all 14 pass), then the new test alone, then the archive suite 83 passed; 0 failed, thencargo test --lib202 passed; 0 failed; 1 ignored; 0 measured; 0 filtered outout of 203, identical under-- --test-threads=1,cargo fmt --all -- --checkclean,cargo clippy --all-targets --all-features -- -D warningsclean with no#[allow]added. The first--all-targets --all-featuresrun failedcommands::tests::manual_start_stays_starting_until_managed_expected_port_is_ready(commands.rs:2108, the child bound its port before the check that it had not yet); it passed alone three times and the whole matrix passed on retry. That test was neither read for this purpose nor modified, and no stable full-suite all-green baseline is claimed.
- Scope:
apps/desktop/src-tauri/src/archive.rsonly — the queue's six settle bodies with the reservation accounting,ArchiveWriter::enqueueandpump, theClosingstate and the private writer-initiated close, and the quota's eviction. Tentodo!bodies became two: the caller-facingclose(archive.rs:2506) andclose_all(:2513), which are slice C. Untouched: theStorage-backedArchiveIndex, the commands, the frontend, CI, the release workflow, tags,.env, and any real database. No commit, no push.lib.rsunchanged, so the feature still has no runtime caller. - Decision: a record for a session that is not
Openis ignored, not charged. The queue's bound refusals are charged as before, but a record whose session isClosingor gone is dropped without a counter, because the drop counters live in that session's row and a closed session's row is already written — charging it would produce a number nobody can ever read. This is the one place where "every line ends up in a counter" does not hold, and it holds nowhere else: the three unreachable arms insidepump(a slot that vanished between two locks, a missing handle, a missing slot on the way back) all charge rather than drop. - Decision:
enqueueis hand-over-hand, andstateis what it reads. It holds the open-session lock only to teststate == SlotState::Open, takes the queue lock, and drops the open lock before touching the queue, so the fixedopen → queueorder holds with no window where both are free. It readsstate, never the presence of the file handle, because the pump borrows that handle out of the slot for the length of a write — a record arriving mid-write is stillOpenand must be accepted. That is what makesSlotStateload-bearing rather than decorative. - Decision:
room_fortakes bytes, not a session id. The per-session cap is checked first because no eviction can relieve it — removing someone else's archive does not shrink this session's file — and only then the directory total, in a loop that evicts one archive per turn. Neither cap is about who is writing, so the function takes the two numbers and nothing else. The first draft passed the session id through and silenced it withlet _ = session_id;; that is exactly the bypass marker this repo forbids, so the parameter was removed instead of suppressed. - Decision: an
Unavailabletotal refuses without evicting. A directory whose size could not be measured must not be grown, and it must also not be emptied on a guess: evicting archives to make room under an unknown total would delete real logs to satisfy arithmetic nobody has. So the session closespartial/quota-exceededand only the next startup sweep can restore a real total. - Decision: eviction filters before it sorts, and credits the disk. Candidates are
narrowed to
Complete/Partialrows with a non-nullended_atthat this writer does not hold open, and only then ordered byended_at,started_at,session_id.index.rows()is read before the open-session lock so the lock order survives. The file is resolved throughverified_file_name— a row naming another session's file is reported, not skipped, becausefile_nameis the single field that decides which bytes go — measured, removed, credited at its measured length, and only then markedremoved/quota-evicted. The credit precedes the row write because the bytes are gone whatever the index answers. - Decision: the writer-initiated close writes no gap line.
writer_closetakes both halves of the drop history fromfinish_sessionand deliberately leavesresidual_gapunwritten. A session closing because its disk failed or because the quota left it no room cannot be asked to append one more line — the write that would carry the gap is the write that just failed, or the byte the quota just refused. So the row'sdropped_lines/dropped_bytesare the only surviving record of the loss, which is why every writer-close path charges the batch remainder instead of discarding it. - Decision: the failure order inside a write is handle, charge, close.
write_onehands the file handle back before charging the record and closes only after, becausewriter_close's critical section callsdiscard_sessionand thenfinish_session, andfinish_sessionrefuses while that session still has records queued. Charging first is what leaves nothing behind for it to refuse over. - Decision:
finish_batchflushes once per session, and a failed flush is that session's close. Oneflushand oneupdate_countersper session that actually wrote, rather than per record, so a batch of a hundred lines makes one index write. A flush that fails becomeswriter_close(WriteError)for that session and the batch continues with the others — a write error is a session-level fact, never a pump error, sopumpstill returnsOk. - Residual limitation, recorded rather than papered over:
line_countcan overstate a session whose closing flush failed. A record small enough to fit the 64 KiBBufWriteris counted intoline_countwhenwrite_allreturns, before the bytes reach the disk. If the batch's closing flush then fails, the close measures the file and correctsbyte_sizeand the directory total from disk, butline_countcan name a line the file does not hold. A pending-line counter would not close the gap either, because a flush can go out partially — saying which buffered lines survived needs each line's byte length, i.e. per-line state the writer does not keep. No current test covers this case, it is reachable with the existingfail_write_ofseam, and it is documented onreturn_file. The exposure is bounded: at most one buffer's worth of lines, in one session, on a failed-disk path, and only in the direction of over-counting what was written — no line the pump refused goes uncounted. - The authorized test rewrite, and its limit.
ArchiveQueue::drainis removed and asettle_all(&mut queue)helper (begin_batch, thentake_front+releaseuntil empty) replaced it at all six call sites, withlines_and_gapsrebuilt on it. Inevery_short_enqueue_sequence_keeps_the_queues_invariantsthe localdrainedbecamesettledand three assertion messages were reworded ("a settled queue holds nothing", "settled in arrival order", "the gap each settled record carries"). That is the whole of the permitted change — "drain 后为空" → "settle 后为空". No assertion was weakened, retargeted, or removed, and the exhaustive sweep still checks every invariant it checked before.allow_write_ofwas not added, as instructed. - Two visibilities narrowed, closing the debt CLAUDE.md recorded.
ArchiveWriter::queueis now a private field read byenqueue,pump, andwriter_close, andArchiveQueue::finish_sessionis module-private withwriter_closeas its production caller.ArchiveQueue::peek_frontremainspubwith test-only callers —next_writeuses the module-privatefront(), which yields the whole record rather than a name and a length — and being on apubtype, that is notdead_code. - Verification in
apps/desktop/src-tauri: each of the 15 target tests run alone first, all 15 green individually, plus the rewritten exhaustive sequence test; then187 passed; 14 failed; 1 ignored; 0 measured; 0 filtered outout of 202 for the lib target, identical in parallel, under-- --test-threads=1, and under--all-targets --all-features;cargo fmt --all -- --checkexits 0 andcargo clippy --all-targets --all-features -- -D warningsis clean with no#[allow]added. The archive suite is 82 tests, 68 green / 14 red, up from 53/29 — 172 + 15 = 187, 29 − 15 = 14, so exactly the fifteen turned and nothing regressed. All 14 failures aretodo!panics: 14panicked atlines, 14not yet implementedlines, zero assertion lines; sitesarchive.rs:2506(close) ×12 and:2513(close_all) ×2, three of the twelve reported on the close-race seam's helper thread.commands::tests::external_termination_with_verified_identity_stops_tree_and_releases_portpassed in all three modes here, but it kills a real process tree and binds a real port and failed on the reviewer's machine, so no stable full-suite all-green baseline is claimed andcommands.rswas not touched.
- Scope:
apps/desktop/src-tauri/src/archive.rsonly, and deliberately red — the five corrections the review attached to its refusal of the B/C plan, as seven new tests, two new test seams, one shared assertion helper applied to seven existing close tests, one test renamed and tightened, and the API surface the retry contract needed in order to be stated at all. No body was implemented. Untouched: theStorage-backedArchiveIndex, the commands, the frontend, CI, the release workflow, tags,.env, and any real database. No commit, no push. - Decision: the queue owns every record until it is settled; the pump owns none of
them. The earlier sketch —
in_flight: usizeon the queue and a localVec<QueuedRecord>insidepump— cannot survive its own failure: apumpthat returnsErrdrops that vector and the records with it. Sobegin_batchmoves the queued records into an in-flight list and frees nothing,peek_frontandtake_fronthand them out without freeing anything, and exactly two calls settle a record —release(written) anddiscard(lost, and charged). A failed pump therefore needs no undo: nothing has to be put back, the nextbegin_batchappends new arrivals behind what the last one left, and the retry resumes at the same record in the same order.requeue_frontwas rejected for the same reason it looked convenient — a record leaving the queue and coming back is two chances to reorder or lose it. - Decision: the four bounds count reservations, not queue length. A record whose
fate is undecided still occupies memory, so
len(),is_empty(), andqueued_bytes()must report queued plus in-flight plus taken-but-unsettled, andenqueue'stotal_recordstest must stop readingself.queued.len(). If any bound stopped counting an in-flight record, a long run of failing pumps would admit an unbounded number of records behind the batch it cannot write, which is the exact failure the bounds exist to prevent. Every existing green queue test keeps passing unchanged, because with no batch in flight the two readings coincide. - Decision: a failed removal is retryable; nothing eligible is terminal. These are
different states and the writer must treat them differently — report the first and try
again on the next tick, close the session
partial/quota-exceededonly for the second. On Windows a removal usually fails because someone else holds a handle for a moment (a scanner, the indexer), and discarding a session's logs over another file's handle is the wrong trade. What makes the retry safe rather than unbounded is the reservation accounting above: memory is capped by the bounds however long the transient lasts. - Finding: only one injectable failure is actually repeatable. Two obvious
candidates are not.
index.fail("mark_removed")progresses — the file is removed and its bytes credited on the first round, so a second round needs no eviction at all — and a write error is terminal for its session, which closes and can never fail again. Only an eviction whose candidate exists and whose file will not go leaves the writer in the state it started in, so stickyTestFs::fail_removeis the seam the repeated-failure test uses. - Decision:
verified_file_namegates eviction, and a crossed row is reported rather than skipped.file_nameis the one row field that decides which bytes go, and the row is data out of a database this build does not exclusively own, so the ownership gate belongs on the eviction path exactly as it does onreadanddelete. Skipping to the next candidate would make a corrupt row invisible; returningErrmakes it visible, and the writing session's record has to survive thatErrstill queued so the retry after the row is repaired writes it exactly once. - Decision: after a short write,
byte_sizeis the residue the disk actually holds and the incomplete record is a drop. Awritemay legally accept less than it was given, so a fragment of a line can be on the disk with the rest gone. Counting the fragment's bytes keeps the hard quota from being under-counted; counting the record as a dropped line keepsline_counthonest, because a fragment is not a line. This has a consequence B must face:io::Write::write_allnever reports how much it wrote, so the write-error close has to measure the file (or write through a counting loop) rather than assume the buffer went in whole. - Decision: no
allow_write_ofseam. A write failure is terminal for its session, so nothing in the writer could ever recover that file's writes and no test can observe a recovery. An uncalled test helper isdead_codeunder-D warnings, and an#[allow]bypass is not permitted here, so the approved by-name injection landed asfail_write_ofandshort_write_then_fail_ofonly. Both inject by file name rather than by call count, so a two-session test says which session's disk failed instead of depending on which one the writer reached first. - Decision: the closing boundary is a deterministic refusal. Because the committed close marks a session closing and extracts its accepted records in one critical section, a record arriving during the file work that follows can only be refused. The boundary test's permissive "written or refused" branch was therefore replaced by a single equality — a tightening, not a retarget: the outcome it used to allow as a second correct answer is the one the design now rules out.
- Residual limitations.
every_short_enqueue_sequence_keeps_the_queues_invariantswas not converted to the batch protocol this round: atodo!at the first sequence'sbegin_batchwould abort the test before any sequence finished and cost a round of its enqueue and bounds coverage, while the reservation contract is already fully stated by the new dedicated tests. It converts in B, green in the same round.ArchiveWriter'squeuefield ispubfor this one slice, for the same reasonArchiveQueue::finish_sessionis, and becomes private whenenqueueandpumpare its production readers;drainis superseded and should go in that same slice. Tentodo!bodies remain — six inArchiveQueue, four inArchiveWriter— and nothing in the application calls this module, so none of it can be switched on by accident. - Verification in
apps/desktop/src-tauri:cargo fmt --all -- --checkexits 0,cargo clippy --all-targets --all-features -- -D warningsis clean, andcargo test --lib --all-featuresreports172 passed; 29 failed; 1 ignored; 0 measured; 0 filtered outout of 202 — identical in parallel, single-threaded, and under--all-targets --all-features. All 29 failures aretodo!panics (29panicked at, 29not yet implemented, zeroassertion/left ==lines), atenqueue×23,begin_batch×3,close×2,peek_front×1. The archive suite is 82 tests, 53 green / 29 red, with the green count unchanged from slice A.commands::tests::external_termination_with_verified_identity_stops_tree_and_releases_port, which the reviewer saw fail with a Windows "Access denied", passed here in all three run modes and again alone; it kills a real process tree and binds a real port, so it is environment-dependent and flaky, and no stable all-green baseline is claimed for it.commands.rswas not modified.
- Scope:
apps/desktop/src-tauri/src/archive.rsonly — the quota total behind a mutex,credit_removed,read,delete, and the sharedrow_ofthey both start from, plus the module header, two stale placeholder comments removed, and one in-test comment whose number was wrong. No test was added, weakened, or retargeted.pump,close,close_all, and eviction were deliberately left alone, so fourtodo!bodies remain instead of six. Untouched: theStorage-backedArchiveIndex, the commands, the frontend, CI, the release workflow, tags,.env, and any real database. No commit, no push. - Decision: implement the two paths that only ever take bytes away, before the one
that adds them.
readanddeleteneed no queue, no buffered writer, and no per-session counters; what they need is the ownership gate and the quota total. Slicing them off first means the total's interior mutability arrives with its one real writer — the delete that decrements it — instead of as a field nothing yet uses, and it puts the wholerow → verified_file_name → resolve_ordinary_archive_filechain under test before any code depends on it. - Decision: the quota total is a mutex and the leaf of the lock order. A delete
arrives on a command thread and gives bytes back while a pump on another takes
them, so the number cannot belong to one thread. Nothing else is taken while it is
held and it is never held across a file operation or an index write, so the two
callers never wait on each other's disk.
total_bytesreads through it rather than copying a field. - Decision:
credit_removedsaturates, and an unavailable total stays unavailable. A total that has drifted below the length of the file just removed must not wrap into a number the size of the disk, so the subtraction saturates. And a delete reports how long one file was, not how much the directory holds — nothing about it can make an unmeasurable directory measurable again, soQuotaTotal::Unavailableis left alone and only the next startup sweep recovers a real total. That is the same asymmetry the sweep already states: an unknown total means "no room", and guessing is worse than refusing. - Decision:
delete's open-session refusal is a check before the work, and the reason it is safe is written down. No state lock spans a file removal, so the check and the removal are not one critical section. What closes the gap is not the check alone: an archive file is created withcreate_new, so abeginracing a delete cannot take the file that is still there, and a session id is generated once for the run that produces it, so the id a user deletes is not one a later run begins. Stating the two real reasons is better than claiming a critical section the code does not have. - Decision: a row whose file is already gone is reported, not quietly marked
removed. The startup sweep already finishes that state, and it finishes it as
removed/file-missing— which is what happened — rather than recording a user delete that removed nothing. The writer's job here is to refuse to guess. - Residual limitation. The slice turns five tests green, not the six the plan
claimed.
deleting_an_archive_is_refused_while_its_writer_is_openasserts the refusal, then closes the session and deletes again, so its panic site moved fromdeletetoclose; its delete half passes and the test stays red until thecloseslice lands. Nothing in this slice has a runtime caller yet, sincelib.rsstill only declares the module, soreadanddeleteare proven by their tests and not by use.readloads the whole archive into aString, which is bounded by the 10 MiB per-session cap and is the shape the plan's viewer expects; a streaming read is not in v0.3.0's scope. - Verification in
apps/desktop/src-tauri: each of the six target tests was run alone first — fiveok, the sixth failing atclose. Thencargo fmt --all -- --checkandcargo clippy --all-targets --all-features -- -D warningsclean;172 passed; 22 failed; 1 ignored; 0 measured; 0 filtered outout of 195 for the lib target, identical in parallel, single-threaded, and under--all-targets --all-features. Every failure is atodo!panic —enqueue×20,close×2 — with zero assertion lines in any run. The archive suite is 75 tests, 53 green / 22 red; four are#[cfg(windows)], so a non-Windows host runs 71.
- Scope:
apps/desktop/src-tauri/src/archive.rsonly — six new red tests, one new capability in the filesystem double, a renamed gate, two rewritten doc comments, one new session id. No production body was written:ArchiveWriter's sixtodo!bodies are the same six. Untouched: theStorage-backedArchiveIndex, the commands, the frontend, CI, the release workflow, tags,.env. No commit, no push. - Decision: the closing boundary is a linearization point, and the rejected design
was a real defect rather than a style preference. Draining a session and marking it
closed afterwards leaves a window between the last drain and the state change,
because
enqueuedoes not take the pump's lock. A record accepted in that window is neither written nor refused: it sits in the queue of a session that will never pump again, which blocksfinish_session, strands the entry, and holds queue room other sessions need. The correction is to mark the session closing and extract everything it has accepted inside one critical section, under the fixed open → queue lock order, and to do the file work only after both locks are released. - Decision: a writer-initiated close counts every record it accepted and never
wrote. A write error or a crossed quota stops a session in the middle of a batch
while
enqueueis still accepting, so records exist that the archive took responsibility for and did not persist. They are charged todropped_lines/dropped_bytes. Discarding the batch remainder without counting it was rejected: the row would then report a smaller loss than the archive actually took, and a partial archive that under-reports is indistinguishable from a complete one, which is the one thing the drop counters exist to prevent. A quota close writes no gap line for those records — the file is stopping precisely because it is at its cap, and a gap line is more bytes in that same file — so the row's counters are the entire record of the loss and have to be exact. - Decision: eviction filters on eligibility before it sorts. A session is a
candidate only when its status is
completeorpartialand itsended_atis non-null, and never while this writer holds it open. The order isended_at, thenstarted_at, thensession_id: the question eviction asks is which archive has been finished longest,started_atonly breaks a tie between archives that ended together, and the session id makes the choice deterministic when both agree. Reading a missingended_atas zero would invert the rule and delete the row this build understands least, first. - Decision: freed bytes are the disk's number, not the row's. When the writer
deletes an archive file and the row update that follows fails, the in-memory quota
total is credited with the length measured on disk. The row's
byte_sizecan be stale by any amount, and crediting it would make the archive refuse room the directory actually has. The inconsistent row is left for the next startup sweep, which already knows how to mark a row whose file is gone, and the failure is reported rather than swallowed. Both paths obey this: quota eviction and explicit delete. - Decision: state
enqueue's guarantee as "no I/O", not "never blocks". It takes the open-session lock and then the queue lock, because whether a session is still accepting is part of the queue's answer, so it is not lock-free and calling it non-blocking is false. What it guarantees is that a capture thread never waits on a disk or a database. For the same reason "no lock is held across I/O" is scoped to the three state locks — open sessions, queue, quota total — while the pump's lock spans the writer's file and index work by design. An overstated invariant is worse than a modest one, because the next reader designs against it. - Residual limitation. All six tests are red at a
todo!, and five of them stop atenqueue— the first unwritten body they touch — so the machinery they were written to exercise is still unexercised. That includes the new seam:TestFs::hold_write_ofholds one named file's nextwrite, which is the only place a test can stand inside the post-drain, pre-closing window, since async_datahold arrives after the state has already changed under every ordering. Neither race test reaches its gate yet. The boundary test deliberately permits two outcomes, written or refused, and forbids only the third, stranded in the queue; which of the two the writer picks is still an open design choice. And the eviction filter'sended_athalf is asserted against a row RunCove's own schema cannot store —CHECK ((status = 'writing') = (ended_at IS NULL))rejects acompleterow with a nullended_at— so it is tested as what it is, a defence against a database this build did not write. - Verification in
apps/desktop/src-tauri:cargo fmt --all -- --checkandcargo clippy --all-targets --all-features -- -D warningsclean;167 passed; 27 failed; 1 ignored; 0 measured; 0 filtered outout of 195 for the lib target, identical in parallel, single-threaded, and under--all-targets --all-features. Every failure is atodo!panic —enqueue×20,delete×5,close×1,read×1 — with zero assertion lines in any run. The archive suite is 75 tests, 48 green / 27 red; four are#[cfg(windows)], so a non-Windows host runs 71.
- Scope:
apps/desktop/src-tauri/src/archive.rsonly, tests and test seam only. Six new red tests, one new capability in the filesystem double, one new bounds fixture. No production body was written:ArchiveWriter's sixtodo!bodies are the same six. Untouched: theStorage-backedArchiveIndex, the commands, the frontend, CI, the release workflow, tags,.env. No commit, no push. - Decision: state the closing boundary as tests before implementing the writer.
The write path's hardest question is not how a line is written but what may follow
a close, because that is where three pieces of state can disagree — the queue's
per-session entry, the file handle, and the row. Three cases are now pinned: a
late record after
close_all(UserDisabled), a record racing a close that is already insidesync_data, and closing a session that produced nothing or has already been closed. In each case the file, the row (drop counters andended_atincluded), and the index call log must be exactly what the close left. - Decision: assert the queue's state through the bounds, not through a field the
writer does not have.
ArchiveWriterstill owns noArchiveQueue—initializediscardsboundsandlimits— so a test cannot read the writer's queue, and adding an accessor for the test to read would be inventing production API for the test's benefit.one_slot_bounds()(one queued record in total) reverses the problem instead: a record wrongly queued for a closed session would be occupying the only slot, so the next live session loses a line, and that loss is an ordinary assertion on that session's row and file. Two of the six tests read the boundary that way. - Decision: the concurrency seam is a rendezvous, not a sleep.
TestFs::hold_sync_of(file_name)holds the nextsync_dataon one named file until the test lets it go, so a close can be stopped exactly where it has drained and flushed the session and has not yet released the handle or written the row. Three properties keep it honest: the gate is taken out from under the state lock before it blocks (no lock is held across the pause, so the concurrentenqueuecannot deadlock against the double); it fires once and by file name, matching the double's existing "by call count or by name, never by timing" rule; andSyncHold::wait_forre-raises the closing thread's panic withresume_unwindwhen that thread finishes without reaching the seam, so an unimplemented body is reported rather than hung on. The 25 ms poll insidewait_foris a liveness check, the only duration in the file, and no assertion depends on it. - Residual limitation. The seam is not yet exercised by anything: both race
tests panic at
enqueueon the main thread before the close thread is spawned, so the gate is machinery waiting for the slice that makesenqueuereal. Whether the writer refuses or ignores a record for a closing session is also still open — the tests deliberately pin the consequences (nothing in the file, nothing in the counters, no room taken) rather than the choice. - Verification in
apps/desktop/src-tauri: fmt clean (one rustfmt diff — a stray blank line — applied and re-checked), clippy clean under--all-targets --all-features -- -D warnings,cargo test --libat167 passed; 21 failed; 1 ignoredout of 189 — identical in parallel, single-threaded, and under--all-targets --all-features. All 21 failures aretodo!panics with zeroassertionlines:enqueue×15,close×1,read×1,delete×4. The archive suite is 69 tests, 48 green / 21 red.
- Scope:
apps/desktop/src-tauri/src/archive.rsonly, in response to a review finding against the queue slice below. One new operation, one new value type, six new tests, and the doc comments that stated the behavior it replaces. Untouched:ArchiveWriter's sixtodo!bodies, theStorage-backedArchiveIndex, the commands, the frontend, CI, the release workflow, tags,.env. No commit, no push. - The finding, accepted.
sessionsheld aSessionQueuefor every session the process had ever seen anddrainwalked all of them on every pump, so both memory and pump cost grew with historical session count rather than with the number of open sessions. The queue slice's notes below state that as intentional; this section supersedes that. - Decision: one terminal operation that returns both halves, or the leak comes
back.
finish_session(session_id) -> AppResult<FinishedSession>whereFinishedSession { residual_gap: Option<DropCounters>, dropped: DropCounters }. Two accessors would have been the obvious shape —take_pending_gapalready returns the residual anddroppedalready returns the totals — and it was rejected: a caller that took one and forgot the other would either write a gap line twice or file a row whose counters no longer match the file, and neither accessor can free the entry safely on its own.AppResultrather thanOptionis not a#[must_use]argument —Optioncarries that attribute too. It is that there are three outcomes and anOptionholds only two: a residual gap to write, nothing owed (Ok(FinishedSession::default()), for a session that never queued anything or has already been finished), and not now (Err, records still queued). Folding the third into the second is precisely the mistake that would let a close file its row while bytes were still owed. - Decision: refuse while records are queued, and take nothing when refusing.
Queued records still owe their bytes and their gaps, so forgetting the session
before a pump would lose both. The check reads
session.recordsbefore anything is taken, which makes "pump, then finish" a safe retry instead of a race with a half-consumed entry. The refusal is per session, so a batch shutdown can finish the sessions that are drained without waiting on the ones that are not. - Decision: a finish is final, and that is a constraint on the writer, not on the
queue. A second finish, or a finish for a session never seen, yields
FinishedSession::default()— owed nothing, no losses — which is what makes writing one gap twice impossible. The price is that a record arriving after a finish gets a fresh entry with no history and carries no gap, so the writer must finish a session only once that session's capture threads can no longer enqueue. That price is written down as the testa_record_after_a_finish_starts_a_fresh_entryrather than as a comment, andArchiveWriter::close's doc comment now namesfinish_sessionas the close path so the next slice cannot reintroduce the leak by reaching fortake_pending_gap. - Deviation from the instruction. The patch was requested with a 私有 (private)
terminal operation.
finish_sessionispubinstead: with no production caller yet, a private orpub(crate)method isdead_codeunder-D warnings, and the alternatives were an#[allow]bypass marker (forbidden here) or writing the writer (out of scope this round). What holds today is that the method has no runtime caller — every call site is a test — not that apubmethod of apubmodule is unreachable; it is reachable by definition. The narrowing to module-private is owed the moment the writer becomes its production caller, which is the next slice. - Residual limitation, unchanged from below. Nothing writes a gap line yet.
pumpis stilltodo!, so the gap's on-disk placement stays pinned in the queue's output and in the still-redgap_records_sum_to_the_dropped_counters_of_a_closed_archive. - Verification in
apps/desktop/src-tauri: fmt clean (one rustfmt diff in a new test applied and re-checked), clippy clean under--all-targets --all-features -- -D warnings,cargo test --libat167 passed; 15 failed; 1 ignoredout of 183 — identical in parallel, single-threaded, and under--all-targets --all-features. The 15 failures are the same writertodo!panics (enqueue×10,delete×4,read×1) and the run holds zeroassertionlines. The 17 queue tests run focused are17 passed; 0 failed. The archive suite is 63 tests, 48 green.
- Scope:
apps/desktop/src-tauri/src/archive.rsonly. The nineArchiveQueuebodies, the gap carrier the contract needed, five new tests, and one rewritten assertion. Untouched, as instructed:ArchiveWriter'senqueue,pump,close,close_all,read, anddelete; theStorage-backedArchiveIndex; the commands; the frontend; CI; the release workflow; tags;.env. No commit, no push.lib.rsis unchanged, sopub mod archive;remains the module's only reference in the crate and the feature is still unreachable from the application. - Decision: the gap rides on a wrapper, not on
ArchiveRecord.V0.3.0_PLAN.md:337-341had left both open; that paragraph now records the resolution and reads atV0.3.0_PLAN.md:335-357.drainnow returnsVec<QueuedRecord>, whereQueuedRecordis{ record: ArchiveRecord, gap_before: Option<DropCounters> }(archive.rs:219). The alternative — agap_beforefield onArchiveRecordthat the capture side never sets — was rejected for two reasons. The gap describes the hand-off, not the line the child process wrote, so a field capture can set but never should is a field that lies; andencode_recordwrites exactly three keys, so a gap sitting onArchiveRecordwould be handed to it and silently discarded, while a caller holding aQueuedRecordmust reach through.recordand therefore seesgap_beforeat the point of use. The predicted cost was paid in full and was nothing more: two test call sites gained one level of nesting, asserting the same values. - Decision: one rewritten assertion, and it is stronger than what it replaced.
every_short_enqueue_sequence_keeps_the_queues_invariantshad required the post-draintake_pending_gapto return everything the session lost. That is false under the approved contract — a loss a later record picked up leaves with that record — and it described the merged-gap design the user rejected, so leaving it would have pinned the losing alternative. The replacement compares the whole drained sequence ofgap_beforevalues against a positional model vector, so the test now checks where each run is reported and not only how much was lost. No other existing assertion changed. - Residual limitation. The queue decides where a gap is reported; nothing yet
writes it.
ArchiveWriter::pumpis stilltodo!, so the claim "the gap line stands immediately before the first line that survived the loss" is pinned in the queue's output and ingap_records_sum_to_the_dropped_counters_of_a_closed_archive— which is still red at the writer — but is not yet demonstrated end to end in a file on disk. Judge the next slice on that test going green without its assertions being touched. - Evidence the new assertions are not vacuous: two mutations were compiled, run, and
reverted. Carrying nothing (the merged-gap behavior) and carrying without clearing
(double-reporting) each failed four of the five new tests plus the exhaustive one;
the exhaustive test caught the second on "the residual is the trailing run, and
nothing else".
a_trailing_drop_has_no_record_to_carry_it_and_stays_the_residualstayed green under both, correctly — it pins the residual half, which neither mutation can reach. Details and the failing traces are inHANDOFF.md. - Verification after the revert, in
apps/desktop/src-tauri: fmt clean, clippy clean under--all-targets --all-features -- -D warnings, andcargo test --libat161 passed; 15 failed; 1 ignoredout of 177 — identical in parallel, single-threaded, and under--all-targets --all-features. All 15 failures aretodo!panics in the writer (enqueue×10,read×1,delete×4) and the run holds zeroassertionlines. The archive suite is 57 tests, 42 green.
-
Scope: the version 2 schema and the documents that describe it. Production code did change — the DDL inside
upgrade_to_version_2(storage.rs:715) is not the text it was, and the pinnedV2_ADDITIONcopy matches it — plus one parametrized rejection case removed and four documents updated. What did not change: no new function body, no new run path, no part of the queue implementation, no schema version, no version 3 migration, no CI, release, tag, or.envfile, no commit and no push. This is the correction the section below paused for, and it happened only after the user approved it, because a schema change is one of this project's stop-and-ask lines. -
The decision, and the alternative it beat. The user answered 「就地改 v2 DDL,不加 v3」: correct the version 2 DDL in place, keep the schema version at 2, add no version 3 step. The alternative was to treat version 2 as frozen and repair it with an
upgrade_to_version_3table rebuild. That would be the right move if a version 2 database existed — SQLite keeps aCHECKinside the table definition, so a DDL edit cannot reach a table that already exists. Neither verified artifact has one: the two developer databases on this machine and the publishedv0.2.1baseline. That is the scope of the census further down, and it is what makes the in-place edit not merely cheaper but complete for the artifacts that were actually checked. -
The new constraint is
CHECK (dropped_bytes = 0 OR dropped_lines > 0). It states the one direction that is genuinely impossible — bytes cannot be lost without a line, because every archived byte belongs to some line — and says nothing about the other, because a dropped empty line costs1 line / 0 bytesand is ordinary data. The old form,((dropped_lines = 0) = (dropped_bytes = 0)), collapsed that implication into an equivalence, which is the whole of the bug. Both copies now carry a four-line SQL comment saying so, in English like the rest of the repository, so the next reader does not re-derive it. -
Applied at five sites in one round, because nothing in the crate compares them to each other: the design DDL (
V0.3.0_PLAN.md:634), the prose invariant restating it (V0.3.0_PLAN.md:844-846), the production migration (storage.rs:715), the pinnedV2_ADDITIONcopy (storage.rs:1240), and the"drop counters agree"case inthe_archive_index_rejects_impossible_rows. The migration and the pinned copy are now held to the same rule by two tests that each run against both (an_archive_row_may_lose_a_line_that_carried_no_bytesandan_archive_row_may_not_lose_bytes_without_losing_a_line, throughcreate_pinned_version_2_database), so the next half-applied edit fails instead of drifting. -
One existing assertion was removed, deliberately and with approval. The
"drop counters agree"case required('sess-exited','a.jsonl','complete',NULL,0,0,5,0,10,11)— five dropped lines, no dropped bytes — to be rejected. That row is five dropped empty lines, which is ordinary data, so the case rested on the same wrong premise as the constraint and could not survive the fix. A comment atstorage.rs:1553records what stood there and names the two tests that now pin both directions, so the removal reads as a contract change rather than as a test that quietly went missing. It is the only assertion this round touched; nothing was weakened, retargeted, or#[ignore]d to reach green. -
Residual limitation, recorded rather than fixed: nothing in the build compares the plan's DDL text to the crate's. The three-way agreement between
V0.3.0_PLAN.md, the migration, andV2_ADDITIONis maintained by hand and by review. Only the last two are pinned against each other, and only through behavior — no test reads DDL text out ofsqlite_master, which is also why SQL comments could be added inside the table definition without breaking a comparison. -
Verification, in
apps/desktop/src-tauri:cargo fmt --all -- --checkclean;cargo clippy --all-targets --all-features -- -D warningsclean with zero warnings and noallowadded;cargo test --libat150 passed; 21 failed; 1 ignored; 0 measured; 0 filtered outout of 172, identical in parallel and with--test-threads=1. The change is visible as exactly one test flipping — the previous149 passed; 22 failed; 1 ignoredminus the storage failure that had been quoting the old constraint verbatim. The other 21 reds are unwritten step 4b bodies (archive.rs:1098×6,:1355×10,:1410×1,:1420×4, every one atodo!), and the suite output holds zeroassertion,left ==, orleft !=lines. Module counts are unchanged at archive 52, storage 24, processes 12, because a parametrized case was removed, not a test. -
Safety was established before the edit rather than inferred from the result: all four
INSERT INTO run_log_archivessites and every row they carry were enumerated, and none writes bytes without a line, so no previously accepted row became rejected. The five storage tests touching the changed DDL also pass individually under--exact, so none is riding on another test's side effect. -
The census that licenses the in-place fix, re-taken on 2026-08-17 from the SQLite header (bytes 60..63 of page 1, no connection opened): both developer databases, under
%LOCALAPPDATA%\com.abysswhale.runcoveand...com.abysswhale.runcove.qa, are atuser_version = 1, and neither directory holds a-walor-shmfile, so the header is authoritative rather than a stale page. The version 2 code is unreleased and uncommitted, and the publishedv0.2.1baseline never mentionsrun_log_archives, so no v0.2.1 install can hold a version 2 database either. Stated at its true scope: the verified local developer databases and the published baseline have no version 2 database. Machines that were never measured are not covered, which is exactly why the rebuild recipe is kept inHANDOFF.md— for this working tree copied elsewhere and run there. -
A separate, smaller pass in the same round repaired stale line-number citations in
V0.3.0_PLAN.md— some shifted by this round's four added comment lines per DDL copy, some already stale from the blocker round, one by 128 lines. The migration-test list now names all nine tests instead of listing bare offsets, and the note above it says outright that the names are authoritative and the numbers rot with each slice. Two factual errors in the plan went with it, both exposed by this round's own evidence: the data directory does not hold-waland-shmcompanions (Storage::opennever setsjournal_mode), andthe_archive_index_rejects_impossible_rowsnow carries eight cases rather than nine. Neither correction changes a design decision. -
Second decision, recorded and not implemented: the gap partition. The user chose 「gap 挂到下一条记录上」 over the recommendation to merge. Pending drop counters attach to the next accepted record for that session, so
take_pending_gapreturns only the trailing residual, which the writer flushes at close. This buys exact placement of gap records in the archive file, at the price of a carrier field on the queued record and a rewritten assertion atarchive.rs:3369. It is the queue slice's first move, and the slice has not started: the user asked to stop for review once the schema correction landed.
- Scope: tests and documentation only. Seven tests were added across three files,
four documents were updated, and nothing else changed — no production DDL, no
CURRENT_SCHEMA_VERSION, no queue or writer body, no CI, release, tag, or.envfile, no commit and no push. The round exists because the user found a defect in the design before the queue was written, and the cheapest place to record a design defect is a failing test that says what the code should do. - The defect. The version 2
run_log_archivestable ends withCHECK ((dropped_lines = 0) = (dropped_bytes = 0)). Read literally it says: a session has lost lines if and only if it has lost bytes. That is a guess about the data, and the guess is wrong, because a line and its bytes are not the same quantity.capture_streamsplits on\nand emits an event per line; a lone newline is a line whose text is empty. Traced by hand and then pinned by test, input"\n"produces exactly one event withline == "", and"a\n\nb\n"produces["a", "", "b"]. Drop one empty record and the honest counters aredropped_lines = 1, dropped_bytes = 0, which the constraint refuses. The archive would have failed to record a loss it was built to record —closewould have written a row the database rejects, on data the queue is specified to produce. - The correct rule is one-directional. Bytes cannot be lost without a line, because
every byte of archived text belongs to some line, so
dropped_bytes > 0withdropped_lines = 0is genuinely impossible and must stay rejected. The reverse is ordinary. The original constraint collapsed an implication into an equivalence, which is the whole bug; it is the same shape of error as writinga == bwhere the domain only justifiesb != 0 -> a != 0. - A knock-on correction to an earlier note: the rejection case labelled
"drop counters agree"inthe_archive_index_rejects_impossible_rows, which pins5 lines / 0 bytesas impossible, is not impossible data. It is five dropped empty lines. That case was written against the same wrong premise as the constraint, and it will have to go when the constraint is fixed. - No analogous defect exists for
line_countandbyte_size. TheirCHECKs are independent (>= 0each), and an archive of five empty lines still has a non-zerobyte_sizebecause each stored record is a JSON object with a timestamp and a stream field around the empty text. So the archive's own counters cannot reach then lines / 0 bytesshape that the drop counters reach naturally. - The defect has three copies, and nothing was comparing them.
V0.3.0_PLAN.md:598holds the design, restated in prose atV0.3.0_PLAN.md:772-773;storage.rs:711is the production migration;storage.rs:1232is theV2_ADDITIONconstant whose stated purpose is that "drift between the two fails a test instead of silently redefining the schema". That purpose was only half met:V2_ADDITIONhad two users, both checking version handling (a_version_2_database_opens_and_only_a_higher_version_is_rejected), and no test compared its text or its behavior to the migration's. A fix applied to production and not to the pinned copy would have gone unnoticed. Both new schema tests therefore run twice — once against a database migrated byStorage::open, once against one built straight fromV2_ADDITIONthrough the newcreate_pinned_version_2_databasehelper — so a half-applied correction fails. - The seven tests, by what they require and where they stop today:
a_lone_newline_is_captured_as_one_empty_log_line(processes.rs:1104) — green, and expected to be. It is a control test: it establishes the premise the whole blocker rests on, so a future change tocapture_streamthat stopped emitting empty lines would show up as a failing premise rather than as silent dead weight in the schema.an_archive_row_may_lose_a_line_that_carried_no_bytes(storage.rs:1592) — red on the constraint, with the panic message quoting it verbatim:CHECK constraint failed: (dropped_lines = 0) = (dropped_bytes = 0). It requires both1 line / 0 bytesand5 lines / 0 bytes.an_archive_row_may_not_lose_bytes_without_losing_a_line(storage.rs:1638) — green, and it must remain green after the fix. Its job is to stop the correction from over-shooting into "no relationship at all": it refuses0 lines / 40 bytesand a negative count in either column.dropping_an_empty_line_counts_one_line_and_no_bytes(archive.rs:3153) — red attodo!inArchiveQueue::new(archive.rs:1098). It requiresDropCounters { lines: 1, bytes: 0 }and the user-visible[RunCove: dropped 1 line / 0 bytes]. Its bounds force the refusal through the record bound on purpose: an empty record consumes no bytes, so a byte bound could never reject it, and a test that tried would be testing nothing.the_queue_counts_utf8_bytes_and_not_characters(archive.rs:3183) — red at the sametodo!. It is written with\u{...}escapes rather than literal characters so the byte counts cannot depend on how this file is encoded, and it asserts its own premise first. A queue that counted characters fails it in two independent places, which is deliberate:queued_bytes()would report 1 for a three-byte character, and a record that must be refused would be accepted.every_short_enqueue_sequence_keeps_the_queues_invariants(archive.rs:3232) anda_pending_gap_is_taken_once_and_the_cumulative_total_is_never_cleared(archive.rs:3388) — both red at the sametodo!.
- The exhaustive test is a deliberate substitute for a property-test dependency, not
a poor imitation of one.
proptestwould bring a new crate, a random seed, and a shrinking step whose output is a fresh puzzle every failure; a full enumeration of a deliberately tiny space brings none of that and fails identically every time. It walks all4^6 = 4096sequences of six records over a four-symbol alphabet — an empty line and a four-byte line on one session, a three-byte multi-byte character on a second, a four-byte line on a third — against bounds{2, 4, 3, 8}chosen so that every one of the four bounds is met exactly by some sequence and exceeded by another. Each step is compared to a model the test recomputes from the records it believes were kept, and every failure message carries a readable trace such as[A:abcd,B:han,A:empty,...] step 3, so a red result names the sequence instead of a seed. Cost is roughly 25,000 enqueues, well under a second. - What it pins: no accepted record puts any per-session or total record or byte count
over its bound; equality with a bound is allowed and only the incoming record is
ever refused, which is checked by comparing
queue.len()with the model at every step so an implementation that evicted something already queued would fail;drainreturns every kept record in global arrival order and resetslen,queued_bytes, andis_empty; a pending gap is taken once andNoneafter, while the cumulativedroppedtotal survives both the take and the drain; anddroppedequals the line count and summed UTF-8 text length of exactly the refused records. - One thing was left unpinned on purpose.
V0.3.0_PLAN.mdpromises onesystemgap record per contiguous gap, but it never settles how a drop → accept → drop run partitions when no take intervenes, andtake_pending_gapreturns a singleOption<DropCounters>per session, so the API has nowhere to hold two. Rather than invent a partition and freeze it in a test, both new tests assert only that a take returns everything lost since the previous take and that gaps sum to the cumulative total, which is whatV0.3.0_PLAN.md:319already requires. If the queue's author later needs a different partition, these tests do not stand in the way. - A contradiction was left standing in the test suite, on instruction and on
purpose. No existing assertion was deleted, weakened, or retargeted, so
storage.rsnow contains two tests that disagree about the same row:the_archive_index_rejects_impossible_rowsrequires5 lines / 0 bytesto be rejected, andan_archive_row_may_lose_a_line_that_carried_no_bytesrequires it to be accepted. Measured, not predicted: the old one passes today, the new one fails. No constraint can satisfy both, so the correction must also delete the"drop counters agree"case — an edit to an existing assertion, which is exactly the kind of change that should be approved deliberately rather than slipped in beside a test-only round. - Verification, in
apps/desktop/src-tauri: fmt clean, clippy clean at--all-targets --all-features -- -D warningswith zero warnings and noallowadded, andcargo test --libat149 passed; 22 failed; 1 ignored; 0 measured; 0 filtered outout of 172 — the same three numbers single-threaded and under--all-targets --all-features. The previous checkpoint was147 passed; 17 failed; 1 ignoredout of 165, so the arithmetic closes exactly: seven tests added, two green, five red, nothing else moved. Module counts are archive 52, storage 24, processes 12; four archive tests are#[cfg(windows)]and nothing added here is platform-gated, so a non-Windows host runs 168. - Failures were classified by panic site rather than by reading names:
archive.rs:1098×6,archive.rs:1355×10,archive.rs:1410×1,archive.rs:1420×4,storage.rs:1620×1. The captured output contains zero lines matchingassertion,left ==, orleft !=. That matters more than the failure count: every red test stops at an unwritten body or at the constraint under discussion, so none of them is making a wrong claim about behavior that exists. - Two small things worth keeping. Clippy's
int_plus_onerejected the model'scount + 1 <= bound, which had been written that way to read in parallel with the byte lines beside it; the resolution wascount < boundplus a comment explaining both readings, not anallow. Andcargo fmt --allreflowed exactly oneassert_eq!in the new gap test and touched nothing else, confirmed by re-running--checkafterwards. - Residual limitation, and the reason this round stops here: the correction is a
schema change, so it waits for the user. When authorized it has five parts, and
omitting any one of them leaves the repository inconsistent — the plan at
:598and its prose at:772-773, the production DDL atstorage.rs:711, the pinnedV2_ADDITIONatstorage.rs:1232, the"drop counters agree"case, and a decision aboutCURRENT_SCHEMA_VERSION. On the last one: v0.3.0 is unreleased, so no user database is at version 2 and correctness needs no version 3 migration — but SQLite stores aCHECKinside the table definition, so any developer database already migrated by an earlier build keeps the wrong constraint however the DDL reads. Editing the DDL alone does not reach it; that database must be discarded by hand or the table rebuilt.
-
Scope:
ArchiveWriter::begin,ArchiveWriter::is_open, and the writer state those two need — the privateOpenArchiveslot, theopen: Mutex<BTreeMap<String, OpenArchive>>map, and thefsandindexhandles the writer now keeps instead of dropping after the sweep. The queue,enqueue,pump,close,close_all,read,delete, quota eviction, theStorage-backedArchiveIndex, the commands, and the frontend are untouched and stilltodo!. Evidence, fromapps/desktop/src-tauri: fmt clean, clippy clean with zero warnings,cargo test --lib --all-features -- --test-threads=1at147 passed; 17 failed; 1 ignored; 0 measured; 0 filtered outout of 165, with the default parallel run and--all-targets --all-featuresreporting the same three numbers and no failure outsidearchive::tests. The archive suite is 48 tests, 31 green / 17 red, up from 24 / 19. All 18 tests this slice could affect were confirmed alone under-- --exact, every one of the 17 remaining failures is atodo!panic, and no run contains an assertion line. -
beginis three phases, and the reason is requirement 8, not tidiness: holding the open-session lock acrosscreate_newandinsert_writingwould put a SQLite write inside a lock that every capture thread wants. So phase one validates the id and resolves the path while touching nothing, phase two takes the session's slot under the lock and releases it, and phase three creates the file and inserts the row with no lock held, re-taking it only to store the handle or drop the slot. -
The slot is what makes concurrency deterministic instead of lucky. It is inserted as
OpenArchive { file: None }before the file exists, so a secondbeginfor the same session finds the key already present and loses immediately. ThatNonewindow is also the only state a concurrent caller can observe, and only to lose, which is why the two refusals read differently: "already has an open archive" when a handle is there, "is already opening its archive" when it is not. Readingfile.is_some()for that message is also what keeps the field from beingdead_codeunder-D warningsbeforepumpexists to write through it — noallowwas needed. -
Failure cleanup order is forced by Windows: the handle is dropped before
remove_file, because this process's own open handle blocks a delete. If the delete fails too,beginstill returns an error, the slot is still released,is_openis false, the total does not grow, and the message says the empty file is left for the next startup sweep rather than pretending it is gone. That is the same orphan the sweep already handles, so the two failure modes compose instead of leaking. -
begindeliberately does not consult the quota, and the doc comment says why: a new archive is an empty file, so there is nothing yet to weigh against a cap. The caps and aQuotaTotal::Unavailabletotal refuse the first record, inpump. This was settled by readingan_unavailable_total_stops_the_archive_instead_of_growing_it, which arranges an unavailable total and then requiresbeginto succeed — guessing the other way would have made that test fail for the wrong reason. -
is_openis true from slot-taking until close or failure, which deliberately covers the instant before the file exists, becausedeletemust refuse a session that is being opened. A session the index knows about but this writer never opened is not open;is_openanswers about this writer's live sessions, not about the database. -
Two of the 25 tests that had been waiting on
initializewent green here:a_refused_writing_row_leaves_no_orphan_file_behindandan_orphan_left_by_a_failed_cleanup_is_deleted_by_the_next_sweep. Eleven advanced to a body the boundary excluded — ten toArchiveWriter::enqueue, anddeleting_an_archive_is_refused_while_its_writer_is_opentodelete. That is the measurable result the boundary allows, and it is the same standard the second slice was judged by: which named tests go green, and where each remaining one stops. -
The 17 remaining failures panic at four sites, each read to confirm the line:
archive.rs:1098ArchiveQueue::new×2,:1355enqueue×10,:1410read×1,:1420delete×4. Fifteentodo!bodies remain, down from 17. -
Five tests were added, all for requirement 7, which a
.begin(/.is_open(grep showed no existing test covered. No existing assertion was touched.beginning_a_session_this_build_could_not_have_generated_touches_nothing— eight bad ids: empty,.,.., a non-UUID, an uppercased UUID, one character short, one long, and an underscored one. Each errors,is_openstays false, and afterwards the directory is empty, the filesystem log has no removal, the index log has no call at all, there is no row, and the total is stillKnown(0). Validation genuinely precedes any effect, rather than being cleaned up afterwards.beginning_a_session_whose_file_already_exists_refuses_and_keeps_the_file— the file is planted afterinitialize, so the sweep cannot have seen or deleted it.create_newfails, the planted bytes are byte-for-byte intact, and no index call happens, which is the assertion thatcreate_newis the guard rather than aremove_file-then-recreate.beginning_the_same_session_twice_is_refused_and_keeps_the_first_archive— the second call errors, the first archive stays open, its row stayswritingwith the firststarted_at, and the index call log does not grow.two_threads_beginning_the_same_session_leave_exactly_one_open_archive— two threads share the writer through anArcand meet at aBarrier. Exactly one call isOk, and there is exactly oneinsert_writing:call, one row, one file, and no removal. No assertion names which thread wins, so the test cannot pass because an interleaving happened to be favorable; it was also repeated 20 times alone, 20 ok / 0 failed.ArchiveWriterisSend + Syncby construction, which is what lets the test share it:Arc<dyn ArchiveFs>andArc<dyn ArchiveIndex>are both, andBox<dyn ArchiveFile>isSend.is_open_is_false_for_a_session_this_writer_never_opened— acompletearchive from an earlier run is on disk and in the index;is_openis false for it, for an unopened id, and for two invalid ids, and true only for the session this writer actually began.
-
One regression this slice caused and fixed, recorded because reading the diff would not explain it: a placeholder-anchored edit used
ArchiveWriter::enqueue's doc comment as its anchor and deleted those three lines.cargo fmt --all -- --checkcaught it (exit 1, diff atsrc\archive.rs:1347), the original text was restored verbatim, and fmt returned to exit 0. Because the fix moved panic line numbers by two or three, the whole suite was re-run to re-record them instead of reusing the earlier values; the four sites above are the post-fix numbers. -
The root
runcovecrate was not touched and was re-verified anyway: fmt clean, clippy clean,cargo test --all-targetsgreen (12, 0, 0, 10, 16). The frontend half of the matrix —npm run lint,npm run typecheck,npm test -- --run,npm run build, the Playwright E2E, andcargo tauri build— was not run, because no frontend file changed in this slice. The full matrix is still required and is scheduled for plan step 7. -
Nothing was committed, pushed, tagged, or released; no CI, release, tag, or
.envfile was touched; andpub mod archive;inlib.rsis still the module's only reference in the crate, so the feature remains unreachable from the application.
- Scope:
ArchiveWriter::initialize, the startup sweep behind it,archive_dir, andtotal_bytes. The queue, the write path, close, quota eviction, theStorage-backedArchiveIndex, the commands, and the frontend are untouched and stilltodo!. Evidence, fromapps/desktop/src-tauri: fmt clean, clippy clean with zero warnings,cargo test --lib --all-features -- --test-threads=1at140 passed; 19 failed; 1 ignored; 0 measured; 0 filtered outout of 160, and the default parallel run at the same numbers with no failure outsidearchive::tests. The archive suite is 43 tests, 24 green / 19 red, up from 14 / 27. Each of the eight newly green tests also passes alone under-- --exact, every one of the 19 remaining failures is atodo!panic that was confirmed alone, none of them stops atinitializeany more, and the run contains zero assertion lines. - Eight of the 25 tests that were waiting on
initializecould go green in this slice; the other 17 assert onbegin,pump,close,read,delete, or eviction, which the authorized boundary excludes. They now stop at their own subject instead — 15 atbegin, 1 atread, 1 atdelete— which is the measurable result the boundary allows. Two of them assert sweep output before reaching theirtodo!, and those assertions run and hold: the total cap test seesKnown(800)from bothmeasured_bytesandtotal_bytes(), proving nothing is evicted at initialization, and the unavailable-total test seesUnavailable. - Only two failures stop the sweep, and both mean it has nothing to work with: an
index that cannot be read at all, and a directory that cannot be listed at all.
Creating the directory is the third failure that stops
initializeitself. Every other failure belongs to one entry or one row, and is reported as an anomaly and stepped over, because one file the filesystem refuses must not cost every other row its repair. - An entry is classified by its name before anything else. A name this build's
archive_file_namecould not have produced is reported once and otherwise left entirely alone — not measured, not deleted, not read — whether or not the filesystem could describe it.is_archive_file_nameand the sweep now sharearchive_file_stem, so the rule that admits a name and the id that name yields cannot drift apart into two rules. - A row's
file_nameis checked against the name its own session generates before the row is acted on, and its status is parsed before anything is decided. The order matters: parsing second is what keeps a row whose status this build does not know — one a newer build wrote — from being mistaken for a session whose file has gone missing. A row that fails either check is reported and left byte-identical. - A
completeorpartialrow whose file is still there is never rewritten. Its numbers were final when it closed, and re-measuring would replace a fact with a guess. Only awritingrow is repaired, topartial/interrupted, and it is repaired from the file rather than from itself: the row stopped being updated the moment the run died and the file did not, so the file's length and its recounted lines win. When the name is taken by something this build did not write the repair records zero bytes and zero lines; when the entry cannot be measured the row keeps its own numbers, which are then the only ones anyone has. Dropped-line and dropped-byte counters are preserved in every case. - A file is deleted as an orphan only when no row at all exists for its session — whatever such a row would have said. A file some row still names is kept even when the sweep refused to act on that row, because reporting a strange row costs a line in a log and guessing at one costs a user their log. A delete the filesystem refuses is an anomaly, and those bytes still count towards the quota, because they are still on the disk.
- An index write that fails during the sweep is reported and stepped over. A repair
that fails leaves the row
writing, which is exactly the state this sweep repairs, so the next startup tries once more; nothing is retried in a loop and nothing is lost. - One entry that nobody can size and no row remembers makes the whole total
Unavailablerather than a smaller number.Unavailablealready means "no room", so the archive stops growing instead of growing against a total it had to guess at: that total feeds the only cap standing between an opt-in feature and a full disk. - Anomaly text may echo an entry name, because the OS listing yields a single path
component, and never a row's
file_name, because that is exactly the column that may be a path. A row is labelled by its session id only when the id is one this build generates, and by a fixed phrase otherwise. - The writer holds
archive_dirand the measuredtotal, and nothing else. The filesystem and index handles are used by the sweep and dropped rather than stored, andboundsandlimitsare accepted and discarded, because a private field no body reads isdead_codeunder-D warnings— every field arrives with its first reader, while the signature stays pinned so it does not move when those readers land. - Two tests were added against code that already existed, with no growth in
implementation scope.
the_most_severe_reason_is_the_documented_order_over_every_pairchecks all 64 pairs against the documented order and its symmetry via an exhaustive-match rank helper, so the privateseveritynumbers cannot move without the documented order moving too, and a ninth reason cannot be added without being placed in the order.a_carriage_return_in_the_text_never_becomes_a_line_of_its_ownpins that\r,\r\n,\n, andNULinside a captured line stay inside the JSON string and out of the file's line structure, which is what keeps one record one line. - The intermittent port and timing failures recorded in earlier sessions did not reproduce today, in either the parallel or the single-threaded run. Nothing was changed to address them; they remain out of this slice's scope.
- Scope: the pure, leaf-level half of
archive.rs— the enum mappings, the file name and path rules, the record and gap encoding, the documented default bounds, andRealArchiveFs. The writer, the sweep, the queue, and the quota are untouched and stilltodo!. Evidence, fromapps/desktop/src-tauri: fmt clean, clippy clean with zero warnings,cargo test --lib --all-featuresat130 passed; 27 failed; 1 ignored; 0 measured; 0 filtered outout of 158. The archive suite is 14 green / 27 red, up from 1 / 40. Each newly green test also passes alone under-- --exact, and all 27 remaining failures aretodo!panics — 25 atArchiveWriter::initialize, 2 atArchiveQueue::new— with zero assertion lines anywhere in the run. - The id rule checks the shape and not the version nibble.
is_generated_session_idrequires 36 characters, hyphens at 8/13/18/23, and lowercase hex elsewhere, which is exactly whatstorage::new_idemits. The version and variant nibbles are deliberately not pinned: the shape is what makes a name one harmless path component — it admits no separator, colon, dot, space, or..— so pinning adds nothing, while it would turn every archive already on disk into an unreadable orphan the day the id generator moved to another UUID version. All 28 names inrejected_file_namesare refused by the shape rule alone, so no subtractive blocklist exists in the code. most_severeis a total order over all eight reasons, not over three. The plan only rankswrite-error>quota-exceeded>queue-overflow, which are the reasons a session can accumulate while writing. Ranking only those would leave the answer for any other pair dependent on argument order, so all eight are ranked and the tie-free order is documented on the privateseverityhelper: what happened to the archive outranks what RunCove or the user chose to do with it.RealArchiveFs::create_newis unbuffered.WRITE_BUFFER_BYTESbelongs toArchiveWriter, which owns every open file, so buffering inside the seam would double-buffer in production and make the shipped seam behave differently from the double the tests substitute. The constant stays unused until the writer lands.- Error messages echo a session id, never a
file_namefrom the database. A session id is short, ours, and the thing a user needs to identify the archive; it is formatted with{:?}on the one path where it may not have passed the shape rule yet. A rejectedfile_nameis exactly the value that may itself be a path, so the message states the rule instead of quoting it back. RealArchiveFs::list_dirturns a single entry's failed metadata read intoErr(UnreadableEntry)inside the vector and keeps going, reservingErrfor a directory that could not be listed at all. TheTestFsdouble reaches the same outcome by injecting on the entry's name, so the shipped implementation is the one holding to the documented contract rather than inheriting it from the double.
- Two review batches took the archive suite from 27 tests to 41 without writing a
line of production behavior. Every body in
apps/desktop/src-tauri/src/archive.rsis stilltodo!("step 4b: ...")andpub mod archive;is still the module's only reference in the crate, so nothing a user can reach changed. The batches were reviewed separately: nine tests in the first, five in the second. - Evidence, from
apps/desktop/src-tauri:cargo fmt --all -- --checkclean,cargo clippy --all-targets --all-features -- -D warningsclean with zero warnings, andcargo test --lib --all-featuresat117 passed; 40 failed; 1 ignored; 0 measured; 0 filtered outout of 158 run.--listreports 41archive::testsentries, so the archive suite is 40 red and 1 green. All 40 failures panic at atodo!("step 4b: ..."); none is a failing assertion. The one green test isthe_test_filesystem_reports_a_link_as_a_reparse_point, the test filesystem's own control, and it is supposed to pass. The group list and the body each test reaches are inV0.3.0_PLAN.md→ Verification. - Twenty-five of the 40 still stop at
ArchiveWriter::initialize, so 4b is judged on each test going green individually rather than on the failure count. The other fifteen reach their own subject, which is what makes a first slice of 4b measurable at all. - Why the suite grew, stated plainly so the count is not mistaken for padding. Each
new test names a way the archive could have been wrong: a row naming another
session's file; an index write refused while a file already exists; an orphan left
by a failed cleanup; a reparse point measured into the quota or read as an
archive; an entry whose metadata the filesystem refuses; an orphan whose delete
is refused; a directory total that cannot be established; and
RealArchiveFsnever having been executed by any test. - Four decisions this round settled, all now in
V0.3.0_PLAN.md:- Read and delete stay keyed by
session_id, and a row is accepted only when itsfile_nameis the name that same session generates. Passing the name rule is necessary and not sufficient — otherwise one row could serve or delete another session's archive, which is the name rule's own failure one level up. Reviewed and kept rather than reverted. - A single entry the filesystem refuses is data, not an error.
list_dirreturns one result per entry, so one file another process holds exclusively becomes an anomaly the sweep reports and does not stop it; only a directory that cannot be listed at all is an error. - The measured quota total is known or unavailable, and unavailable means "no
room". Under-counting a hard byte cap is how a tool fills someone's disk, so the
archive refuses to grow a directory it cannot measure. It is not a fatal
initialization failure and not a new row state: a session that tries to write
closes
partial/quota-exceeded, which is the only truthful reason the version 2CHECKadmits. - What each entry contributes to the total: an ordinary file its own length; a
reparse point nothing; an unreadable entry with a row that row's last known
byte_size; the same entry with no row nothing measurable, making the total unavailable; an orphan whose delete was refused its own length, because those bytes are still on the disk. "Definitely not our file" and "could not be read" are deliberately different rules, and the reparse-point test seeds a non-zerobyte_sizeso an implementation cannot satisfy both with one branch.
- Read and delete stay keyed by
- A test double that classified entries differently from the real filesystem would
make every sweep test pass for the wrong reason, and until this round the double
was the only implementation any test had executed.
RealArchiveFsand the double are now compared over the same directory, entry by entry, for an ordinary file, a nested directory, a file symlink, and a directory symlink. Residual limit, recorded rather than papered over: no non-name-surrogate reparse point — cloud placeholder, dedup stub,AppExecLink— can be created throughstd, so those rest on both implementations readingFILE_ATTRIBUTE_REPARSE_POINTfrom the file attributes instead ofFileType::is_symlink, which is true only for name surrogates.list_diralso had to promise ascending name order for the comparison to be an equality. - Four of the 41 tests are
#[cfg(windows)]. On a non-Windows host the archive suite is 37 tests with none green, because the one green test is Windows-only. CI'sdesktopjob is windows-latest, so all 41 compile and run there. - Test boundaries held: every test works inside a
tempfile::TempDir, no archive test opens a database, the real application data directory is never opened, and no test sleeps. Failure injection is by call count or by entry name, never by timing. - Working-tree scope is still the same nine paths, and nothing was committed, pushed, tagged, or released. No CI or release file was touched and no software-copyright application material was produced.
- Step 4 was split: 4a writes the archive API surface and its red tests, 4b writes
the behavior after the user reviews the signatures. The reason for a split at all
is that a Rust test cannot run in a crate that does not compile, so the API has
to exist before the tests can. Every body in
apps/desktop/src-tauri/src/archive.rsistodo!("step 4b: ..."), and thepub mod archive;line inlib.rsis the module's only reference in the crate, so nothing a user can reach changed. - Evidence, from
apps/desktop/src-tauri:cargo fmt --all -- --checkclean,cargo clippy --all-targets --all-features -- -D warningsclean with zero warnings,cargo test --all-targetsat116 passed; 27 failed; 1 ignored; 0 measured; 0 filtered outfor the lib target out of 144 run and0 passed; 0 failedformain.rs. All 27 failures arearchive::tests::*, every one at atodo!("step 4b: ..."), and none passes. The full name list with the body each test reaches is inV0.3.0_PLAN.md→ Verification. - Fourteen of the 27 stop at
ArchiveWriter::initialize, the first line of their arrangement, so their later assertions are written but not yet exercised. 4b is judged on each test going green individually, not on the failure count reaching zero. - Two lint facts decided the shape of the file, and both were confirmed by probe
rather than recalled. A
pubitem inside a private module, returned by apub fnin apub mod, does not triggerprivate_interfaces— so the module can bepuband every item in it is a dead-code root, which is why an unreached constant or method is not a warning under-D warnings. A private field no body reads is dead code and a derivedDebugdoes not excuse it — so the stub structs are fieldless and the state 4b gives them lives in their doc comments.#[allow(dead_code)]was rejected as exactly the bypass markerCLAUDE.mdforbids, and#[expect(dead_code)]needs Rust 1.81 against a declared 1.77. - Four things the tests decided that the plan had left open, all now written into
V0.3.0_PLAN.md: the gap line is singular-aware ([RunCove: dropped 1 line / 1 byte]);QueueBoundsandQuotaLimitsare parameters with documented defaults, so an overflow or eviction test costs a few hundred bytes instead of 200 MiB; the symlink-refusal test is#[cfg(windows)]and needs an elevated shell or Developer Mode, which CI's windows-latest runner has; and the write-failure test enqueues a record larger thanWRITE_BUFFER_BYTES, because with a 64 KiB buffer a short record would not reach the file until close and the injection would fire at the wrong moment. - No archive test opens a database. The index seam is a recording in-memory double,
so the tests need only a
tempfile::TempDir; the real application data directory is never opened. TheStorage-backedArchiveIndexis deferred to 4b, because apub(crate)adapter that only tests use is dead code in the library build and would duplicate SQL that belongs instorage.rs. - Working-tree scope is now nine paths: modified
AGENTS.md,HANDOFF.md, this file,apps/desktop/src-tauri/src/lib.rs,apps/desktop/src-tauri/src/models.rs,apps/desktop/src-tauri/src/storage.rs, plus untrackedCLAUDE.md,V0.3.0_PLAN.md, andapps/desktop/src-tauri/src/archive.rs. Nothing was committed or pushed, no CI/release file, tag, or remote state was changed, and no software-copyright application material was produced.
- Approved scope for v0.3.0: the opt-in run log archive only — writing, reading, the run history summary, the viewer, delete, and the documentation those require. Project Git status is deferred out of the release and will only be evaluated separately if it can be shown to provide value the editor and the Git client do not already provide.
- The SQLite schema upgrade from version 1 to version 2 was approved in principle, conditionally: red tests first, covering a complete version 1 upgrade, idempotent reopening, rejection of a version above 2, a failed migration leaving an openable version 1 database, and no regression in the existing queries and settings behavior. That condition is discharged — the tests were written and run red, then the migration was implemented.
- Red-test evidence, before the migration,
cargo test --libinapps/desktop/src-tauri:109 passed; 6 failed; 1 ignored. The six failures aremigration_is_idempotent_and_sets_version(left: 1, right: 2),a_populated_version_1_database_upgrades_to_version_2(version stays 1),reopening_an_upgraded_database_is_idempotentandthe_archive_index_rejects_impossible_rows(run_log_archivesdoes not exist),a_version_2_database_opens_and_only_a_higher_version_is_rejected(version 2 is currently rejected as too new), anda_failed_migration_leaves_the_version_1_database_intact(opening succeeds today because nothing tries to create the table).cargo fmt --all -- --checkandcargo clippy --all-targets --all-features -- -D warningsare clean in that crate. - Honest limitation of that evidence:
version_1_user_data_survives_the_upgradewas green rather than red, because a version 1 database already opened and read correctly on the pre-migration build; it is now the upgrade's data guarantee. And "the file is still openable by the previous build" is asserted by proxy — the test cannot run the v0.2.1 binary, so it checks that the version is still 1, that every version 1 row still reads, that no partial object was left behind, and that opening succeeds once the conflict is removed. - Verification-matrix correction found while running this: the desktop crate is
not a workspace member of the root package, so
cargo test --all-targetsat the repository root never coversstorage.rs. CI runs the desktop Rust tests withworking-directory: apps/desktop/src-tauri, andV0.3.0_PLAN.mdnow records that as a separate required invocation. - Migration implemented the same day. Post-migration verification in
apps/desktop/src-tauri:cargo fmt --all -- --checkclean;cargo clippy --all-targets --all-features -- -D warningsclean with zero warnings;cargo test --all-targetsat116 passed; 0 failed; 1 ignoredfor the lib target and0 passed; 0 failedfor themain.rstarget. The single ignored test predates this work. All six red tests are green and none was weakened, relaxed, or deleted to get there. Thenpmhalf of the matrix andcargo tauri buildwere not run because no frontend file changed; the full matrix is still owed before the milestone is called done. - Production code changed, and nothing else:
models.rsgainedRunLogArchiveSummaryandRunSession.archive;storage.rsgainedSCHEMA_VERSION, the> SCHEMA_VERSIONguard,upgrade_to_version_2and its call site, and thelist_sessionsLEFT JOIN run_log_archives. No writer, no commands, no frontend, no startup sweep. - Defect the red tests caught, worth recording because reading the DDL did not
reveal it: the constraint arms were written
status = 'partial' AND reason IN (...), which does not reject a null reason.NULL IN (...)evaluates to NULL,1 AND NULLto NULL, the surrounding0 OR NULL OR 0to NULL, and a SQLiteCHECKpasses when its expression is NULL instead of failing. The constraint therefore accepted exactly what it was written to forbid — apartialorremovedarchive with no reason, which the UI would render as a badge with no explanation.the_archive_index_rejects_impossible_rowsreported it aspartial must carry a reason: ('sess-exited','a.jsonl','partial',NULL,...) was accepted. Fixed with an explicitreason IS NOT NULLin both arms, in the migration and in the test's pinnedV2_ADDITION, and pinned by a ninth rejection case for theremovedarm. - Second, smaller finding: that same test had been passing vacuously. Its eight
rejection assertions all held while
run_log_archivesdid not exist, and the run only failed at its final acceptance insert. Anobject_existsguard now runs before the loop, so a missing table fails at the guard rather than silently validating nothing. A rejection test needs a positive control. - Migration direction, one sentence used unchanged in every document and release
note: 迁移失败时 SQLite 事务回滚并保持 v1;迁移成功后没有应用级回退或数据库降级
路径。In English: a failed migration rolls back the SQLite transaction and stays
at v1; a successful migration has no application-level fallback and no database
downgrade path. Concretely, a v1 database is opened unchanged by this build and by
v0.2.1 with user data untouched, while a v2 database is refused by v0.2.1 because
that build rejects any
user_versionabove 1, and no code returns a version 2 database to version 1. The two halves are not a pair and neither is a rollback of the other; do not call a successful upgrade revertible. A fresh install runs 0 → 1 → 2 as two separate atomic transactions, each resumable on the next launch, while the version 1 to version 2 upgrade itself is a single transaction withPRAGMA user_version=2as its last statement. - The startup sweep was deliberately kept out of this step even though the plan's
execution order had bundled it with the migration. It re-measures archive files,
repairs
writingrows, marksfile-missing, deletes orphan files, and initializes the quota counter — all of which require an archive directory that does not exist until the writer exists. It moves to the writer step and owes red tests together with the writer, the queue, the quota, and the lifecycle. - Working-tree scope is now seven paths: modified
AGENTS.md,HANDOFF.md, this file,apps/desktop/src-tauri/src/models.rs, andapps/desktop/src-tauri/src/storage.rs, plus untrackedCLAUDE.mdandV0.3.0_PLAN.md. No database file, CI/release file, tag, or remote state was changed, nothing was committed or pushed, and no software-copyright application material was produced.
- The user is temporarily handing RunCove to Claude for possible product
expansion and may later return the resulting changes to Codex for review.
CLAUDE.mdnow provides a concise cross-agent entry point whileAGENTS.md,HANDOFF.md, and this file remain the authoritative project record. - The intended use in a software copyright registration application is a future direction only. No jurisdiction-specific checklist, legal conclusion, submission package, feature scope, or version number has been approved. Current authoritative requirements must be checked when the user starts that work; the project should not be padded with meaningless code or features.
- The verified baseline remains published
v0.2.1: localmainandorigin/mainwere synchronized at97943d7fabbbd400481171568bf970b38a2c9afa, and the release tag targets5e3e0d4d63ae04fe8e27c37c4500d3bd9ef75f13. - The project entered a waiting state on 2026-08-14. This handoff changes only project documentation. It does not authorize code, database, CI/release, tag, published-asset, runtime-process, or remote changes.
- The handoff documentation is intentionally uncommitted and unpushed. Its
expected working-tree scope is five paths: modified
AGENTS.md,HANDOFF.md, and this file, plus untrackedCLAUDE.mdandV0.3.0_PLAN.md. Superseded on 2026-08-15: six paths after the red tests, seven after the migration. See the section above. V0.3.0_PLAN.mdis a reviewed proposal for an opt-in on-disk run log archive. Its second draft answers an external review, drops project Git status to a deferred proposal, and gates implementation on three explicit approvals: the SQLite schema version 1 to version 2 migration, archive-only v0.3.0 scope, and whether project Git status is worth building at all. None has been granted, so no implementation is authorized. Superseded on 2026-08-15: scope approved, Git status deferred, the migration approved conditionally and then implemented after its red tests ran red.
- Implemented the approved
V0.2.1_PLAN.mdfrommaincommitbf3d532without changing CI/release workflows. PR #2 passed every configured check and merged intomainasa771c55f402bcdce3d0ec29fe739d4e47bd847c5; publication evidence is recorded separately after the tag workflow completes. - Overview now shows the five newest managed run sessions, with a searchable
and filterable drawer capped at 200 records. Orphaned history remains
readable, known projects can be located, and future unknown stored statuses
degrade to
Unknown. - Structured port conflicts carry an optional port/protocol through start,
status events, and restore results.
View occupantrechecks the snapshot and focuses only the exact protocol row; stale occupancy and stale error-action context are explicitly cleared. - Saved-root discovery exposes scanning, candidates, empty, and error states, coalesces concurrent scans, preserves candidates while review is closed, and supports retry. Imports and starts always remain user-confirmed.
- Project editing can duplicate a launch profile without persistent IDs or
observed-runtime state. Frontend and backend validation cover required
fields, blank arguments, working directories, port range, and duplicate
port/protocol pairs while retaining PATH-resolved programs such as
npm.cmd. - Port details can copy PID, executable path, and command line with explicit failure feedback. Help and all new controls have English and Simplified Chinese coverage, keyboard semantics, and accessible labels.
- No SQLite schema migration was added. Existing
run_sessionsdata is reused; polling snapshots and console logs remain non-persistent.
- Root Rust: format passed; Clippy passed with warnings denied; all targets
passed
38/38tests. - Desktop Rust: format passed; Clippy passed with warnings denied; all targets
passed
109tests with the one explicitly configured live-service test ignored by default. - Frontend: ESLint, TypeScript, and production Vite build passed. Vitest passed
114/114tests across 20 files after the final audit fixes, including polling-safe port/project focus timers and partial-import candidate cleanup. - Microsoft Edge Playwright passed
6/6: the complete primary workflow at900x600,1280x720, and1440x900, plus profile copy/validation, exact conflict navigation, and saved-root failure/retry. All three viewports cover English and Simplified Chinese run history and Help with no captured console warnings/errors or horizontal overflow. npm run tauri buildproducedapps/desktop/src-tauri/target/release/runcove-desktop.exeas RunCove0.2.1. Size:25,418,344bytes. SHA-256:4B00DD7F72B6AAD29646684DB7F852D691C7183A4BF78DA703C06556A9BA3A78. The executable is unsigned and was built but not launched, installed, packaged, or published.- Root and desktop Cargo metadata, npm metadata, Tauri metadata, and embedded
executable file/product versions all report
0.2.1.git diff --checkpassed.
- The desktop app remains Windows-first; only the port-inspection CLI is cross-platform in this version.
- RunCove cannot guarantee metadata for every protected kernel/system process, even after an explicit UAC relaunch. Administrator mode remains monitor-only.
- Historical session metadata does not include archived console logs. The one environment-driven live-services acceptance test still requires explicit fixture configuration and remains ignored in the normal suite.
- The local release executable has no code signature. No installer, startup
automation, Docker/remote management,
.envediting, or persistent log archive was added.
- PR #2 merged into
mainasa771c55f402bcdce3d0ec29fe739d4e47bd847c5. Main CI run31691103911completed successfully across all configured CLI, Rust lint, frontend, browser, and Windows desktop checks. - Release documentation was recorded without changing the code tree. Annotated
tag
v0.2.1targets release commit5e3e0d4d63ae04fe8e27c37c4500d3bd9ef75f13. - Release workflow
31692200475completed successfully and published the latest, non-draft, non-prerelease release at https://github.com/AbyssWhalen/RunCove/releases/tag/v0.2.1. - The release contains six assets: four cross-platform CLI archives, the
Windows x64 portable desktop archive, and
SHA256SUMS.txt. GitHub's SHA-256 digest for every archive matches the corresponding checksum-file entry:- Linux x64 CLI:
109FE838A097D4312434FB4D4597DF9110E9A9BFCF8D0FC37A8C676284BA5A7F - macOS arm64 CLI:
CD689CD9635A40C4D243CEC2759BE45DD796FC343FD1869B685D7E4826114EEE - macOS x64 CLI:
4D287D0EDC27C0C76D527124B97634FF9B972062FB9E5699C35F509D98B6E688 - Windows x64 CLI:
7C2DB4C3BA9DE7641DF92F5363D478BBACC2BCF206F8614A6B075FD37F45676A - Windows x64 desktop:
58CAB696B97FAE7EDF5E273FA57D32984F826B01F3473FE7AB008CEBF8436096
- Linux x64 CLI:
- A redundant main CI run created by the no-content release marker was canceled
because the identical code tree had just passed run
31691103911; no job was manually rerun. Release annotations only report GitHub's Node.js 20 action deprecation warning and did not affect the successful build or publication. - Local Git HTTPS pushes triggered a Windows
git-remote-https.execrash dialog. Further publication writes used the authenticated GitHub API instead, and no Git or Git remote helper process remained afterward. No unrelated application, project, or development process was stopped or modified.
- Windows 11 is the first desktop target; the scanning core and CLI retain their existing cross-platform behavior.
- The desktop stack is Tauri 2 with React, TypeScript, and Vite.
- The app does not start with Windows. Projects never start automatically.
- The previous run set is restored only through an explicit user action.
- Current-session logs are bounded in memory and are not persisted by default.
- Explicit application exit stops all RunCove-managed process trees after confirmation.
- External process-tree termination requires current identity verification and explicit confirmation. RunCove never elevates automatically; enhanced monitoring requires a separate explanatory confirmation and Windows UAC.
- Desktop IPC uses lowercase
tcp/udpprotocol values and epoch-millisecond timestamps. Port state refreshes through the typedport-snapshotevent. - Project import uses the native directory picker. Inferred associations remain suggestions until the user confirms them.
- Development-root discovery uses bounded recursive scanning, skips dependency and build directories, and presents a selectable batch before persistence.
- Profile lifecycle commands use atomic reservations. Process watchers verify session identity, drain both output pipes before completing a session, and continuously synchronize the ordered restore set.
RunCove creates its own versioned SQLite database in the application-local data directory. It does not inspect or modify databases belonging to registered projects.
- Unchanged baseline formatting and Clippy checks passed after dependencies were
available. The unchanged full test suite timed out because Windows process
names were resolved by spawning
tasklistfor every scanned entry; the fixed-port kill integration test was also unsuitable for a shared machine. - Root CLI verification after the scanner and compatibility work:
cargo fmt --all -- --check: passed.cargo clippy --offline --all-targets --all-features -- -D warnings: passed with warnings denied.cargo test --offline --all-targets: passed, 34 tests total.
- Both the primary and compatibility CLI help paths executed successfully.
- Both binaries passed the same non-destructive argument and exit-code compatibility matrix; a fixture test locks the legacy JSON field names.
- A live Windows scan resolved process names for 133 of 133 entries and took about 64 ms on this machine.
- Final desktop/frontend verification:
npm run lint: passed.npm run typecheck: passed.npm test -- --run: passed, 14 tests across 5 files.npm run build: passed.- Desktop Rust format and Clippy with warnings denied passed.
cargo test --offline --all-targetsinapps/desktop/src-tauripassed 49 tests; the environment-driven live acceptance test remains ignored by default.- The ignored live acceptance test passed separately for active services on ports 3100, 4321, 8080, and 1420. Each launch attempt returned Conflict before creating a child or run session, and the listener PID, start time, and executable path were unchanged after the test.
- Playwright covered overview, ports, logs, project import, and association
confirmation at
1280x720and1440x900. The final pass also covered the development-root selection list at both viewports with zero console errors. npm run tauri build: passed with the final source in the isolated targetC:\Users\93137\AppData\Local\Temp\runcove-tauri-final-20260807-212342. The final executable isrelease\runcove-desktop.exe, size24,740,334bytes, SHA-256F135EDE741512E112E1F01FC0519E5278FEB7510C56E1C968053F96974314423.
- Idle performance checks:
- The earlier pre-dedup main-process baseline ran for 602.3 seconds at 0.4185% average CPU; memory decreased from 92.93 MB to 36.45 MB.
- The earlier pre-dedup whole-tree baseline ran for 656.3 seconds at 0.3533% average CPU; memory decreased from 470.99 MB to 176.04 MB.
- The final-source executable ran idle for 604.924 seconds with 122 samples across a stable seven-process tree. CPU, normalized across 28 logical processors, averaged 0.977930% (minimum 0.430837%, maximum 1.946985%), passing the under-1% target with 0.022070 percentage points of headroom.
- Final-source private memory was 321.566 MiB at the first sample and 323.027 MiB at the last (296.891-337.750 MiB range); the second-half mean exceeded the first by 1.346 MiB and downward steps were more common than upward steps. This is not evidence of a sustained or monotonic private-memory leak.
- Final-source working set moved from 165.895 MiB to 196.934 MiB (165.320-210.996 MiB range), with a +11.400 MiB second-half mean and +2.340186 MiB/min linear slope. This looks like warm-up/residency growth, but it prevents claiming memory was fully flat; a longer soak remains a residual performance check.
- A second isolated final-source QA run sampled the complete process tree 114
times over 597.328 seconds. The process count stayed at seven and normalized
CPU averaged 0.346124% (0.052815-0.867248%), comfortably passing the target.
Working set fell from 543.973 MiB to 461.965 MiB. Private memory moved from
289.645 MiB to 301.973 MiB, ranged from 277.625 MiB to 330.863 MiB, and had
a +4.409032 MiB/minute fitted slope. The samples do not show unbounded
process-count or working-set growth, but private memory is not claimed flat;
a longer soak was not completed before
v0.2.0publication and remains a post-release performance follow-up.
- Image generation was unavailable with HTTP 403. The checked-in deterministic
source.svgand generated Tauri icon set are the local fallback. - Runtime data was observed at
C:\Users\93137\AppData\Local\com.abysswhale.runcove\runcove.sqlite3, outside the repository. - Final root Rust checks passed: format, Clippy with warnings denied, and 34 tests including real TCP/UDP listeners and legacy CLI compatibility.
- Final executable smoke after the endpoint fix launched PID 84412 from the
isolated release target. The real window reported
0 running,0 conflicts, 74 active ports, and no configured profiles. A same-source CLI scan returned 124 endpoint rows, zero exact duplicate groups, and retained the original listener PIDs for ports 3100, 4321, 8080, and 1420. - Computer Use successfully selected and read the unique final RunCove window,
but its first view-switch click returned
node_repl exec context not found. The same error recurred on a read-only recovery capture, so no further input or blind-coordinate fallback was attempted. The equivalent navigation, search, detail, and refresh flows remain covered by the completed Playwright browser QA rather than being claimed as final executable input automation.
- Added typed English and Simplified Chinese dictionaries with
system / en / zh-CNselection. WebView storage provides the immediate preference while the existing version-1 settings JSON remains the durable source; no SQLite schema migration was introduced. - The native tray menu, tooltip, and running/conflict/unexpected-exit summary update in the selected language. System mode now follows the primary locale consistently in the WebView and Windows backend.
- Invalid, blank, or missing WebView language values no longer overwrite a valid backend preference. A successful database save is the language commit point; tray refresh failures are reported separately and the next status refresh retries the complete tray text without rolling the UI back.
- Removed the root 980-pixel minimum width and set the Tauri minimum window to
900x600. Compact tables retain the high-value columns and expose all hidden data in the port detail row. Ports keep compact columns through the last width that cannot fit all eight columns. - Expanded port details now switch their table
colSpanbetween four and eight as the compact media query changes. This prevents hidden columns from distorting the visible row widths after a detail row opens. - Fixed top/right tooltip placement, including the log drawer and project-card actions; modal actions stay visible, and dialogs trap focus, close on Escape when idle, restore trigger focus, and cannot be dismissed while busy.
- Operation errors are no longer cleared by the two-second port refresh. Log loading, subscription, clipboard, and clear failures leave recoverable state and localized user-facing conclusions while retaining raw technical detail.
- Browser actions are enabled only for running profiles with an active trusted TCP association matching that profile.
- Frontend verification passed: lint, typecheck, 31 tests across 10 files, and the production build. Root Rust format/Clippy/tests passed with 34 tests; desktop Rust format/Clippy/tests passed with 57 tests and one explicitly configured live acceptance test ignored.
- Playwright verified English and Chinese at
900x600,964x601,1024x640,1120x700,1280x760, and1440x900, plus1121,1198,1199, and1200boundary widths. Document, content, and table client/scroll widths matched; Actions stayed visible; detail rows, import modal, log drawer, and hovered right-edge tooltips stayed in bounds; the console had zero errors and zero warnings. Screenshots are underapps/desktop/output/playwright/. - The final isolated Tauri build passed. Executable:
C:\Users\93137\AppData\Local\Temp\runcove-tauri-i18n-20260808-0047\release\runcove-desktop.exe; size24,797,285bytes; SHA-25657DCF54FE1663D6EF6814AFB06C99DF0AE29C1D55F12B08FC14864B41502E550. - PID 84412 still runs the previously accepted executable. It was not stopped, and the new release was not launched. Read-only post-build scanning confirmed the original listeners on 3100/PID 68036, 4321/PID 66584, 8080/PID 105280, and 1420/PID 86304 remained present.
- After the second isolated soak, QA PID 133636 and its WebView2 descendants were identity-checked and stopped. PID 84412 plus listeners 1420/PID 86304, 3100/PID 68036, and 4321/PID 66584 were still present. The previously observed external 8080/PID 105280 process and listener were no longer present; RunCove did not terminate or restart them.
- A QA-flavored build used the inline identifier
com.abysswhale.runcove.qa, started as PID 116720, createdC:\Users\93137\AppData\Local\com.abysswhale.runcove.qa\runcove.sqlite3, and initialized a six-process WebView2 descendant tree without any Node/npm project process. The QA process identity was rechecked before it was stopped; its executable and isolated database remain recoverable in the temp and app data directories. Computer Use could uniquely enumerate and bind the window, but both launch/capture paths returnednode_repl exec context not found. OS handle capture exposed only cloaked helper surfaces, so no blind input or full-desktop screenshot was used.
- Process output uses a streaming 16 KiB per-line bound. Oversized lines are drained through the following newline and marked as truncated; ordinary non-newline tails remain visible.
- Process exit commits keep the managed entry visible while persisting status, session, restore-set, log, and event state. PID and session generation checks prevent a stale watcher from touching a restarted profile.
- CLI termination considers only listening endpoints and rechecks PID identity. Established client connections cannot become kill candidates.
- Project suggestions normalize extended drive and UNC paths before boundary comparisons. Matching inactive expected ports and association history merge for the same owner while different-owner history remains separate.
- The log drawer registers its event listener before requesting history and merges overlap by occurrence counts. Real duplicate lines are preserved and delayed listener creation is cleaned up after unmount.
- Conflict, unknown, and explicitly unexpected status events use the error
notification channel. The event contract carries a backward-compatible
unexpectedflag across Rust and TypeScript. - Editing a stopped project clears cached runtime state for only profiles removed by that edit, under the old profile lifecycle reservations.
- Tray language/status snapshots are copied under the mutex; native menu and tooltip APIs run after release to avoid lock-order stalls.
- Red-to-green evidence covers failure-event UI, removed-profile state, tray mutex release, log subscription ordering, watcher generation, oversized output, listener-only CLI selection, path normalization, and port-history deduplication.
- Final verification: root Rust format/Clippy/37 tests; desktop Rust format/Clippy/86 tests plus one ignored live test; frontend lint, typecheck, 57 Vitest tests, production build, and three Playwright flows all passed.
- Running profiles now become
Unknownwhen a managed process remains alive but an expected listener disappears,Conflictwhen another owner takes the port, andRunningagain only when every expected listener is managed by the profile.Startingremains controlled by readiness logic. - Managed/confirmed association write failures propagate through
dashboard(). The background refresh loop deduplicates repeated failures and publishes them through the frontend lifecycle-error channel. - The log store is globally bounded across profiles in addition to the 16 KiB line limit. Project discovery is async, best-effort across unrelated bad subtrees, and supports both block and inline pnpm workspace package lists.
- Windows scanner reports partial IPv6 failures as degraded snapshots while preserving IPv4 entries and disabling status reconciliation for incomplete data. Legacy CLI output and exit behavior are unchanged.
- Final visual QA covered
900x600,1280x720, and1440x900across all main views, logs, import dialogs, long tooltips, and long toasts with no overflow, console errors, or warnings. The same core flow is now reproducible throughnpm run e2eusing Microsoft Edge. - The repeatable Edge suite now builds and serves
dist/withvite previewinstead of loading the Vite development module graph. This removed a cold start failure where HTML had committed while required modules were still pending. A retry is limited to a page that remains exactlyabout:blank, so HTTP, rendering, assertion, console, and page errors still fail normally. - The
900x600browser flow changes the durable UI selection tozh-CN, checks<html lang="zh-CN">and the translated launch-profile heading, verifies no horizontal overflow, then returns to English. All three viewports now scroll expanded port details into view and assert their full bounding box, matching the existing log-drawer and import-dialog boundary checks. - Post-reboot verification on 2026-08-10 passed frontend lint, typecheck, all 57
Vitest tests, a production build, and all three Microsoft Edge E2E flows.
Vitest, Vite, and Edge were run outside the Windows process sandbox because
in-sandbox child creation failed before execution with
spawn EPERM. - The same post-reboot matrix passed root Rust format, warnings-denied Clippy,
and 37 tests, plus desktop Rust format, warnings-denied Clippy, and 86 tests;
the one environment-configured live-service test remained explicitly ignored.
The desktop termination fixture first hit the sandbox's expected
taskkilldenial, then passed with the complete suite outside that sandbox. - Final hygiene checks report
git diff --checkclean, the Playwright run marker aspassed, no listener on 14231, and no remaining RunCove Playwright/Edge process..gitignorenow excludes all generated Playwright output by default and re-includes only the 19 curated final QA PNGs. - An independent final read-only audit found no reproducible P0/P1 functional or security issue with high confidence. Its scope included Windows scan degradation, external identity revalidation, Job Object process ownership, lifecycle reservation/generation handling, ordered restore, transactional SQLite persistence, settings compatibility, and the Tauri IPC capability boundary. It did not mutate files or run live-service tests.
- The 2026-08-10 isolated Tauri release build passed. Executable:
apps/desktop/src-tauri/target/final-20260810/release/runcove-desktop.exe; size25,109,671bytes; SHA-2565CE330BF09F0E4E867925EF66A346B60CE121D56D3E1EE14AC26C711C771FD0E.
- The last successfully scanned development root is stored in the existing version-1 settings JSON with a serde default, so old settings remain valid and no SQLite schema migration was introduced.
- Startup scans that root once in a worker thread. Safe service-script candidates produce a non-blocking notice and a Projects review action; no project, command, or port association is saved until the user confirms it.
- Paths are compared case-insensitively on Windows with trailing separators removed, preventing registered projects from returning as new candidates.
- Standard monitoring remains the default. The top-bar shield opens a localized
explanation before
ShellExecuteWuses the Windowsrunasverb to start the same absolute executable with--elevated-monitor. - The elevated copy validates its administrator token instead of trusting the command-line flag, then waits up to 15 seconds for the old instance to save its restore set, stop managed process trees, and release the instance mutex.
- UAC denial is distinguished from other launch failures. Administrator access improves process metadata coverage but does not promise access to protected kernel processes; the port table continues to degrade explicitly when details remain unavailable.
- Verification passed: frontend lint/typecheck, 70 Vitest tests, production
build, and three Edge Playwright flows at
900x600,1280x720, and1440x900; root Rust format/Clippy/37 tests; desktop Rust format/Clippy/91 tests with one configured live test ignored; and the isolated Windows Tauri release build. - Current executable before the Help milestone:
apps/desktop/src-tauri/target/final-20260810-auto-elevation/release/runcove-desktop.exe; size25,249,498bytes; SHA-256E221A0077A9D2630CD549B95DFB010BF472A3761AFFC08E6BD150B1D3C9A6A1A. - The final temporary preview tree was identity-checked and stopped. Ports 1421, 1422, and 14231 were free afterward, and Playwright recorded a passed run.
- Added a top-bar
CircleHelpentry that opens a right-side Help drawer without changing the current view. The default topic follows Overview, Ports, or Projects; users can switch among Quick start, Ports, Projects, Access, and Safety. - Each topic uses short numbered items instead of a long static manual. The drawer supports tablist semantics, Arrow/Home/End topic navigation, Escape, focus trapping, focus restoration, and direct navigation to Ports or Projects.
- Added complete English and Simplified Chinese copy for discovery, profiles, expected ports, status meanings, logs, restore, UAC behavior, process limits, ownership confirmation, and icon hover labels.
- The drawer uses an independently scrolling content region and a horizontal
topic rail at narrow widths. A real
900x600browser snapshot showed the Chinese content wrapping naturally with no clipping or horizontal overflow. - Verification passed:
npm run lint,npm run typecheck,npm test -- --runwith 70 tests,npm run build, and three Edge Playwright flows covering the Help drawer at900x600,1280x720, and1440x900. - The latest release executable is:
apps/desktop/src-tauri/target/final-20260810-help/release/runcove-desktop.exe; size25,254,860bytes; SHA-256E0CFA78FBE99F9441835F0030C016AB1E6FB776D0502CC950654811156A05AFA.
- Removed the top-bar hide and quit controls because they duplicated Windows title-bar behavior and the old frontend hide path did not provide a reliable native result.
- The native close request now prevents immediate destruction and resolves one
persisted
ask | hideToTray | quitpolicy.askfocuses the main window and emits the typed close-choice event;hideToTrayuses a Rust IPC/native hide;quitcompletes the existing restore-set and managed-process shutdown before allowing application exit. Failures keep the window available and surface a lifecycle error. - The default remains
ask. Remembering is opt-in and saved in the existing version-1 settings JSON with serde backward compatibility, so no SQLite migration or schema change was introduced. - The close-choice dialog defaults focus to the non-destructive tray option, states that quit stops all RunCove-managed services, disables every control while an action is pending, and leaves the window open on cancel or failure. Repeated native close events coalesce while the dialog is open.
- Help > Safety shows the current title-bar close behavior and resets a remembered choice to ask every time. Tray Exit retains its separate explicit confirmation and is not changed by the title-bar preference.
- Regression coverage verifies old settings compatibility, stable wire values, persistence without schema changes, native decision mapping, save-before- action ordering, save failure containment, modal preservation, native hide IPC use, reset behavior, accessibility, and removal of duplicate toolbar actions.
- Final verification passed: root Rust format/Clippy/37 tests; desktop Rust
format/Clippy/96 tests with one configured live acceptance test ignored;
frontend lint/typecheck/82 Vitest tests/production build; and three Edge
Playwright flows at
900x600,1280x720, and1440x900. - Browser visual QA confirmed the Chinese close dialog and Help reset control at
900x600; screenshots are underapps/desktop/output/playwright/qa-20260810-close/. This proves the WebView UI, not a physical click on the native Windows title bar. - The isolated release is
apps/desktop/src-tauri/target/final-20260810-shutdown-race/release/runcove-desktop.exe; size25,314,336bytes; SHA-2564EFFB8F27994B74D75DDBEC6706DC73EDE7A993313B6F66AA3CB02C0ABC5CE20. - The older Help build was gone after the post-reboot check, so no RunCove executable was stopped or replaced. The newest release's real title-bar X and tray recovery remain a manual smoke check. Preview/browser QA was closed and ports 1421, 1422, and 14231 were free.
- Formal trademark, domain, crates.io, and npm registry clearance for
RunCovewas not part of thev0.2.0repository release. Repeat name clearance before wider package-manager or commercial distribution. - The real Windows UAC cancel and successful relaunch paths were not invoked in unattended validation because they require desktop interaction and would close the current RunCove instance. Token checks, denial mapping, instance handoff, IPC, and UI confirmation are covered deterministically.
- The environment-driven live acceptance checks that each target port is present but does not compare the complete observed port set. The deterministic Astro/workerd unit fixture provides the exact auxiliary-port exclusion check; the live assertion can be tightened later if its case schema is expanded.
- The final-source ten-minute CPU result passed the under-1% target by only 0.022070 percentage points, and working set increased during the observation window. No sustained private-memory growth was observed, but a longer idle soak is advisable before treating performance headroom as settled.
- Read-only inventory on 2026-08-07 confirmed external development listeners on ports 3100, 4321, 8080, and 1420. These processes are acceptance inputs only and must not be stopped or adopted without separate approval.
- The first real-data audit found that importing every
package.jsonscript would expose deployment, database, and port-killing commands as one-click actions. Automatic discovery now selects only exactdev,start,serve, andpreviewscripts. Projects with no safe service candidate remain available for manual configuration but are excluded from batch import. - Confirmed port ownership must be treated as historical when the current process points to a different registered project. The active suggestion is shown separately, and confirming a new owner replaces other confirmed owners for the same port and protocol without discarding the original first-seen timestamp when reconfirming the same owner.
- Lifecycle hardening keeps manual starts in
Startinguntil every expected port is owned by the managed process tree. User stops return toIdle, normal exits remain non-alertingExited, and only unexpected exits contribute to the tray exit count. - The ordered restore set is updated after start, stop, and watcher-observed
exit. Explicit shutdown freezes watcher synchronization, persists the
pre-stop order, and is idempotent when
app.exitraises a second exit event. - Tray stop-all emits
tray-stop-all-errorwith{ action, message, timestamp }and reveals the main window on failure; successful stop-all persists an empty restore set. - A Tauri mock-app npm fixture now covers stdout, stderr, non-newline tail fragments, exit code 7, SQLite session completion, abnormal classification, restore-set removal, and port release without touching the production DB.
- Runtime import only persists wrapper arguments after filtering sensitive flag
names, including camelCase credentials and opaque payload carriers such as
--headerand--define. - Astro exposed the intended
4321/tcplistener plus auxiliary listeners on 62853 and 62859 under the samenpm run devtree. Runtime observation now reads exact descendant--portevidence solely to filter those listeners; descendant argv is not copied into the saved launch profile. A red-to-green fixture preserves this behavior while the existing ambiguous-listener rule remains conservative. - External termination fixtures reject mismatched start time, executable path, and RunCove-managed PIDs. A verified self-created Node parent/child tree is terminated and its port released without touching unrelated processes.
- Live-import failure diagnostics report PID, cwd, and executable file name but never print the full process argv, which may contain credentials or headers.
- Real Windows UI inspection exposed repeated 5353/mDNS rows for Chrome and
Termius. Windows legitimately reported multiple reusable UDP sockets with the
same observable endpoint, but RunCove could neither distinguish nor operate
on individual socket handles. Scanner-level identity now uses
port + protocol + state + PID + bind address; an interleaved[A, B, A]red-to-green test proves exact duplicates collapse while distinct PID, protocol, state, and IPv4/IPv6 bindings remain. The live scan dropped from 136 rows to 122 at that instant and reported zero exact duplicate groups.
- Final
git status --short --untracked-files=allreports 137 changed paths: 15 tracked modifications and 122 new files. The tracked modifications are.gitignore,Cargo.lock,Cargo.toml,README.md,src/cli.rs,src/lib.rs,src/main.rs,src/model.rs,src/process/mod.rs,src/render/json.rs,src/render/watch.rs,src/scanner/mod.rs,src/scanner/windows.rs,tests/cli_tests.rs, andtests/scanner_tests.rs. - New root paths are
AGENTS.md,HANDOFF.md,notes.md, the compatibility CLI entry point, andsrc/cli_app.rs. The remaining 117 new paths are underapps/desktop, including the complete frontend/backend application, 53 generated Tauri icon assets, and 17 Playwright QA screenshots. - Root changes cover crate naming, shared primary/compatibility CLI entrypoints, Windows scanning and verified process-tree termination, plus regression tests.
apps/desktopcontains the React client, typed IPC adapter, deterministic browser mock, Tauri backend, SQLite migrations, generated icon set, and QA screenshots.git diff --checkpassed. Build outputs, Playwright session files, logs, PID files, dependencies, and runtime data are ignored rather than source files.- A browser preview was served during the 2026-08-08 evaluation. The final 2026-08-10 QA preview on port 1421 was identity-checked and stopped; no QA preview was intentionally left running.
- No commit, push, remote rename, CI/release edit, installer publication, or package publication was performed.
- Native
CloseBehavior::Quitnow runsshutdownthrough Tauri's async runtime andspawn_blocking, so the close event thread does not wait up to the eight-second managed-process cleanup limit. Successful cleanup callsapp.exit(0); worker or cleanup failures restore the window and emit the existingshutdown-errorevent. ProcessManager::shutdown_is_in_progress()gates nativeCloseRequestedhandling. A second title-bar X during an active shutdown is prevented and ignored instead of starting a competing hide/quit action.- React now shares
shutdownInFlightbetween tray quit and title-bar quit. New tray or native close requests are ignored while the promise is pending, and the ref is released infinallyon success or failure. Two App tests cover the tray and title-bar pending-shutdown races. - Verification after reboot:
- Root
cargo fmt --all -- --check, Clippy with warnings denied, andcargo test --offline --all-targets:37/37passed. - Desktop Rust format, Clippy, and
cargo test --offline --all-targets:96 passed / 1 ignored. - Frontend lint, typecheck,
npm test -- --run:82/82passed, andnpm run build: passed. npm run e2e:3/3Playwright flows passed at900x600,1280x720, and1440x900.npm run tauri buildpassed with the isolated release atapps/desktop/src-tauri/target/final-20260810-shutdown-race/release/runcove-desktop.exe; size25,314,336bytes; SHA-2564EFFB8F27994B74D75DDBEC6706DC73EDE7A993313B6F66AA3CB02C0ABC5CE20.
- Root
- Replaced implementation-oriented restore-set copy across Overview, Help,
elevation guidance, tests, mock data, and the native tray. Users now see
Previously running profiles,Restore previous run, andStartup order, localized as上次运行的配置,恢复上次运行, and启动顺序. - The restore action still starts only the profiles that were running before RunCove last exited, in their saved order. It does not import projects, restore every registered profile, or enable Windows startup automation.
- Added a visible trash action to every project card. It is disabled when any profile is starting, running, has a PID, or has a lifecycle operation in flight. The confirmation dialog states that RunCove registration, launch profiles, and port associations are removed without deleting project files from the computer.
- Removed the editor-only deletion path so there is one confirmation flow. Ref-level in-flight protection prevents rapid duplicate submissions; failure leaves the dialog and project visible and surfaces the backend detail.
- Added focused frontend coverage for successful deletion, deletion failure,
and repeated confirmation. The frontend suite now passes
85/85tests. - Final verification passed:
- Root
cargo fmt --all -- --check, Clippy with warnings denied, andcargo test --offline --all-targets:37/37passed. - Desktop Rust format, Clippy, and
cargo test --offline --all-targets:96 passed / 1 ignored. - Frontend lint, typecheck,
85/85Vitest tests, and production build passed. - Playwright passed
3/3workflows at900x600,1280x720, and1440x900, including disabled and active project deletion plus cancel.
- Root
- In-app browser QA at
900x600produced900x600-restore-previous-run.png,900x600-project-delete-actions.png, and900x600-project-delete-confirm.pngunderapps/desktop/output/playwright/qa-20260811-understandability/. Document and viewport widths were both 900 pixels; the confirmation dialog stayed inside the viewport and the browser console had no errors or warnings. - The verified preview was PID 27960, the project's Vite preview on port 1424. Its identity was rechecked before stopping it, and port 1424 was released.
npm run tauri buildpassed withCARGO_BUILD_JOBS=2and the isolated release atapps/desktop/src-tauri/target/final-20260811-understandability/release/runcove-desktop.exe; size25,314,504bytes; SHA-25680A5729A1D58531BCC005B0CBB1F6216C36B9D2908DC260150C5ED4A69664CEC.- The release was built but not launched, installed, committed, pushed, or published. No unrelated development service was stopped or modified.
- Added a named Windows wake event beside the single-instance mutex. A normal
duplicate launch signals the existing process, whose listener shows,
unminimizes, and focuses the main window.
acquire_after_previoussuppresses that signal so administrator handoff does not flash the old window while waiting for shutdown. Two Windows regression tests cover wake and no-wake. - A native final-executable smoke proved the real behavior: title-bar X hid PID
33824, the window disappeared from the targetable window list, relaunching the
same path restored the same window, and exactly one
runcove-desktop.exeprocess remained. The UI reported live local ports with zero managed runs or conflicts; port 14231 was free after Playwright. - Administrator instances are monitor-only. Typed backend guards and UI/tray disabled states cover project start/stop/restart, restore, browser/folder opening, tray stop-all, and external termination. This closes the path where a user-writable launch profile could otherwise execute with elevated rights.
- Desktop and CLI Windows termination use the absolute System32
taskkill.exe. The helper is covered by a path test and external identity tests still recheck listener, PID, start time, executable, and managed ownership. - Upgraded Vitest to 4.1.10 and Vite resolved to 6.4.3. The lockfile uses only
registry.npmjs.org; complete and production-onlynpm auditboth report zero vulnerabilities. A stricter Vitest mock type exposed and fixed one test-onlyLaunchProfilecallback annotation. - Version
0.2.0is synchronized across root Cargo, desktop Cargo, npm, Tauri, CLI version output, and the WebView child--webview-exe-versionargument. - Final verification after all code and dependency changes:
- Root format passed, warnings-denied Clippy passed,
38/38tests passed, and both CLI binaries reportruncove 0.2.0. - Desktop Rust format passed, warnings-denied Clippy passed,
98 passed / 1 ignored; the ignored test requires explicitly configured external local services. - Frontend lint, typecheck,
85/85Vitest tests, production build, and all three Edge Playwright workflows passed. git diff --checkpassed and no listener remained on Playwright port 14231.
- Root format passed, warnings-denied Clippy passed,
- Final isolated executable:
apps/desktop/src-tauri/target/final-20260811-v0.2.0/release/runcove-desktop.exe; size25,348,756bytes; SHA-256C80B1B6601ED42A34F333773A3C5997FDDFBA74E7B81610F490479F931192A13. - Local packaging rehearsal:
- Desktop portable zip contains
runcove-desktop.exe,README.md,CHANGELOG.md, andLICENSE; SHA-256E49A9408FE2BE7A89ACCC23376956C4627E8C781682CC05C68B00860C94CEA50. - Windows CLI zip contains the primary and compatibility executables, plus the same three
documents; SHA-256
13D11F813CE72FB9164B7C4452F925E106938C89559CAE15CACE710A4FEF141C.
- Desktop portable zip contains
- CI/release workflows now cover the desktop and new artifact names. GitHub is the authoritative workflow parser because no local YAML checker is installed; at this checkpoint publication was incomplete pending PR CI, merge, tag, workflow, and asset download verification.
- PR CI run
31493311756, while PR #1 was still a draft, reached the desktop Rust test link and failed withCVT1100: duplicate resource. type:VERSIONandLNK1123. The linker command contained the same generatedresource.libtwice becausetauri_build::build()linked it to binary targets and the custom build step also usedcompile_for_everything. - Commit
f10c4d4limited the custom embedding to GNU. CI run31496735900confirmed that MSVC linking no longer duplicated VERSION, but the library test executable then exited before the harness with0xc0000139 / STATUS_ENTRYPOINT_NOT_FOUND. The same behavior reproduced in a clean local GNU target when no manifest was linked to the library test. - The final local implementation separates resource ownership instead of
choosing between those failures:
- Tauri uses
WindowsAttributes::new_without_app_manifest()and remains the only producer of VERSION and icon resources. windows/app-manifest.rccontains only resource type 24 and is linked to every executable, including unit-test harnesses.- Both the
.rcand.xmlinputs emitcargo:rerun-if-changed, preventing a manifest-only edit from being hidden by an incremental build cache. objdumpshows the Tauri archive contains types 3, 14, and 16, while the manifest archive contains only type 24. The final MSVC executable contains exactly those four top-level resource types.
- Tauri uses
- Local verification after the split:
- Windows GNU format, warnings-denied Clippy, and full desktop tests passed:
98 passed / 1 ignored. - A project-local Rust
1.97.1 x86_64-pc-windows-msvctoolchain used Visual Studio 2022link.exe 14.44and Windows SDK10.0.26100; the exactcargo test --all-targets --no-fail-fastcommand passed with98 passed / 1 ignored. - Full GNU and MSVC
npm run tauri buildcommands passed. The MSVC executable reports product/file version0.2.0, is unsigned as documented, and has SHA-25651A29AB8313DD87D40D1442661E75A03EEA3786E35F07CE69D27E81BE2A7A9D1.
- Windows GNU format, warnings-denied Clippy, and full desktop tests passed:
- One full GNU suite initially exposed a timing-only npm fixture failure: the fixture closed its listener after two seconds while parallel scanner tests were active. The focused test passed, and extending the fixture lifetime to eight seconds made the subsequent complete GNU and MSVC suites pass without changing application behavior.
- No commit or push of the final fix and no new GitHub workflow run followed
31496735900; PR #1 remains Draft. Live checks confirmed that the repository is public and both workflows use only standard hosted runner labels. GitHub's billing documentation states that standard hosted runner minutes are free and unlimited for public repositories; larger runners remain billable and are not used here. Artifact/cache storage remains quota-bound, so release artifacts keep the existing seven-day retention. References: https://docs.github.com/en/actions/concepts/billing-and-usage and https://docs.github.com/en/actions/how-tos/write-workflows/choose-where-workflows-run/choose-the-runner-for-a-job. - The accidental 1.85 GB
apps/desktop/src-tauri/src-tauri/build tree was moved without deletion to the ignored project-local pathapps/desktop/src-tauri/target/accidental-nested-build-20260811/. No project source or file outside RunCove was removed or changed. - A final post-review rerun after adding manifest change tracking passed desktop
Rust format, warnings-denied Clippy, and the complete GNU suite (
98 passed / 1 ignored). The already-built MSVC library test executable was also launched directly and passed a filtered test, proving it no longer exits withSTATUS_ENTRYPOINT_NOT_FOUND; its PE resource table contains only one type,RT_MANIFEST(24). No commit, push, or new GitHub workflow was triggered.
- Commit
c281340was pushed once while PR #1 was still a draft. CI run31501585406passed all three CLI jobs, root Rust lint, frontend audit/lint/typecheck/tests/build, Edge browser workflows, desktop formatting, and desktop Clippy. The MSVC test harness started normally and ran 99 tests, proving the manifest resource fix. - The single failure was
tests::successful_tray_stop_all_synchronizes_an_empty_restore_set: GitHub's Windows runner uses an elevated token, so the real production guard correctly returnedAdministrator monitoring mode is read-onlyand the test's unconditionalunwrap()failed. This was a machine-dependent test defect, not a product failure or resource-link regression. - The private
stop_all_from_trayhelper now accepts a permission-check closure. Its only production call passesprivileges::ensure_process_action_allowed, and the guard remains the first operation before process reservation or storage mutation. Tests inject deterministic allow/deny results; the denied path verifies the error and confirms that the saved restore set is unchanged. - Independent review found no production security regression. Post-fix desktop
format, warnings-denied Clippy, and all targets passed locally:
99 passed / 1 ignored. No CI or release workflow file was changed.
- PR #1 merged into
mainas9b935857fcc79b2811a5a1fb16df9aae55a91e7a. PR CI run31561867655and the resultingmainCI run31562443457both completed successfully across the configured CLI and Windows desktop jobs. - Annotated tag
v0.2.0points to9b93585. Release workflow run31563084142completed successfully and published the non-draft, non-prerelease latest release at https://github.com/AbyssWhalen/RunCove/releases/tag/v0.2.0. - The published release has six assets: Linux x64, macOS x64, macOS arm64, and
Windows x64 CLI archives; the Windows x64 portable desktop archive; and
SHA256SUMS.txt. Freshly downloaded asset digests matched GitHub metadata, all five archive hashes matched the checksum file, and every archive extracted with the expected files and executable format. - Verified archive SHA-256 values:
- Linux x64 CLI:
817BE042DCBCB3C747E5E08E450E7B4C2957FB00BDDC8BEA247F8B29C611A3E2 - macOS arm64 CLI:
795671AA37EB384FD943C8C8AA95527F90878902486113FD181A3AF6E84F1E87 - macOS x64 CLI:
6401B3B126358708B39F6E4157EB085B2DE0E91A74E1339C8309740F078F98CF - Windows x64 CLI:
CFE0090E62180D7779A76418A57FE513746B06EEB6A86E5DA2A672BAAF9B5041 - Windows x64 desktop:
BE99C188B6DEF910D5DE4ADEA02028EF8410D0D46AD6B500FF2095D341BF6A7E
- Linux x64 CLI:
- The published Windows desktop executable is
12,692,992bytes, reports file and product version0.2.0, and is unsigned as documented. The CLI archives contain the primary and compatibility CLI executables; release-package compatibility checks matched JSON, range-filter, and error exit-code behavior. gh release downloadcould not validate the local certificate chain (x509: certificate signed by unknown authority). Public release URLs were therefore downloaded with Windowscurl.exe, then independently hashed and inspected. This was a local client trust-store limitation, not a release failure.- The public repository is now named
RunCove; its historical v0.1.0 release remains available. Its description and topics identify the current product. No force-push, history rewrite, larger runner, or removal of the old release occurred.
- GitHub renamed the public repository to
AbyssWhalen/RunCove. The localoriginremote and Cargo package metadata now use the canonical URLhttps://github.com/AbyssWhalen/RunCove. - The repository description now says:
RunCove is a Windows-first desktop app for monitoring local ports, launching npm projects, managing process trees, viewing logs, and restoring development services. - README, changelog, release notes, handoff, and implementation notes now use
RunCove as the public product name. The compatibility CLI remains in code and
tests for existing scripts, but new documentation presents
runcoveas the only primary command. - The existing
v0.2.0tag, six release assets, historicalv0.1.0release, and CI/release workflows were not rebuilt or rewritten. Root Rust format, warnings-denied Clippy, and all root targets passed after the metadata and unsupported-platform message update.