Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
bb40e9a
Add cold storage for archived conversations
Quicksaver Jul 14, 2026
b56f8f5
Harden archived thread lifecycle recovery
Quicksaver Jul 14, 2026
1b6c373
Harden cold archive restoration
Quicksaver Jul 14, 2026
71f28ad
Harden cold storage lifecycle races
Quicksaver Jul 15, 2026
6ac4c9d
Evict idle cold storage lifecycle locks
Quicksaver Jul 15, 2026
9e5cad6
Infer cold storage service contract
Quicksaver Jul 15, 2026
a42fe41
Fix cold storage attachment ownership
Quicksaver Jul 15, 2026
47719d6
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 16, 2026
cebc5c4
Preserve fork storage migration metadata
Quicksaver Jul 16, 2026
70cc4f1
Remove unrelated fork deployment metadata
Quicksaver Jul 16, 2026
1ace10b
Hide optimistically archived sidebar threads
Quicksaver Jul 16, 2026
b824213
Refresh shell state from an authoritative snapshot per session
Quicksaver Jul 17, 2026
d919a1b
Document conversation data savings branch
Quicksaver Jul 17, 2026
ddff9d2
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 18, 2026
2db6465
Document upstream integration state
Quicksaver Jul 18, 2026
e9cffa4
Harden archive shell integration after upstream merge
Quicksaver Jul 18, 2026
269c8b8
Clarify conversation data savings behavior
Quicksaver Jul 19, 2026
99f25a0
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 19, 2026
99dbeb7
Support inspecting cold archive SQLite state
Quicksaver Jul 19, 2026
17b4906
Document cold archive inspection workflow
Quicksaver Jul 19, 2026
2bba1bf
Fix cold archive inspection query
Quicksaver Jul 19, 2026
4b76b15
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 19, 2026
aab4d68
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 19, 2026
20b7f6c
Enable pnpm global virtual store
Quicksaver Jul 19, 2026
622d762
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 20, 2026
7e9c031
Test forced project cleanup of cold threads
Quicksaver Jul 20, 2026
e04c86f
Document cold cleanup after forced project removal
Quicksaver Jul 20, 2026
2362493
Synchronize cold thread deletion test
Quicksaver Jul 20, 2026
2cc2cac
Verify cold shell before forced project cleanup
Quicksaver Jul 20, 2026
7dca0f6
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 20, 2026
1ceb005
Update cold storage tests for latest sequence
Quicksaver Jul 20, 2026
441a064
Protect shell sync and unarchive handoffs
Quicksaver Jul 20, 2026
925af20
Reject unarchive without restored storage
Quicksaver Jul 20, 2026
7f177b0
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 20, 2026
4307842
Re-archive stale restored conversations
Quicksaver Jul 20, 2026
6c124a6
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 21, 2026
0f783c5
Harden thread archive cleanup and evict stale detail caches
Quicksaver Jul 21, 2026
559569f
Evict cached thread details after archive reconciliation
Quicksaver Jul 21, 2026
20c31e5
Prevent archived thread cache restoration
Quicksaver Jul 21, 2026
c6b4c64
Fix archive cache revival lifecycle
Quicksaver Jul 21, 2026
299048e
Invalidate tombstoned cache writes on revival
Quicksaver Jul 21, 2026
57ffb32
Strengthen archived thread cache assertions
Quicksaver Jul 22, 2026
1a94992
Clear legacy mobile thread caches during migration
Quicksaver Jul 22, 2026
6422de5
Harden archive cleanup retry and cache eviction
Quicksaver Jul 22, 2026
2b7ab7f
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 22, 2026
f61f477
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 22, 2026
75c813d
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 23, 2026
66296d2
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 23, 2026
7bb3daa
Preserve archived cleanup in Sidebar V2 project removal
Quicksaver Jul 23, 2026
68a30ae
Document archived cleanup in Sidebar V2 project removal
Quicksaver Jul 23, 2026
e189a80
Preserve live-thread safeguard during archived project deletion
Quicksaver Jul 23, 2026
f19b615
Harden archived project deletion safeguards
Quicksaver Jul 23, 2026
e35e66f
Fix project delete compatibility test narrowing
Quicksaver Jul 23, 2026
fbb83c2
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 23, 2026
d12c4a2
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 23, 2026
64bd965
Test archived project removal grouping
Quicksaver Jul 23, 2026
bfb35d2
Document archived project removal helpers
Quicksaver Jul 23, 2026
355d1eb
Cover grouped archived project removal cases
Quicksaver Jul 23, 2026
5af3948
Cover grouped project removal planning
Quicksaver Jul 23, 2026
d9035e4
Merge upstream/main into split/conversation-data-savings
Quicksaver Jul 23, 2026
7d47346
Retry archived thread cache synchronization
Quicksaver Jul 23, 2026
e328438
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 23, 2026
8254e75
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 23, 2026
0d939ee
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 23, 2026
e9abbf0
Retry restored archive cleanup on unarchive replay
Quicksaver Jul 23, 2026
3f385ab
Keep archive sorting consistent during transitions
Quicksaver Jul 23, 2026
6e25770
Keep hidden archive threads out of palette actions
Quicksaver Jul 23, 2026
64be4aa
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 24, 2026
cee756e
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 24, 2026
edd2f3b
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 24, 2026
1641ed9
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 25, 2026
eb8fb79
Remove main-only pnpm global store pollution
Quicksaver Jul 26, 2026
4ebfc29
Fix archived thread action return type
Quicksaver Jul 26, 2026
fc1d179
Require cold storage error causes
Quicksaver Jul 26, 2026
219c60c
Stop retrying evicted thread subscriptions
Quicksaver Jul 26, 2026
4fabe73
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 27, 2026
6bbb223
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 27, 2026
ba3ec7c
Clean up previews across thread lifecycle
Quicksaver Jul 27, 2026
6f8e371
Close previews from thread lifecycle events
Quicksaver Jul 27, 2026
92426d2
Protect preview cleanup across shell synchronization
Quicksaver Jul 27, 2026
14292a3
Clarify queued preview lifecycle cleanup
Quicksaver Jul 27, 2026
587d386
Fix preview cleanup lifecycle races
Quicksaver Jul 27, 2026
97212a3
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Quicksaver Jul 27, 2026
9f66a18
Merge commit '32843c2551bd5832d436ef46bff5ab1198cdc39c' into split/co…
Quicksaver Jul 27, 2026
ed76eea
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Jul 28, 2026
cbea4d3
Refactor archive and preview integration boundaries
Jul 28, 2026
02bfb7e
Document archive integration boundaries
Jul 28, 2026
f9f882e
Preserve lifecycle state across archive retries
Jul 28, 2026
c57ce74
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Jul 28, 2026
0d8c267
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Jul 28, 2026
db348f1
test(client-runtime): update archive cache fixtures
Jul 28, 2026
e32d50c
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Jul 29, 2026
7548be8
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Jul 29, 2026
61565ad
Recover orphaned restore reservations
Jul 29, 2026
ccae34c
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Jul 29, 2026
0981003
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Jul 29, 2026
c2bfc52
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Jul 29, 2026
eb55ca2
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Jul 29, 2026
8dc7b15
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Jul 29, 2026
20a7a77
Serialize archive quiescing with restore
Jul 29, 2026
a7332c6
Merge remote-tracking branch 'upstream/main' into split/conversation-…
Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/test-t3-app/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Always pass `--dev-url` for a dev-runner environment so the generated pairing UR

