Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c2b1034
feat(chat): preserve exact local continuity and reviewed side notes
BunsDev Sep 11, 2026
c9e4534
Merge origin/main with viewport containment into local continuity
BunsDev Sep 11, 2026
8bcb389
test(chat): cover retained continuity across constrained viewports
BunsDev Sep 11, 2026
cc7db60
fix(chat): represent remembered familiars outside the loaded roster
BunsDev Sep 11, 2026
a789b3d
fix(chat): recover reviewed notes and bound durable admission
BunsDev Sep 11, 2026
a89029c
Merge origin/main gitignore update into continuity branch
BunsDev Sep 11, 2026
476279a
fix(chat): close side-note replay and retry guidance gaps
BunsDev Sep 11, 2026
5062fd4
fix(chat): retain creation retries across session navigation
BunsDev Sep 11, 2026
71d8b23
fix(chat): reconcile writes before notifying local observers
BunsDev Sep 11, 2026
782e273
fix: own local mutation refresh and preserve exact review selections
BunsDev Sep 11, 2026
142513b
Merge approved main quota diagnostics into continuity branch
BunsDev Sep 11, 2026
d0d58f1
fix: fence side-note reads and familiar metadata
BunsDev Sep 11, 2026
e386817
fix: reconcile root saves and enforce scoped reviewed writes
BunsDev Sep 11, 2026
a873bf3
fix: verify exact root recovery receipt identity
BunsDev Sep 11, 2026
932b66c
fix: preserve commit truth and bind initial hydration freshness
BunsDev Sep 11, 2026
a060b19
fix: surface local storage startup failures with safe retry
BunsDev Sep 11, 2026
1a22dcd
merge: integrate current main authority into local continuity
BunsDev Sep 11, 2026
d28df3d
fix: retain uncertain review identity and stabilize local refresh
BunsDev Sep 11, 2026
e9b7f45
Merge accepted Windows quota reproduction into local continuity
BunsDev Sep 11, 2026
1061c31
fix: retain unresolved recovery and reject failed IndexedDB opens
BunsDev Sep 11, 2026
1fbdf23
test: wait for rendered review controls before clicking
BunsDev Sep 11, 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
158 changes: 158 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,164 @@ The desktop app can also connect or pair with Cave through a least-privilege
native adapter and render bounded, read-only canonical chat data. Explicit demo
routes remain available for design exploration.

## Conversation chapters

Open **Ongoing** in a conversation to navigate UTC-day chapters without
changing its original transcript. This is an exact-conversation view, not a
merge of conversations that share a familiar name. **This device** notes stay
separate, and the Cave source stays read-only.

Switching familiars restores the exact conversation you last selected, including
an available message anchor. References and unsent drafts stay in memory, scoped
to the live source, writer, familiar ID, and conversation ID. Changing sources
never copies a draft. Reloading clears drafts and navigation preferences, not
durably saved local notes. A missing remembered conversation or message shows an
unavailable notice rather than silently choosing a newer conversation.

The installed frozen SDK does not yet expose `listConversationChapters`.
Production Chat says so and offers navigation over already-loaded messages
only, marking partial history rather than prefetching bodies. The additive
DEVELOPMENT read port can consume typed producer headers after a separately
qualified SDK/native capability is admitted. It keeps the eight-page ceiling,
bounded memory-only query cache, revision checks, and source-switch isolation.
Malformed or cross-conversation chapter headers and repeated anchors are rejected.
It does not replace `vendor/opencoven-sdk` bytes or either conformance lock.

## Local save recovery

If local storage cannot be read during startup, **Retry local storage** retries
opening it without starting Cave or substituting an empty successful session.

Conversation creation and ordinary message saves distinguish a confirmed commit
followed by a failed history refresh from an unconfirmed commit acknowledgement.
Neither outcome invites a second write. **Reconcile local save** reads the exact
allocated record and refreshes history without resubmitting it. A committed save
whose exact record is available clears the matching draft. Only a no-commit
outcome permits retrying the retained draft. A known successful commit remains
committed even if its record is later deleted or discarded: commit outcome and
current availability are separate facts. Recovery keeps the unavailable saved
text or conversation title copyable until **Dismiss unavailable save**; it never
resubmits or recreates deleted data. Read failures stay visible and keep the retry
blocked. If an
unavailable note prevents determining an unconfirmed message's outcome, it is
not treated as an unsaved message.

