Conversation
The Conversation cluster covers JP's central abstraction: the persistent record of "talking to the assistant" and the event log that backs it. Six terms describe its structure — a Conversation is the stored entity, a Turn is one slice of it, an Event is the atomic unit inside a Turn, Tool Calls and Inquiries are specific event kinds, and a Thread is the projection of a Conversation that gets sent to an LLM provider. An Event ID names one entry in the log, so a reference to it survives edits that move the entry.
These terms are tightly coupled — paraphrasing one usually breaks the model for another. Use them as written.
The cluster also covers a conversation's state: where it sits and how it is annotated. Active, Live, and Archived answer the first; Label answers the second.
NOTE
Cluster status: Turn, Event ID, Active Conversation, Live Conversation, Archived Conversation, and Label are defined below. The remaining terms are placeholders and will land in subsequent passes. Until then, see the legacy single-page glossary for the older definitions of the unfilled terms.
Terms
Active Conversation
The one conversation a session is currently working on. It is what jp query continues and what every conversation command targets when given no explicit target.
Active is a property of the session, not of the conversation: two terminals working in the same workspace each have their own active conversation, and a conversation is not marked as active anywhere in its own metadata.
Implementation. The head of the session's activation history — SessionMapping::active_conversation_id, reached through Workspace::session_active_conversation. The . / active target keyword resolves to it.
Not the same as. A Live Conversation (every conversation outside the archive; the active one is a single member of that set). Also not the session's previous conversation, which is the entry behind it in the same history and answers to the s / session keyword.
Live Conversation
A conversation in the default storage partition. Live conversations are indexed when JP starts, so they are what every whole-corpus operation sees without being asked: jp c ls, jp c grep, the picker, and the recent / newest / +pinned target keywords.
Implementation. The conversations/ partition of a storage root, scanned by LoadBackend::load_conversation_index with a default ConversationFilter. Workspace::conversations() iterates the resulting index.
In context. Live and Archived are the two values of a conversation's location — mutually exclusive, and a conversation is always in exactly one. This is a separate axis from flags such as Pinned, which apply to a conversation in either location.
Not the same as. The Active Conversation (the single conversation the current session is working on, which is one particular live conversation).
Avoid. Non-archived, unarchived, indexed. The first two define the term by negation, which stops working as more locations appear; the third names a storage strategy rather than a state.
Archived Conversation
A conversation moved out of the default partition into the archive, to keep it out of listings and whole-corpus operations without deleting it.
Archived conversations are deliberately not indexed at startup. That is what keeps jp c ls, sanitization, and the picker fast in a workspace where most conversations have been archived, and it is why reaching one costs an explicit target (a, ?a, +a) or --archived.
Implementation. The conversations/.archive/ partition, reached via ConversationFilter { archived: true }. Conversation::archived_at records when it happened, but the directory location is the source of truth.
Not the same as. A removed conversation (archiving is reversible with jp c unarchive; removal is not).
Label
A key=value annotation on a conversation, used to find it later by the context it was created in: the VCS branch, the crates it touches, a review stage. A key holds a set of values, so crate=jp_config and crate=jp_llm coexist. A label with an empty value is a bare label, and filters treat it as "key present, any value".
Labels live in two places with distinct roles. The rules that produce them are configuration, declared under conversation.labels.<key> and layered like any other config; a rule's value is a literal string, a list of them, or a command producing one value per line of stdout. The resolved set is the labels themselves, written at creation, on fork, and by jp c label. A rule is not a label until it has been resolved. Naming a label on the CLI applies it without a rule, so a rule is one way a label arrives rather than the only one.
Implementation. Conversation::labels, a Labels from jp_label, persisted in metadata.json. Labels maps a key to an ordered set of values and enforces the empty-set invariant; a value is read as either a string or an array of strings, and is always written as an array. The type is shared with tickets, so the invariants above hold wherever a label is stored. Rules are LabelConfig in jp_config::conversation::label, turned into labels by the resolver in jp_cli::cmd::label::resolve. Keys match [A-Za-z][A-Za-z0-9_-]*; the grammar and its diagnostics live in jp_label, shared with every other consumer of labels, so a key accepted on a conversation is a key accepted on a ticket. Every excluded character is significant somewhere else — . separates dotted config paths, = splits a key from its value, : marks a rule reference, and a leading - would read as a flag where keys are written as bare command arguments.
In context. A key never holds an empty set: removing the last value removes the key. jp c label add inserts into the key's set, set replaces it, and rm takes either a whole key or a single key=value. Mutation lives on jp c label alone — neither jp query nor jp c fork sets labels — while filters (--label on jp c ls and jp c grep) match on set membership. Labels are a fact recorded about a conversation: they are not sent to the LLM and not exposed to tools.
Not the same as. A conversation's title, which is free-form, singular, and serves as the conversation's name rather than a fact about it. Also not an Attachment, which is content added to a conversation.
A ticket carries the same Labels, but its vocabulary is closed: a ticket label is checked against docs/ticket/.labels.json, which declares each key and the values it accepts, while a conversation label only has to match the key grammar. Closedness is a policy a consumer opts into through jp_label's Vocabulary, not a difference in the concept. Where a label set is stored is each consumer's own business: a conversation keeps one in metadata.json, a ticket writes one - **Label**: line per pair.
Avoid. Tag, annotation, marker. When you mean a key=value pair stored on a conversation or a ticket, the word is Label.
Event ID
The identity of one entry in a conversation's event log, unique within that stream. It belongs to the entry rather than to the entry's position, so editing an entry's content or moving it leaves its Event ID alone. Every entry has one, whatever it holds.
Implementation. EventId in jp_conversation, held by the stream-entry wrapper and persisted as event_id. A stream hands out its own IDs, and a removed entry takes its ID with it for as long as that stream is in memory. Retired IDs are not persisted, so the guarantee is per-load rather than for all time.
In context. The point of an intrinsic identity is that a reference to an entry survives the edits a reference to "the third entry" would not. A duplicate introduced by hand-editing is repaired on load, but the duplicated value is reported as ambiguous rather than silently bound to whichever entry kept it: a reference into an edited file resolves to the right entry or to nothing, never to the wrong one. Projection builds a provider-facing copy whose synthetic entries carry IDs of the same type, but those exist only in that copy and are not references into the stored stream.
Not the same as. The payload id on a tool call or inquiry, which pairs a request with its response and means something to the provider. An entry can carry both, and they answer different questions. Also not a Conversation's id, which names the whole log rather than one entry in it.
Scope. An Event ID carries identity alone: no ordering, no content, and no uniqueness beyond its own stream. Two conversations may hold the same value, and code must not read one for anything but identity-within-a-stream. See RFD-097.
Avoid. Event key, entry hash, event index. The last is actively wrong — an index is the position an Event ID exists to replace.
Turn
A contiguous group of conversation events bracketed by a TurnStart: one user chat request through the assistant's final response for that request, including any intermediate tool calls and inquiries.
Implementation. Turn<'a> in jp_conversation::stream::turn_iter. Constructed by iterating a ConversationStream and splitting on TurnStart events.
In context. A Conversation is an ordered sequence of Turns. Each Turn contains a ChatRequest and the Events that flow from it — typically one or more ChatResponses from the assistant, optionally interleaved with Tool Calls and Inquiries. A Thread is assembled across many Turns at query time; a Thread is not a Turn.
Not the same as. A Conversation (a Conversation contains Turns), a Thread (a provider-facing projection assembled across many Turns), a Tool Call (a Tool Call is one Event within a Turn).
Avoid. Round, exchange, message, interaction. None of these are project terms. When you mean a single user-prompt-to-final-response cycle with the assistant, the word is Turn.