Read [references/sqlite-fixtures.md](references/sqlite-fixtures.md) before changing the database.

- Use `node apps/server/scripts/t3-sqlite-state.ts query` for schema discovery and read-only checks.
- Use `node apps/server/scripts/t3-sqlite-state.ts query` for schema discovery and read-only checks. It targets `state.sqlite` by default; pass `--database archive` when inspecting cold archive manifests or chunks.
- Stop the dev server before using `node apps/server/scripts/t3-sqlite-state.ts exec`, then restart it with the same base directory.
- Seed projection tables only for disposable UI fixtures. Use application commands and APIs when testing business behavior or projection correctness.
- Use the auth CLI, not direct `auth_*` table edits, for pairing and sessions.
Expand Down
13 changes: 12 additions & 1 deletion .agents/skills/test-t3-app/references/sqlite-fixtures.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Load this reference only when inspecting or seeding local T3 state directly.

## Select the correct database

When `--base-dir` or `--home-dir` is explicit, runtime state lives under `<base-dir>/userdata` and the database path is `<base-dir>/userdata/state.sqlite`. The `<base-dir>/dev` state directory is only the fallback for an implicit development home, preventing an ordinary `vp run dev` from touching production state.
When `--base-dir` or `--home-dir` is explicit, runtime state lives under `<base-dir>/userdata`. Hot projections and runtime state are stored in `state.sqlite`; cold archive manifests and compressed chunks are stored in `archive.sqlite`. The `<base-dir>/dev` state directory is only the fallback for an implicit development home, preventing an ordinary `vp run dev` from touching production state.

