loki-mode 7.66.1 → 7.68.0
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.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +128 -7
- package/autonomy/loki +202 -87
- package/autonomy/run.sh +161 -87
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +25 -4
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +112 -110
- package/mcp/__init__.py +1 -1
- package/memory/consolidation.py +86 -12
- package/memory/retrieval.py +18 -1
- package/memory/storage.py +161 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
package/mcp/__init__.py
CHANGED
package/memory/consolidation.py
CHANGED
|
@@ -223,7 +223,17 @@ class ConsolidationPipeline:
|
|
|
223
223
|
merged = False
|
|
224
224
|
for idx, existing in enumerate(existing_patterns):
|
|
225
225
|
if self._patterns_similar(new_pattern, existing):
|
|
226
|
-
|
|
226
|
+
# Re-read the target pattern fresh immediately before
|
|
227
|
+
# merging (BUG-MEM C1, lost-update). The whole-run
|
|
228
|
+
# snapshot at step 4 can be stale by now: a concurrent
|
|
229
|
+
# engine.increment_pattern_usage() (load_pattern then
|
|
230
|
+
# save_pattern) may have bumped usage_count/last_used
|
|
231
|
+
# AFTER the snapshot. merge_with_existing() builds the
|
|
232
|
+
# merged record from best_match.usage_count/last_used,
|
|
233
|
+
# so merging from the stale snapshot clobbers that bump.
|
|
234
|
+
# Re-reading narrows the window to this single write.
|
|
235
|
+
merge_base = self._reload_pattern(existing)
|
|
236
|
+
merged_pattern = self.merge_with_existing(new_pattern, [merge_base])
|
|
227
237
|
self.storage.update_pattern(merged_pattern)
|
|
228
238
|
# Refresh the in-memory copy so a later new pattern in
|
|
229
239
|
# this same run that also merges into this existing
|
|
@@ -262,7 +272,11 @@ class ConsolidationPipeline:
|
|
|
262
272
|
for idx, existing in enumerate(existing_patterns):
|
|
263
273
|
if (existing.incorrect_approach and
|
|
264
274
|
self._patterns_similar(anti_pattern, existing, threshold=0.6)):
|
|
265
|
-
|
|
275
|
+
# Re-read fresh before merge (same C1 lost-update guard as the
|
|
276
|
+
# cluster merge loop above): merge from current on-disk state,
|
|
277
|
+
# not the potentially-stale step-4 snapshot.
|
|
278
|
+
merge_base = self._reload_pattern(existing)
|
|
279
|
+
merged_pattern = self.merge_with_existing(anti_pattern, [merge_base])
|
|
266
280
|
self.storage.update_pattern(merged_pattern)
|
|
267
281
|
# Refresh in-memory copy (same data-loss guard as the cluster
|
|
268
282
|
# merge loop above): a later anti-pattern merging into this same
|
|
@@ -305,6 +319,35 @@ class ConsolidationPipeline:
|
|
|
305
319
|
result.duration_seconds = time.time() - start_time
|
|
306
320
|
return result
|
|
307
321
|
|
|
322
|
+
def _reload_pattern(self, fallback: SemanticPattern) -> SemanticPattern:
|
|
323
|
+
"""Re-read a pattern fresh from storage immediately before merging.
|
|
324
|
+
|
|
325
|
+
Used by the merge branches to avoid the C1 lost-update: the step-4
|
|
326
|
+
snapshot may be stale (a concurrent usage bump can land after it), and
|
|
327
|
+
merge_with_existing() copies usage_count/last_used from the base. Reading
|
|
328
|
+
the current on-disk record makes the merge build on live state.
|
|
329
|
+
|
|
330
|
+
Mirrors the snapshot's dict/object handling. If load_pattern returns
|
|
331
|
+
nothing (e.g. the record vanished), fall back to the in-memory copy so the
|
|
332
|
+
merge still proceeds rather than crashing.
|
|
333
|
+
|
|
334
|
+
Residual limitation (honest): load_pattern and update_pattern are SEPARATE
|
|
335
|
+
lock acquisitions, so a bump landing between this re-read and the write is
|
|
336
|
+
still lost. This narrows the race window from the whole run to a single
|
|
337
|
+
write; it is a mitigation, not cross-process atomicity. A full fix needs a
|
|
338
|
+
compare-and-set or merge-callback update in storage, which is out of scope
|
|
339
|
+
for this file (storage.py is frozen this batch).
|
|
340
|
+
"""
|
|
341
|
+
try:
|
|
342
|
+
fresh = self.storage.load_pattern(fallback.id)
|
|
343
|
+
except Exception:
|
|
344
|
+
return fallback
|
|
345
|
+
if not fresh:
|
|
346
|
+
return fallback
|
|
347
|
+
if isinstance(fresh, dict):
|
|
348
|
+
return SemanticPattern.from_dict(fresh)
|
|
349
|
+
return fresh
|
|
350
|
+
|
|
308
351
|
# -------------------------------------------------------------------------
|
|
309
352
|
# Clustering Methods
|
|
310
353
|
# -------------------------------------------------------------------------
|
|
@@ -475,7 +518,10 @@ class ConsolidationPipeline:
|
|
|
475
518
|
|
|
476
519
|
def _episode_to_text(self, episode: EpisodeTrace) -> str:
|
|
477
520
|
"""Convert episode to text for embedding."""
|
|
478
|
-
|
|
521
|
+
# Guard explicit-null goal (C2): from_dict's .get(key, default) returns
|
|
522
|
+
# None on a JSON null, and the " ".join(parts) below crashes on a None
|
|
523
|
+
# member. Mirror the wave-6 (episode.goal or "") idiom.
|
|
524
|
+
parts = [episode.goal or ""]
|
|
479
525
|
|
|
480
526
|
# Add action summaries (handle both ActionEntry objects and dicts)
|
|
481
527
|
for action in episode.action_log[:5]: # Limit to first 5 actions
|
|
@@ -505,7 +551,8 @@ class ConsolidationPipeline:
|
|
|
505
551
|
# Find common words in goals
|
|
506
552
|
all_words: Dict[str, int] = defaultdict(int)
|
|
507
553
|
for episode in episodes:
|
|
508
|
-
|
|
554
|
+
# Guard explicit-null goal (C2): None has no .lower().
|
|
555
|
+
for word in (episode.goal or "").lower().split():
|
|
509
556
|
if len(word) > 3:
|
|
510
557
|
all_words[word] += 1
|
|
511
558
|
|
|
@@ -562,7 +609,11 @@ class ConsolidationPipeline:
|
|
|
562
609
|
tool_counts: Dict[str, int] = defaultdict(int)
|
|
563
610
|
for episode in cluster:
|
|
564
611
|
for action in episode.action_log:
|
|
565
|
-
|
|
612
|
+
# Skip explicit-null tools (C2): an explicit JSON null tool would
|
|
613
|
+
# become a None dict key here, harmless until ", ".join(common_tools)
|
|
614
|
+
# at the end of _extract_correct_approach crashes on it.
|
|
615
|
+
if action.tool:
|
|
616
|
+
tool_counts[action.tool] += 1
|
|
566
617
|
|
|
567
618
|
# Filter to tools used in most episodes
|
|
568
619
|
common_tools = [
|
|
@@ -641,8 +692,10 @@ class ConsolidationPipeline:
|
|
|
641
692
|
for episode in episodes:
|
|
642
693
|
# Get actions before error
|
|
643
694
|
if episode.action_log:
|
|
695
|
+
# Filter explicit-null tools (C2): pre_error_actions feeds
|
|
696
|
+
# _summarize_actions, whose ", ".join crashes on a None member.
|
|
644
697
|
pre_error_actions.extend(
|
|
645
|
-
[a.tool for a in episode.action_log[-3:]] # Last 3 actions
|
|
698
|
+
[a.tool for a in episode.action_log[-3:] if a.tool] # Last 3 actions
|
|
646
699
|
)
|
|
647
700
|
|
|
648
701
|
# Collect resolutions
|
|
@@ -718,7 +771,9 @@ class ConsolidationPipeline:
|
|
|
718
771
|
for i, tool in enumerate(common_seq[:5], 1):
|
|
719
772
|
steps.append(f"{i}. Use {tool}")
|
|
720
773
|
|
|
721
|
-
|
|
774
|
+
# Defensively drop any None tool before join (C2): callers filter Nones,
|
|
775
|
+
# but guard here too since this join crashes on a None member.
|
|
776
|
+
return "; ".join(steps) if steps else f"Use: {', '.join(t for t in common_tools if t)}"
|
|
722
777
|
|
|
723
778
|
def _summarize_actions(self, actions: List[str]) -> str:
|
|
724
779
|
"""Summarize a list of actions into a description."""
|
|
@@ -784,6 +839,20 @@ class ConsolidationPipeline:
|
|
|
784
839
|
if best_match is None or best_similarity < 0.5:
|
|
785
840
|
return new_pattern
|
|
786
841
|
|
|
842
|
+
# Idempotency guard (consolidation-C4): only boost confidence when the
|
|
843
|
+
# merge actually introduces NEW evidence. consolidate() reloads every
|
|
844
|
+
# episode in the since-window on each run (storage.list_episodes has no
|
|
845
|
+
# consolidated-state filter), so re-running over an unchanged episode set
|
|
846
|
+
# re-extracts identical patterns that re-match this existing pattern. A
|
|
847
|
+
# flat +0.05 every time would ratchet confidence up artificially with no
|
|
848
|
+
# new data. Comparing source_episodes (which round-trips through storage)
|
|
849
|
+
# makes the merge a no-op for confidence when no new source episode is
|
|
850
|
+
# present, while still rewarding a genuinely new similar episode.
|
|
851
|
+
new_source_episodes = (
|
|
852
|
+
set(new_pattern.source_episodes) - set(best_match.source_episodes)
|
|
853
|
+
)
|
|
854
|
+
confidence_boost = 0.05 if new_source_episodes else 0.0
|
|
855
|
+
|
|
787
856
|
# Merge patterns
|
|
788
857
|
merged = SemanticPattern(
|
|
789
858
|
id=best_match.id,
|
|
@@ -792,7 +861,7 @@ class ConsolidationPipeline:
|
|
|
792
861
|
conditions=list(set(best_match.conditions + new_pattern.conditions)),
|
|
793
862
|
correct_approach=best_match.correct_approach or new_pattern.correct_approach,
|
|
794
863
|
incorrect_approach=best_match.incorrect_approach or new_pattern.incorrect_approach,
|
|
795
|
-
confidence=min(best_match.confidence +
|
|
864
|
+
confidence=min(best_match.confidence + confidence_boost, 0.99),
|
|
796
865
|
source_episodes=list(set(best_match.source_episodes + new_pattern.source_episodes)),
|
|
797
866
|
usage_count=best_match.usage_count,
|
|
798
867
|
last_used=best_match.last_used,
|
|
@@ -981,7 +1050,8 @@ def compress_episode_to_summary(episode: EpisodeTrace) -> str:
|
|
|
981
1050
|
action_count = len(episode.action_log)
|
|
982
1051
|
error_count = len(episode.errors_encountered)
|
|
983
1052
|
|
|
984
|
-
|
|
1053
|
+
# Guard explicit-null goal (C2): None is not subscriptable.
|
|
1054
|
+
summary = f"Task '{(episode.goal or '')[:50]}' {outcome}"
|
|
985
1055
|
|
|
986
1056
|
if action_count > 0:
|
|
987
1057
|
summary += f" after {action_count} actions"
|
|
@@ -1012,10 +1082,13 @@ def compress_episodes_to_pattern_desc(episodes: List[EpisodeTrace]) -> str:
|
|
|
1012
1082
|
return "Unknown pattern"
|
|
1013
1083
|
|
|
1014
1084
|
if len(episodes) == 1:
|
|
1015
|
-
|
|
1085
|
+
# No slice here, so a None goal would f-string as "None" without crashing;
|
|
1086
|
+
# normalize to "" for cleaner output (C2).
|
|
1087
|
+
return f"Pattern from: {episodes[0].goal or ''}"
|
|
1016
1088
|
|
|
1017
1089
|
# Find common goal elements
|
|
1018
|
-
|
|
1090
|
+
# Guard explicit-null goal (C2): None has no .lower().
|
|
1091
|
+
goals = [(ep.goal or "").lower() for ep in episodes]
|
|
1019
1092
|
|
|
1020
1093
|
# Find common words
|
|
1021
1094
|
word_counts: Dict[str, int] = defaultdict(int)
|
|
@@ -1035,4 +1108,5 @@ def compress_episodes_to_pattern_desc(episodes: List[EpisodeTrace]) -> str:
|
|
|
1035
1108
|
return f"Pattern for {theme} tasks ({len(episodes)} instances)"
|
|
1036
1109
|
|
|
1037
1110
|
# Fallback to first episode's goal
|
|
1038
|
-
|
|
1111
|
+
# Guard explicit-null goal (C2): None is not subscriptable.
|
|
1112
|
+
return f"Pattern: {(episodes[0].goal or '')[:100]} (and {len(episodes)-1} similar)"
|
package/memory/retrieval.py
CHANGED
|
@@ -414,6 +414,7 @@ class MemoryRetrieval:
|
|
|
414
414
|
context: Dict[str, Any],
|
|
415
415
|
top_k: int = 5,
|
|
416
416
|
token_budget: Optional[int] = None,
|
|
417
|
+
persist_boost: bool = False,
|
|
417
418
|
) -> List[Dict[str, Any]]:
|
|
418
419
|
"""
|
|
419
420
|
Retrieve memories with task-type-aware weighting.
|
|
@@ -427,6 +428,12 @@ class MemoryRetrieval:
|
|
|
427
428
|
token_budget: Optional maximum token budget for returned memories.
|
|
428
429
|
If specified, results will be optimized to fit within
|
|
429
430
|
this budget using importance/recency/relevance scoring.
|
|
431
|
+
persist_boost: When True, persist the retrieval-time importance boost
|
|
432
|
+
to disk ("use it or lose it" reinforcement). Default
|
|
433
|
+
False so manual/on-demand retrievals (dashboard, MCP)
|
|
434
|
+
do NOT silently reinforce importance; only the autonomous
|
|
435
|
+
RARV loop opts in. The in-memory boost that shapes the
|
|
436
|
+
returned ranking is applied either way.
|
|
430
437
|
|
|
431
438
|
Returns:
|
|
432
439
|
List of memory items with source field indicating origin
|
|
@@ -476,10 +483,20 @@ class MemoryRetrieval:
|
|
|
476
483
|
# Apply recency boost
|
|
477
484
|
merged = self._apply_recency_boost(merged, boost_factor=0.1)
|
|
478
485
|
|
|
479
|
-
# Boost importance for retrieved memories (use it or lose it)
|
|
486
|
+
# Boost importance for retrieved memories (use it or lose it). The
|
|
487
|
+
# in-memory boost shapes the returned ranking; persist_boost writes the
|
|
488
|
+
# reinforcement to disk (retrieval-F1: boost_on_retrieval alone never
|
|
489
|
+
# persisted). Persistence is best-effort: a locked/missing record must
|
|
490
|
+
# never break retrieval, so failures are swallowed (mirrors other
|
|
491
|
+
# best-effort writes).
|
|
480
492
|
if hasattr(self.storage, 'boost_on_retrieval'):
|
|
481
493
|
for memory in merged[:top_k]:
|
|
482
494
|
self.storage.boost_on_retrieval(memory, boost=0.05)
|
|
495
|
+
if persist_boost and hasattr(self.storage, 'persist_boost'):
|
|
496
|
+
try:
|
|
497
|
+
self.storage.persist_boost(memory, boost=0.05)
|
|
498
|
+
except Exception:
|
|
499
|
+
pass
|
|
483
500
|
|
|
484
501
|
# Apply token budget optimization if specified
|
|
485
502
|
if token_budget is not None and token_budget > 0:
|
package/memory/storage.py
CHANGED
|
@@ -595,8 +595,32 @@ class MemoryStorage:
|
|
|
595
595
|
lock_path.unlink()
|
|
596
596
|
except OSError:
|
|
597
597
|
pass
|
|
598
|
-
# Clean up any remaining lock files before checking if dir
|
|
598
|
+
# Clean up any remaining lock files before checking if dir
|
|
599
|
+
# is empty. A blanket unlink of every *.lock here is the same
|
|
600
|
+
# flock+unlink inode-replacement race fixed in _file_lock and
|
|
601
|
+
# _cleanup_stale_locks: a lock held by a concurrent writer of
|
|
602
|
+
# a DIFFERENT episode in this same date dir would have its
|
|
603
|
+
# inode unlinked, letting a third writer create a new inode
|
|
604
|
+
# and enter the critical section concurrently (data loss).
|
|
605
|
+
# Only unlink a lock we can take ourselves (nobody holds it);
|
|
606
|
+
# held locks are left in place (their writer is still active).
|
|
599
607
|
for stale_lock in date_dir.glob("*.lock"):
|
|
608
|
+
probe_fd = None
|
|
609
|
+
try:
|
|
610
|
+
probe_fd = open(stale_lock, "a")
|
|
611
|
+
fcntl.flock(probe_fd.fileno(),
|
|
612
|
+
fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
613
|
+
except (OSError, BlockingIOError):
|
|
614
|
+
# Held by a live writer -- leave it alone.
|
|
615
|
+
continue
|
|
616
|
+
finally:
|
|
617
|
+
if probe_fd is not None:
|
|
618
|
+
try:
|
|
619
|
+
fcntl.flock(probe_fd.fileno(),
|
|
620
|
+
fcntl.LOCK_UN)
|
|
621
|
+
except OSError:
|
|
622
|
+
pass
|
|
623
|
+
probe_fd.close()
|
|
600
624
|
try:
|
|
601
625
|
stale_lock.unlink()
|
|
602
626
|
except OSError:
|
|
@@ -1359,6 +1383,142 @@ class MemoryStorage:
|
|
|
1359
1383
|
|
|
1360
1384
|
return memory
|
|
1361
1385
|
|
|
1386
|
+
def persist_boost(
|
|
1387
|
+
self,
|
|
1388
|
+
memory: Dict[str, Any],
|
|
1389
|
+
boost: float = 0.1,
|
|
1390
|
+
) -> bool:
|
|
1391
|
+
"""
|
|
1392
|
+
Persist a retrieval-time boost to disk ("use it or lose it").
|
|
1393
|
+
|
|
1394
|
+
boost_on_retrieval mutates an in-memory dict only; without this the
|
|
1395
|
+
stored importance/access_count never rises, so repeated retrieval can
|
|
1396
|
+
never reinforce a memory against decay (retrieval-F1). This method
|
|
1397
|
+
applies the SAME boost math to the record as it currently exists on
|
|
1398
|
+
disk, under one exclusive _file_lock spanning a FRESH read -> mutate
|
|
1399
|
+
-> _atomic_write (mirrors _decay_episodic / _decay_semantic).
|
|
1400
|
+
|
|
1401
|
+
Race-safety: the boost is applied to the freshly-read record, NOT to
|
|
1402
|
+
the passed-in `memory` dict. So a concurrent content edit landed by
|
|
1403
|
+
another writer is preserved (we only overwrite importance,
|
|
1404
|
+
access_count, last_accessed), and no retrieval-only transient fields
|
|
1405
|
+
(_score, _source, _collection) leak into the stored record. This is
|
|
1406
|
+
the lost-update-safe pattern WAVE6 established for decay.
|
|
1407
|
+
|
|
1408
|
+
Keyed by memory["id"] and the collection marker retrieval attaches
|
|
1409
|
+
(_source, falling back to _collection). Covers episodic (per-file) and
|
|
1410
|
+
semantic patterns.json. Collections without an updater degrade
|
|
1411
|
+
gracefully (return False, no crash):
|
|
1412
|
+
- skills are keyed on disk by name, not id, so an id-keyed boost
|
|
1413
|
+
cannot reliably target the file; skipped honestly.
|
|
1414
|
+
- the legacy semantic/anti-patterns.json store has NO updater
|
|
1415
|
+
anywhere in this module, so there is nothing to write back to;
|
|
1416
|
+
skipped honestly rather than fabricating a writer.
|
|
1417
|
+
|
|
1418
|
+
Args:
|
|
1419
|
+
memory: A retrieved memory dict (must carry "id" and a source
|
|
1420
|
+
marker). The dict itself is not written to disk.
|
|
1421
|
+
boost: Amount to boost importance (default 0.1).
|
|
1422
|
+
|
|
1423
|
+
Returns:
|
|
1424
|
+
True if a record was found and persisted, False otherwise.
|
|
1425
|
+
"""
|
|
1426
|
+
memory_id = memory.get("id")
|
|
1427
|
+
if not memory_id:
|
|
1428
|
+
return False
|
|
1429
|
+
|
|
1430
|
+
source = memory.get("_source") or memory.get("_collection") or ""
|
|
1431
|
+
|
|
1432
|
+
if source == "episodic":
|
|
1433
|
+
return self._persist_boost_episodic(str(memory_id), boost)
|
|
1434
|
+
if source == "semantic":
|
|
1435
|
+
return self._persist_boost_semantic(str(memory_id), boost)
|
|
1436
|
+
|
|
1437
|
+
# skills (keyed by name on disk) and the legacy anti-patterns.json
|
|
1438
|
+
# store (no updater exists in this module) cannot be safely targeted
|
|
1439
|
+
# by an id-keyed boost; skip rather than fabricate a writer.
|
|
1440
|
+
return False
|
|
1441
|
+
|
|
1442
|
+
def _persist_boost_episodic(self, memory_id: str, boost: float) -> bool:
|
|
1443
|
+
"""Apply and persist a boost to one episodic record, keyed by id.
|
|
1444
|
+
|
|
1445
|
+
Locates the per-file record (task-<id>.json across date dirs) then does
|
|
1446
|
+
a lock-spanning fresh-read -> boost -> atomic-write, mirroring
|
|
1447
|
+
_decay_episodic. The id is sanitized exactly as save_episode does so a
|
|
1448
|
+
sanitized-on-write filename is still found.
|
|
1449
|
+
"""
|
|
1450
|
+
episodic_dir = self.base_path / "episodic"
|
|
1451
|
+
if not episodic_dir.exists():
|
|
1452
|
+
return False
|
|
1453
|
+
|
|
1454
|
+
safe_id = "".join(
|
|
1455
|
+
c if c.isalnum() or c in "-_" else "_"
|
|
1456
|
+
for c in memory_id
|
|
1457
|
+
)
|
|
1458
|
+
|
|
1459
|
+
for date_dir in episodic_dir.iterdir():
|
|
1460
|
+
if not date_dir.is_dir():
|
|
1461
|
+
continue
|
|
1462
|
+
file_path = date_dir / f"task-{safe_id}.json"
|
|
1463
|
+
if not file_path.exists():
|
|
1464
|
+
continue
|
|
1465
|
+
|
|
1466
|
+
# One exclusive lock spanning read-mutate-write. boost_on_retrieval
|
|
1467
|
+
# mutates the freshly-read record in place (importance/access_count/
|
|
1468
|
+
# last_accessed only), so a concurrent content edit on disk is
|
|
1469
|
+
# preserved. _atomic_write re-enters the same reentrant lock.
|
|
1470
|
+
with self._file_lock(file_path, exclusive=True):
|
|
1471
|
+
if not file_path.exists():
|
|
1472
|
+
return False
|
|
1473
|
+
try:
|
|
1474
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
1475
|
+
data = json.load(f)
|
|
1476
|
+
except (json.JSONDecodeError, OSError, UnicodeDecodeError):
|
|
1477
|
+
return False
|
|
1478
|
+
if not data:
|
|
1479
|
+
return False
|
|
1480
|
+
self.boost_on_retrieval(data, boost=boost)
|
|
1481
|
+
self._atomic_write(file_path, data)
|
|
1482
|
+
return True
|
|
1483
|
+
|
|
1484
|
+
return False
|
|
1485
|
+
|
|
1486
|
+
def _persist_boost_semantic(self, memory_id: str, boost: float) -> bool:
|
|
1487
|
+
"""Apply and persist a boost to one semantic pattern, keyed by id.
|
|
1488
|
+
|
|
1489
|
+
Patterns live in a single semantic/patterns.json list. Lock-spanning
|
|
1490
|
+
fresh read -> boost the matching entry -> atomic write, mirroring
|
|
1491
|
+
_decay_semantic / save_pattern.
|
|
1492
|
+
"""
|
|
1493
|
+
patterns_path = self.base_path / "semantic" / "patterns.json"
|
|
1494
|
+
if not patterns_path.exists():
|
|
1495
|
+
return False
|
|
1496
|
+
|
|
1497
|
+
with self._file_lock(patterns_path, exclusive=True):
|
|
1498
|
+
if not patterns_path.exists():
|
|
1499
|
+
return False
|
|
1500
|
+
try:
|
|
1501
|
+
with open(patterns_path, "r", encoding="utf-8") as f:
|
|
1502
|
+
patterns_file = json.load(f)
|
|
1503
|
+
except (json.JSONDecodeError, OSError, UnicodeDecodeError):
|
|
1504
|
+
return False
|
|
1505
|
+
if not patterns_file:
|
|
1506
|
+
return False
|
|
1507
|
+
|
|
1508
|
+
patterns = patterns_file.get("patterns", [])
|
|
1509
|
+
for pattern in patterns:
|
|
1510
|
+
if not isinstance(pattern, dict):
|
|
1511
|
+
continue
|
|
1512
|
+
if pattern.get("id") == memory_id:
|
|
1513
|
+
self.boost_on_retrieval(pattern, boost=boost)
|
|
1514
|
+
patterns_file["last_updated"] = datetime.now(
|
|
1515
|
+
timezone.utc
|
|
1516
|
+
).isoformat()
|
|
1517
|
+
self._atomic_write(patterns_path, patterns_file)
|
|
1518
|
+
return True
|
|
1519
|
+
|
|
1520
|
+
return False
|
|
1521
|
+
|
|
1362
1522
|
def batch_apply_decay(
|
|
1363
1523
|
self,
|
|
1364
1524
|
collection: str = "all",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
3
|
"mcpName": "io.github.asklokesh/loki-mode",
|
|
4
|
-
"version": "7.
|
|
4
|
+
"version": "7.68.0",
|
|
5
5
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "7.
|
|
5
|
+
"version": "7.68.0",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Autonomi",
|