Pending save receipts and completed reconciliation results are scoped to the
live local store. They survive navigation within this app session, not reload
or restart. After restarting, inspect saved conversations and messages before
submitting again; there is no cross-session exactly-once guarantee.

## Retained local side notes

You can explore a separate note without changing your parent conversation:

1. Select **This device**, open a conversation, and choose **New retained side note**.
2. Save messages in the empty side note. Your parent draft stays separate.
3. Select individual messages, choose **Review Bring back**, and edit the excerpt.
4. Choose **Bring back reviewed excerpt** to save exactly that text to the original
local parent as an inert user note.

**Close note** retains its messages. **Reopen note** permits more writing.
**Discard note** requires confirmation and removes the note's local messages;
previously reviewed imports remain in the parent. A minimal creation tombstone
prevents a retried create operation from recreating a discarded note.
An uncertain creation retries with its original key. Once a replay confirms that
the note was discarded, the next explicit **New retained side note** starts a
fresh creation request.
Pending creation keys and acknowledgements survive navigation in exact
source/writer/familiar/parent-scoped session memory. **Retry retained side note
creation** replays that request instead of starting another one. Late
acknowledgements cannot clear a newer request or navigate another parent.
These keys do **not** survive reload or restart: inspect the durable retained-note
list and open any already-created note before choosing to create another.
There is no cross-session exactly-once creation guarantee or durable pending outbox.

An attempted review keeps its operation key, selected message IDs, edited text,
and local branch preconditions in source- and writer-scoped memory. Returning
to the parent or switching sources does not cancel it. After an uncertain save,
retry the unchanged review to reconcile the result before editing again.
Ordinary cancellation is disabled while the result is uncertain, so it cannot
release the original key and enable a duplicate import. The immutable excerpt
remains selectable and copyable, including while a save is pending; navigation
does not release its retry identity.
If a stale branch is definitively rejected before committing, **Review again**
captures current branch preconditions and a new operation key while retaining
your edited excerpt. An uncertain operation never becomes editable under its
old key. Failed side-note metadata reads offer **Retry local side notes** without
enabling mutations against unknown metadata.
If the exact source is missing, the rejected review stays available as a
read-only, copyable excerpt. **Choose available messages** retains that text
while preparing a new selection with a new key. If the note is gone, copy the
excerpt before explicitly canceling; uncertain acknowledgements still cannot
be edited or reselected.
If the selected note itself becomes unavailable, its exact stored review remains
in a read-only recovery panel with **Cancel unavailable review**. That panel
cannot import, reselect, or navigate another familiar. Its explicit dismissal
only forgets inaccessible recovery text; it does not undo a commit or authorize
resubmission. Copy valuable text and inspect the parent before dismissing.
Fresh or reselected reviews require every selected message to be loaded. If
navigation resets the loaded pages, load the missing page or use **Clear message
selection** to choose a new exact selection; edited excerpts are retained.
Already-captured reviews retry their unchanged payload without requiring the
source page to be loaded again.

**Pending reviews do not survive reload or restart.** Saved imports do. If you
reload after an uncertain save, inspect the parent before starting another import;
the app cannot recover that pending review's key across restarts.

Side notes, lineage, and import receipts use the existing `ChatStore` and
IndexedDB transactions. Repeated operation keys reconcile to the same result;
changed payloads with reused keys are rejected. Competing windows are checked
at commit, and failed imports leave no partial parent record. Imports neither
merge the transcript nor execute instructions, generate replies, copy attachments,
or write to memory services.
The public import API requires validated parent/side branch preconditions;
unprepared selections cannot bypass review admission. Already-committed exact
receipts still reconcile before checking later branch changes. Legacy persisted
receipts lacking stored preconditions may also replay read-only after mandatory
request validation and exact key, parent, side, source-ID and excerpt matching.
This does not prove their historical branch snapshot or authorize a new write,
and their provenance is never rewritten. Modern receipts still reject changed
preconditions.