Start the target runtime once before seeding so all migrations have run. Use an isolated base directory. Stop the server before writes to avoid racing application state or an active projection.

Expand All @@ -18,6 +18,15 @@ node apps/server/scripts/t3-sqlite-state.ts query \
--sql "SELECT name FROM sqlite_schema WHERE type = 'table' ORDER BY name"
```

The helper targets `state.sqlite` by default. Select the cold archive database explicitly for read-only inspection:

```bash
node apps/server/scripts/t3-sqlite-state.ts query \
--base-dir <base-dir> \
--database archive \
--sql "SELECT t.thread_id, t.archive_version, COUNT(c.chunk_index) AS chunk_count FROM archive_threads AS t LEFT JOIN archive_thread_chunks AS c ON c.thread_id = t.thread_id GROUP BY t.thread_id, t.archive_version ORDER BY t.thread_id"
```

Inspect current columns before writing a fixture:

```bash
Expand All @@ -36,6 +45,8 @@ node apps/server/scripts/t3-sqlite-state.ts exec \

Use one statement per invocation for both `query` and `exec`; the helper wraps writes in a transaction and prints the backup path after a successful mutation. Use a single insert with multiple value rows when a fixture needs several records.

Cold archive manifests and chunks are authoritative recovery data. Prefer `--database archive` for queries only. Exercise archive and restore behavior through application commands and APIs instead of constructing or changing archive bundles directly; only use archive writes for a deliberately isolated corruption or compatibility fixture.

## Seed projection data carefully

The web UI primarily reads these projection tables:
Expand Down
55 changes: 55 additions & 0 deletions BRANCH_DETAILS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Conversation Data Savings

Archived conversations use cold storage instead of retaining full hot projections and diagnostics in `state.sqlite`.

