memorytalk 0.8.2__tar.gz → 0.8.3__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.
Files changed (89) hide show
  1. {memorytalk-0.8.2 → memorytalk-0.8.3}/PKG-INFO +1 -1
  2. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/cli/_format.py +19 -0
  3. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/cli/server.py +92 -1
  4. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/cli/setup.py +51 -20
  5. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk.egg-info/PKG-INFO +1 -1
  6. {memorytalk-0.8.2 → memorytalk-0.8.3}/pyproject.toml +1 -1
  7. {memorytalk-0.8.2 → memorytalk-0.8.3}/LICENSE +0 -0
  8. {memorytalk-0.8.2 → memorytalk-0.8.3}/README.md +0 -0
  9. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/__init__.py +0 -0
  10. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/__main__.py +0 -0
  11. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/adapters/__init__.py +0 -0
  12. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/adapters/base.py +0 -0
  13. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/adapters/claude_code.py +0 -0
  14. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/adapters/codex.py +0 -0
  15. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/adapters/openclaw.py +0 -0
  16. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/api/__init__.py +0 -0
  17. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/api/cards.py +0 -0
  18. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/api/read.py +0 -0
  19. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/api/recall.py +0 -0
  20. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/api/reviews.py +0 -0
  21. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/api/search.py +0 -0
  22. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/api/sessions.py +0 -0
  23. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/api/status.py +0 -0
  24. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/api/sync.py +0 -0
  25. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/cli/__init__.py +0 -0
  26. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/cli/_http.py +0 -0
  27. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/cli/_render.py +0 -0
  28. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/cli/card.py +0 -0
  29. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/cli/read.py +0 -0
  30. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/cli/recall.py +0 -0
  31. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/cli/review.py +0 -0
  32. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/cli/search.py +0 -0
  33. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/cli/session.py +0 -0
  34. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/cli/sync.py +0 -0
  35. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/cli/upgrade.py +0 -0
  36. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/config.py +0 -0
  37. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/provider/__init__.py +0 -0
  38. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/provider/embedding.py +0 -0
  39. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/provider/lancedb.py +0 -0
  40. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/provider/storage.py +0 -0
  41. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/repository/__init__.py +0 -0
  42. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/repository/cards.py +0 -0
  43. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/repository/recall.py +0 -0
  44. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/repository/reviews.py +0 -0
  45. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/repository/schema.py +0 -0
  46. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/repository/search_log.py +0 -0
  47. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/repository/sessions.py +0 -0
  48. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/repository/store.py +0 -0
  49. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/repository/sync_checkpoint.py +0 -0
  50. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/schemas/__init__.py +0 -0
  51. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/schemas/card.py +0 -0
  52. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/schemas/cards.py +0 -0
  53. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/schemas/read.py +0 -0
  54. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/schemas/recall.py +0 -0
  55. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/schemas/review.py +0 -0
  56. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/schemas/reviews.py +0 -0
  57. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/schemas/search.py +0 -0
  58. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/schemas/session.py +0 -0
  59. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/schemas/status.py +0 -0
  60. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/schemas/sync.py +0 -0
  61. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/server.py +0 -0
  62. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/service/__init__.py +0 -0
  63. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/service/backfill.py +0 -0
  64. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/service/cards.py +0 -0
  65. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/service/events.py +0 -0
  66. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/service/index_buffer.py +0 -0
  67. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/service/read.py +0 -0
  68. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/service/recall.py +0 -0
  69. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/service/reviews.py +0 -0
  70. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/service/search.py +0 -0
  71. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/service/sessions.py +0 -0
  72. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/service/sync.py +0 -0
  73. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/util/__init__.py +0 -0
  74. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/util/console.py +0 -0
  75. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/util/dsl.py +0 -0
  76. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/util/env_template.py +0 -0
  77. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/util/formula.py +0 -0
  78. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/util/highlight.py +0 -0
  79. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/util/ids.py +0 -0
  80. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/util/indexes.py +0 -0
  81. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/util/settings_io.py +0 -0
  82. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/util/tag_filter.py +0 -0
  83. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk/util/tags.py +0 -0
  84. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk.egg-info/SOURCES.txt +0 -0
  85. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk.egg-info/dependency_links.txt +0 -0
  86. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk.egg-info/entry_points.txt +0 -0
  87. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk.egg-info/requires.txt +0 -0
  88. {memorytalk-0.8.2 → memorytalk-0.8.3}/memorytalk.egg-info/top_level.txt +0 -0
  89. {memorytalk-0.8.2 → memorytalk-0.8.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memorytalk
3
- Version: 0.8.2
3
+ Version: 0.8.3
4
4
  Summary: Persistent cross-session memory for AI agents — Talk-Card architecture with forum-dynamics sinking/floating (v3)
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -48,6 +48,25 @@ def fmt_server_stop(payload: dict) -> str:
48
48
  return f"**{status}**\n"
49
49
 
50
50
 
51
+ def fmt_server_restart(payload: dict) -> str:
52
+ status = payload.get("status", "")
53
+ if status == "restarted":
54
+ return (
55
+ f"**restarted** · prev pid `{payload.get('previous_pid')}` "
56
+ f"→ pid `{payload['pid']}` · port `{payload['port']}`\n"
57
+ )
58
+ if status == "started":
59
+ # No previous daemon — restart degenerated into a fresh start.
60
+ return (
61
+ f"**started** · pid `{payload['pid']}` · port `{payload['port']}` "
62
+ "(was not running)\n"
63
+ )
64
+ if status == "failed":
65
+ # Reuse start's failure shape — same fields are populated.
66
+ return fmt_server_start(payload)
67
+ return f"**{status}**\n"
68
+
69
+
51
70
  def fmt_status(payload: dict) -> str:
52
71
  if payload.get("status") == "not_running":
53
72
  return (
@@ -14,7 +14,9 @@ from pathlib import Path
14
14
 
15
15
  import click
16
16
 
17
- from memorytalk.cli._format import fmt_server_start, fmt_server_stop, fmt_status
17
+ from memorytalk.cli._format import (
18
+ fmt_server_restart, fmt_server_start, fmt_server_stop, fmt_status,
19
+ )
18
20
  from memorytalk.cli._http import ApiError, api
19
21
  from memorytalk.cli._render import emit_json, emit_md
20
22
  from memorytalk.config import Config
@@ -115,6 +117,84 @@ def stop_server_proc(cfg: Config) -> dict:
115
117
  return {"status": "stopped", "pid": pid}
116
118
 
117
119
 
120
+ # Graceful-exit budget after SIGTERM. The lifespan __aexit__ has to
121
+ # stop the sync watcher, drain the IndexWriteBuffer, close the LanceDB
122
+ # connection and shut SQLite down — on a busy install with a half-full
123
+ # buffer that's ~1–3 s typical, with headroom for outliers. After this
124
+ # window we escalate to SIGKILL so ``restart`` never hangs the CLI.
125
+ _RESTART_GRACE_SECONDS = 10.0
126
+ _RESTART_POLL_INTERVAL = 0.1
127
+
128
+
129
+ def restart_server_proc(cfg: Config) -> dict:
130
+ """Stop the running daemon (if any) + wait for graceful exit +
131
+ start fresh. Returns one of:
132
+
133
+ {"status": "restarted",
134
+ "previous_pid": int, "pid": int, "port": int}
135
+ — there was a daemon, it died cleanly, new one is up.
136
+
137
+ {"status": "started", "pid": int, "port": int}
138
+ — no daemon was running; same shape as a plain ``start``.
139
+
140
+ {"status": "failed",
141
+ "exit_code": int, "error": str, "previous_pid": int | None}
142
+ — the *new* daemon failed startup; the previous one is
143
+ already gone (we don't roll back — leaves logs intact for
144
+ debugging).
145
+
146
+ Waits up to ``_RESTART_GRACE_SECONDS`` for the old process to exit,
147
+ then SIGKILLs if it's still around. Without this wait, the new
148
+ daemon races the old one for the TCP port and gets EADDRINUSE.
149
+ """
150
+ previous_pid: int | None = None
151
+ if cfg.pid_path.exists():
152
+ try:
153
+ previous_pid = int(cfg.pid_path.read_text().strip())
154
+ except ValueError:
155
+ previous_pid = None
156
+
157
+ if previous_pid is not None and pid_alive(previous_pid):
158
+ try:
159
+ os.kill(previous_pid, signal.SIGTERM)
160
+ except ProcessLookupError:
161
+ pass
162
+ deadline = time.monotonic() + _RESTART_GRACE_SECONDS
163
+ while time.monotonic() < deadline:
164
+ if not pid_alive(previous_pid):
165
+ break
166
+ time.sleep(_RESTART_POLL_INTERVAL)
167
+ else:
168
+ # 10 s and still alive — escalate. SIGKILL is uncatchable;
169
+ # the kernel reaps the process so ``pid_alive`` flips fast.
170
+ try:
171
+ os.kill(previous_pid, signal.SIGKILL)
172
+ except ProcessLookupError:
173
+ pass
174
+ # Tiny window for the kernel to actually reap.
175
+ time.sleep(0.3)
176
+
177
+ # Stale pid/port files would let start_server_proc misread "already
178
+ # running" — clear them regardless of how we got here.
179
+ cfg.pid_path.unlink(missing_ok=True)
180
+ cfg.port_path.unlink(missing_ok=True)
181
+
182
+ start_payload = start_server_proc(cfg)
183
+ status = start_payload.get("status")
184
+ if status == "failed":
185
+ return {**start_payload, "previous_pid": previous_pid}
186
+ if previous_pid is not None:
187
+ # Distinguish from a fresh start so the user sees "the old
188
+ # daemon was actually replaced", not just "a daemon started".
189
+ return {
190
+ "status": "restarted",
191
+ "previous_pid": previous_pid,
192
+ "pid": start_payload["pid"],
193
+ "port": start_payload["port"],
194
+ }
195
+ return start_payload # status="started" — no previous daemon
196
+
197
+
118
198
  def _emit(payload: dict, json_out: bool, md_formatter) -> None:
119
199
  if json_out:
120
200
  emit_json(payload)
@@ -140,6 +220,17 @@ def server_stop(json_out: bool) -> None:
140
220
  _emit(payload, json_out, fmt_server_stop)
141
221
 
142
222
 
223
+ @server.command("restart")
224
+ @click.option("--json", "json_out", is_flag=True, default=False, help="Emit JSON")
225
+ def server_restart(json_out: bool) -> None:
226
+ """Stop the running daemon (if any) and start it again."""
227
+ cfg = Config()
228
+ payload = restart_server_proc(cfg)
229
+ _emit(payload, json_out, fmt_server_restart)
230
+ if payload.get("status") == "failed":
231
+ sys.exit(1)
232
+
233
+
143
234
  @server.command("status")
144
235
  @click.option("--json", "json_out", is_flag=True, default=False, help="Emit JSON")
145
236
  def server_status(json_out: bool) -> None:
@@ -85,8 +85,11 @@ def _wizard(cfg: Config, old_raw: dict | None, is_first_install: bool) -> dict:
85
85
  err_console.print(f"[bold]memory.talk setup[/bold] · {mode}")
86
86
  err_console.print(f"data_root: [cyan]{cfg.data_root}[/cyan]\n")
87
87
 
88
- base = dict(old_raw) if old_raw else Settings().model_dump()
89
- new = dict(base)
88
+ # ``base`` is the raw existing settings.json — NOT a Settings model
89
+ # dump. Mixing model defaults in would re-introduce the old bug
90
+ # where wizard-untouched fields (search.ranking_formula etc.) got
91
+ # materialized to disk and stopped tracking schema-default changes.
92
+ base = dict(old_raw) if old_raw else {}
90
93
 
91
94
  # ── embedding ───────────────────────────────────────────────────────
92
95
  section("Embedding")
@@ -108,7 +111,6 @@ def _wizard(cfg: Config, old_raw: dict | None, is_first_install: bool) -> dict:
108
111
  else:
109
112
  emb_base = dict(base.get("embedding") or {})
110
113
  emb = _step_embedding(emb_base)
111
- new["embedding"] = emb
112
114
 
113
115
  # Probe the embedding before writing — fail-fast.
114
116
  err_console.print("[dim]probing embedding provider...[/dim]")
@@ -117,14 +119,14 @@ def _wizard(cfg: Config, old_raw: dict | None, is_first_install: bool) -> dict:
117
119
 
118
120
  # ── storage ─────────────────────────────────────────────────────────
119
121
  section("Storage")
120
- new["vector"] = {"provider": _choice(
122
+ vector_provider = _choice(
121
123
  "vector provider", ["lancedb"],
122
124
  (base.get("vector") or {}).get("provider", "lancedb"),
123
- )}
124
- new["relation"] = {"provider": _choice(
125
+ )
126
+ relation_provider = _choice(
125
127
  "relation provider", ["sqlite"],
126
128
  (base.get("relation") or {}).get("provider", "sqlite"),
127
- )}
129
+ )
128
130
 
129
131
  # ── server ──────────────────────────────────────────────────────────
130
132
  section("Server")
@@ -135,7 +137,6 @@ def _wizard(cfg: Config, old_raw: dict | None, is_first_install: bool) -> dict:
135
137
  validate=lambda v: (v.strip().isdigit() and 1 <= int(v) <= 65535)
136
138
  or "must be an integer in 1..65535",
137
139
  )
138
- new["server"] = {"port": int(port_str)}
139
140
 
140
141
  # ── sync ────────────────────────────────────────────────────────────
141
142
  section("Sync")
@@ -149,19 +150,23 @@ def _wizard(cfg: Config, old_raw: dict | None, is_first_install: bool) -> dict:
149
150
  "Enable backend sync? (auto-ingest Claude Code / Codex sessions etc.)",
150
151
  default=enabled_default,
151
152
  )