The version-2 IndexedDB upgrade preserves existing records and adds operation-key
indexes plus an atomic shared mutation revision. Warm writes read only the exact
conversation preconditions and indexed operation receipts; unchanged history is
not scanned. Initial hydration and refresh after another window's writes still
load history. A successful open carries the revision sampled **before** its
initial snapshot into the store, avoiding a duplicate scan on the first write.
Concurrent changes still force refresh; a backend without revisions or a failed
initial snapshot cannot mark an unchecked snapshot fresh. Revision-read failures
during opening surface rather than silently trusting the history.
Close older app windows if they block the database upgrade.
Successful writes recheck the shared revision after their local update and
reconcile detected competing commits before notifying observers. This is not a
continuous subscription to other windows. A detected refresh compares revisions
before and after its snapshot, retries at most three snapshots, and fails
explicitly without publishing an unstable snapshot if history keeps changing.
Known root commits retain their confirmed-save recovery; keyed import/creation
replays notify observers after recovery without writing again. Commits after the
final sample still require a later refresh; no instantaneous cross-window view
is promised. The memory-only backend also uses
keyed preconditions and operation-key counts, maintained across overwrites,
deletions and discarded-note tombstones. Admission touches only the requested
records and changed rows; loading snapshots and initial hydration still scan
history.
App owns the local store subscription independently of transient panels, so
late committed writes refresh the active local transcript, sidebar and side-note
metadata without another click. These notifications neither navigate another
conversation nor refetch an active Cave source.

These are local-only notes with **no connected familiar**, not Cave-backed side
chats. If durable storage is unavailable, the UI discloses memory-only custody.
There is no Temporary or provider-deletion guarantee. The frozen Cave source
has no side-chat writer, so it shows an unavailable notice rather than an enabled
no-op or a local fallback for canonical content.

The installed Cave SDK omits import provenance. The source-level notice states
that limitation; canonical messages remain read-only text without inferred
import markers or changed roles.

## Security boundaries

- The main window can invoke only the reviewed `app_identity`,
Expand Down
10 changes: 5 additions & 5 deletions e2e/app.smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { expect, test } from '@playwright/test';

test('preserves the local demo routes alongside the default app', async ({ page }) => {
test('preserves the local demo routes alongside the default app', async ({ page, baseURL }) => {
await page.goto('/?demo=chat');

await expect(page).toHaveURL('http://127.0.0.1:4174/?demo=chat');
await expect(page).toHaveURL(new URL('/?demo=chat', baseURL).href);
await expect(page.getByRole('complementary', { name: 'Conversations sidebar' })).toBeVisible();
await expect(page.getByRole('region', { name: 'Held action' })).toBeVisible();
// Exact: the composer also has a "Send options" caret beside Send.
Expand All @@ -12,19 +12,19 @@ test('preserves the local demo routes alongside the default app', async ({ page

await page.goto('/?demo=messages');

await expect(page).toHaveURL('http://127.0.0.1:4174/?demo=messages');
await expect(page).toHaveURL(new URL('/?demo=messages', baseURL).href);
await expect(page.getByRole('complementary', { name: 'Conversations' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Send' })).toBeVisible();

await page.goto('/?demo=minimal');

await expect(page).toHaveURL('http://127.0.0.1:4174/?demo=minimal');
await expect(page).toHaveURL(new URL('/?demo=minimal', baseURL).href);
await expect(page.getByText('Chats', { exact: true })).toBeVisible();
await expect(page.getByText('Familiars', { exact: true })).toBeVisible();

await page.goto('/?demo=familiars-reads');

await expect(page).toHaveURL('http://127.0.0.1:4174/?demo=familiars-reads');
await expect(page).toHaveURL(new URL('/?demo=familiars-reads', baseURL).href);
await expect(page.getByRole('complementary', { name: 'Conversations sidebar' })).toBeVisible();
await expect(page.getByRole('button', { name: /Q3 pricing evidence map/ })).toBeVisible();
// Stage 1 has no send capability yet; the composer notice says so rather
Expand Down
Loading
Loading