- `state.sqlite` keeps only the lightweight archived thread shell needed by the Archive page plus command receipts required for retry idempotency. Conversation events, messages, activities, turns, checkpoints, plans, session/runtime rows, and content attachments are written as bounded gzip-compressed chunks in the separate `archive.sqlite` database, then removed from hot storage. Command receipts remain hot until permanent deletion so retrying an accepted archive command returns its original sequence.
- `apps/server/scripts/t3-sqlite-state.ts` is the supported isolated-database inspection path. It targets hot `state.sqlite` by default and accepts `--database archive` for cold manifest and chunk queries, with both paths derived from the shared server configuration. Archive and restore behavior should use application commands rather than direct archive-bundle writes.
- Content attachments are part of the archive bundle and return on unarchive. Attachment collection follows exact ids persisted in thread messages, while retry cleanup may reuse exact attachment chunk filenames already recorded in the cold bundle; normalized thread-name collisions cannot claim another thread's files. Provider diagnostic logs and terminal history logs are deliberately destructive on archive: they are deleted, never copied into the bundle, and never restored. Provider-log cleanup matches only the exact thread log and its numeric rotations so similarly prefixed thread ids are not affected.
- Cold restore preserves binary SQL values, validates attachment entry names, and atomically replaces attachment files before marking SQL rows restored. It keeps compressed chunks authoritative on failure, pages chunk reads to bound memory, rejects unknown tables/chunk kinds, and intersects archived row columns with the current schema so older bundles remain recoverable after compatible migrations.
- Permanent thread deletion removes the shell, event stream, command receipts, every thread-owned projection/runtime/checkpoint row, attachments, terminal history, provider logs and rotations, and any cold-archive manifest/chunks. Exact attachment ownership metadata remains available until external attachment and provider-log cleanup succeeds, after which the SQL and cold-chunk rows are removed; cross-thread plan references are cleared rather than leaving dangling ids, and the durable cleanup-queue entry is retained until filesystem cleanup and free-page reclamation succeed so interrupted deletes retry safely. Startup recovery also rediscovers soft-deleted shells that have not reached the cleanup queue, covering commits made while the lifecycle reactor was offline.
- Sidebar V2 project removal in `apps/web/src/components/SidebarV2.tsx` applies the archived-history warning and command options from `apps/web/src/components/Sidebar.logic.ts` independently to each selected project member. Members with known live threads use forced deletion; when a member has no live shell threads, it requests archived-thread deletion without force because archived shells are excluded from normal navigation state. The server removes archived cold bundles but still rejects any unseen live thread. Project removal emits the same per-thread deletion events for archived shells, so already-cold threads pass through the durable lifecycle worker and lose their hot shell, archive manifest, and compressed chunks before cleanup completes. `apps/web/src/components/Sidebar.logic.test.ts` covers standalone and grouped warning text plus the per-member thread partitioning that selects each member's command options, `apps/server/src/orchestration/decider.delete.test.ts` covers the archived-only live-thread precondition, and `apps/server/src/orchestration/Layers/ThreadDeletionReactor.test.ts` covers the forced-project decider, lifecycle reactor, and cold-storage boundary together.
- Archive/delete filesystem work runs through a durable background lifecycle queue so command acknowledgement and the UI are not blocked by compression or large cleanup operations. Archive and restore lifecycle work is serialized per project tree, with reference-counted idle-lock eviction after the final user or waiter. Archive eligibility and provider/terminal/log-writer shutdown run under that same tree lock, so a queued archive job that becomes stale after unarchive cannot stop the newly active session or delete its terminal history. Archive creation uses a retry-safe two-phase boundary: compressed chunks and hot-row deletion commit before the manifest enters `cleanup_pending`, and destructive attachment/log cleanup must finish before it becomes `cold`. Destructive archive transitions recheck the archived shell inside the transaction and require terminal/log-writer shutdown to succeed. Provider shutdown also remains required while the projected session is `starting` or `running`; a missing provider binding is terminal for a settled or absent projected session, allowing legacy subagent shells that never owned a provider binding to move cold safely. Failed lifecycle work requests one coalesced delayed rescan of the durable pending state instead of being dropped or introducing a permanent polling loop, enqueue interruptions release their deduplication reservation, and restart recovery preserves the same retry boundary. Filesystem failures other than a genuinely missing directory also keep retry state. Incomplete cleanup, including a `cleanup_pending` manifest whose archived shell has already been removed, and archived shells missing a manifest resume after restart without rebuilding an already durable bundle.
- Unarchive restores `cold` or `cleanup_pending` bundles before dispatching the domain command and treats an existing `restored` manifest as an in-flight or post-commit reservation without replaying its stale chunks over live rows or attachments. A rejected or failed command re-archives the restored rows and files, while a successful command finalizes the cold bundle only when that request actually performed a restore; this prevents unrelated or already-hot unarchive commands from deleting archive data. Once SQL restoration commits, later publication or metrics failures cannot re-archive the restored data. Retrying an accepted unarchive receipt also retries idempotent bundle finalization for that same thread, so a transient post-commit cleanup failure cannot strand restored archive chunks.
- Unarchive reserves still-hot archived rows with a `restored` manifest before releasing the archive-tree lock. This prevents queued lifecycle work from moving those rows cold between the restore check and the unarchive command commit; command failure archives the reserved rows, while success removes the reservation. Process-local restore ownership distinguishes those live reservations from `restored` manifests abandoned by a crashed process, which startup lifecycle discovery moves cold again while the shell remains archived. If storage cannot restore or reserve the archived conversation, the command is rejected before an active-shell event can commit.
- A `restored` manifest protects only the archive epoch that an unarchive command reserved. If post-commit bundle finalization fails and the user later archives the thread again, the newer shell timestamp lets lifecycle work replace the stale manifest and move the conversation cold.
- Migration `035_ThreadColdArchive` registers existing archived conversations for background conversion on the next update. Migration `036_DeletedThreadCleanupQueue` registers legacy soft-deleted conversations for the same permanent cleanup used by new deletes.
- After the legacy queues drain, a retryable one-time `VACUUM` physically compacts `state.sqlite` and enables incremental auto-vacuum. Later lifecycle operations reclaim bounded free-page batches from both `state.sqlite` and `archive.sqlite`, avoiding a full compaction on every archive.
- Normal provider, server, trace, and terminal logging behavior is unchanged. Space is reclaimed at the conversation lifecycle boundary instead of by weakening diagnostics for active work.