152
- new["sync"] = {
153
- "enabled": sync_enabled,
154
- "debounce_ms": old_sync.get("debounce_ms", 200),
155
- }
156
153
 
157
- # Carry over sections we don't prompt for (search / recall / explore).
158
- # They keep whatever the existing file had; defaults filled in by the
159
- # Settings model on first install.
160
- for key in ("search", "recall", "explore"):
161
- if key in base:
162
- new[key] = base[key]
163
- else:
164
- new[key] = getattr(Settings(), key).model_dump()
154
+ # ── PATCH onto base ─────────────────────────────────────────────────
155
+ # Setup writes ONLY the fields it actually prompted for. Anything
156
+ # else the user had in settings.json — sync.debounce_ms, search.*,
157
+ # recall.*, explore.*, embedding.batch_size, future fields — is
158
+ # left untouched. This keeps "defaults that haven't been explicitly
159
+ # overridden" tracking the Settings schema, so future default
160
+ # changes (like the 0.8.2 ranking_formula -> "relevance") flow
161
+ # through on next load without manual intervention.
162
+ owned = {
163
+ "server": {"port": int(port_str)},
164
+ "vector": {"provider": vector_provider},
165
+ "relation": {"provider": relation_provider},
166
+ "embedding": emb,
167
+ "sync": {"enabled": sync_enabled},
168
+ }
169
+ new = _patch_owned(base, owned)
165
170
 
