deja-cli 0.2.0__tar.gz → 0.2.1__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.
- {deja_cli-0.2.0 → deja_cli-0.2.1}/PKG-INFO +1 -1
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/extractor.py +19 -9
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/reflection.py +86 -19
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/store/__init__.py +26 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/store/policy.py +6 -3
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/store/repos/memories.py +17 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/interfaces/cli/backfill.py +23 -3
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/interfaces/cli/maintenance.py +6 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/interfaces/cli/session.py +20 -4
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/interfaces/cli/setup.py +25 -7
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/interfaces/mcp_server.py +20 -3
- {deja_cli-0.2.0 → deja_cli-0.2.1}/pyproject.toml +1 -1
- {deja_cli-0.2.0 → deja_cli-0.2.1}/.github/workflows/ci.yml +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/.gitignore +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/LICENSE +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/README.pypi.md +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/config/default.yaml +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/__init__.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/cloud.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/config.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/__init__.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/store/_helpers.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/store/_schema.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/store/connection.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/store/model.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/store/queries.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/store/repos/__init__.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/store/repos/observations.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/store/repos/reflection.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/store/services/__init__.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/store/services/load.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/store/services/maintenance.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/store/services/ranking.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/store/services/save.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/core/store/services/search.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/ingest/__init__.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/ingest/watchers/__init__.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/ingest/watchers/base.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/ingest/watchers/claude_code.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/ingest/watchers/codex_cli.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/ingest/watchers/gemini_cli.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/interfaces/__init__.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/interfaces/cli/__init__.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/interfaces/cli/_helpers.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/interfaces/cli/cloud.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/interfaces/cli/memory.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/interfaces/cli/transfer.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/interfaces/cli/watch.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/interfaces/web.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/interfaces/web_ui/index.html +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/llm/__init__.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/llm/base.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/llm/embedding.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/llm/factory.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/llm/providers/__init__.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/llm/providers/anthropic.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/llm/providers/ollama.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/deja/main.py +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/hooks/deja-post-fail.sh +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/hooks/deja-precompact.sh +0 -0
- {deja_cli-0.2.0 → deja_cli-0.2.1}/hooks/deja-recall.sh +0 -0
|
@@ -97,15 +97,25 @@ async def extract_memories(
|
|
|
97
97
|
|
|
98
98
|
user_prompt = f"Session transcript/summary to extract memories from:\n\n{transcript}"
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
100
|
+
# Bug R2 (2026-04-22): let adapter / transport / JSON-parse errors
|
|
101
|
+
# propagate. The previous ``except Exception: return []`` made every
|
|
102
|
+
# LLM outage indistinguishable from "the model had nothing to
|
|
103
|
+
# extract." The watcher's ``_process`` then treated the empty list
|
|
104
|
+
# as a successful extraction and stamped ``_processed[path]`` —
|
|
105
|
+
# silently burning the transcript. Same bug class as H5 on the read
|
|
106
|
+
# side. Callers decide:
|
|
107
|
+
#
|
|
108
|
+
# - Watcher (``deja/ingest/watchers/base.py``) wraps this call in
|
|
109
|
+
# its own try/except and routes to ``_schedule_extraction_retry``
|
|
110
|
+
# (bounded N8/P2 backoff: 30s / 2min / 10min, then give up loudly).
|
|
111
|
+
# - CLI callers (``deja save-session``, ``deja ingest-skills``,
|
|
112
|
+
# ``deja backfill``) catch and either exit cleanly or log + skip
|
|
113
|
+
# the one file and continue.
|
|
114
|
+
result = await adapter.complete_structured(
|
|
115
|
+
system=EXTRACTION_SYSTEM,
|
|
116
|
+
user=user_prompt,
|
|
117
|
+
schema=EXTRACTION_SCHEMA,
|
|
118
|
+
)
|
|
109
119
|
|
|
110
120
|
memories = result.get("memories", [])
|
|
111
121
|
if not isinstance(memories, list):
|
|
@@ -336,6 +336,37 @@ class ReflectionEngine:
|
|
|
336
336
|
await self.store.set_reflection_meta(None, last_archive_at=_now_iso())
|
|
337
337
|
return count
|
|
338
338
|
|
|
339
|
+
async def run_dedup_fuzzy(self, project: Optional[str] = None) -> dict:
|
|
340
|
+
"""Bug R4 (2026-04-22): wire fuzzy dedup into scheduled reflection.
|
|
341
|
+
|
|
342
|
+
``MaintenanceService.dedup_fuzzy`` has shipped since the Phase 7
|
|
343
|
+
restructure but was never invoked from ``run_full`` — while
|
|
344
|
+
docstrings (MemoryStore, SaveService, MaintenanceService) and
|
|
345
|
+
user-facing docs (code-reading-guide, plan.md, AGENTS.md,
|
|
346
|
+
GEMINI.md, repo-strategic-assessment) all asserted it ran at
|
|
347
|
+
reflection time. Save-side dedup stayed exact-match only, so
|
|
348
|
+
two-character edits accumulated forever and the vault's
|
|
349
|
+
top-ranked results filled with near-duplicates over time.
|
|
350
|
+
Returns ``{"merged": N, "archived": N}``.
|
|
351
|
+
|
|
352
|
+
When ``project is None``, fan out per-project + globals (mirrors
|
|
353
|
+
``run_reflector(None)``'s N2 pattern). The underlying service's
|
|
354
|
+
``dedup_fuzzy(project=None)`` touches only the global bucket
|
|
355
|
+
because ``fetch_active(None)`` is global-only by convention —
|
|
356
|
+
so a single call would silently leave every project-scoped
|
|
357
|
+
near-duplicate unmerged. Fanning out keeps the scheduled pass
|
|
358
|
+
honest.
|
|
359
|
+
"""
|
|
360
|
+
if project is not None:
|
|
361
|
+
return await self.store.dedup_fuzzy(project=project)
|
|
362
|
+
merged = 0
|
|
363
|
+
archived = 0
|
|
364
|
+
for p in await self.store.list_memory_projects():
|
|
365
|
+
result = await self.store.dedup_fuzzy(project=p)
|
|
366
|
+
merged += result.get("merged", 0)
|
|
367
|
+
archived += result.get("archived", 0)
|
|
368
|
+
return {"merged": merged, "archived": archived}
|
|
369
|
+
|
|
339
370
|
# ── Agent mode ─────────────────────────────────────────────────────────
|
|
340
371
|
|
|
341
372
|
async def agent_mode_prompt(self, project: Optional[str] = None) -> str:
|
|
@@ -354,26 +385,53 @@ class ReflectionEngine:
|
|
|
354
385
|
lines = [
|
|
355
386
|
f"You are acting as a memory reflector for project '{project_label}'.",
|
|
356
387
|
"",
|
|
357
|
-
f"
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
388
|
+
f"PROCESS EVERY ONE of the {len(memories)} active memories below — not just",
|
|
389
|
+
"whatever topic the user happened to ask about in this session. The default",
|
|
390
|
+
"mode of `deja reflect --agent-mode` is a complete sweep: comb every row,",
|
|
391
|
+
"build a punch list of all obvious issues, then execute the punch list in",
|
|
392
|
+
"one pass.",
|
|
393
|
+
"",
|
|
394
|
+
"Scan for, in roughly this priority order:",
|
|
395
|
+
" - Exact-content duplicates (same words across multiple IDs — pick the",
|
|
396
|
+
" one with highest reuse_count, archive the rest)",
|
|
397
|
+
" - Scope-leaks (same content saved at scope:global AND scope:project:X —",
|
|
398
|
+
" keep the correctly-scoped one, archive the other)",
|
|
399
|
+
" - Junk / save errors (single-word patterns, truncated content, malformed",
|
|
400
|
+
" entries)",
|
|
401
|
+
" - Stale stub TODO lists ('next items: 1. X, 2. Y') — typically archive",
|
|
402
|
+
" - Untriggered gotchas tied to a specific command boundary (add --trigger)",
|
|
403
|
+
" - Misclassified entries (pattern that is really a gotcha; gotcha that is",
|
|
404
|
+
" really a preference)",
|
|
405
|
+
" - Semantic duplicates (same rule, different wording — fuzzy threshold",
|
|
406
|
+
" won't catch these, you must)",
|
|
407
|
+
"",
|
|
408
|
+
"Actions:",
|
|
409
|
+
" 1. Archive (stale, no longer relevant):",
|
|
410
|
+
" deja archive <id>",
|
|
411
|
+
" 2. Invalidate (actively contradicted by newer information):",
|
|
412
|
+
" deja invalidate <id>",
|
|
413
|
+
" 3. Consolidate (two or more memories express the same thing):",
|
|
414
|
+
" deja archive <id1>",
|
|
415
|
+
" deja archive <id2>",
|
|
365
416
|
f' deja save "<condensed content>" --type <type>{project_flag}',
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
417
|
+
" (If one existing version is clearly better, just archive the lesser",
|
|
418
|
+
" and keep the better as-is — no new save needed.)",
|
|
419
|
+
" 4. Trigger-tag (gotcha clearly tied to a specific command, no trigger yet):",
|
|
420
|
+
' deja update <id> --trigger "cmd1, cmd2"',
|
|
421
|
+
" Use this for gotchas about what to do right before/after a specific",
|
|
422
|
+
" command. Example triggers: 'kubectl apply', 'alembic upgrade',",
|
|
423
|
+
" 'terraform apply'. Only tag gotchas — not preferences, decisions,",
|
|
424
|
+
" or progress.",
|
|
425
|
+
" 5. Reclassify (saved as the wrong type — e.g. pattern that is really a",
|
|
426
|
+
" gotcha):",
|
|
427
|
+
" deja update <id> --type gotcha",
|
|
373
428
|
"",
|
|
374
|
-
"Be conservative
|
|
375
|
-
"
|
|
376
|
-
"
|
|
429
|
+
"Be conservative on each ACTION (skip a memory when intent is unclear),",
|
|
430
|
+
"but exhaustive on COVERAGE (visit every memory, not just user-flagged ones).",
|
|
431
|
+
"For trigger tagging: if a gotcha is already tagged (shown as [trigger:...]),",
|
|
432
|
+
"skip it.",
|
|
433
|
+
f"If after sweeping all {len(memories)} you find nothing actionable, that's",
|
|
434
|
+
"fine — say so.",
|
|
377
435
|
"",
|
|
378
436
|
"--- MEMORIES ---",
|
|
379
437
|
"",
|
|
@@ -395,7 +453,15 @@ class ReflectionEngine:
|
|
|
395
453
|
# ── Full pass + auto-trigger ────────────────────────────────────────────
|
|
396
454
|
|
|
397
455
|
async def run_full(self, project: Optional[str] = None) -> dict:
|
|
398
|
-
"""Full reflection pass: observer → reflector → decay → promote → archive.
|
|
456
|
+
"""Full reflection pass: observer → reflector → decay → promote → dedup_fuzzy → archive.
|
|
457
|
+
|
|
458
|
+
Bug R4 (2026-04-22): ``dedup_fuzzy`` slots between ``promote``
|
|
459
|
+
and ``archive`` — after promotion (so promoted patterns are in
|
|
460
|
+
the active set for cross-project dedup) and before archival (so
|
|
461
|
+
archive sweeps low-confidence rows AFTER near-duplicates have
|
|
462
|
+
been merged, giving the survivor the full merged confidence/
|
|
463
|
+
reuse bump).
|
|
464
|
+
"""
|
|
399
465
|
results: dict = {}
|
|
400
466
|
if self.adapter:
|
|
401
467
|
results["observer"] = await self.run_observer(project)
|
|
@@ -405,6 +471,7 @@ class ReflectionEngine:
|
|
|
405
471
|
results["reflector"] = 0
|
|
406
472
|
results["decay"] = await self.run_decay()
|
|
407
473
|
results["promote"] = await self.run_promote()
|
|
474
|
+
results["dedup_fuzzy"] = await self.run_dedup_fuzzy(project)
|
|
408
475
|
results["archive"] = await self.run_archive()
|
|
409
476
|
return results
|
|
410
477
|
|
|
@@ -267,6 +267,21 @@ class MemoryStore:
|
|
|
267
267
|
async def archive_below_threshold(self, threshold: float) -> int:
|
|
268
268
|
return await self._maint().archive_below_threshold(threshold)
|
|
269
269
|
|
|
270
|
+
async def dedup_fuzzy(
|
|
271
|
+
self, project: Optional[str] = None, threshold: Optional[float] = None
|
|
272
|
+
) -> dict:
|
|
273
|
+
"""Token-overlap dedup across active memories for the scope.
|
|
274
|
+
|
|
275
|
+
Merges near-duplicates (overlap > ``threshold``) into the older
|
|
276
|
+
row and archives the newer — preserves audit trail via archive,
|
|
277
|
+
never deletes. Returns ``{"merged": N, "archived": N}``.
|
|
278
|
+
Idempotent. See ``MaintenanceService.dedup_fuzzy``.
|
|
279
|
+
"""
|
|
280
|
+
# Keep policy default in the service so the facade stays thin.
|
|
281
|
+
if threshold is None:
|
|
282
|
+
return await self._maint().dedup_fuzzy(project=project)
|
|
283
|
+
return await self._maint().dedup_fuzzy(project=project, threshold=threshold)
|
|
284
|
+
|
|
270
285
|
async def increment_reuse_count(self, project: Optional[str] = None) -> int:
|
|
271
286
|
return await self._maint().increment_reuse_count(project)
|
|
272
287
|
|
|
@@ -544,6 +559,17 @@ class MemoryStore:
|
|
|
544
559
|
db = await self._connection.get()
|
|
545
560
|
return await self._obs().list_distinct_projects(db)
|
|
546
561
|
|
|
562
|
+
async def list_memory_projects(self) -> list[Optional[str]]:
|
|
563
|
+
"""Distinct project values with at least one active memory.
|
|
564
|
+
|
|
565
|
+
Used by ``ReflectionEngine.run_dedup_fuzzy(None)`` (Bug R4,
|
|
566
|
+
2026-04-22) to fan dedup out across every scope instead of
|
|
567
|
+
touching only globals (the historical ``dedup_fuzzy(None)``
|
|
568
|
+
semantics).
|
|
569
|
+
"""
|
|
570
|
+
db = await self._connection.get()
|
|
571
|
+
return await self._mem().list_active_projects(db)
|
|
572
|
+
|
|
547
573
|
async def replace_observations(
|
|
548
574
|
self, project: Optional[str], new_texts: list[str],
|
|
549
575
|
*,
|
|
@@ -12,9 +12,12 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
# ── dedup ──────────────────────────────────────────────────────────────────────
|
|
14
14
|
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
15
|
+
# At reflection time (MaintenanceService.dedup_fuzzy → ReflectionEngine.run_full),
|
|
16
|
+
# a candidate memory with token-overlap above this ratio against an existing
|
|
17
|
+
# memory of the same type+scope+project is treated as a near-duplicate: the
|
|
18
|
+
# survivor's confidence and reuse_count are bumped, the loser is archived.
|
|
19
|
+
# Save-time dedup is exact-match only (idx_memories_dedup composite index)
|
|
20
|
+
# to keep the hot path <1ms; fuzzy work batches into reflection (R4, 2026-04-22).
|
|
18
21
|
DEDUP_OVERLAP_THRESHOLD: float = 0.8
|
|
19
22
|
|
|
20
23
|
# How much to add to an existing memory's confidence when a dedup match
|
|
@@ -117,6 +117,23 @@ class MemoryRepository:
|
|
|
117
117
|
rows = await cur.fetchall()
|
|
118
118
|
return [Memory.from_row(r) for r in rows]
|
|
119
119
|
|
|
120
|
+
async def list_active_projects(
|
|
121
|
+
self, db: aiosqlite.Connection
|
|
122
|
+
) -> list[Optional[str]]:
|
|
123
|
+
"""Distinct ``project`` values with at least one active memory.
|
|
124
|
+
|
|
125
|
+
Includes ``None`` (global bucket) iff any active global rows
|
|
126
|
+
exist. Parallels ``ObservationRepository.list_distinct_projects``
|
|
127
|
+
and is used by ``ReflectionEngine.run_dedup_fuzzy(None)`` (Bug
|
|
128
|
+
R4, 2026-04-22) to fan out dedup across every scope instead of
|
|
129
|
+
operating only on globals.
|
|
130
|
+
"""
|
|
131
|
+
async with db.execute(
|
|
132
|
+
f"SELECT DISTINCT project FROM memories WHERE {ACTIVE_FILTER}"
|
|
133
|
+
) as cur:
|
|
134
|
+
rows = await cur.fetchall()
|
|
135
|
+
return [r["project"] for r in rows]
|
|
136
|
+
|
|
120
137
|
async def fetch_for_sync(self, db: aiosqlite.Connection) -> list[Memory]:
|
|
121
138
|
"""Active + archived memories across every scope, for cloud sync push.
|
|
122
139
|
|
|
@@ -275,7 +275,15 @@ def register(app: typer.Typer) -> None:
|
|
|
275
275
|
typer.echo(f" [dry-run] summary.md ({len(content)} chars)")
|
|
276
276
|
continue
|
|
277
277
|
|
|
278
|
-
|
|
278
|
+
try:
|
|
279
|
+
memories = await extract_memories(content, proj_name, "backfill", adapter)
|
|
280
|
+
except Exception as e:
|
|
281
|
+
# Bug R2 (2026-04-22): extract_memories now raises on
|
|
282
|
+
# adapter / transport failures. Log + skip this one
|
|
283
|
+
# source so a transient LLM blip doesn't abort the
|
|
284
|
+
# whole backfill run.
|
|
285
|
+
typer.echo(f" summary.md (extraction failed: {e}, skipped)", err=True)
|
|
286
|
+
continue
|
|
279
287
|
total_memories += await _embed_and_save(memories, store, embedding_adapter)
|
|
280
288
|
typer.echo(f" summary.md → {len(memories)} memories")
|
|
281
289
|
|
|
@@ -290,7 +298,13 @@ def register(app: typer.Typer) -> None:
|
|
|
290
298
|
typer.echo(f" [dry-run] sessions-index.json ({entry_count} sessions)")
|
|
291
299
|
else:
|
|
292
300
|
entry_count = len(json.loads(index_file.read_text()).get("entries", []))
|
|
293
|
-
|
|
301
|
+
try:
|
|
302
|
+
memories = await extract_memories(transcript, proj_name, "backfill", adapter)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
# Bug R2 (2026-04-22): skip this source on
|
|
305
|
+
# extraction failure instead of aborting.
|
|
306
|
+
typer.echo(f" sessions-index.json (extraction failed: {e}, skipped)", err=True)
|
|
307
|
+
continue
|
|
294
308
|
total_memories += await _embed_and_save(memories, store, embedding_adapter)
|
|
295
309
|
typer.echo(f" sessions-index.json ({entry_count} sessions) → {len(memories)} memories")
|
|
296
310
|
|
|
@@ -309,7 +323,13 @@ def register(app: typer.Typer) -> None:
|
|
|
309
323
|
typer.echo(f" [dry-run] {jsonl_file.name} ({len(transcript)} chars)")
|
|
310
324
|
continue
|
|
311
325
|
|
|
312
|
-
|
|
326
|
+
try:
|
|
327
|
+
memories = await extract_memories(transcript, proj_name, "backfill", adapter)
|
|
328
|
+
except Exception as e:
|
|
329
|
+
# Bug R2 (2026-04-22): skip this jsonl on
|
|
330
|
+
# extraction failure; keep processing others.
|
|
331
|
+
typer.echo(f" {jsonl_file.name} (extraction failed: {e}, skipped)", err=True)
|
|
332
|
+
continue
|
|
313
333
|
total_memories += await _embed_and_save(memories, store, embedding_adapter)
|
|
314
334
|
typer.echo(f" {jsonl_file.name} → {len(memories)} memories")
|
|
315
335
|
|
|
@@ -61,6 +61,12 @@ def register(app: typer.Typer) -> None:
|
|
|
61
61
|
lines.append(f" Decay: {results['decay']} memories decayed")
|
|
62
62
|
if results.get("promote"):
|
|
63
63
|
lines.append(f" Promote: {results['promote']} patterns promoted to global")
|
|
64
|
+
dedup = results.get("dedup_fuzzy") or {}
|
|
65
|
+
if dedup.get("merged") or dedup.get("archived"):
|
|
66
|
+
lines.append(
|
|
67
|
+
f" Dedup: {dedup.get('merged', 0)} merged, "
|
|
68
|
+
f"{dedup.get('archived', 0)} near-duplicates archived"
|
|
69
|
+
)
|
|
64
70
|
if results.get("archive"):
|
|
65
71
|
lines.append(f" Archive: {results['archive']} memories archived")
|
|
66
72
|
if len(lines) == 1:
|
|
@@ -122,9 +122,17 @@ def register(app: typer.Typer) -> None:
|
|
|
122
122
|
await store.init_db()
|
|
123
123
|
try:
|
|
124
124
|
from deja.core.extractor import extract_memories
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
# Bug R2 (2026-04-22): extract_memories now raises on
|
|
126
|
+
# transport / adapter / JSON-parse failures instead of
|
|
127
|
+
# returning []. Catch and exit cleanly so the user sees
|
|
128
|
+
# the real cause instead of a bare traceback.
|
|
129
|
+
try:
|
|
130
|
+
memories = await extract_memories(
|
|
131
|
+
content, project or "unknown", "save-session", adapter
|
|
132
|
+
)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
typer.echo(f"Extraction failed: {e}", err=True)
|
|
135
|
+
raise typer.Exit(1)
|
|
128
136
|
embedding_adapter = await create_embedding_adapter(config)
|
|
129
137
|
saved = await _embed_and_save(memories, store, embedding_adapter)
|
|
130
138
|
return saved
|
|
@@ -186,7 +194,15 @@ def register(app: typer.Typer) -> None:
|
|
|
186
194
|
from deja.core.extractor import extract_memories
|
|
187
195
|
return await extract_memories(content, project or "unknown", "ingest-skills", adapter)
|
|
188
196
|
|
|
189
|
-
|
|
197
|
+
# Bug R2 (2026-04-22): extract_memories propagates adapter
|
|
198
|
+
# errors; catch and exit cleanly with the error message.
|
|
199
|
+
try:
|
|
200
|
+
memories = asyncio.run(_extract())
|
|
201
|
+
except typer.Exit:
|
|
202
|
+
raise
|
|
203
|
+
except Exception as e:
|
|
204
|
+
typer.echo(f"Extraction failed: {e}", err=True)
|
|
205
|
+
raise typer.Exit(1)
|
|
190
206
|
if not memories:
|
|
191
207
|
typer.echo("Extraction returned no memories. Try --no-llm or check the file format.", err=True)
|
|
192
208
|
raise typer.Exit(1)
|
|
@@ -359,6 +359,31 @@ def _detect_deja_setup(target_path: Path, agent: str) -> list[str]:
|
|
|
359
359
|
|
|
360
360
|
def _install_claude_hooks() -> None:
|
|
361
361
|
"""Write hook scripts and register them in ~/.claude/settings.json."""
|
|
362
|
+
# Bug R1 (2026-04-22): validate settings.json BEFORE any disk writes.
|
|
363
|
+
# Previously this function parsed settings.json with
|
|
364
|
+
# ``except json.JSONDecodeError: settings = {}`` then overwrote the
|
|
365
|
+
# file. A user with any syntax error (stray comma, unmatched
|
|
366
|
+
# bracket, half-finished hand edit) lost every other hook,
|
|
367
|
+
# permission rule, MCP server registration, env var, and status-line
|
|
368
|
+
# entry they had configured — replaced with just Deja's four hook
|
|
369
|
+
# entries, with zero log or prompt. Refuse loudly instead; the user
|
|
370
|
+
# fixes the syntax error and re-runs. The on-disk bytes stay
|
|
371
|
+
# untouched on the failure path.
|
|
372
|
+
settings_path = Path("~/.claude/settings.json").expanduser()
|
|
373
|
+
settings_text = settings_path.read_text(encoding="utf-8") if settings_path.exists() else ""
|
|
374
|
+
if settings_text.strip():
|
|
375
|
+
try:
|
|
376
|
+
settings = json.loads(settings_text)
|
|
377
|
+
except json.JSONDecodeError as e:
|
|
378
|
+
typer.echo(
|
|
379
|
+
f"deja: refusing to proceed — ~/.claude/settings.json is not valid "
|
|
380
|
+
f"JSON ({e}). Fix the syntax error and re-run `deja setup claude-code`.",
|
|
381
|
+
err=True,
|
|
382
|
+
)
|
|
383
|
+
raise typer.Exit(1)
|
|
384
|
+
else:
|
|
385
|
+
settings = {}
|
|
386
|
+
|
|
362
387
|
hooks_dir = Path("~/.claude/hooks").expanduser()
|
|
363
388
|
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
364
389
|
|
|
@@ -378,13 +403,6 @@ def _install_claude_hooks() -> None:
|
|
|
378
403
|
display = str(path).replace(str(Path.home()), "~")
|
|
379
404
|
typer.echo(f"deja: hooks installed → {display}")
|
|
380
405
|
|
|
381
|
-
settings_path = Path("~/.claude/settings.json").expanduser()
|
|
382
|
-
settings_text = settings_path.read_text(encoding="utf-8") if settings_path.exists() else ""
|
|
383
|
-
try:
|
|
384
|
-
settings = json.loads(settings_text) if settings_text.strip() else {}
|
|
385
|
-
except json.JSONDecodeError:
|
|
386
|
-
settings = {}
|
|
387
|
-
|
|
388
406
|
hooks_obj = settings.setdefault("hooks", {})
|
|
389
407
|
changed = False
|
|
390
408
|
|
|
@@ -109,15 +109,32 @@ async def memory_save(
|
|
|
109
109
|
"limit": e.limit,
|
|
110
110
|
})
|
|
111
111
|
|
|
112
|
+
# Bug R3 (2026-04-22): surface the cloud-push outcome to the agent.
|
|
113
|
+
# Previously the ``(ok, msg)`` tuple returned by ``push_memory`` was
|
|
114
|
+
# discarded and the response was always ``{"status": "saved"}`` —
|
|
115
|
+
# so an agent with an expired PAT saw every save "succeed" while
|
|
116
|
+
# memories silently piled up local-only until the user ran
|
|
117
|
+
# ``deja sync`` by hand. The agent now sees ``cloud`` in
|
|
118
|
+
# {"ok", "skipped", "failed"} and can react (surface to user,
|
|
119
|
+
# prompt re-login) instead of trusting a misleading success.
|
|
120
|
+
cloud_status = "skipped"
|
|
121
|
+
cloud_reason: Optional[str] = None
|
|
112
122
|
if config.cloud.sync_on_save:
|
|
113
123
|
from deja.cloud import get_token, push_memory
|
|
114
124
|
if get_token(config):
|
|
115
125
|
# Bug N5 (2026-04-19): trigger → triggerCmds translation now lives
|
|
116
126
|
# in ``_sanitize_for_push``; eager push delegates to the same path
|
|
117
127
|
# as batch ``sync_push`` so the three codepaths can't drift again.
|
|
118
|
-
push_memory(result.cloud_data, config)
|
|
119
|
-
|
|
120
|
-
|
|
128
|
+
ok, msg = push_memory(result.cloud_data, config)
|
|
129
|
+
cloud_status = "ok" if ok else "failed"
|
|
130
|
+
cloud_reason = None if ok else msg
|
|
131
|
+
|
|
132
|
+
return json.dumps({
|
|
133
|
+
"id": result.id,
|
|
134
|
+
"status": "saved",
|
|
135
|
+
"cloud": cloud_status,
|
|
136
|
+
"cloud_reason": cloud_reason,
|
|
137
|
+
})
|
|
121
138
|
|
|
122
139
|
|
|
123
140
|
@mcp.tool()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|