Primary files:

- `apps/server/src/orchestration/Layers/ThreadColdStorage.ts`
- `apps/server/src/orchestration/Layers/ThreadDeletionReactor.ts`
- `apps/server/src/orchestration/Layers/ThreadDeletionReactor.test.ts`
- `apps/server/src/persistence/Migrations/035_ThreadColdArchive.ts`
- `apps/server/src/persistence/Migrations/036_DeletedThreadCleanupQueue.ts`
- `apps/server/scripts/t3-sqlite-state.ts`
- `apps/web/src/browser/previewThreadLifecycle.ts`
- `apps/web/src/browser/usePreviewThreadLifecycleCleanup.ts`
- `apps/web/src/components/settings/ArchivedThreadsPanel.tsx`
- `apps/web/src/components/Sidebar.logic.ts`
- `apps/web/src/components/Sidebar.logic.test.ts`
- `apps/web/src/components/SidebarV2.tsx`

## Sidebar And Shell Consistency

Sidebar archive visibility is centralized across persisted and optimistic archive states. Project rows, project status, sorting, keyboard navigation, prewarming, and command-palette thread/project actions exclude an optimistically archived conversation immediately, so durable cold-storage work cannot leave a stale row or navigation target visible while the server shell catches up.

The web Archive settings route renders `apps/web/src/components/settings/ArchivedThreadsPanel.tsx`, which reads archived snapshots across connected environments, groups archived threads by project, and exposes unarchive plus permanent-delete actions. Keeping this branch-owned surface separate from the general panels in `apps/web/src/components/settings/SettingsPanels.tsx` limits archive behavior to the Archive route.

Authoritative shell synchronization is shared runtime behavior in `packages/client-runtime/src/state/shell.ts`, `packages/client-runtime/src/rpc/client.ts`, and `apps/server/src/ws.ts`, rather than a branch-specific subscription implementation. HTTP snapshots provide an early shell, while completion-capable WebSocket sessions revalidate it with a socket-owned snapshot after live buffering begins; the same refresh runs when the app returns to the foreground. Cold archive storage relies on this contract because compacted per-thread history cannot prove that cached projects and threads still exist.

Persisted web and mobile thread details are also fast-paint caches, not an archive source of truth. A `thread.archived` detail event, an authoritative archived detail snapshot, successful local archive acknowledgement, or shell removal evicts the persisted detail through `packages/client-runtime/src/state/threadCache.ts`. Per-thread cache generations, eviction tombstones, and write locks prevent queued or idle-finalizer writes from recreating the snapshot. Shell additions and `thread.unarchived` events revive the cache while invalidating only writes captured during the tombstoned generation. Web IndexedDB migration version 5 and mobile SQLite migration version 2 each clear legacy thread-detail caches once; active details repopulate on demand.

Failed shell-driven detail-cache removals remain pending while the authoritative shell omits the thread and retry on later shell items. An authoritative active detail snapshot also revives a cache tombstone before it queues persistence, covering reconnects where the corresponding unarchive event was compacted.

