Skip to content

Delete a context's stashes with it, instead of stranding them - #25

Merged
EarMaster merged 3 commits into
mainfrom
develop
Sep 27, 2026
Merged

EarMaster merged 3 commits into
mainfrom
develop

Conversation

@EarMaster

Copy link
Copy Markdown
Owner

Deleting a context - locally, from another device, or over MCP - only marked the context deleted. Its stashes kept pointing at the dead id, so no view showed them (every filter compares against contextId || "default"), yet they were still stored, counted, exported and synced. The cloud had the same gap: it never deleted a context's stashes either, and a stash with no context at all - an unknown id on create, an explicit null on move, a device pushing a context this server had not seen - was stored with context_id NULL, which the app renders as Default but the server never queries as "default".

Both sides now apply the same two rules. No context, or one that does not exist, means the default context: every installation creates it and nothing can delete it. A stash whose context is deleted is deleted with it. Deleting a context now deletes the stashes in it, and the confirmation dialog says how many. A database trigger refuses a NULL context_id on both sides rather than a NOT NULL column, since the column has been nullable since the first migration and adding the constraint would mean rebuilding the table under attachments' ON DELETE CASCADE.

Every stash write now runs enforce_context_rules(): on startup, after importing contexts or stashes from a sync pull, and after a local context deletion. It moves stashes with no valid context to Default and deletes any still sitting under an already-deleted one, returning what it deleted so the caller can remove their cached attachment files.

Matches the migration and rules just committed to cloud/, so a stash ends up in the same place whichever side writes it.

Ran the Rust suite (159 tests, 6 new) and the frontend suite (219 tests, 2 new), plus tsc and svelte-check with zero errors.

Claude-Session: https://claude.ai/code/session_01KkYvnuq8Y4nYD1jSCi9GCR

EarMaster and others added 3 commits September 27, 2026 01:07
Deleting a context - locally, from another device, or over MCP - only
marked the context deleted. Its stashes kept pointing at the dead id, so
no view showed them (every filter compares against contextId || "default"),
yet they were still stored, counted, exported and synced. The cloud had
the same gap: it never deleted a context's stashes either, and a stash
with no context at all - an unknown id on create, an explicit null on
move, a device pushing a context this server had not seen - was stored
with context_id NULL, which the app renders as Default but the server
never queries as "default".

Both sides now apply the same two rules. No context, or one that does
not exist, means the default context: every installation creates it and
nothing can delete it. A stash whose context is deleted is deleted with
it. Deleting a context now deletes the stashes in it, and the confirmation
dialog says how many. A database trigger refuses a NULL context_id on
both sides rather than a NOT NULL column, since the column has been
nullable since the first migration and adding the constraint would mean
rebuilding the table under attachments' ON DELETE CASCADE.

Every stash write now runs enforce_context_rules(): on startup, after
importing contexts or stashes from a sync pull, and after a local context
deletion. It moves stashes with no valid context to Default and deletes
any still sitting under an already-deleted one, returning what it deleted
so the caller can remove their cached attachment files.

Matches the migration and rules just committed to cloud/, so a stash ends
up in the same place whichever side writes it.

Ran the Rust suite (159 tests, 6 new) and the frontend suite (219 tests,
2 new), plus `tsc` and `svelte-check` with zero errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkYvnuq8Y4nYD1jSCi9GCR
… none

Exporting a context wrote each attachment as <8 chars of stash id>_<name>.
Two pasted screenshots in one stash, both image.png, collided on that name;
the zip writer refused the second write and the export died part way with
the first file written and the dialog left open with nothing said. Files are
now keyed by attachment id, which cannot repeat, with the original name kept
after it so a particular file can still be found by eye.

An export also used to drop any attachment not yet downloaded to this device
(synced from elsewhere, background download still pending) while the
document went on linking to it - an archive that looked complete and was
not. It now refuses instead, names what is missing, and the dialog fetches
those files and tries again before giving up with a real error.

Separately, importing never found a single attachment: the importer looked
for each file under the *new* id an imported stash gets, which is never the
id the archive was written with. The markdown now carries the archive path
itself, which the importer reads back directly. Both the new attachment-id
naming and every older stash-id-prefixed archive are recognised. A copy that
fails now rolls back what it had already placed, and both dialogs show an
error instead of failing to the console with nothing on screen.

Added i18n for the save/pick file-type labels, which were hardcoded English.

Rust: 164 tests (new: the name clash, refusing an incomplete archive, older
archives still resolving, a copy failure rolling back). Frontend: 219 tests,
tsc and svelte-check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@EarMaster
EarMaster merged commit 3c1d8d2 into main Sep 27, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant