memorytalk 0.9.2__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {memorytalk-0.9.2 → memorytalk-1.0.0}/PKG-INFO +1 -1
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/api/__init__.py +71 -49
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/api/cards.py +23 -1
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/api/sync.py +24 -27
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/cli/_format.py +24 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/cli/card.py +67 -4
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/config.py +18 -1
- memorytalk-1.0.0/memorytalk/migration/__init__.py +23 -0
- memorytalk-1.0.0/memorytalk/migration/_types.py +27 -0
- memorytalk-1.0.0/memorytalk/migration/discover.py +50 -0
- memorytalk-1.0.0/memorytalk/migration/runner.py +202 -0
- memorytalk-1.0.0/memorytalk/migration/state.py +140 -0
- memorytalk-1.0.0/memorytalk/migrations/__init__.py +15 -0
- memorytalk-1.0.0/memorytalk/migrations/v1/__init__.py +13 -0
- memorytalk-1.0.0/memorytalk/migrations/v1/init_database.py +136 -0
- memorytalk-1.0.0/memorytalk/migrations/v1/init_searchbase.py +20 -0
- memorytalk-1.0.0/memorytalk/migrations/v1/up_database.py +137 -0
- memorytalk-1.0.0/memorytalk/migrations/v1/up_searchbase.py +48 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/provider/storage.py +23 -6
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/repository/cards.py +48 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/repository/reviews.py +13 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/repository/store.py +13 -6
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/schemas/__init__.py +3 -2
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/schemas/cards.py +15 -0
- memorytalk-1.0.0/memorytalk/searchbase/__init__.py +48 -0
- memorytalk-1.0.0/memorytalk/searchbase/_types.py +139 -0
- memorytalk-1.0.0/memorytalk/searchbase/local/__init__.py +0 -0
- memorytalk-1.0.0/memorytalk/searchbase/local/_admin.py +138 -0
- memorytalk-1.0.0/memorytalk/searchbase/local/_logging.py +81 -0
- memorytalk-1.0.0/memorytalk/searchbase/local/backend.py +237 -0
- memorytalk-1.0.0/memorytalk/searchbase/local/index.py +294 -0
- memorytalk-1.0.0/memorytalk/searchbase/local/maintenance.py +239 -0
- memorytalk-1.0.0/memorytalk/searchbase/local/util.py +224 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/service/__init__.py +2 -2
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/service/backfill.py +31 -147
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/service/cards.py +78 -10
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/service/read.py +4 -4
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/service/recall.py +10 -23
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/service/search.py +27 -44
- memorytalk-1.0.0/memorytalk/service/searchbase_schema.py +73 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/service/sessions.py +34 -86
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk.egg-info/PKG-INFO +1 -1
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk.egg-info/SOURCES.txt +21 -3
- {memorytalk-0.9.2 → memorytalk-1.0.0}/pyproject.toml +1 -1
- memorytalk-0.9.2/memorytalk/provider/lancedb.py +0 -413
- memorytalk-0.9.2/memorytalk/repository/schema.py +0 -259
- memorytalk-0.9.2/memorytalk/service/index_buffer.py +0 -206
- {memorytalk-0.9.2 → memorytalk-1.0.0}/LICENSE +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/README.md +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/__init__.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/__main__.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/adapters/__init__.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/adapters/base.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/adapters/claude_code.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/adapters/codex.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/adapters/openclaw.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/api/read.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/api/recall.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/api/reviews.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/api/search.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/api/sessions.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/api/status.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/cli/__init__.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/cli/_http.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/cli/_render.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/cli/read.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/cli/recall.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/cli/review.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/cli/search.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/cli/server.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/cli/session.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/cli/setup.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/cli/sync.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/cli/upgrade.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/hook_assets/claude_code/.claude-plugin/marketplace.json +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/hook_assets/claude_code/plugins/memory-talk-recall/.claude-plugin/plugin.json +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/hook_assets/claude_code/plugins/memory-talk-recall/hooks/hooks.json +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/hook_assets/codex/.agents/plugins/marketplace.json +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/hook_assets/codex/plugins/memory-talk-recall/.codex-plugin/plugin.json +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/hook_assets/codex/plugins/memory-talk-recall/hooks/hooks.json +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/hooks/__init__.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/hooks/base.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/hooks/claude_code.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/hooks/codex.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/hooks/materialize.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/hooks/probe.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/hooks/state.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/provider/__init__.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/provider/embedding.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/repository/__init__.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/repository/recall.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/repository/search_log.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/repository/sessions.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/repository/sync_checkpoint.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/schemas/card.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/schemas/read.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/schemas/recall.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/schemas/review.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/schemas/reviews.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/schemas/search.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/schemas/session.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/schemas/status.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/schemas/sync.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/server.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/service/events.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/service/reviews.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/service/sync.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/util/__init__.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/util/console.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/util/dsl.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/util/env_template.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/util/formula.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/util/highlight.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/util/ids.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/util/indexes.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/util/settings_io.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/util/tag_filter.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk/util/tags.py +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk.egg-info/dependency_links.txt +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk.egg-info/entry_points.txt +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk.egg-info/requires.txt +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/memorytalk.egg-info/top_level.txt +0 -0
- {memorytalk-0.9.2 → memorytalk-1.0.0}/setup.cfg +0 -0
|
@@ -20,10 +20,10 @@ from memorytalk.config import Config, ConfigValidationError
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
_log = logging.getLogger("memorytalk.api")
|
|
23
|
+
from memorytalk.migration import MigrationRunner
|
|
23
24
|
from memorytalk.provider.embedding import (
|
|
24
|
-
EmbedderValidationError,
|
|
25
|
+
EmbedderValidationError, validate_embedder,
|
|
25
26
|
)
|
|
26
|
-
from memorytalk.provider.lancedb import LanceStore
|
|
27
27
|
from memorytalk.provider.storage import LocalStorage
|
|
28
28
|
from memorytalk.repository import SQLiteStore
|
|
29
29
|
from memorytalk.repository.sync_checkpoint import SyncCheckpointStore
|
|
@@ -32,8 +32,8 @@ from memorytalk.service import (
|
|
|
32
32
|
RecallService, ReviewService,
|
|
33
33
|
)
|
|
34
34
|
from memorytalk.service.backfill import IndexBackfill
|
|
35
|
-
from memorytalk.service.index_buffer import IndexWriteBuffer
|
|
36
35
|
from memorytalk.service.search import SearchService
|
|
36
|
+
from memorytalk.service.searchbase_schema import build_search_backend
|
|
37
37
|
from memorytalk.service.sync import SyncWatcher
|
|
38
38
|
|
|
39
39
|
|
|
@@ -44,50 +44,76 @@ def create_app(config: Config | None = None) -> FastAPI:
|
|
|
44
44
|
@asynccontextmanager
|
|
45
45
|
async def lifespan(app: FastAPI):
|
|
46
46
|
storage = LocalStorage(config.data_root)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
config.vectors_dir
|
|
47
|
+
# Capture "is this a pre-existing install?" BEFORE we open any
|
|
48
|
+
# file or directory — opening the SQLite conn creates
|
|
49
|
+
# memory.db, and the searchbase backend populates vectors/, so
|
|
50
|
+
# the runner's own heuristic would fire false-positive on a
|
|
51
|
+
# fresh install if asked later.
|
|
52
|
+
existing_install = (
|
|
53
|
+
config.db_path.exists()
|
|
54
|
+
or (
|
|
55
|
+
config.vectors_dir.exists()
|
|
56
|
+
and any(config.vectors_dir.iterdir())
|
|
56
57
|
)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
)
|
|
59
|
+
# Open the raw SQLite connection (no schema yet) so the
|
|
60
|
+
# migration runner can apply DDL against it before we wrap it
|
|
61
|
+
# in a SQLiteStore.
|
|
62
|
+
conn = await SQLiteStore.open_connection(config.db_path)
|
|
61
63
|
|
|
62
64
|
try:
|
|
63
65
|
await validate_embedder(config)
|
|
64
66
|
except EmbedderValidationError as e:
|
|
67
|
+
await conn.close()
|
|
65
68
|
_log.exception("embedding startup check failed; aborting boot")
|
|
66
69
|
raise SystemExit(2) from e
|
|
67
70
|
|
|
71
|
+
# searchbase is optional at boot. If it can't open (missing
|
|
72
|
+
# pyarrow, bad dir perms, ...) we still want read/status to work;
|
|
73
|
+
# vector-backed endpoints then return 503 ``unavailable``. The
|
|
74
|
+
# migration runner skips the searchbase subsystem in that case
|
|
75
|
+
# and picks it up on the next boot.
|
|
76
|
+
searchbase = None
|
|
77
|
+
try:
|
|
78
|
+
searchbase = await build_search_backend(config)
|
|
79
|
+
except Exception:
|
|
80
|
+
_log.exception("searchbase init failed; vector-backed endpoints will 503")
|
|
81
|
+
|
|
82
|
+
# Bring persistent state up to v1 (creates tables on a fresh
|
|
83
|
+
# install, runs the 0.8.x → v1 deltas on an upgrade). Aborts
|
|
84
|
+
# boot on failure — partial schemas would let services start
|
|
85
|
+
# and corrupt data.
|
|
86
|
+
runner = MigrationRunner(
|
|
87
|
+
db_conn=conn,
|
|
88
|
+
admin=searchbase.admin() if searchbase is not None else None,
|
|
89
|
+
state_path=config.migrations_state_path,
|
|
90
|
+
data_root=config.data_root,
|
|
91
|
+
existing_install=existing_install,
|
|
92
|
+
)
|
|
93
|
+
try:
|
|
94
|
+
await runner.run()
|
|
95
|
+
except Exception as e:
|
|
96
|
+
_log.exception("migration failed; aborting boot")
|
|
97
|
+
await conn.close()
|
|
98
|
+
if searchbase is not None:
|
|
99
|
+
try:
|
|
100
|
+
await searchbase.close()
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
raise SystemExit(3) from e
|
|
104
|
+
|
|
105
|
+
db = SQLiteStore(conn, config.db_path, storage)
|
|
106
|
+
sync_checkpoints = await SyncCheckpointStore.create(config.sync_db_path)
|
|
107
|
+
|
|
68
108
|
events = EventWriter(db)
|
|
69
109
|
app.state.config = config
|
|
70
110
|
app.state.storage = storage
|
|
71
111
|
app.state.db = db
|
|
72
|
-
app.state.
|
|
73
|
-
app.state.embedder = embedder
|
|
112
|
+
app.state.searchbase = searchbase
|
|
74
113
|
app.state.events = events
|
|
75
|
-
# IndexWriteBuffer aggregates LanceDB inserts across sessions so
|
|
76
|
-
# one ``table.add()`` carries many embedder batches' worth of
|
|
77
|
-
# rows. Without it the ingest path creates one fragment per
|
|
78
|
-
# embedder batch (10 with DashScope) → vector search eventually
|
|
79
|
-
# EMFILEs on fd ceiling. See service/index_buffer.py and
|
|
80
|
-
# docs/issue #4 §4.3.
|
|
81
|
-
app.state.index_buffer = IndexWriteBuffer(
|
|
82
|
-
vectors=vectors, db=db,
|
|
83
|
-
flush_rows=config.settings.index.lance_flush_rows,
|
|
84
|
-
flush_interval_seconds=config.settings.index.lance_flush_interval_seconds,
|
|
85
|
-
)
|
|
86
|
-
app.state.index_buffer.start()
|
|
87
114
|
app.state.read = ReadService(db=db, events=events)
|
|
88
115
|
app.state.ingest = IngestService(
|
|
89
|
-
db=db,
|
|
90
|
-
index_buffer=app.state.index_buffer,
|
|
116
|
+
db=db, search=searchbase, events=events,
|
|
91
117
|
)
|
|
92
118
|
app.state.sync_checkpoints = sync_checkpoints
|
|
93
119
|
app.state.sync = SyncWatcher(
|
|
@@ -95,14 +121,14 @@ def create_app(config: Config | None = None) -> FastAPI:
|
|
|
95
121
|
checkpoints=sync_checkpoints,
|
|
96
122
|
)
|
|
97
123
|
app.state.search = SearchService(
|
|
98
|
-
config=config, db=db,
|
|
124
|
+
config=config, db=db, search=searchbase,
|
|
99
125
|
)
|
|
100
126
|
app.state.cards = CardService(
|
|
101
|
-
db=db,
|
|
127
|
+
db=db, search=searchbase, events=events,
|
|
102
128
|
)
|
|
103
129
|
app.state.reviews = ReviewService(db=db, events=events)
|
|
104
130
|
app.state.recall = RecallService(
|
|
105
|
-
config=config, db=db,
|
|
131
|
+
config=config, db=db, search=searchbase,
|
|
106
132
|
)
|
|
107
133
|
|
|
108
134
|
# Spin up the watcher if settings says so. start() returns fast
|
|
@@ -121,16 +147,12 @@ def create_app(config: Config | None = None) -> FastAPI:
|
|
|
121
147
|
# when no embedder / lance is available; loop is cancelled on
|
|
122
148
|
# lifespan shutdown.
|
|
123
149
|
app.state.backfill = IndexBackfill(
|
|
124
|
-
db=db,
|
|
125
|
-
index_buffer=app.state.index_buffer,
|
|
150
|
+
db=db, search=searchbase,
|
|
126
151
|
)
|
|
127
152
|
app.state.backfill.start()
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
# loop: gated only on vectors, runs in the background, never
|
|
132
|
-
# blocks startup. See IndexBackfill.trigger_startup_compaction.
|
|
133
|
-
app.state.backfill.trigger_startup_compaction()
|
|
153
|
+
# Startup compaction is now owned by searchbase (compact_all runs
|
|
154
|
+
# in the background from the backend's create()), so backfill no
|
|
155
|
+
# longer triggers it here.
|
|
134
156
|
|
|
135
157
|
yield
|
|
136
158
|
|
|
@@ -144,12 +166,12 @@ def create_app(config: Config | None = None) -> FastAPI:
|
|
|
144
166
|
await app.state.backfill.stop()
|
|
145
167
|
except Exception:
|
|
146
168
|
pass
|
|
147
|
-
#
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
169
|
+
# Shut down the searchbase instance (stops its maintenance task).
|
|
170
|
+
if app.state.searchbase is not None:
|
|
171
|
+
try:
|
|
172
|
+
await app.state.searchbase.close()
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
153
175
|
await db.close()
|
|
154
176
|
await sync_checkpoints.close()
|
|
155
177
|
|
|
@@ -12,11 +12,12 @@ import datetime as _dt
|
|
|
12
12
|
from fastapi import APIRouter, HTTPException, Query, Request
|
|
13
13
|
|
|
14
14
|
from memorytalk.schemas import (
|
|
15
|
-
CardListResponse, CardMeta, CardTagResponse,
|
|
15
|
+
CardDeleteResponse, CardListResponse, CardMeta, CardTagResponse,
|
|
16
16
|
CreateCardRequest, CreateCardResponse,
|
|
17
17
|
TagPatchRequest,
|
|
18
18
|
)
|
|
19
19
|
from memorytalk.service import CardConflict, CardServiceError
|
|
20
|
+
from memorytalk.service.cards import CardNotFound
|
|
20
21
|
from memorytalk.util.tag_filter import parse_tag_arg
|
|
21
22
|
from memorytalk.util.tags import TagValidationError, apply_patch
|
|
22
23
|
|
|
@@ -142,3 +143,24 @@ async def patch_card_tags(
|
|
|
142
143
|
status_code=404, detail=f"card {card_id!r} not found",
|
|
143
144
|
)
|
|
144
145
|
return CardTagResponse(card_id=card_id, tags=merged)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@router.delete("/cards/{card_id}", response_model=CardDeleteResponse)
|
|
149
|
+
async def delete_card(card_id: str, request: Request) -> CardDeleteResponse:
|
|
150
|
+
"""Hard-delete a card: SQLite row + reviews + outbound source_cards
|
|
151
|
+
+ vector embedding + per-card filesystem dir.
|
|
152
|
+
|
|
153
|
+
Inbound source_cards (other cards that reference this one) are NOT
|
|
154
|
+
cascaded — they become dangling references, which the response
|
|
155
|
+
surfaces as ``inbound_refs_dangling`` so callers can warn the
|
|
156
|
+
user. recall_event rows that mention this card_id are NOT touched
|
|
157
|
+
— they're historical records, deleting the card doesn't rewrite
|
|
158
|
+
history."""
|
|
159
|
+
svc = request.app.state.cards
|
|
160
|
+
if svc is None:
|
|
161
|
+
raise HTTPException(status_code=503, detail="cards service unavailable")
|
|
162
|
+
try:
|
|
163
|
+
result = await svc.delete(card_id)
|
|
164
|
+
except CardNotFound as e:
|
|
165
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
166
|
+
return CardDeleteResponse(**result)
|
|
@@ -12,18 +12,22 @@ from fastapi import APIRouter, Query, Request
|
|
|
12
12
|
router = APIRouter()
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def _gather_lance_health(state) -> dict:
|
|
16
|
-
"""Collect
|
|
15
|
+
async def _gather_lance_health(state) -> dict:
|
|
16
|
+
"""Collect vector-layer observability for ``index.lance``.
|
|
17
17
|
|
|
18
|
-
Pulls
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
still returns a well-shaped response.
|
|
18
|
+
Pulls compaction + EMFILE-recovery state from the searchbase
|
|
19
|
+
instance's ``health().detail``. The write path is now immediate
|
|
20
|
+
(no IndexWriteBuffer), so the legacy buffer fields report idle.
|
|
21
|
+
All fields default to safe zeros / None when searchbase is absent
|
|
22
|
+
so a partially-disabled boot still returns a well-shaped response.
|
|
23
23
|
"""
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
searchbase = getattr(state, "searchbase", None)
|
|
25
|
+
detail: dict = {}
|
|
26
|
+
if searchbase is not None:
|
|
27
|
+
try:
|
|
28
|
+
detail = (await searchbase.health()).detail
|
|
29
|
+
except Exception:
|
|
30
|
+
detail = {}
|
|
27
31
|
|
|
28
32
|
soft = hard = None
|
|
29
33
|
try:
|
|
@@ -35,22 +39,15 @@ def _gather_lance_health(state) -> dict:
|
|
|
35
39
|
pass
|
|
36
40
|
|
|
37
41
|
return {
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
),
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
),
|
|
48
|
-
"emfile_recoveries_since_boot": (
|
|
49
|
-
vectors.emfile_recoveries if vectors is not None else 0
|
|
50
|
-
),
|
|
51
|
-
"last_emfile_at": (
|
|
52
|
-
vectors.last_emfile_at_iso if vectors is not None else None
|
|
53
|
-
),
|
|
42
|
+
# Writes are immediate now — no buffer to drain.
|
|
43
|
+
"pending_vector_rows": 0,
|
|
44
|
+
"last_flush_at": None,
|
|
45
|
+
"last_flush_error": None,
|
|
46
|
+
"flush_count_since_boot": 0,
|
|
47
|
+
"last_compaction_at": detail.get("last_compact_at_iso"),
|
|
48
|
+
"last_compaction_error": detail.get("last_compact_error"),
|
|
49
|
+
"emfile_recoveries_since_boot": detail.get("emfile_recoveries", 0),
|
|
50
|
+
"last_emfile_at": detail.get("last_emfile_at_iso"),
|
|
54
51
|
"fd_soft_limit": soft,
|
|
55
52
|
"fd_hard_limit": hard,
|
|
56
53
|
}
|
|
@@ -75,7 +72,7 @@ async def get_sync_status(request: Request, limit: int = Query(5, ge=0, le=20)):
|
|
|
75
72
|
index["last_index_error"] = (
|
|
76
73
|
backfill.last_error if backfill is not None else None
|
|
77
74
|
)
|
|
78
|
-
index["lance"] = _gather_lance_health(request.app.state)
|
|
75
|
+
index["lance"] = await _gather_lance_health(request.app.state)
|
|
79
76
|
|
|
80
77
|
if not config.settings.sync.enabled:
|
|
81
78
|
return {"status": "disabled", "index": index}
|
|
@@ -858,6 +858,30 @@ def fmt_card_list(payload: dict, filter_summary: str = "") -> str:
|
|
|
858
858
|
return "\n".join(lines).rstrip() + "\n"
|
|
859
859
|
|
|
860
860
|
|
|
861
|
+
def fmt_card_delete(payload: dict) -> str:
|
|
862
|
+
"""Render DELETE /v3/cards/<cid> response.
|
|
863
|
+
|
|
864
|
+
One-line confirmation + a hint about inbound-ref dangling when
|
|
865
|
+
applicable. We deliberately don't moan about every detail (vector
|
|
866
|
+
cleared / files cleared / etc.) — those are best-effort and
|
|
867
|
+
typically silent."""
|
|
868
|
+
cid = payload.get("card_id") or "?"
|
|
869
|
+
reviews = int(payload.get("reviews_deleted", 0) or 0)
|
|
870
|
+
dangling = int(payload.get("inbound_refs_dangling", 0) or 0)
|
|
871
|
+
|
|
872
|
+
bits = [f"deleted · `{cid}`"]
|
|
873
|
+
if reviews:
|
|
874
|
+
bits.append(
|
|
875
|
+
f"{reviews} review{'s' if reviews != 1 else ''} removed",
|
|
876
|
+
)
|
|
877
|
+
if dangling:
|
|
878
|
+
bits.append(
|
|
879
|
+
f"⚠ {dangling} inbound `source_cards` reference"
|
|
880
|
+
f"{'s' if dangling != 1 else ''} now dangling",
|
|
881
|
+
)
|
|
882
|
+
return " · ".join(bits) + "\n"
|
|
883
|
+
|
|
884
|
+
|
|
861
885
|
def fmt_card_tag(payload: dict, *, is_query: bool) -> str:
|
|
862
886
|
"""Render PATCH /v3/cards/<cid>/tags response.
|
|
863
887
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"""CLI: memory.talk card {create, list, tag} — card write + maintenance.
|
|
1
|
+
"""CLI: memory.talk card {create, list, tag, delete} — card write + maintenance.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Subcommands:
|
|
4
4
|
|
|
5
5
|
create write a new immutable card (was the bare ``card '<json>'`` form
|
|
6
6
|
in 0.7.x; hard-renamed in 0.8.x because the top level now hosts
|
|
@@ -8,6 +8,7 @@ Three subcommands:
|
|
|
8
8
|
list multi-filter listing (tag / created_at) — no source/cwd: cards
|
|
9
9
|
aren't from a source the way sessions are
|
|
10
10
|
tag query / set / unset kv tags on one card
|
|
11
|
+
delete hard-delete a card (SQLite row + reviews + vector + files)
|
|
11
12
|
|
|
12
13
|
The HTTP-call shape is identical to ``cli/session.py`` — both go through
|
|
13
14
|
the same ``api()`` helper and share fmt_/parse_ helpers. See
|
|
@@ -20,7 +21,7 @@ import sys
|
|
|
20
21
|
import click
|
|
21
22
|
|
|
22
23
|
from memorytalk.cli._format import (
|
|
23
|
-
fmt_card_created, fmt_card_list, fmt_card_tag, fmt_error,
|
|
24
|
+
fmt_card_created, fmt_card_delete, fmt_card_list, fmt_card_tag, fmt_error,
|
|
24
25
|
)
|
|
25
26
|
from memorytalk.cli._http import ApiError, api, extract_error_message
|
|
26
27
|
from memorytalk.cli._render import emit_json, emit_json_err, emit_md, emit_md_err
|
|
@@ -33,7 +34,7 @@ from memorytalk.util.tags import TagValidationError, parse_kv_args
|
|
|
33
34
|
|
|
34
35
|
@click.group("card")
|
|
35
36
|
def card() -> None:
|
|
36
|
-
"""Card write + maintenance: create / list / tag."""
|
|
37
|
+
"""Card write + maintenance: create / list / tag / delete."""
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
# ────────── card create ──────────
|
|
@@ -159,6 +160,68 @@ def tag(card_id: str, kv_args: tuple[str, ...], json_out: bool) -> None:
|
|
|
159
160
|
emit_md(fmt_card_tag(result, is_query=is_query))
|
|
160
161
|
|
|
161
162
|
|
|
163
|
+
# ────────── card delete ──────────
|
|
164
|
+
|
|
165
|
+
@card.command("delete")
|
|
166
|
+
@click.argument("card_id")
|
|
167
|
+
@click.option("--yes", "-y", is_flag=True, default=False,
|
|
168
|
+
help="Skip the interactive confirmation prompt.")
|
|
169
|
+
@click.option("--json", "json_out", is_flag=True, default=False,
|
|
170
|
+
help="Emit JSON")
|
|
171
|
+
def delete(card_id: str, yes: bool, json_out: bool) -> None:
|
|
172
|
+
"""Hard-delete a card: SQLite row + reviews + outbound source_cards
|
|
173
|
+
+ vector + per-card filesystem dir.
|
|
174
|
+
|
|
175
|
+
Other cards that reference this one via ``source_cards`` are NOT
|
|
176
|
+
cascaded — their references become dangling. ``recall_event``
|
|
177
|
+
history that mentions this card is NOT rewritten (history is
|
|
178
|
+
history). Pass ``--yes`` to skip the confirm prompt.
|
|
179
|
+
"""
|
|
180
|
+
cfg = Config()
|
|
181
|
+
|
|
182
|
+
# Interactive path: pre-fetch the card to show the user WHAT they're
|
|
183
|
+
# about to delete (insight + created + reviews count). The DELETE
|
|
184
|
+
# response also surfaces those numbers, but only after the deed.
|
|
185
|
+
if not yes and not json_out:
|
|
186
|
+
try:
|
|
187
|
+
preview = api("POST", "/v3/read", cfg, json_body={"id": card_id})
|
|
188
|
+
except ApiError as e:
|
|
189
|
+
emit_md_err(fmt_error(extract_error_message(e.payload)))
|
|
190
|
+
sys.exit(1)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
_emit_err(json_out, f"cannot reach server: {e}")
|
|
193
|
+
sys.exit(1)
|
|
194
|
+
|
|
195
|
+
card_doc = preview.get("card") or {}
|
|
196
|
+
reviews = card_doc.get("reviews") or []
|
|
197
|
+
click.echo("", err=True)
|
|
198
|
+
click.echo(f"card delete · {card_id}", err=True)
|
|
199
|
+
click.echo(f" insight: {card_doc.get('insight', '?')}", err=True)
|
|
200
|
+
click.echo(f" created: {card_doc.get('created_at', '?')}", err=True)
|
|
201
|
+
click.echo(f" reviews: {len(reviews)} (will be deleted with the card)", err=True)
|
|
202
|
+
click.echo("", err=True)
|
|
203
|
+
if not click.confirm("Delete this card?", default=False, err=True):
|
|
204
|
+
click.echo("aborted.", err=True)
|
|
205
|
+
sys.exit(1)
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
result = api("DELETE", f"/v3/cards/{card_id}", cfg)
|
|
209
|
+
except ApiError as e:
|
|
210
|
+
if json_out:
|
|
211
|
+
emit_json_err(e.payload)
|
|
212
|
+
else:
|
|
213
|
+
emit_md_err(fmt_error(extract_error_message(e.payload)))
|
|
214
|
+
sys.exit(1)
|
|
215
|
+
except Exception as e:
|
|
216
|
+
_emit_err(json_out, f"cannot reach server: {e}")
|
|
217
|
+
sys.exit(1)
|
|
218
|
+
|
|
219
|
+
if json_out:
|
|
220
|
+
emit_json(result)
|
|
221
|
+
else:
|
|
222
|
+
emit_md(fmt_card_delete(result))
|
|
223
|
+
|
|
224
|
+
|
|
162
225
|
# ────────── helpers ──────────
|
|
163
226
|
|
|
164
227
|
def _summarize_filters(
|
|
@@ -188,6 +188,15 @@ class Config:
|
|
|
188
188
|
def vectors_dir(self) -> Path:
|
|
189
189
|
return self.data_root / "vectors"
|
|
190
190
|
|
|
191
|
+
@property
|
|
192
|
+
def migrations_state_path(self) -> Path:
|
|
193
|
+
"""Where the migration runner tracks which (version, subsystem)
|
|
194
|
+
pairs have been applied. JSON file — see
|
|
195
|
+
``memorytalk.migration.state``. Empty / missing on a fresh
|
|
196
|
+
install or on the first 0.8.x → 0.9 boot; the runner picks the
|
|
197
|
+
mode from that + a heuristic on ``memory.db`` existence."""
|
|
198
|
+
return self.data_root / "migrations_state.json"
|
|
199
|
+
|
|
191
200
|
@property
|
|
192
201
|
def sessions_dir(self) -> Path:
|
|
193
202
|
return self.data_root / "sessions"
|
|
@@ -220,6 +229,14 @@ class Config:
|
|
|
220
229
|
chatty watcher doesn't crowd out request/error logs."""
|
|
221
230
|
return self.sync_log_dir / "watch.log"
|
|
222
231
|
|
|
232
|
+
@property
|
|
233
|
+
def searchbase_log_dir(self) -> Path:
|
|
234
|
+
"""Per-category searchbase logs (maintenance / query / index).
|
|
235
|
+
See ``searchbase/local/_logging.py`` — three files, daily
|
|
236
|
+
rotation, 14-day retention. Distinct from ``search_log_dir``
|
|
237
|
+
(business audit, JSONL) — this one is the backend-side trail."""
|
|
238
|
+
return self.logs_dir / "searchbase"
|
|
239
|
+
|
|
223
240
|
@property
|
|
224
241
|
def pid_path(self) -> Path:
|
|
225
242
|
return self.data_root / "server.pid"
|
|
@@ -235,7 +252,7 @@ class Config:
|
|
|
235
252
|
for d in [
|
|
236
253
|
self.data_root, self.vectors_dir, self.sessions_dir,
|
|
237
254
|
self.cards_dir, self.logs_dir, self.search_log_dir,
|
|
238
|
-
self.sync_log_dir,
|
|
255
|
+
self.sync_log_dir, self.searchbase_log_dir,
|
|
239
256
|
]:
|
|
240
257
|
d.mkdir(parents=True, exist_ok=True)
|
|
241
258
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""memorytalk.migration — schema-evolution runner.
|
|
2
|
+
|
|
3
|
+
The framework that brings persistent state (SQLite + searchbase) from
|
|
4
|
+
whatever shape it has on disk up to what the current code declares.
|
|
5
|
+
The version content lives in ``memorytalk.migrations`` (peer package);
|
|
6
|
+
this module is the runner that discovers, picks a mode, and applies.
|
|
7
|
+
|
|
8
|
+
Design: docs/works/v3/migration.md.
|
|
9
|
+
|
|
10
|
+
Public surface:
|
|
11
|
+
|
|
12
|
+
runner = MigrationRunner(
|
|
13
|
+
db_conn=...,
|
|
14
|
+
admin=backend.admin(),
|
|
15
|
+
state_path=config.migrations_state_path,
|
|
16
|
+
)
|
|
17
|
+
summary = await runner.run()
|
|
18
|
+
"""
|
|
19
|
+
from memorytalk.migration._types import Mode, Summary
|
|
20
|
+
from memorytalk.migration.runner import MigrationRunner
|
|
21
|
+
from memorytalk.migration.state import MigrationState
|
|
22
|
+
|
|
23
|
+
__all__ = ["MigrationRunner", "MigrationState", "Mode", "Summary"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Value types for the migration runner."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
Mode = Literal["init_latest", "upgrade_from_zero", "catch_up"]
|
|
10
|
+
Subsystem = Literal["database", "searchbase"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Summary(BaseModel):
|
|
14
|
+
"""What the runner did on this invocation. Returned by
|
|
15
|
+
:meth:`MigrationRunner.run`; useful for log lines + tests."""
|
|
16
|
+
|
|
17
|
+
mode: Mode
|
|
18
|
+
applied: list[tuple[str, Subsystem]] = Field(default_factory=list)
|
|
19
|
+
skipped: list[tuple[str, Subsystem]] = Field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def applied_count(self) -> int:
|
|
23
|
+
return len(self.applied)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def skipped_count(self) -> int:
|
|
27
|
+
return len(self.skipped)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Discover migration version directories under ``memorytalk.migrations``.
|
|
2
|
+
|
|
3
|
+
The runner doesn't import migrations eagerly — they're imported lazily
|
|
4
|
+
when needed (``import_init`` / ``import_up``) so that a broken
|
|
5
|
+
migration in version N doesn't keep the runner from listing and
|
|
6
|
+
applying versions < N.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib
|
|
11
|
+
import pkgutil
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_VERSION_RE = re.compile(r"^v(\d+)$")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def discover_versions(package: str = "memorytalk.migrations") -> list[str]:
|
|
19
|
+
"""Return all ``vN`` subpackages under ``package``, sorted by N.
|
|
20
|
+
|
|
21
|
+
Returns an empty list if the package doesn't exist (no migrations
|
|
22
|
+
declared yet — fresh-install case before v1 lands)."""
|
|
23
|
+
try:
|
|
24
|
+
pkg = importlib.import_module(package)
|
|
25
|
+
except ImportError:
|
|
26
|
+
return []
|
|
27
|
+
versions: list[tuple[int, str]] = []
|
|
28
|
+
for info in pkgutil.iter_modules(pkg.__path__):
|
|
29
|
+
if not info.ispkg:
|
|
30
|
+
continue
|
|
31
|
+
m = _VERSION_RE.match(info.name)
|
|
32
|
+
if m:
|
|
33
|
+
versions.append((int(m.group(1)), info.name))
|
|
34
|
+
versions.sort()
|
|
35
|
+
return [name for _, name in versions]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def import_migration_module(
|
|
39
|
+
version: str, subsystem: str, method: str,
|
|
40
|
+
*, package: str = "memorytalk.migrations",
|
|
41
|
+
):
|
|
42
|
+
"""Import e.g. ``memorytalk.migrations.v1.up_searchbase``. Returns
|
|
43
|
+
the module so the caller can call ``await module.run(handle)``.
|
|
44
|
+
|
|
45
|
+
``method`` is ``"init"`` or ``"up"``.
|
|
46
|
+
``subsystem`` is ``"database"`` or ``"searchbase"``.
|
|
47
|
+
"""
|
|
48
|
+
return importlib.import_module(
|
|
49
|
+
f"{package}.{version}.{method}_{subsystem}",
|
|
50
|
+
)
|