`apps/server/src/orchestration/Layers/ThreadDeletionReactor.ts` schedules lifecycle cleanup when it observes a thread archive or deletion event, including deletions expanded from a project command; it makes a best-effort attempt to close every server preview session at event observation time so delayed archive work cannot close a preview reopened after unarchive. `apps/web/src/browser/ElectronBrowserHost.tsx` calls `apps/web/src/browser/usePreviewThreadLifecycleCleanup.ts`, whose renderer lifecycle effect uses the pure reconciliation logic in `apps/web/src/browser/previewThreadLifecycle.ts` to retain the last authoritative per-environment thread baseline while a shell synchronizes, then detect removals once that environment is live or discard the baseline when the environment leaves the catalog. It clears both `apps/web/src/previewStateStore.ts` and `apps/web/src/previewMiniPlayerStore.ts` state even when server preview-close delivery arrives first. Removing the preview state unmounts the cross-thread background webview, whose `apps/web/src/browser/desktopTabLifetime.ts` lease stops recording/capture before it closes the desktop tab. Unarchive therefore starts with clean preview state instead of reviving a stale mini-player or capture session. `apps/server/src/orchestration/Layers/ThreadDeletionReactor.test.ts`, `apps/web/src/browser/previewThreadLifecycle.test.ts`, and `apps/web/src/previewMiniPlayerStore.test.ts` cover the server and renderer cleanup boundaries.

`apps/server/src/server.test.ts` verifies that HTTP-seeded shell subscriptions preserve archive removals published while WebSocket catch-up reads persisted events, then emit the synchronization completion marker. Initial-snapshot event replay in `apps/server/src/ws.ts` and deferred active-thread cache writes in `packages/client-runtime/src/state/threads.ts` remain complementary to authoritative shell refresh. Cache lifecycle behavior is covered through `packages/client-runtime/src/state/shell-sync.test.ts`, `packages/client-runtime/src/state/threadCommands.test.ts`, `packages/client-runtime/src/state/threads-sync.test.ts`, `apps/web/src/connection/storage.test.ts`, and the mobile storage/database tests.

Mobile Archive rows omit invalid lifecycle timestamps instead of presenting corrupt or legacy values as newly archived, and sort missing or invalid archive dates after valid dates in both sort directions. General mobile time rendering is unchanged.

## Development Ports

- Web: `5736`
- Server/WebSocket: `13776`
24 changes: 23 additions & 1 deletion apps/mobile/src/features/archive/ArchivedThreadsRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@ import {
refreshArchivedThreadsForEnvironment,
useArchivedThreadSnapshots,
} from "./useArchivedThreadSnapshots";
import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
import { scopedThreadKey } from "../../lib/scopedEntities";

export function ArchivedThreadsRouteScreen() {
const { expand } = useClerkSettingsSheetDetent();
const { savedConnectionsById } = useSavedRemoteConnections();
const [searchQuery, setSearchQuery] = useState("");
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<EnvironmentId | null>(null);
const [sortOrder, setSortOrder] = useState<ArchivedThreadSortOrder>("newest");
const [unarchivingThreadKeys, setUnarchivingThreadKeys] = useState<ReadonlySet<string>>(
() => new Set(),
);
const environments = useMemo<ReadonlyArray<ArchivedThreadsHeaderEnvironment>>(
() =>
Arr.sort(
Expand Down Expand Up @@ -67,6 +72,22 @@ export function ArchivedThreadsRouteScreen() {
);
const { unarchiveThread, confirmDeleteThread } =
useArchivedThreadListActions(refreshChangedEnvironment);
const handleUnarchiveThread = useCallback(
async (thread: EnvironmentThreadShell) => {
const threadKey = scopedThreadKey(thread.environmentId, thread.id);
setUnarchivingThreadKeys((current) => new Set(current).add(threadKey));
try {
await unarchiveThread(thread);
} finally {
setUnarchivingThreadKeys((current) => {
const next = new Set(current);
next.delete(threadKey);
return next;
});
}
},
[unarchiveThread],
);

useFocusEffect(
useCallback(() => {
Expand All @@ -86,10 +107,11 @@ export function ArchivedThreadsRouteScreen() {
onRefresh={refresh}
onSearchQueryChange={setSearchQuery}
onSortOrderChange={setSortOrder}
onUnarchiveThread={unarchiveThread}
onUnarchiveThread={(thread) => void handleUnarchiveThread(thread)}
searchQuery={searchQuery}
selectedEnvironmentId={selectedEnvironmentId}
sortOrder={sortOrder}
unarchivingThreadKeys={unarchivingThreadKeys}
/>
);
}
Loading
Loading