loki-mode 7.67.0 → 7.69.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 +14 -2
- package/autonomy/completion-council.sh +34 -3
- package/autonomy/loki +162 -75
- package/autonomy/run.sh +39 -54
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +57 -25
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +66 -66
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +12 -0
- package/memory/consolidation.py +15 -1
- package/memory/engine.py +25 -5
- package/memory/retrieval.py +18 -1
- package/memory/storage.py +136 -0
- package/memory/token_economics.py +9 -0
- package/memory/vector_index.py +13 -0
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -122,32 +122,43 @@ class _RateLimiter:
|
|
|
122
122
|
self._window = window_seconds
|
|
123
123
|
self._max_keys = max_keys
|
|
124
124
|
self._calls: dict[str, list[float]] = defaultdict(list)
|
|
125
|
+
# Sync route handlers (plain `def`) run in Starlette's threadpool, so
|
|
126
|
+
# check() can be entered by several threads at once against this one
|
|
127
|
+
# shared instance. Without a guard, one thread iterating self._calls
|
|
128
|
+
# (the empty-key prune or the LRU-eviction sort) while another inserts
|
|
129
|
+
# or deletes a key raises "dictionary changed size during iteration",
|
|
130
|
+
# which surfaces to the caller as a 500 on a trivial rate-limit guard.
|
|
131
|
+
# The lock is held only around the in-memory bookkeeping (no I/O, no
|
|
132
|
+
# await), so contention is negligible and it cannot deadlock async
|
|
133
|
+
# callers that reach this via run_in_threadpool.
|
|
134
|
+
self._lock = threading.Lock()
|
|
125
135
|
|
|
126
136
|
def check(self, key: str) -> bool:
|
|
127
137
|
now = time.time()
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
# Evict least-recently-accessed keys if max_keys exceeded
|
|
137
|
-
if len(self._calls) > self._max_keys:
|
|
138
|
-
# Sort by last-access time (most recent timestamp), evict least recent
|
|
139
|
-
sorted_keys = sorted(
|
|
140
|
-
self._calls.items(),
|
|
141
|
-
key=lambda x: max(x[1]) if x[1] else 0
|
|
142
|
-
)
|
|
143
|
-
keys_to_remove = len(self._calls) - self._max_keys
|
|
144
|
-
for k, _ in sorted_keys[:keys_to_remove]:
|
|
138
|
+
with self._lock:
|
|
139
|
+
# Prune old timestamps for this key
|
|
140
|
+
self._calls[key] = [t for t in self._calls[key] if now - t < self._window]
|
|
141
|
+
|
|
142
|
+
# Remove keys with empty timestamp lists
|
|
143
|
+
empty_keys = [k for k, v in self._calls.items() if not v]
|
|
144
|
+
for k in empty_keys:
|
|
145
145
|
del self._calls[k]
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
# Evict least-recently-accessed keys if max_keys exceeded
|
|
148
|
+
if len(self._calls) > self._max_keys:
|
|
149
|
+
# Sort by last-access time (most recent timestamp), evict least recent
|
|
150
|
+
sorted_keys = sorted(
|
|
151
|
+
self._calls.items(),
|
|
152
|
+
key=lambda x: max(x[1]) if x[1] else 0
|
|
153
|
+
)
|
|
154
|
+
keys_to_remove = len(self._calls) - self._max_keys
|
|
155
|
+
for k, _ in sorted_keys[:keys_to_remove]:
|
|
156
|
+
del self._calls[k]
|
|
157
|
+
|
|
158
|
+
if len(self._calls[key]) >= self._max_calls:
|
|
159
|
+
return False
|
|
160
|
+
self._calls[key].append(now)
|
|
161
|
+
return True
|
|
151
162
|
|
|
152
163
|
|
|
153
164
|
_control_limiter = _RateLimiter(max_calls=10, window_seconds=60)
|
|
@@ -8675,6 +8686,17 @@ def _get_migration_imports():
|
|
|
8675
8686
|
return _migration_imports
|
|
8676
8687
|
|
|
8677
8688
|
|
|
8689
|
+
def _get_migration_terminal_phase():
|
|
8690
|
+
"""Return the last phase in the migration PHASE_ORDER (the terminal phase),
|
|
8691
|
+
or None if the migration engine is unavailable. Used to let the terminal
|
|
8692
|
+
phase be advanced/completed without a successor to_phase (WAVE9 F1)."""
|
|
8693
|
+
try:
|
|
8694
|
+
from dashboard.migration_engine import PHASE_ORDER
|
|
8695
|
+
return PHASE_ORDER[-1] if PHASE_ORDER else None
|
|
8696
|
+
except (ImportError, IndexError):
|
|
8697
|
+
return None
|
|
8698
|
+
|
|
8699
|
+
|
|
8678
8700
|
@app.get("/api/migration/list", dependencies=[Depends(auth.require_scope("read"))])
|
|
8679
8701
|
def list_migrations_endpoint():
|
|
8680
8702
|
"""List all migrations."""
|
|
@@ -8825,7 +8847,16 @@ def advance_migration(migration_id: str, request_body: dict):
|
|
|
8825
8847
|
MigrationPipeline, list_migrations = imports
|
|
8826
8848
|
from_phase = request_body.get("from_phase")
|
|
8827
8849
|
to_phase = request_body.get("to_phase")
|
|
8828
|
-
|
|
8850
|
+
# The terminal phase (the last in PHASE_ORDER, e.g. "verify") has no
|
|
8851
|
+
# successor, so check_phase_gate can never pass for it and to_phase is
|
|
8852
|
+
# meaningless. Without this carve-out the terminal phase could never be
|
|
8853
|
+
# completed via the API, so overall_status could never reach "completed"
|
|
8854
|
+
# (WAVE9 migration-F1). For the terminal phase we require only from_phase
|
|
8855
|
+
# and skip the gate; advance_phase still validates the phase and the
|
|
8856
|
+
# (ValueError, RuntimeError) -> 409 handler below preserves idempotency.
|
|
8857
|
+
terminal_phase = _get_migration_terminal_phase()
|
|
8858
|
+
is_terminal = terminal_phase is not None and from_phase == terminal_phase
|
|
8859
|
+
if not from_phase or (not to_phase and not is_terminal):
|
|
8829
8860
|
raise HTTPException(status_code=400, detail="from_phase and to_phase are required")
|
|
8830
8861
|
# Load pipeline and check phase gate before the try/except to let
|
|
8831
8862
|
# HTTPException and FileNotFoundError propagate naturally.
|
|
@@ -8833,9 +8864,10 @@ def advance_migration(migration_id: str, request_body: dict):
|
|
|
8833
8864
|
pipeline = MigrationPipeline.load(migration_id)
|
|
8834
8865
|
except FileNotFoundError:
|
|
8835
8866
|
raise HTTPException(status_code=404, detail=f"Migration not found: {migration_id}")
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8867
|
+
if not is_terminal:
|
|
8868
|
+
passed, reason = pipeline.check_phase_gate(from_phase, to_phase)
|
|
8869
|
+
if not passed:
|
|
8870
|
+
raise HTTPException(status_code=409, detail=reason)
|
|
8839
8871
|
try:
|
|
8840
8872
|
result = pipeline.advance_phase(from_phase)
|
|
8841
8873
|
return asdict(result) if hasattr(result, '__dataclass_fields__') else result
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v7.
|
|
5
|
+
**Version:** v7.69.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -395,7 +395,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
|
|
|
395
395
|
# Run Loki Mode in Docker (Claude provider, API-key auth)
|
|
396
396
|
docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
|
397
397
|
-v $(pwd):/workspace -w /workspace \
|
|
398
|
-
asklokesh/loki-mode:7.
|
|
398
|
+
asklokesh/loki-mode:7.69.0 start ./my-spec.md
|
|
399
399
|
```
|
|
400
400
|
|
|
401
401
|
##### docker compose + .env (no host install)
|