| 1 | # Local Session Draft Lifecycle |
| 2 | |
| 3 | Reasonix Desktop treats a manually opened local conversation as an editor draft |
| 4 | until the first execution is durably accepted. Opening a draft does not create a |
| 5 | Session, Topic, Controller, session lease, MCP runtime, or session-list row. Once |
| 6 | the first submission creates the formal Session, a temporarily untitled row uses |
| 7 | the localized new-session title (and then the normal preview/title projection); |
| 8 | internal canonical routes such as `session-id:...` are never display names. |
| 9 | |
| 10 | ## Identity and storage |
| 11 | |
| 12 | The visible conversation surface is either a draft target (`workspaceId`, |
| 13 | `draftId`) or a formal Session target (`SessionRef`, `tabId`). Drafts never use a |
| 14 | fake tab or Topic ID. |
| 15 | |
| 16 | Draft state is stored in `desktop/drafts-v1.sqlite` under the Reasonix user state |
| 17 | directory. Schema version 4 contains active drafts, versioned frozen submission operations, |
| 18 | conflict copies, and the last restorable draft surface. There is one active draft |
| 19 | per local Workspace, including one independent global Workspace draft. |
| 20 | |
| 21 | Persisted content includes composer text, structured invocations, attachment and |
| 22 | workspace references, pasted blocks, selected text references, Session |
| 23 | references, model provenance plus a compatibility mirror, effort/mode/approval/ |
| 24 | quality settings, Goal settings, and MCP selection. Runtime objects, credentials, |
| 25 | preview URLs, pending paste state, and submission UI flags are not persisted. |
| 26 | |
| 27 | Edits synchronously update an application-level store and are saved with a 250 ms |
| 28 | debounce and a per-DraftID revision compare-and-swap loop. Navigation never waits |
| 29 | for a failed or conflicting save: the source draft continues in the background |
| 30 | and remains marked dirty/error/conflict in the project tree. Submission waits for |
| 31 | the captured source edit version, and normal Electron shutdown waits for every |
| 32 | loaded draft, pending attachment task, and restore-target write. Failure or |
| 33 | conflict cancels normal close; the last backend-confirmed revision remains the |
| 34 | crash-recovery boundary. |
| 35 | |
| 36 | Opening draft data and committing the page restore target are separate writes. |
| 37 | Every navigation claims the shared navigation intent before its first await, so |
| 38 | a stale draft open cannot install a page or overwrite the next startup target. |
| 39 | |
| 40 | ## First execution |
| 41 | |
| 42 | The renderer captures DraftID, Workspace, generation, content, settings, and |
| 43 | navigation intent before context expansion. The backend records that immutable, |
| 44 | versioned snapshot before creating runtime state: |
| 45 | |
| 46 | ```text |
| 47 | active draft |
| 48 | -> reserved (OperationID + SessionID + TopicID + SubmissionID) |
| 49 | -> starting (create/open exactly that Session and bind its Workspace) |
| 50 | -> dispatching | dispatching_shell |
| 51 | -> accepted (durable submission receipt) |
| 52 | -> converted draft |
| 53 | ``` |
| 54 | |
| 55 | The operation does not call `EnsureBlankTab`. Retries reuse its reserved Session |
| 56 | and Topic. A terminal retry gets a new OperationID and SubmissionID but retains |
| 57 | the already reserved Session and Topic. Workspace pending-create cleanup is |
| 58 | conditional on the old OperationID so a late failure cannot erase a newer claim. |
| 59 | |
| 60 | Normal turns, structured invocations, initial Goals, and shell commands share |
| 61 | this path. New shell submissions persist an acceptance receipt before command dispatch. An unknown shell or |
| 62 | model dispatch result is never replayed automatically. |
| 63 | |
| 64 | An untouched new draft inherits the current settings default live. The persisted |
| 65 | `modelSource: "default"` marker distinguishes that inheritance from an explicit |
| 66 | picker or `/model` choice; its concrete `model` field is only a compatibility |
| 67 | mirror for previous readers and is excluded from editable-draft identity. A |
| 68 | catalog/settings refresh updates the visible effective model without making the |
| 69 | draft dirty. An explicit choice writes `modelSource: "explicit"` and remains |
| 70 | pinned. |
| 71 | |
| 72 | Frontend and backend draft comparisons both exclude the inherited concrete model, |
| 73 | including lost-save-acknowledgement recovery and pre-submit validation. Catalog |
| 74 | refresh, save, and reopen reads share version fencing so late responses cannot |
| 75 | replace a newer effective-model projection. |
| 76 | |
| 77 | The first submission resolves the effective inherited model again, validates and |
| 78 | canonicalizes it, and freezes it in snapshot v5 before Session reservation. |
| 79 | Frozen and explicitly selected draft models are strict execution choices: an |
| 80 | unavailable model is reported before reservation and never falls back to another |
| 81 | model. Plugin model references are passed to the runtime resolver and fail there |
| 82 | when unavailable. A retry applies the operation's frozen settings to the same |
| 83 | reserved Session identity. |
| 84 | Request fingerprints are computed before model-alias normalization, so retrying |
| 85 | the original request still identifies the same operation. Restoring a resumable |
| 86 | operation shows its frozen model in both the composer and picker; cancellation |
| 87 | or terminal failure releases editing to inherit the live default again. |
| 88 | |
| 89 | The renderer switches to the formal Session only after the acceptance receipt |
| 90 | and draft conversion are committed. Transcript snapshot/follow then supplies any |
| 91 | events that arrived before the RPC response. Completion in a background project |
| 92 | updates project state but does not steal the visible surface. |
| 93 | |
| 94 | ## Recovery and cancellation |
| 95 | |
| 96 | Sending synchronously locks the source editor, then saves its captured version |
| 97 | before expanding references. A request ID and source digest bind that snapshot |
| 98 | to the durable operation. Lost Begin responses are reconciled by reading state; |
| 99 | they do not unlock the editor or allocate another submission. Restoring a draft |
| 100 | also restores its operation, including the explicit Continue action. Reads do |
| 101 | not create a Controller or dispatch work. Polling continues for unknown results. |
| 102 | |
| 103 | Cancellation first records `cancel_requested`; editing remains locked until the |
| 104 | worker has stopped. A cross-process execution lease prevents another process |
| 105 | from declaring a live worker interrupted. A separate short publication lease |
| 106 | serializes cancellation with Controller publication. Retry configuration applies |
| 107 | to the real Controller under turn admission, and admission checks its identity. |
| 108 | |
| 109 | Attachment completion always settles its task registration, even if generation |
| 110 | has changed. Only current-generation results may edit content. Discard captures |
| 111 | the project and versions before confirmation; navigation cannot retarget it. |
| 112 | Normal exit also waits for local submission preparation to persist its operation. |
| 113 | |
| 114 | | Persisted phase or interruption | Recovery behavior | |
| 115 | | --- | --- | |
| 116 | | No operation record | Keep the editable draft; submit normally. | |
| 117 | | `reserved` or `starting` after restart | Mark `resume_required`; user continuation reuses the same identities. | |
| 118 | | Session exists but is not yet bound | Bind the same Session to the recorded Workspace. | |
| 119 | | Controller startup failed | Keep the Session identity; a terminal retry reuses it. | |
| 120 | | `dispatching` with a known receipt | Atomically accept and convert the draft. | |
| 121 | | `dispatching` without a known receipt | Mark `dispatch_unknown`; do not replay. | |
| 122 | | `dispatching_shell` after interruption | Reconcile the durable receipt; accepted commands become sessions. A legacy dispatch marker without proof remains unknown. Never rerun the command automatically. | |
| 123 | | `accepted` but draft still active | Complete conversion without redispatch. | |
| 124 | | Session was archived or deleted externally | Stop recovery and report the lifecycle conflict. | |
| 125 | |
| 126 | Cancellation is keyed by OperationID. Before dispatch it conditionally cancels |
| 127 | the operation and its matching Workspace reservation. At or after durable |
| 128 | acceptance it becomes cancellation of the formal Session; it never deletes a |
| 129 | receipt to pretend execution did not happen. |
| 130 | |
| 131 | ## Commands, attachments, and capabilities |
| 132 | |
| 133 | - `/new`, `/clear`, `/compact`, and other history-dependent management commands |
| 134 | are unavailable in a draft and do not create a Session. |
| 135 | - `/model` and `/effort` edit draft settings. `/theme` changes appearance |
| 136 | directly. Goals, skills, custom commands, and ordinary text start the unified |
| 137 | first-execution flow. |
| 138 | - Configured MCP servers are projected from Workspace configuration without a |
| 139 | Controller or process launch. Draft selection is persisted per Session; |
| 140 | runtime capability availability is revalidated during Controller startup. |
| 141 | - Attachment operations use an explicit composer target and Workspace root. |
| 142 | Async completion writes directly to the captured DraftID even when another |
| 143 | project is visible. Pending saves block submission. Reads reject path escapes and symlinked files |
| 144 | or attachment directories. Missing files remain repairable draft references. |
| 145 | |
| 146 | ## Compatibility |
| 147 | |
| 148 | ### One-time legacy empty-session cleanup |
| 149 | |
| 150 | The first upgraded startup freezes a versioned set of default-title Sessions |
| 151 | and legacy Topic placeholders that already belong to registered local or global |
| 152 | Workspaces. The background worker runs only after Session migration, draft |
| 153 | operation reconciliation, and tab restoration. Identities created after that |
| 154 | freeze, including draft reservation and retry Sessions, can never enter the |
| 155 | batch. |
| 156 | |
| 157 | A candidate moves to Trash only when the complete canonical or legacy artifact |
| 158 | set proves it has no user, assistant, or tool message; accepted model or shell |
| 159 | submission; Goal; pinned context; inbox item; job; checkpoint; recovery state; |
| 160 | derived relationship; active draft operation; runtime; or unsent restored UI |
| 161 | state. An empty-string message is still usage. System-only initialization and |
| 162 | empty derived containers are not usage. Corrupt, inaccessible, unsupported, or |
| 163 | future-version data is retained as `unknown` rather than treated as empty. |
| 164 | |
| 165 | Default titles are matched exactly after trimming. Sessions with content keep |
| 166 | their title, order, identity, and data; no numbered-title migration is applied. |
| 167 | Conclusive empty Sessions use the existing reversible archive lifecycle without |
| 168 | opening a fallback Controller. Topic-only placeholders keep a schema-v1 recovery |
| 169 | snapshot of their original scope, title, order, pin, and group position and are |
| 170 | shown in Trash without fabricating a SessionRef. Restoring either form preserves |
| 171 | its identity and permanently excludes it from this cleanup batch. |
| 172 | Busy or unreadable candidates stay visible in their original project. Trash |
| 173 | shows the pending count and a **Recheck** action that retries only the frozen |
| 174 | batch; it never discovers identities created after the upgrade boundary. |
| 175 | |
| 176 | The independent |
| 177 | `desktop/legacy-empty-session-cleanup-v1.json` sidecar has atomic writes, |
| 178 | cross-process worker ownership, stable operation IDs, and strict unknown-version |
| 179 | protection. Older releases ignore it. Legacy JSONL migration sources are retained |
| 180 | even when their mapped canonical Session is archived. |
| 181 | |
| 182 | | Format or API | New-reader behavior | Previous-reader behavior | Result | |
| 183 | | --- | --- | --- | --- | |
| 184 | | Session v5 | Unchanged identity, events, and transcript protocol. | Unchanged. | Compatible. | |
| 185 | | Workspace registry v3 | Formal Session ownership only; recorded create operations remain recoverable. | Existing Sessions remain readable. | Compatible. | |
| 186 | | Draft SQLite v1 | Migrated to v2 by adding the reserved Topic ID, then to v3. | Not applicable to releases without drafts. | Forward migration covered. | |
| 187 | | Draft SQLite v2 | Transactionally upgraded to v3. Pending operations resume only when their draft revision proves the settings match. | A v2 build refuses to write v3. | No inferred execution settings. | |
| 188 | | Draft SQLite v3 | Versioned frozen request and settings in `request_json`; all saves use CAS. | Releases without drafts ignore the independent file. | Rollback preserves drafts for a later upgrade. | |
| 189 | | Draft SQLite v4 | Transactional migration adds request identity, source digest, operation revision and separate expanded execution bytes. Snapshot versions remain independent of schema versions. | v3 refuses to write v4. | Original fingerprints and Session/Submission IDs remain intact. | |
| 190 | | Draft model provenance / snapshot v5 | New drafts persist an additive `modelSource`; inherited defaults keep a concrete compatibility mirror but freeze the current effective model only at submission. Untouched legacy revision-1 drafts are safely marked inherited; edited legacy drafts retain their concrete model because provenance is ambiguous. Existing v3/v4 operation snapshots remain resumable. | Previous readers ignore `modelSource` and use the valid concrete mirror. They reject a future v5 operation snapshot rather than resuming it with changed semantics. If a previous reader edits and rewrites a draft, a later upgrade conservatively treats the concrete model as pinned. | No explicit user choice is silently replaced and no operation is resumed with an inferred model. | |
| 191 | | Unknown future draft schema | Read/write is refused and the file is retained. | Not applicable. | No silent downgrade overwrite. | |
| 192 | | Upgrade-time blank Sessions | Only frozen, default-title, conclusively unused local candidates are moved to Trash once. Content-bearing or uncertain Sessions are unchanged. | Older releases ignore the cleanup sidecar and continue to read formal Sessions and Trash. | Reversible lifecycle-compatible cleanup. | |
| 193 | | Legacy cleanup sidecar v1 | Records frozen identities, decisions, stable archive operations, placeholder recovery metadata, and restore protection. Unknown versions are retained and cleanup stops. | Ignored. | No downgrade overwrite or candidate rescan. | |
| 194 | | Legacy runtime APIs | `EnsureBlankSurface` and related APIs retain their semantics. | Unchanged. | Remote, IM, automation, recovery, and worktree flows remain isolated. | |
| 195 | |
| 196 | Draft IDs and submission IDs are host metadata only. They are excluded from |
| 197 | provider-visible requests, and the existing prompt/tool ordering is unchanged. |
| 198 | |
| 199 | ## Operational notes |
| 200 | |
| 201 | Diagnostics record IDs, phase, reuse/recovery counts, and durations for draft |
| 202 | open/save, Session creation, runtime readiness, and acceptance. They do not log |
| 203 | draft text, attachment contents, or credentials. |
| 204 | |
| 205 | The first implementation deliberately does not delete attachment files when a |
| 206 | draft is discarded. MCP prompt discovery uses existing configured/cached |
| 207 | capability data; actual capability discovery and validation occur at execution. |
| 208 |