Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ The project publishes 0.x prerelease versions; a stable release line is not yet
repository coordinate the installer itself uses, so the next rename cannot
leave a stale identifier behind unnoticed.

### Fixed

- Recursive folder delete now removes the corresponding objects from bucket
storage after the database transaction commits (`#177`). Previously, the DB
rows were deleted but the blobs remained orphaned in the bucket. The cleanup
is best-effort: a failed object delete does not roll back the folder delete,
and failures are logged at WARN level so the operator can see which keys
remain. The folder service now accepts an optional `ObjectStore` and logger
via `folder.WithStore` and `folder.WithLogger` options. Recursive delete
also refuses when an active or archived memory outside the folder still
cites a file in the tree via `source_file_id`, so blob cleanup cannot
destroy a live citation through `ON DELETE SET NULL`.

### Security

- Normalize the client-declared MIME type of a stored file before deciding how
Expand Down
30 changes: 30 additions & 0 deletions docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,36 @@ Redis AOF protects normal restarts but is not in the portable backup. A restore
therefore starts with an empty queue/replay window. Requeue or reindex any file
whose processing did not reach a terminal state before the backup.

### Object storage retention

Object keys are per-file by construction: each key embeds the row's own file ID
(`users/<user_id>/<file_id>/<basename>`), so deleting one row's key cannot
remove another row's bytes. No reference counting is needed.

When a file or folder is deleted, the database row is removed first, then the
corresponding object is deleted from bucket storage on a best-effort basis. A
failed object delete does not roll back the database change; the orphaned key
is logged at `WARN` level so the operator can see which keys remain. If the
shared 30-second cleanup budget is exhausted mid-batch, later keys log that
the budget ran out rather than a per-object store error.

Recursive folder delete refuses with the existing `forget` sentinel when an
active or archived memory — including one whose `path` is outside the folder
— still cites a file in the tree through `source_file_id`. That keeps blob
cleanup from destroying a live citation via `ON DELETE SET NULL`.

**Crash window**: if the process is killed after the database transaction
commits but before the blob delete lands, the object remains in the bucket
permanently. There is currently no reaper or garbage-collection pass to sweep
these residues. The server has no listing capability against the bucket (the
`storage.Store` interface exposes only `Put`/`Get`/`Delete`), so a reaper would
need to record keys whose delete was never attempted. This is a known gap;
operators should monitor bucket growth against expected database row counts.

To manually reconcile, compare the bucket contents against the `files` table's
`storage_key` column. Objects present in the bucket but absent from the database
are safe to delete — they cannot be referenced by any live row.

### Restore drill

Restore only into an empty installation. The script verifies every checksum
Expand Down
5 changes: 4 additions & 1 deletion server/cmd/memd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ func run() error {
"providers", cfg.ManagedEmbeddingProviders,
)
}
folderSvc := folder.New(database.Pool)
folderSvc := folder.New(database.Pool,
folder.WithStore(store),
folder.WithLogger(logger),
)
fileSvc := file.New(database.Pool, store, folderSvc)
memorySvc := memory.New(database.Pool)
durableContextSvc := durablecontext.New(database.Pool, memorySvc)
Expand Down
Loading
Loading