Conversation
Astro-Han
left a comment
There was a problem hiding this comment.
Independent review at b237ed1.
What changed: This PR adds an opt-in, per-Host retention policy that deletes archived tasks after 30/60/90 days. It adds storage.retention.query/set (epoch 202 -> 203) and a State Root document archive-retention.json. It also adds a maintenance lane: 8 families/tick, 1s active, 15 min idle. Each family is deleted through session.remove's path with a guard that runs under the removal admission. A Settings > Archived tasks section is added.
Scope checked, found sound:
- Opt-in: an absent or invalid document is treated as disabled. There is no SQLite migration (the schema-41
archived_atis reused). - Retention clock: it starts at
max(archivedAt ?? enabledAt, enabledAt), so pre-schema-41 rows count from enablement. Any enable or period change restampsenabledAtto at least the newest recorded time. - Restore race: the guard runs inside
#withStableRemovalPlan->#admission.runMany, the same gate unarchive takes. It rechecks archive state, pin state, setting revision andarchived_at. The commit is CAS-versioned. archived_atmaintenance: unarchive clears it, re-archive restamps it, and the archive-on-remove branch stamps it.- Excluded from deletion: pinned families, Agent Graph operators, and plans that would archive active subtasks or reclaim worktrees (held as
needs_review). - Clock going backwards: the sweep pauses.
- Setting changes: a change waits for the in-flight tick, and the guard holds while a change is pending.
- Merges cleanly into main. There were no prior reviews.
Tests: runtime-host retention/retirement/maintenance/protocol tests pass (72/72), as do the storage archive-retention-store tests (5/5).
Findings: P3 epoch collision; P3 no guard against the wall clock jumping forward before an unattended, permanent deletion; P3 preview wording.
Not exercised:
- The desktop UI was not run.
- Not verified: legacy
importSessionrows that are archived but have NULLarchived_atwould count fromenabledAtand could become eligible right after import. Worth a look.
Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.
| // Increment when the same protocol version no longer guarantees safe Client-Host | ||
| // interoperability. Mismatches are rejected before domain commands are admitted. | ||
| export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 202 as const; | ||
| export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 203 as const; |
There was a problem hiding this comment.
| const wentBack = now < this.#observedAt; | ||
| this.#observedAt = Math.max(this.#observedAt, now); | ||
| // Nothing can be eligible before the policy itself is `days` old. | ||
| if (now <= archiveRetentionDeadline(setting.enabledAt, setting.days)) return false; |
There was a problem hiding this comment.
P3: Only backward clock motion is detected (wentBack, newest metadata time). Nothing bounds a forward jump: a bad RTC or NTP, a VM restore, or a manual date change. If now jumps at least days past enabledAt, the whole backlog becomes eligible at once, because pre-schema-41 rows count from enabledAt. The sweep then deletes 8 families per second. The deletion is permanent, and the user never had the real retention window to notice or restore. This is the one unattended path where a misread clock destroys data. Two possible guards:
- Compare the wall-clock delta since the last observation with the monotonic delta inside a running process. If wall time leaps far ahead, pause (as for backward motion) until the user re-confirms.
- At minimum, cap how far a single sweep may advance past the last recorded
lastSweep.at.
Once such a jump has persisted lastSweep.at, the pause correctly blocks later backward motion, so the gap is only on the forward side.
| conflict: 'The setting changed elsewhere and has been reloaded.', | ||
| previewNone: 'No archived tasks would be deleted yet.', | ||
| preview: (count: number, date: string) => | ||
| `Covers at least ${count === 1 ? '1 archived task' : `${count} archived tasks`}; the first can be deleted after ${date}.`, |
There was a problem hiding this comment.
P3: "Covers at least N archived tasks" (and "At least N tasks are covered" in the confirm) is presented as a lower bound. But countArchiveRetentionCandidates counts families that the sweep will hold: those that would archive an active subtask or reclaim a worktree (needs_review), those whose removal plan contains a pinned subtask, and busy ones. So the count can overstate what will actually be deleted. Suggest neutral wording ("N archived tasks are subject to cleanup") or excluding held families from the count.
|
Thanks @Astro-Han. All four points are addressed in P3: forward clock jump. Agreed, this was the one unattended path where a misread clock destroys data. Neither suggested guard fits a local Host, though:
What I did instead is a gap hold. When the wall clock has moved more than
The trade-off is that someone genuinely away for more than 7 days sees the banner and a one-day delay. A bad clock, on the other hand, can no longer delete a backlog in the minute it is noticed. Tests run on an injected clock and cover:
Each of these was mutation-checked. Your import note was a real gap, and it's fixed.
P3: preview wording. You're right: the count includes families the sweep will hold, and it also misses subtasks orphaned later, so it is neither a lower nor an upper bound. The copy is now neutral, "N archived tasks are subject to automatic cleanup", in the section and in the confirm, in all three locales. P3: epoch. Checks on |
Astro-Han
left a comment
There was a problem hiding this comment.
Follow-up review at fefa3dc (delta since b237ed1: one commit, "hold retention after a forward clock jump").
Earlier points:
- Forward clock jump: addressed by the gap hold. The in-run detection, re-arming, the setting-change clear and the post-deadline restart check (persisted floor plus the newest metadata time) look correct. There is one regression in the restart path before the deadline; see the inline comment.
- Import: fixed.
mergeAttachedBundlerestampsarchived_aton archived bundle rows inside the import transaction, and there is a test. ExternalimportSessionalways writes non-archived headers, so it is unaffected. - Preview wording: fixed in all three locales and in the confirm text.
- Epoch: unchanged.
mainis still at 202, and this PR claims 203. #5753 and #5495 also claim 203 (#5826 now claims 204). The re-pin plan in the thread is fine; I am just flagging that the collision is still open.
Non-blocking design note: the hold defers deletions by only 24h. For a clock that really is wrong, the safety net is that the user opens Settings > Storage within a day. A clock that is wrong and unnoticed for more than a day still deletes the backlog. This is an acceptable trade-off for a local Host, but consider making the notice visible outside the Storage settings page (for example, a one-time notification) if that is cheap.
Tests: the runtime-host retention coordinator, retention protocol and retirement tests pass (68/68), including a temporary probe that I later reverted. The storage archive-retention-store and session-bundle-policy tests pass (8/8).
CI: test and windows_recovery pass. audit and Build immutable tarball fail on dependency advisories in shipped dependencies (axios, fast-uri). This PR changes no lockfile, so those failures are unrelated. The PR merges cleanly.
Verdict: one P2 (a false clock-jump hold, with a banner that stays up, after any restart before the deadline) and one P3 (epoch coordination). No data-loss paths found in the delta.
Automated review by Claude (Anthropic), posted on behalf of the maintainer. It is not an independent human review.
| // A forward jump is caught even when it lands short of the deadline. Before | ||
| // the deadline no SQL runs, so a fresh process measures from the persisted | ||
| // floor alone. | ||
| if ((observedThisRun || now <= deadline) && now - previous > gap) { |
There was a problem hiding this comment.
P2: a restart before the deadline records a false clock-jump hold, and its banner stays up until the deadline.
When a process is fresh (observedThisRun === false) and now <= deadline, previous is only the persisted floor. Before the deadline that floor is effectively enabledAt, because no lastSweep or hold is written yet. The newest metadata time is never consulted on this arm. With the allowed periods of 30, 60 and 90 days, the gap is always 7 days. So any Host restart more than 7 days after enabling, and before the deadline, records a hold, and the user sees "the system clock moved ahead by about N days ... check your system time". That happens even if the app was in use a minute earlier.
I reproduced it with the rig:
set(true, 30).- Advance 9 days with in-run sweeps (no hold).
restart(), then wait 1h withnewest = now - 1min.sweep(): the hold is{since: enabledAt, detectedAt: enabledAt+9.04d}.- Five days later (past
until, still before the deadline),query().holdis still present.
The hold persists because the clear at L203-210 only runs after the deadline and L413 returns the hold regardless of until. It also re-arms on every restart that comes more than 7 days after the previous detectedAt.
Fix: on this arm, measure from Math.max(previous, await readLatestSessionMetadataTime()). It is a single MAX query, so the "no SQL before the deadline" property can give way here. Also drop or ignore an expired hold independently of the deadline: clear it when now >= hold.until, or omit it from storage.retention.query once expired. Please add a test for a restart at day 9 of a 30-day policy with recent metadata, asserting no hold.
fefa3dc to
5000b16
Compare
|
Thanks @Astro-Han, good catch: your repro reproduces exactly. The fix is in P2: false hold on a fresh process before the deadline.
Notice outside Settings. Agreed that this would make the 24 h window more useful. It is the optional follow-up already listed in #5899 (a one-time notice for new automatic deletions), and I'll fold the hold into that follow-up rather than widen this PR. Epoch. Checks after the rebase:
|
|
One more fix, found while designing the next step (idle archiving): What was wrong. The pinned-family exclusion was a correlated What changed. The exclusion is now an uncorrelated Synthetic timings, same result sets:
Tests: storage retention 13/13, the candidate/page parity test, and the runtime-host retention coordinator 26/26. Biome is clean. The "Candidate rows" paragraph in the description still holds. Only its index claim was wrong. |
Astro-Han
left a comment
There was a problem hiding this comment.
Re-review at 5000b16 (rebased onto main 0b25078; the only new PR commit is 5000b16).
Prior findings
- P2, a false clock-jump hold when a fresh process restarts before the deadline: fixed. A fresh process now measures the gap from
max(persisted floor, newest metadata time)(archive-retention-coordinator.ts:197-200). A hold is cleared oncenow >= until, whatever the deadline (L188-192).storage.retention.queryno longer reports a hold that has expired (L417-420). The new tests reproduce my earlier probe exactly (a restart on day 9 with metadata written a minute earlier records no hold), and they also check that a real jump still holds and that an expired hold is cleared with a single write. Before the deadline the candidate list is still never read. - P3, epoch 203: still open, coordination only.
mainis at 202. #5709, #5495 and #5753 also claim 203, and #5826 claims 204. Whichever PR lands second needs to re-pin, as the author has already said.
Remaining (P3): see the inline comment. A fresh process can only tell when the Host last ran from Session metadata writes. A Host that kept running with no task activity for more than 7 days and is then restarted still shows the "system clock moved ahead" banner. It now lasts 24h instead of weeks, so this is cosmetic.
Verification: I built core, storage, mcp, runtime and runtime-host. archive-retention-coordinator, storage-retention-protocol and the session-retirement* tests pass 76/76. I also ran a temporary probe test, since reverted, for the idle-restart case below.
CI: audit and Build immutable tarball now pass because the rebase picked up #5906. windows_recovery passes. test was still pending when I reviewed. Mergeable.
Update for head fd9c3f36 (pushed after this review was drafted at 5000b160): the only new commit rewrites the pinned-family exclusion in archiveRetentionCandidatePredicate from a correlated NOT EXISTS to an uncorrelated NOT IN. Both sides are COALESCE(revision_root_session_id, session_id), so no NULL can reach the NOT IN list and the semantics are unchanged; the findings above still apply at this head.
This is an automated review by Claude (Anthropic), run on behalf of the maintainer.
| // last ran: one MAX query, once per process. | ||
| const since = observedThisRun | ||
| ? previous | ||
| : Math.max(previous, (await this.#catalog.readLatestSessionMetadataTime()) ?? 0); |
There was a problem hiding this comment.
P3: on a fresh process, the only signal for when the Host last ran is MAX(committed_at, archived_at) from session_metadata. The in-memory high-water mark is never persisted. lastSweep is not written before the deadline, and it is not written on unchanged passes either. So a Host that ran continuously with no task activity for more than 7 days still records a hold when it restarts, and the banner tells the user to "check your system time".
I confirmed this with a temporary test (reverted): set(true, 30), newest = enabledAt, nine daily in-run sweeps with no hold, then restart() and one minute later → hold {since: enabledAt, ...}.
The banner now lasts only 24h, so this is minor. One fix: persist a coarse heartbeat, e.g. write latest.observedAt from #tick at most once a day, and fold it into the #read floor. Alternatively, reword the banner so it also covers "this Host was not running for a while".
Add one opt-in, per-Host setting that deletes archived tasks after 30, 60
or 90 days. It is off by default; the Host decides and deletes, and
Desktop shows the setting and what the last sweep did.
Setting. A dedicated Host document, archive-retention.json in the State
Root, holds { version, revision, enabled, days, enabledAt, observedAt,
latest }. It is not part of the runtime policy, agent settings or config
export/import; the only writer is the new storage.retention.set command.
enabledAt is stamped by the Host clock and re-stamped by any change
(enabling, or new days while enabled), so enabling never deletes a
backlog and shortening never deletes at once; disabling clears it. A
revision CAS rejects stale writes. A document that cannot be fully
validated, including one from a newer Host, reads as disabled.
Sweep. A new HostStorageMaintenance lane, started after Ready, runs one
bounded step a second while work remains and every 15 minutes otherwise,
deleting at most 8 revision families a tick. No SQL runs before
enabledAt + days. Candidates are the rows Settings > Archived tasks shows
(archived roots and orphaned archived subtasks, no graph operators, no
preparing copies) in a family no member of which is pinned, oldest first,
with a keyset cursor so kept families do not starve the rest. The
existing (is_flagged, is_archived, ...) index bounds the scan; no
migration.
Deletion goes through session.remove's own path: #remove becomes
removal admission, after the plan is stable and before any retirement
work. It rechecks the policy (no pending change, same revision, days and
enabledAt), that every member is archived and unpinned, and that the
family's newest clock start, max(archivedAt ?? enabledAt, enabledAt), is
more than `days` old. A family whose removal would archive an active
subtask or reclaim a subagent worktree is kept as needs-review. The
existing busy guards apply unchanged and count as skipped-busy. Manual
session.remove is unchanged.
Clock. Wall time with two guards: the Host keeps the latest time it has
observed (persisted with the document), and a sweep pauses, records the
pause once and deletes nothing while the clock reads earlier than that or
than the newest committed_at/archived_at in session_metadata.
Results are latest-only: lastSweep { at, deleted, skippedBusy,
needsReview, failed, paused? } and lastDeletion { at, count, bytes? },
with bytes measured before deletion as the batch preview measures them.
The document is written only when a sweep changed something, and logs
carry counts only.
Protocol (epoch 202 -> 203): storage.retention.query returns the setting,
a Host preview (candidate families and when the first becomes eligible;
previewDays previews enabling or changing days now) and the latest
results; storage.retention.set { expectedRevision, enabled, days }
answers committed or revision_conflict. Both are renderer pass-through
and remote-owner operations.
Desktop. Settings > Archived tasks gains an Automatic cleanup section
for the selected Host (the page now shows the Host picker): a switch, the
period, the Host preview, the last automatic cleanup with its size, a
needs-review count and a paused notice. Enabling or changing the period
asks a confirm that states the Host preview. The copy says the setting
covers every archived task, starts its clock when enabled, keeps pinned
tasks and deletes permanently. The legacy page only renders the feature
section.
Refs apache#5899
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Address the adversarial review of the opt-in retention commit.
Safety
- A setting change now waits for the sweep step in flight. It marks
itself pending first, so the guard admits nothing new and the loop
stops before the next family; the family already admitted finishes
and is recorded, and only then does storage.retention.set commit.
Once set answers, no deletion admitted under the old setting runs.
- A pass records its results only under the setting revision it
started with.
- Draining stops a sweep before the next family.
- enabledAt is max(now, the observed high-water, the newest metadata
time), so enabling behind a clock that went back cannot backdate the
deadline; any setting change clears a recorded pause.
- A candidate row that no longer decodes is returned as such, counted
as failed once per pass and passed over, so it cannot wedge a sweep.
One authority
- The guard's worktree check uses the count the removal preview uses
(one shared helper, so no worktree executor means none reclaimed).
- readSessionArchiveTimes is gone; the guard reads archivedAt through
readCatalogRecord, the reader the manual age guard uses.
- The archived-task row is one SQL predicate next to the catalog's
visibility predicate, which the catalog page query now shares.
Retention joins session_catalog_projection, and "orphaned" means the
parent is not a catalog-visible Session, as the rail treats it. A
Desktop test checks the candidates equal archivedTaskRows on a mixed
fixture.
- Sweep and deletion records have one decoder in core, used by the
State Root document and the protocol.
Simpler
- previewDays is gone. The query always returns preview { count,
eligibleAt? }; eligibleAt is present exactly while enabled with
candidates. Desktop states "at least N" and "no earlier than about
<its own now + days>" in the confirm.
- The preview is one aggregate query in storage.
- observedAt is no longer persisted; after a restart the floor is
enabledAt, the last sweep time and the newest metadata time.
- The guard compares the setting revision only.
- The section names the Host it applies to.
Refs apache#5899
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Address the review of the opt-in retention for archived tasks.
Forward clock jump. A wall clock set far ahead (bad RTC or NTP, VM
restore, a manual date change) could make a whole backlog eligible at
once. A sweep now holds deletions for 24 hours when the clock moved
ahead of the last time the Host observed by more than the smaller of
the retention window and 7 days. In a running Host that reference is
the in-memory high-water; after a restart it is the persisted floor
(enabledAt, the last sweep, a previous hold) and, once past the
deadline, the newest Session metadata time, which says when the Host
last ran. The hold is recorded as latest.hold { since, detectedAt,
until }, a further jump re-arms it, it is cleared once the clock
reaches `until`, and any setting change clears it. No uptime or
monotonic clock is used: a sleeping laptop would read as a jump, and a
credited clock was rejected in apache#5899. Desktop shows when cleanup
resumes and how far the clock moved, and suggests turning cleanup off
if the time is wrong.
Preview wording. The count includes families a sweep keeps (busy, for
review) and misses subtasks a deletion orphans later, so it is neither
bound; the copy now says "N archived tasks are subject to automatic
cleanup" in all three locales, in the section and the confirm.
Imported archived tasks. A Session bundle copies session_metadata rows
verbatim, so an archived task arrived with the archive time (or none)
of the machine it left and could be deleted right after import. The
import now stamps archived_at with the import time for every imported
archived Session.
Refs apache#5899
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ata write A Host restarted more than 7 days after enabling retention, but before the deadline, measured the forward gap from the persisted floor alone (effectively enabledAt). It recorded a false clock-jump hold even when the app had been in use a minute earlier, and because a hold was only cleared after the deadline and always reported, its banner stayed up until then. - A fresh process now measures the gap from the later of the persisted floor and the newest Session metadata time, before and after the deadline alike: one MAX query, once per process. No candidate is read before the deadline. - A hold expires on its own: once the clock reaches `until` it is cleared with one write, whatever the deadline, and the query never reports a hold whose day is over. Refs apache#5899 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The retention candidate predicate excluded pinned families with a correlated NOT EXISTS over session_metadata. No index covers is_flagged (migration 26 dropped session_metadata_by_flag, which the old comment still cited), so every candidate rescanned the table. The cost was quadratic and ran synchronously on the Host thread, for both the sweep page and the Settings preview count. Synthetic timings: about 47 ms at 2k Sessions, 1.1 s at 10k, 20 s at 40k. Use an uncorrelated NOT IN over the pinned family roots. SQLite builds it once per statement: 2 ms at 2k Sessions, 4 ms at 10k, 21 ms at 40k. The result set is identical (checked on the synthetic data). The family root is COALESCE(..., session_id), and session_id is NOT NULL, so NOT IN has no NULL pitfall. The parent check in the row predicate stays correlated: it is a primary-key lookup. Refs apache#5899 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fd9c3f3 to
ffaa68e
Compare
Astro-Han
left a comment
There was a problem hiding this comment.
Re-review at ffaa68e7. The branch was rebased onto main c7fa6bb6. git range-diff shows the five earlier PR commits unchanged (=), so the only new code is ffaa68e7 "persist idle retention heartbeat".
Prior findings
- P3, a Host that stays up with no task activity for more than 7 days and then restarts shows a false "clock moved ahead" hold: fixed. Sweeps now persist
latest.observedAtat most once a day (#heartbeatDue/#heartbeat,archive-retention-coordinator.ts:336-353). The heartbeat is also carried on#record,#pauseand#clearHold, and#loadfolds it into the floor (L579-584). It is written only after the gap check passes (L202-207), so a real forward jump is never laundered into the floor. The storage decoder accepts the new key and rejects non-integers, andstorage.retention.querydoes not expose it. I checked this with temporary probe tests, since reverted: 22 days idle past the deadline with sweeps every 12h, then a restart a minute later, records no hold; 9 days idle before the deadline, then a restart, records no hold (this was my earlier repro); and a real 8-day jump after heartbeats still holds, withsinceset to the last heartbeat. - P3, compatibility epoch 203: still open, coordination only.
mainis still at 202, and #5866, #5709, #5495 and #5753 are all still open and also claim 203.
New: P2, the test job will fail. See the inline comment. Because of the heartbeat, #finishPass now writes an unchanged pass once a day, so lastSweep is no longer undefined after a pass that deleted nothing. session-retirement-coordinator.test.ts:1561 still asserts that it is undefined, and it fails locally, deterministically, at this head. The other retirement and retention tests pass.
Verification: I built core, storage, mcp, runtime, acp, antigravity and runtime-host (tsc OK). Results: archive-retention-coordinator, storage-retention-protocol, session-retirement* and storage-maintenance pass 81/82 (the one failure is the test above); storage archive-retention-store and session-bundle-policy pass 8/8.
CI: audit, Build immutable tarball, the direct-peer addons, the installed-CLI validations and windows_recovery pass. test is still pending, and I expect it to fail on the test above. Mergeable; blocked only on review.
This is an automated review by Claude (Anthropic), run on behalf of the maintainer.
| const result = await rig.sweepUntilIdle(); | ||
| assert.deepEqual(await rig.present([task]), []); | ||
| assert.equal(result.lastDeletion, undefined); | ||
| assert.equal(result.lastSweep, undefined); |
There was a problem hiding this comment.
P2: this fails at ffaa68e7, so test will go red. enabledAt + 31 * DAY is more than a day after enabledAt, so #heartbeatDue is true, and #finishPass (archive-retention-coordinator.ts:390) now records the unchanged pass with lastSweep: { at, deleted: 0, ... } instead of returning early. The point of this test is that a task removed by hand is not counted as a deletion, which lastDeletion === undefined and deleted === 0 already show. Suggested fix: replace this line with assert.equal(result.lastSweep?.deleted, 0);. Alternatively, if lastSweep should keep its old meaning ("last time the result changed"), let the heartbeat-only path write just observedAt and leave lastSweep alone.
Closes #5899 (step 2 of #5776). It adds an opt-in, per-Host setting, delete archived tasks after N days, which is off by default. The Host decides and executes; Desktop exposes the setting and shows what happened.
Rules (as specified in #5899)
max(archivedAt ?? enabledAt, enabledAt), and the boundary is strict. Enabling the setting never deletes a backlog at once, and legacy tasks withoutarchivedAtcount from enablement.enabledAton the Host clock, so it is never earlier than the latest time the Host has observed. Disabling clears it.archivedTaskRowsfor a mixed fixture: revisions, live-parent subtasks, orphans, a graph operator, preparing rows, and rows with a missing projection.session.remove, together with every existing busy guard. Manual removal behaves exactly as before.How it works
Setting document.
archive-retention.jsonin the State Root holds{ version, revision, enabled, days, enabledAt?, latest? }.storage.retention.set. It is not part of the runtime policy, agent settings patches, or config export/import.Sweep lane in
HostStorageMaintenance. It starts after Ready.enabledAt + days), it runs no SQL at all.setreturns only once nothing more can be deleted.Results are latest-only:
lastSweep { at, deleted, skippedBusy, needsReview, failed, paused? }lastDeletion { at, count, bytes? }, wherebytesis measured before deletion with the same helper as the batch preview.The document is written only when a sweep changes something. Logs carry counts only.
Protocol (epoch 202 → 203)
storage.retention.query {}returns{ revision, enabled, days, enabledAt?, preview: { count, eligibleAt? }, lastSweep?, lastDeletion? }.storage.retention.set { expectedRevision, enabled, days }returnscommittedorrevision_conflict.session.remove.Desktop: Settings › Archived tasks
mixedfor that reason.Verification
dist, are all caught.build:test, Biome,format:check,lint, Knip (desktop and ui),protocol-epoch-check(202 → 203),check-renderer-architecture --strict-base, locale hygiene, the Astryx inventory, the Windows inventory, app-shell hooks, ASF headers,git diff --check, and the desktop typecheck.main.dev:worktreeon a disposable copy of a real workspace.archive-retention.jsonrecordedenabledAt.enabledAtback 31 days and the archive times of three tasks back 40 days in the copy, then restarted.Size: production code is +2171/−28, of which about 145 lines are copy and about 57 are the test-only memory store. Tests are +1868.
AI use
Implemented and reviewed with Claude Code; the commits carry a
Co-Authored-Bytrailer.🤖 Generated with Claude Code