166
171
  # ── diff + persist ──────────────────────────────────────────────────
167
172
  diff = diff_settings(base, new)
@@ -228,6 +233,32 @@ def _step_embedding(base: dict) -> dict:
228
233
  return out
229
234
 
230
235
 
236
+ def _patch_owned(base: dict, owned: dict[str, dict]) -> dict:
237
+ """Apply ``owned`` updates onto ``base`` at field-level granularity.
238
+
239
+ For each section in ``owned``:
240
+ - Merge ``owned[section]`` keys into ``base[section]`` (creating
241
+ the section if missing).
242
+ - Keys that are in ``base[section]`` but NOT in ``owned[section]``
243
+ are preserved as-is.
244
+
245
+ Sections that aren't in ``owned`` at all (e.g. ``search``, ``recall``,
246
+ ``explore``, ``index``) are returned untouched.
247
+
248
+ Provider switch (openai → local) note: stale provider-specific
249
+ keys like ``endpoint`` / ``auth_key`` survive in the file. They're
250
+ inert when ``provider != "openai"`` (Settings just ignores them on
251
+ load). Strict patch — wizard owns what wizard prompted for; it
252
+ doesn't synthesize cleanup of unrelated keys.
253
+ """
254
+ new = dict(base)
255
+ for section, fields in owned.items():
256
+ section_data = dict(new.get(section, {}))
257
+ section_data.update(fields)
258
+ new[section] = section_data
259
+ return new
260
+
261
+
231
262
  def _show_detected_endpoints() -> None:
232
263
  """List adapters whose ``DEFAULT_LOCATION`` exists on the user's
233
264
  machine — those will be auto-attached on server start. Adapters
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memorytalk
3
- Version: 0.8.2
3
+ Version: 0.8.3
4
4
  Summary: Persistent cross-session memory for AI agents — Talk-Card architecture with forum-dynamics sinking/floating (v3)
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "memorytalk"
3
- version = "0.8.2"
3
+ version = "0.8.3"
4
4
  description = "Persistent cross-session memory for AI agents — Talk-Card architecture with forum-dynamics sinking/floating (v3)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
File without changes
File without changes
File without changes