RFD 071: Conversation Archiving
- Status: Implemented
- Category: Design
- Authors: Jean Mertz git@jeanmertz.com
- Date: 2026-04-17
- Requires: RFD 073
Summary
This RFD adds conversation archiving to JP. Archived conversations are physically moved out of the active storage directory, excluded from the workspace index, and hidden from listings and pickers. Two new subcommands — jp conversation archive and jp conversation unarchive — manage the lifecycle.
Motivation
Long-running workspaces accumulate conversations. Most are no longer relevant but contain valuable history that shouldn't be deleted. Today, the only options are to keep every conversation in the active listing (noise) or delete them permanently (data loss).
Archiving provides a middle ground: conversations are hidden from day-to-day operations but preserved on disk and recoverable at any time. The existing expires_at / --tmp mechanism handles truly ephemeral conversations that should be garbage-collected. Archiving is for conversations the user wants to keep but not see.
Without this feature, users resort to manual filesystem operations (moving directories) or ignore the clutter, which degrades the picker and listing experience as the workspace grows.
Design
User-Facing Behavior
Archiving
# Archive the session's active conversation (same fallback chain as
# `jp c show`: session active → conversation.default_id → picker)
jp conversation archive
jp c a
# Archive specific conversations by ID
jp c archive jp-c123 jp-c456
# Archive multiple via picker
jp c archive ?
# Archive all pinned conversations
jp c archive +pinned
# Archive a range of conversations by creation date. Both bounds accept a
# conversation ID (uses its creation timestamp), a relative duration
# (3w, 30d, 6h), or an absolute date (2026-01-01). The range is half-open:
# --from is inclusive, --until is exclusive.
jp c archive --from 3w --until 1d
jp c archive --from jp-c123
# Archive every conversation unused since a given time (last_activated_at,
# not creation date). Accepts the same syntax as --from.
jp c archive --inactive-since 30d
# Filters AND-compose; this archives conversations created in the last
# month that have been idle for at least a week.
jp c archive --from 30d --inactive-since 7d
# Skip the per-conversation confirmation prompt for pinned/active entries.
jp c archive --inactive-since 6mo --yesWhen archiving a pinned or active session conversation, JP prompts for confirmation:
Archive the active conversation jp-c123? [y/n/?]The prompt defaults to "no" and the conversation is skipped if declined. --yes (-y) suppresses the prompt for batch use.
Unarchiving
# Show a picker of archived conversations to restore
jp conversation unarchive
jp c ua
# Unarchive specific conversations by ID
jp c unarchive jp-c123 jp-c456
# Unarchive multiple via picker
jp c unarchive ?
# Unarchive all archived conversations
jp c unarchive +archivedNon-archived IDs passed to unarchive are skipped with a warning.
Listing
# List archived conversations
jp c ls --archivedThe --archived flag switches ls to scan the archive partition. All existing ls flags (--sort, --limit, --full, --local) work as expected.
Targeting Keywords
The conversation targeting system supports archive-related keywords:
| Keyword | Alias | Description |
|---|---|---|
archived | a | Most recently archived conversation |
+archived | +a | All archived conversations |
?archived | ?a | Interactive picker of archived conversations |
The archived keyword resolves to the most recently archived conversation
The archived and ?archived keywords work with jp c use to unarchive-and-activate in one step:
# Pick an archived conversation, unarchive it, and activate it
jp c use ?archived
# Unarchive the most recently archived conversation and activate it
jp c use archivedStorage Layout
Archived conversations are moved into a .archive/ subdirectory within the conversations/ directory:
.jp/conversations/
├── 17729599457-active-conversation/
├── 17729621655/
└── .archive/
├── 17729596932-old-conversation/
└── 17729598000-another-old-one/The .archive/ directory uses the existing dot-prefix convention that the storage layer already skips during index scans (same pattern as .trash/, .old-*, .staging-*). This means archived conversations are invisible to the active index with zero filtering overhead.
Both workspace and user storage roots have independent .archive/ directories. The archive operation preserves which root a conversation belongs to.
Backend Traits
The storage backend traits are extended to support archiving:
PersistBackend gains archive(id) and unarchive(id) methods. The filesystem backend moves directories between the active and archive partitions. The in-memory backend moves entries between two HashMaps. The null backend discards both operations.
LoadBackend gains a ConversationFilter parameter on load_conversation_ids:
#[derive(Debug, Default, Clone, Copy)]
pub struct ConversationFilter {
pub archived: bool,
}The default filter returns active conversations. Setting archived: true scans the .archive/ directories instead. This is a storage-level partition filter, not a metadata filter — the cost is the same as the existing directory scan (no per-conversation file I/O).
load_conversation_metadata searches both regular and archive partitions transparently, so commands like jp c show <archived-id> work without special handling.
The archived partition is not cached in the workspace's in-memory index. archived_conversations() scans the filesystem on each call. This is the right trade-off: archive operations are infrequent, the directory scan is cheap, and caching would consume memory for conversations the user explicitly wanted out of their working set. If repeated scans become a bottleneck, adding a cached archived index to State is straightforward.
Workspace Layer
Workspace exposes three methods:
archive_conversation(conv: ConversationMut)— moves the conversation to the archive partition, removes it from the in-memory index.unarchive_conversation(id: &ConversationId) -> Result<ConversationHandle>— restores from the archive, inserts into the in-memory index.archived_conversations()— returns an iterator over archived conversation metadata, loaded on demand from the archive partition. Not cached in the workspace index.
CLI Integration
jp c archive participates in the standard conversation resolution pipeline. Its conversation_load_request behaves in two modes:
- Filter mode (any of
--from/--until/--inactive-sinceset): returnsConversationLoadRequest::none(). The subcommand iterates the workspace and selects matching conversations internally, mirroringjp c unarchive's internal-resolution pattern. - Direct mode (no filter flags): returns
explicit_or_session(target)— explicit IDs when provided, otherwise the same fallback chain asjp c show(session active →conversation.default_id→ picker). Resolution happens in the startup pipeline and the subcommand receives pre-resolved handles.
The shared --from/--until range filter is implemented as a flattened CreationRange args struct in crates/jp_cli/src/cmd/time.rs, reused by jp c rm so the two commands' creation-range semantics stay in lockstep.
jp c unarchive returns ConversationLoadRequest::none() because its targets are in the archive partition and cannot be resolved through the active index. It handles resolution internally.
Drawbacks
No toggle. Archiving and unarchiving are separate subcommands. There is no single command that flips the state. This is a deliberate trade-off: the two operations have different user intents ("clean up" vs "I need that back"), different discovery paths, and different resolution needs (active index vs archive partition).
Directory rename under lock. Archiving requires holding the conversation lock and then renaming the directory. If the process crashes between clearing dirty state and completing the rename, the conversation could be in an inconsistent state. The startup validation pass does not currently scan
.archive/for recovery. A crash during archiving could leave the conversation missing from both partitions. The risk is low (the rename is a single syscall) but should be addressed by extending the validation pass to check.archive/in a follow-up.
Alternatives
Metadata Flag
Add an archived: bool field to Conversation metadata instead of moving directories. Filter at the conversations() iterator level.
Rejected because: every consumer of workspace.conversations() would need to filter, the index still pays the cost of tracking archived conversations in memory, and session mappings would need awareness of the archived state. The physical separation approach has zero cost for normal operations.
--archive Flag on jp c edit
Implement archive as a property flag on the edit subcommand, alongside --pin, --local, etc.
Rejected because: archiving is not a property mutation — it changes whether a conversation exists in the working set, not how it appears. --pin and --local modify metadata fields; --archive moves the entire conversation to a different storage partition. Separate subcommands make this distinction clear to the user and give each operation its own help text, argument handling, and aliases.
Dot-Prefixed Directory Names
Prefix archived conversations with .archived- in the main conversations/ directory instead of using a subdirectory.
Rejected because: it clutters the conversations/ directory with dot-prefixed entries, collides conceptually with .old-*/.staging-* which are transient states, and the .trash/ precedent already uses a subdirectory.
Non-Goals
Automatic archiving. Rules like "archive after N days of inactivity" that run without user intervention are out of scope.
Archive-specific metadata. Archived conversations retain their original metadata. There is no
archived_attimestamp or archive-specific fields. This means thearchivedkeyword resolves bylast_activated_atrather than by when the conversation was archived.
Implementation Plan
Phase 1: Storage and Workspace
Add ConversationFilter, archive/unarchive to the backend traits. Implement for FsStorageBackend (directory moves), InMemoryStorageBackend (separate HashMap), and NullPersistBackend (no-op). Add archive_conversation, unarchive_conversation, and archived_conversations to Workspace. Add parity tests.
Can be merged independently.
Phase 2: CLI Subcommands
Add jp c archive and jp c unarchive subcommands. Add --archived flag to jp c ls. Add archived/+archived/?archived targeting keywords. Update jp c use to support unarchive-and-activate via the archived keyword.
Archive's selection flags ship as --from/--until (creation-date range) and --inactive-since (last-activity threshold); all three accept the TimeThreshold syntax (conversation ID, relative duration, or absolute date) and AND-compose. --from/--until are extracted into a shared CreationRange args struct reused by jp c rm.
Depends on Phase 1.
References
- RFD 052: Workspace Data Store Sanitization —
.trash/pattern for invalid conversations - RFD 073: Layered Storage Backend for Workspaces — backend trait architecture