loki-mode 5.47.0 → 5.48.1
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/loki +47 -2
- package/autonomy/run.sh +26 -7
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +36 -14
- package/dashboard/static/index.html +2 -2
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/memory/embeddings.py +5 -0
- package/memory/engine.py +2 -1
- package/memory/retrieval.py +1 -0
- package/memory/token_economics.py +13 -2
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with zero human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v5.
|
|
6
|
+
# Loki Mode v5.48.1
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -262,4 +262,4 @@ The following features are documented in skill modules but not yet fully automat
|
|
|
262
262
|
| Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
|
|
263
263
|
| Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
|
|
264
264
|
|
|
265
|
-
**v5.
|
|
265
|
+
**v5.48.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.
|
|
1
|
+
5.48.1
|
package/autonomy/loki
CHANGED
|
@@ -820,6 +820,14 @@ cmd_resume() {
|
|
|
820
820
|
|
|
821
821
|
# Show current status
|
|
822
822
|
cmd_status() {
|
|
823
|
+
# Check for --json flag
|
|
824
|
+
while [[ $# -gt 0 ]]; do
|
|
825
|
+
case "$1" in
|
|
826
|
+
--json) cmd_status_json; return $? ;;
|
|
827
|
+
*) shift ;;
|
|
828
|
+
esac
|
|
829
|
+
done
|
|
830
|
+
|
|
823
831
|
require_jq
|
|
824
832
|
|
|
825
833
|
if [ ! -d "$LOKI_DIR" ]; then
|
|
@@ -1502,6 +1510,24 @@ cmd_dashboard_start() {
|
|
|
1502
1510
|
tls_info=" (TLS enabled)"
|
|
1503
1511
|
fi
|
|
1504
1512
|
|
|
1513
|
+
# Ensure dashboard Python dependencies are installed
|
|
1514
|
+
if ! "$python_cmd" -c "import fastapi" 2>/dev/null; then
|
|
1515
|
+
echo -e "${YELLOW}Installing dashboard dependencies...${NC}"
|
|
1516
|
+
local req_file="${SKILL_DIR}/dashboard/requirements.txt"
|
|
1517
|
+
if [ -f "$req_file" ]; then
|
|
1518
|
+
pip3 install -q -r "$req_file" 2>/dev/null || pip install -q -r "$req_file" 2>/dev/null || {
|
|
1519
|
+
echo -e "${RED}Failed to install dashboard dependencies${NC}"
|
|
1520
|
+
echo "Run manually: pip install fastapi uvicorn pydantic websockets"
|
|
1521
|
+
exit 1
|
|
1522
|
+
}
|
|
1523
|
+
else
|
|
1524
|
+
pip3 install -q fastapi uvicorn pydantic websockets 2>/dev/null || pip install -q fastapi uvicorn pydantic websockets 2>/dev/null || {
|
|
1525
|
+
echo -e "${RED}Failed to install dashboard dependencies${NC}"
|
|
1526
|
+
exit 1
|
|
1527
|
+
}
|
|
1528
|
+
fi
|
|
1529
|
+
fi
|
|
1530
|
+
|
|
1505
1531
|
echo -e "${GREEN}Starting dashboard server...${NC}"
|
|
1506
1532
|
echo -e "${CYAN}Host:${NC} $host"
|
|
1507
1533
|
echo -e "${CYAN}Port:${NC} $port"
|
|
@@ -3117,13 +3143,32 @@ cmd_api() {
|
|
|
3117
3143
|
fi
|
|
3118
3144
|
fi
|
|
3119
3145
|
|
|
3146
|
+
# Ensure dashboard Python dependencies are installed
|
|
3147
|
+
if ! python3 -c "import fastapi" 2>/dev/null; then
|
|
3148
|
+
echo -e "${YELLOW}Installing dashboard dependencies...${NC}"
|
|
3149
|
+
local req_file="${SKILL_DIR}/dashboard/requirements.txt"
|
|
3150
|
+
if [ -f "$req_file" ]; then
|
|
3151
|
+
pip3 install -q -r "$req_file" 2>/dev/null || pip install -q -r "$req_file" 2>/dev/null || {
|
|
3152
|
+
echo -e "${RED}Failed to install dashboard dependencies${NC}"
|
|
3153
|
+
echo "Run manually: pip install fastapi uvicorn pydantic websockets"
|
|
3154
|
+
exit 1
|
|
3155
|
+
}
|
|
3156
|
+
else
|
|
3157
|
+
pip3 install -q fastapi uvicorn pydantic websockets 2>/dev/null || {
|
|
3158
|
+
echo -e "${RED}Failed to install dashboard dependencies${NC}"
|
|
3159
|
+
exit 1
|
|
3160
|
+
}
|
|
3161
|
+
fi
|
|
3162
|
+
fi
|
|
3163
|
+
|
|
3120
3164
|
# Start server
|
|
3121
3165
|
mkdir -p "$LOKI_DIR/logs" "$LOKI_DIR/dashboard"
|
|
3122
|
-
local
|
|
3166
|
+
local host="${LOKI_DASHBOARD_HOST:-127.0.0.1}"
|
|
3167
|
+
local uvicorn_args="--host $host --port $port"
|
|
3123
3168
|
if [ -n "${LOKI_TLS_CERT:-}" ] && [ -n "${LOKI_TLS_KEY:-}" ]; then
|
|
3124
3169
|
uvicorn_args="$uvicorn_args --ssl-certfile ${LOKI_TLS_CERT} --ssl-keyfile ${LOKI_TLS_KEY}"
|
|
3125
3170
|
fi
|
|
3126
|
-
LOKI_DIR="$LOKI_DIR" nohup python3 -m uvicorn dashboard.server:app $uvicorn_args > "$LOKI_DIR/logs/api.log" 2>&1 &
|
|
3171
|
+
LOKI_DIR="$LOKI_DIR" PYTHONPATH="$SKILL_DIR" nohup python3 -m uvicorn dashboard.server:app $uvicorn_args > "$LOKI_DIR/logs/api.log" 2>&1 &
|
|
3127
3172
|
local new_pid=$!
|
|
3128
3173
|
echo "$new_pid" > "$pid_file"
|
|
3129
3174
|
|
package/autonomy/run.sh
CHANGED
|
@@ -698,7 +698,7 @@ print(json.dumps(event))
|
|
|
698
698
|
if [ -z "$json_event" ]; then
|
|
699
699
|
# Escape quotes and special chars for JSON
|
|
700
700
|
local escaped_data
|
|
701
|
-
escaped_data=$(
|
|
701
|
+
escaped_data=$(printf '%s' "$event_data" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr -d '\n')
|
|
702
702
|
json_event="{\"timestamp\":\"$timestamp\",\"type\":\"$event_type\",\"data\":\"$escaped_data\"}"
|
|
703
703
|
fi
|
|
704
704
|
|
|
@@ -733,8 +733,8 @@ emit_event_json() {
|
|
|
733
733
|
if [[ "$value" =~ ^[0-9]+$ ]] || [[ "$value" =~ ^(true|false|null)$ ]]; then
|
|
734
734
|
json_data+="\"$key\":$value"
|
|
735
735
|
else
|
|
736
|
-
# Escape quotes in value
|
|
737
|
-
value=$(
|
|
736
|
+
# Escape backslashes, quotes, and special chars in value
|
|
737
|
+
value=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
|
|
738
738
|
json_data+="\"$key\":\"$value\""
|
|
739
739
|
fi
|
|
740
740
|
shift
|
|
@@ -3511,8 +3511,10 @@ update_agents_state() {
|
|
|
3511
3511
|
|
|
3512
3512
|
agents_json="${agents_json}]"
|
|
3513
3513
|
|
|
3514
|
-
# Write aggregated data
|
|
3515
|
-
|
|
3514
|
+
# Write aggregated data (atomic via temp file + mv)
|
|
3515
|
+
local tmp_file="${output_file}.tmp.$$"
|
|
3516
|
+
echo "$agents_json" > "$tmp_file"
|
|
3517
|
+
mv -f "$tmp_file" "$output_file" 2>/dev/null || rm -f "$tmp_file"
|
|
3516
3518
|
}
|
|
3517
3519
|
|
|
3518
3520
|
#===============================================================================
|
|
@@ -5145,11 +5147,28 @@ start_dashboard() {
|
|
|
5145
5147
|
log_info "TLS enabled for dashboard"
|
|
5146
5148
|
fi
|
|
5147
5149
|
|
|
5150
|
+
# Ensure dashboard Python dependencies are installed
|
|
5151
|
+
local skill_dir="${SCRIPT_DIR%/*}"
|
|
5152
|
+
local req_file="${skill_dir}/dashboard/requirements.txt"
|
|
5153
|
+
if ! python3 -c "import fastapi" 2>/dev/null; then
|
|
5154
|
+
log_step "Installing dashboard dependencies..."
|
|
5155
|
+
if [ -f "$req_file" ]; then
|
|
5156
|
+
pip3 install -q -r "$req_file" 2>/dev/null || pip install -q -r "$req_file" 2>/dev/null || {
|
|
5157
|
+
log_warn "Failed to install dashboard dependencies"
|
|
5158
|
+
log_warn "Run manually: pip install fastapi uvicorn pydantic websockets"
|
|
5159
|
+
}
|
|
5160
|
+
else
|
|
5161
|
+
pip3 install -q fastapi uvicorn pydantic websockets 2>/dev/null || pip install -q fastapi uvicorn pydantic websockets 2>/dev/null || {
|
|
5162
|
+
log_warn "Failed to install dashboard dependencies"
|
|
5163
|
+
}
|
|
5164
|
+
fi
|
|
5165
|
+
fi
|
|
5166
|
+
|
|
5148
5167
|
# Start the FastAPI dashboard server
|
|
5149
5168
|
# Dashboard module is at project root (parent of autonomy/)
|
|
5150
5169
|
# LOKI_SKILL_DIR tells server.py where to find static files
|
|
5151
5170
|
LOKI_TLS_CERT="${LOKI_TLS_CERT:-}" LOKI_TLS_KEY="${LOKI_TLS_KEY:-}" \
|
|
5152
|
-
LOKI_SKILL_DIR="${
|
|
5171
|
+
LOKI_SKILL_DIR="${skill_dir}" PYTHONPATH="${skill_dir}" nohup python3 -m dashboard.server > "$log_file" 2>&1 &
|
|
5153
5172
|
DASHBOARD_PID=$!
|
|
5154
5173
|
|
|
5155
5174
|
# Save PID for later cleanup
|
|
@@ -5874,7 +5893,7 @@ save_state() {
|
|
|
5874
5893
|
"status": "$status",
|
|
5875
5894
|
"lastExitCode": $exit_code,
|
|
5876
5895
|
"lastRun": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
5877
|
-
"prdPath": "${PRD_PATH:-}",
|
|
5896
|
+
"prdPath": "$(printf '%s' "${PRD_PATH:-}" | sed 's/\\/\\\\/g; s/"/\\"/g')",
|
|
5878
5897
|
"pid": $$,
|
|
5879
5898
|
"maxRetries": $MAX_RETRIES,
|
|
5880
5899
|
"baseWait": $BASE_WAIT
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -62,6 +62,15 @@ except ImportError:
|
|
|
62
62
|
LOKI_TLS_CERT = os.environ.get("LOKI_TLS_CERT", "") # Path to PEM certificate
|
|
63
63
|
LOKI_TLS_KEY = os.environ.get("LOKI_TLS_KEY", "") # Path to PEM private key
|
|
64
64
|
|
|
65
|
+
|
|
66
|
+
def _safe_int_env(name: str, default: int) -> int:
|
|
67
|
+
"""Read an integer from an environment variable, returning *default* on bad values."""
|
|
68
|
+
try:
|
|
69
|
+
return int(os.environ.get(name, str(default)))
|
|
70
|
+
except (ValueError, TypeError):
|
|
71
|
+
return default
|
|
72
|
+
|
|
73
|
+
|
|
65
74
|
# ---------------------------------------------------------------------------
|
|
66
75
|
# Simple in-memory rate limiter for control endpoints
|
|
67
76
|
# ---------------------------------------------------------------------------
|
|
@@ -214,7 +223,10 @@ class StatusResponse(BaseModel):
|
|
|
214
223
|
class ConnectionManager:
|
|
215
224
|
"""Manages WebSocket connections for real-time updates."""
|
|
216
225
|
|
|
217
|
-
|
|
226
|
+
try:
|
|
227
|
+
MAX_CONNECTIONS = int(os.environ.get("LOKI_MAX_WS_CONNECTIONS", "100"))
|
|
228
|
+
except (ValueError, TypeError):
|
|
229
|
+
MAX_CONNECTIONS = 100
|
|
218
230
|
|
|
219
231
|
def __init__(self):
|
|
220
232
|
self.active_connections: list[WebSocket] = []
|
|
@@ -493,7 +505,7 @@ async def list_projects(
|
|
|
493
505
|
return response
|
|
494
506
|
|
|
495
507
|
|
|
496
|
-
@app.post("/api/projects", response_model=ProjectResponse, status_code=201)
|
|
508
|
+
@app.post("/api/projects", response_model=ProjectResponse, status_code=201, dependencies=[Depends(auth.require_scope("control"))])
|
|
497
509
|
async def create_project(
|
|
498
510
|
project: ProjectCreate,
|
|
499
511
|
db: AsyncSession = Depends(get_db),
|
|
@@ -718,7 +730,7 @@ async def list_tasks(
|
|
|
718
730
|
return all_tasks
|
|
719
731
|
|
|
720
732
|
|
|
721
|
-
@app.post("/api/tasks", response_model=TaskResponse, status_code=201)
|
|
733
|
+
@app.post("/api/tasks", response_model=TaskResponse, status_code=201, dependencies=[Depends(auth.require_scope("control"))])
|
|
722
734
|
async def create_task(
|
|
723
735
|
task: TaskCreate,
|
|
724
736
|
db: AsyncSession = Depends(get_db),
|
|
@@ -1041,7 +1053,7 @@ async def list_registered_projects(include_inactive: bool = False):
|
|
|
1041
1053
|
return projects
|
|
1042
1054
|
|
|
1043
1055
|
|
|
1044
|
-
@app.post("/api/registry/projects", response_model=RegisteredProjectResponse, status_code=201)
|
|
1056
|
+
@app.post("/api/registry/projects", response_model=RegisteredProjectResponse, status_code=201, dependencies=[Depends(auth.require_scope("control"))])
|
|
1045
1057
|
async def register_project(request: RegisterProjectRequest):
|
|
1046
1058
|
"""Register a new project."""
|
|
1047
1059
|
try:
|
|
@@ -1087,7 +1099,7 @@ async def get_project_health(identifier: str):
|
|
|
1087
1099
|
return health
|
|
1088
1100
|
|
|
1089
1101
|
|
|
1090
|
-
@app.post("/api/registry/projects/{identifier}/access")
|
|
1102
|
+
@app.post("/api/registry/projects/{identifier}/access", dependencies=[Depends(auth.require_scope("control"))])
|
|
1091
1103
|
async def update_project_access(identifier: str):
|
|
1092
1104
|
"""Update the last accessed timestamp for a project."""
|
|
1093
1105
|
project = registry.update_last_accessed(identifier)
|
|
@@ -1104,7 +1116,7 @@ async def discover_projects(max_depth: int = Query(default=3, ge=1, le=10)):
|
|
|
1104
1116
|
return discovered
|
|
1105
1117
|
|
|
1106
1118
|
|
|
1107
|
-
@app.post("/api/registry/sync", response_model=SyncResponse)
|
|
1119
|
+
@app.post("/api/registry/sync", response_model=SyncResponse, dependencies=[Depends(auth.require_scope("control"))])
|
|
1108
1120
|
async def sync_registry():
|
|
1109
1121
|
"""Sync the registry with discovered projects."""
|
|
1110
1122
|
if not _read_limiter.check("registry_sync"):
|
|
@@ -1792,7 +1804,17 @@ async def trigger_aggregation():
|
|
|
1792
1804
|
|
|
1793
1805
|
if events_file.exists():
|
|
1794
1806
|
try:
|
|
1795
|
-
|
|
1807
|
+
# Guard against unbounded reads: if file > 10 MB, read only the tail
|
|
1808
|
+
_MAX_EVENTS_BYTES = 10 * 1024 * 1024 # 10 MB
|
|
1809
|
+
_fsize = events_file.stat().st_size
|
|
1810
|
+
if _fsize > _MAX_EVENTS_BYTES:
|
|
1811
|
+
with open(events_file, "rb") as _fh:
|
|
1812
|
+
_fh.seek(-_MAX_EVENTS_BYTES, 2)
|
|
1813
|
+
_fh.readline() # discard partial first line
|
|
1814
|
+
_raw_text = _fh.read().decode("utf-8", errors="replace")
|
|
1815
|
+
else:
|
|
1816
|
+
_raw_text = events_file.read_text()
|
|
1817
|
+
for raw_line in _raw_text.strip().split("\n"):
|
|
1796
1818
|
if not raw_line.strip():
|
|
1797
1819
|
continue
|
|
1798
1820
|
try:
|
|
@@ -2396,8 +2418,8 @@ async def get_council_convergence():
|
|
|
2396
2418
|
convergence_file = _get_loki_dir() / "council" / "convergence.log"
|
|
2397
2419
|
data_points = []
|
|
2398
2420
|
if convergence_file.exists():
|
|
2399
|
-
|
|
2400
|
-
|
|
2421
|
+
for line in convergence_file.read_text().strip().split("\n"):
|
|
2422
|
+
try:
|
|
2401
2423
|
parts = line.split("|")
|
|
2402
2424
|
if len(parts) >= 5:
|
|
2403
2425
|
data_points.append({
|
|
@@ -2407,8 +2429,8 @@ async def get_council_convergence():
|
|
|
2407
2429
|
"no_change_streak": int(parts[3]),
|
|
2408
2430
|
"done_signals": int(parts[4]),
|
|
2409
2431
|
})
|
|
2410
|
-
|
|
2411
|
-
|
|
2432
|
+
except Exception:
|
|
2433
|
+
continue
|
|
2412
2434
|
return {"dataPoints": data_points}
|
|
2413
2435
|
|
|
2414
2436
|
|
|
@@ -3002,7 +3024,7 @@ async def get_github_status(token: Optional[dict] = Depends(auth.get_current_tok
|
|
|
3002
3024
|
"pr_enabled": os.environ.get("LOKI_GITHUB_PR", "false") == "true",
|
|
3003
3025
|
"labels_filter": os.environ.get("LOKI_GITHUB_LABELS", ""),
|
|
3004
3026
|
"milestone_filter": os.environ.get("LOKI_GITHUB_MILESTONE", ""),
|
|
3005
|
-
"limit":
|
|
3027
|
+
"limit": _safe_int_env("LOKI_GITHUB_LIMIT", 100),
|
|
3006
3028
|
"imported_tasks": 0,
|
|
3007
3029
|
"synced_updates": 0,
|
|
3008
3030
|
"repo": None,
|
|
@@ -3232,7 +3254,7 @@ def _build_metrics_text() -> str:
|
|
|
3232
3254
|
lines.append("")
|
|
3233
3255
|
|
|
3234
3256
|
# 3. loki_iteration_max (gauge) -------------------------------------------
|
|
3235
|
-
max_iterations =
|
|
3257
|
+
max_iterations = _safe_int_env("LOKI_MAX_ITERATIONS", 1000)
|
|
3236
3258
|
lines.append("# HELP loki_iteration_max Maximum configured iterations")
|
|
3237
3259
|
lines.append("# TYPE loki_iteration_max gauge")
|
|
3238
3260
|
lines.append(f"loki_iteration_max {max_iterations}")
|
|
@@ -3720,7 +3742,7 @@ def run_server(host: str = None, port: int = None) -> None:
|
|
|
3720
3742
|
# Default to localhost-only for security
|
|
3721
3743
|
host = os.environ.get("LOKI_DASHBOARD_HOST", "127.0.0.1")
|
|
3722
3744
|
if port is None:
|
|
3723
|
-
port =
|
|
3745
|
+
port = _safe_int_env("LOKI_DASHBOARD_PORT", 57374)
|
|
3724
3746
|
|
|
3725
3747
|
uvicorn_kwargs = {
|
|
3726
3748
|
"host": host,
|
|
@@ -5000,7 +5000,7 @@ var LokiDashboard=(()=>{var X=Object.defineProperty;var gt=Object.getOwnProperty
|
|
|
5000
5000
|
<p>App runner not started</p>
|
|
5001
5001
|
<p class="hint">App runner will start after the first successful build iteration.</p>
|
|
5002
5002
|
</div>
|
|
5003
|
-
`}_attachEventListeners(){let t=this.shadowRoot;if(!t)return;let e=t.querySelector('[data-action="restart"]'),a=t.querySelector('[data-action="stop"]');e&&e.addEventListener("click",()=>this._handleRestart()),a&&a.addEventListener("click",()=>this._handleStop())}_escapeHtml(t){return t?String(t).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""):""}};customElements.define("loki-app-status",J);var Ct={opus:{input:5,output:25,label:"Opus 4.6",provider:"claude"},sonnet:{input:3,output:15,label:"Sonnet 4.5",provider:"claude"},haiku:{input:1,output:5,label:"Haiku 4.5",provider:"claude"},"gpt-5.3-codex":{input:1.5,output:12,label:"GPT-5.3 Codex",provider:"codex"},"gemini-3-pro":{input:1.25,output:10,label:"Gemini 3 Pro",provider:"gemini"},"gemini-3-flash":{input:.1,output:.4,label:"Gemini 3 Flash",provider:"gemini"}},K=class extends c{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._data={total_input_tokens:0,total_output_tokens:0,estimated_cost_usd:0,by_phase:{},by_model:{},budget_limit:null,budget_used:0,budget_remaining:null,connected:!1},this._api=null,this._pollInterval=null,this._modelPricing={...Ct}}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadPricing(),this._loadCost(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling()}attributeChangedCallback(t,e,a){e!==a&&(t==="api-url"&&this._api&&(this._api.baseUrl=a,this._loadCost()),t==="theme"&&this._applyTheme())}_setupApi(){let t=this.getAttribute("api-url")||window.location.origin;this._api=u({baseUrl:t})}async _loadPricing(){try{let t=await this._api.getPricing();if(t&&t.models){let e={};for(let[a,i]of Object.entries(t.models))e[a]={input:i.input,output:i.output,label:i.label||a,provider:i.provider||"unknown"};this._modelPricing=e,this._pricingSource=t.source||"api",this._pricingDate=t.updated||"",this._activeProvider=t.provider||"claude",this.render()}}catch{}}async _loadCost(){try{let t=await this._api.getCost();this._updateFromCost(t)}catch{this._data.connected=!1,this.render()}}_updateFromCost(t){t&&(this._data={...this._data,connected:!0,total_input_tokens:t.total_input_tokens||0,total_output_tokens:t.total_output_tokens||0,estimated_cost_usd:t.estimated_cost_usd||0,by_phase:t.by_phase||{},by_model:t.by_model||{},budget_limit:t.budget_limit,budget_used:t.budget_used||0,budget_remaining:t.budget_remaining},this.render())}_startPolling(){this._pollInterval=setInterval(async()=>{try{let t=await this._api.getCost();this._updateFromCost(t)}catch{this._data.connected=!1,this.render()}},5e3)}_stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}_formatTokens(t){return!t||t===0?"0":t>=1e6?(t/1e6).toFixed(2)+"M":t>=1e3?(t/1e3).toFixed(1)+"K":String(t)}_formatUSD(t){return!t||t===0?"$0.00":t<.01?"<$0.01":"$"+t.toFixed(2)}_getBudgetPercent(){return!this._data.budget_limit||this._data.budget_limit<=0?0:Math.min(100,this._data.budget_used/this._data.budget_limit*100)}_getBudgetStatusClass(){let t=this._getBudgetPercent();return t>=90?"critical":t>=70?"warning":"ok"}_renderPhaseRows(){let t=this._data.by_phase;return!t||Object.keys(t).length===0?'<tr><td colspan="4" class="empty-cell">No phase data yet</td></tr>':Object.entries(t).map(([e,a])=>{let i=a.input_tokens||0,s=a.output_tokens||0,r=a.cost_usd||0;return`
|
|
5003
|
+
`}_attachEventListeners(){let t=this.shadowRoot;if(!t)return;let e=t.querySelector('[data-action="restart"]'),a=t.querySelector('[data-action="stop"]');e&&e.addEventListener("click",()=>this._handleRestart()),a&&a.addEventListener("click",()=>this._handleStop())}_escapeHtml(t){return t?String(t).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""):""}};customElements.define("loki-app-status",J);var Ct={opus:{input:5,output:25,label:"Opus 4.6",provider:"claude"},sonnet:{input:3,output:15,label:"Sonnet 4.5",provider:"claude"},haiku:{input:1,output:5,label:"Haiku 4.5",provider:"claude"},"gpt-5.3-codex":{input:1.5,output:12,label:"GPT-5.3 Codex",provider:"codex"},"gemini-3-pro":{input:1.25,output:10,label:"Gemini 3 Pro",provider:"gemini"},"gemini-3-flash":{input:.1,output:.4,label:"Gemini 3 Flash",provider:"gemini"}},K=class extends c{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._data={total_input_tokens:0,total_output_tokens:0,estimated_cost_usd:0,by_phase:{},by_model:{},budget_limit:null,budget_used:0,budget_remaining:null,connected:!1},this._api=null,this._pollInterval=null,this._modelPricing={...Ct}}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadPricing(),this._loadCost(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling()}attributeChangedCallback(t,e,a){e!==a&&(t==="api-url"&&this._api&&(this._api.baseUrl=a,this._loadCost()),t==="theme"&&this._applyTheme())}_setupApi(){let t=this.getAttribute("api-url")||window.location.origin;this._api=u({baseUrl:t})}async _loadPricing(){try{let t=await this._api.getPricing();if(t&&t.models){let e={};for(let[a,i]of Object.entries(t.models))e[a]={input:i.input,output:i.output,label:i.label||a,provider:i.provider||"unknown"};this._modelPricing=e,this._pricingSource=t.source||"api",this._pricingDate=t.updated||"",this._activeProvider=t.provider||"claude",this.render()}}catch{}}async _loadCost(){try{let t=await this._api.getCost();this._updateFromCost(t)}catch{this._data.connected=!1,this.render()}}_updateFromCost(t){t&&(this._data={...this._data,connected:!0,total_input_tokens:t.total_input_tokens||0,total_output_tokens:t.total_output_tokens||0,estimated_cost_usd:t.estimated_cost_usd||0,by_phase:t.by_phase||{},by_model:t.by_model||{},budget_limit:t.budget_limit,budget_used:t.budget_used||0,budget_remaining:t.budget_remaining},this.render())}_startPolling(){this._pollInterval=setInterval(async()=>{try{let t=await this._api.getCost();this._updateFromCost(t)}catch{this._data.connected=!1,this.render()}},5e3),this._visibilityHandler=()=>{document.hidden?this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null):this._pollInterval||(this._loadCost(),this._pollInterval=setInterval(async()=>{try{let t=await this._api.getCost();this._updateFromCost(t)}catch{this._data.connected=!1,this.render()}},5e3))},document.addEventListener("visibilitychange",this._visibilityHandler)}_stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null),this._visibilityHandler&&(document.removeEventListener("visibilitychange",this._visibilityHandler),this._visibilityHandler=null)}_formatTokens(t){return!t||t===0?"0":t>=1e6?(t/1e6).toFixed(2)+"M":t>=1e3?(t/1e3).toFixed(1)+"K":String(t)}_formatUSD(t){return!t||t===0?"$0.00":t<.01?"<$0.01":"$"+t.toFixed(2)}_getBudgetPercent(){return!this._data.budget_limit||this._data.budget_limit<=0?0:Math.min(100,this._data.budget_used/this._data.budget_limit*100)}_getBudgetStatusClass(){let t=this._getBudgetPercent();return t>=90?"critical":t>=70?"warning":"ok"}_renderPhaseRows(){let t=this._data.by_phase;return!t||Object.keys(t).length===0?'<tr><td colspan="4" class="empty-cell">No phase data yet</td></tr>':Object.entries(t).map(([e,a])=>{let i=a.input_tokens||0,s=a.output_tokens||0,r=a.cost_usd||0;return`
|
|
5004
5004
|
<tr>
|
|
5005
5005
|
<td class="phase-name">${this._escapeHTML(e)}</td>
|
|
5006
5006
|
<td class="mono-cell">${this._formatTokens(i)}</td>
|
|
@@ -5690,7 +5690,7 @@ var LokiDashboard=(()=>{var X=Object.defineProperty;var gt=Object.getOwnProperty
|
|
|
5690
5690
|
color: var(--loki-red);
|
|
5691
5691
|
font-size: 12px;
|
|
5692
5692
|
}
|
|
5693
|
-
`}};customElements.get("loki-checkpoint-viewer")||customElements.define("loki-checkpoint-viewer",V);var Y=class extends c{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._data=null,this._connected=!1,this._activeTab="gauge",this._api=null,this._pollInterval=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadContext(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling()}attributeChangedCallback(t,e,a){e!==a&&(t==="api-url"&&this._api&&(this._api.baseUrl=a,this._loadContext()),t==="theme"&&this._applyTheme())}_setupApi(){let t=this.getAttribute("api-url")||window.location.origin;this._api=u({baseUrl:t})}async _loadContext(){try{let t=this.getAttribute("api-url")||window.location.origin,e=await fetch(t+"/api/context");e.ok&&(this._data=await e.json(),this._connected=!0)}catch{this._connected=!1}this.render()}_startPolling(){this._pollInterval=setInterval(()=>{this._loadContext()},5e3)}_stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}_setTab(t){this._activeTab=t,this.render()}_formatTokens(t){return!t||t===0?"0":t>=1e6?(t/1e6).toFixed(2)+"M":t>=1e3?(t/1e3).toFixed(1)+"K":String(t)}_formatUSD(t){return!t||t===0?"$0.00":t<.01?"<$0.01":"$"+t.toFixed(2)}_escapeHTML(t){return t?String(t).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""):""}_getGaugeColor(t){return t>80?"var(--loki-red)":t>=60?"var(--loki-yellow)":"var(--loki-green)"}_getGaugeColorClass(t){return t>80?"gauge-red":t>=60?"gauge-yellow":"gauge-green"}_renderGaugeTab(){let t=this._data?.current||{},e=this._data?.totals||{},a=t.context_window_pct||0,i=this._getGaugeColor(a),s=this._getGaugeColorClass(a),r=70,o=2*Math.PI*r,l=o-a/100*o;return`
|
|
5693
|
+
`}};customElements.get("loki-checkpoint-viewer")||customElements.define("loki-checkpoint-viewer",V);var Y=class extends c{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._data=null,this._connected=!1,this._activeTab="gauge",this._api=null,this._pollInterval=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadContext(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling()}attributeChangedCallback(t,e,a){e!==a&&(t==="api-url"&&this._api&&(this._api.baseUrl=a,this._loadContext()),t==="theme"&&this._applyTheme())}_setupApi(){let t=this.getAttribute("api-url")||window.location.origin;this._api=u({baseUrl:t})}async _loadContext(){try{let t=this.getAttribute("api-url")||window.location.origin,e=await fetch(t+"/api/context");e.ok&&(this._data=await e.json(),this._connected=!0)}catch{this._connected=!1}this.render()}_startPolling(){this._pollInterval=setInterval(()=>{this._loadContext()},5e3),this._visibilityHandler=()=>{document.hidden?this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null):this._pollInterval||(this._loadContext(),this._pollInterval=setInterval(()=>this._loadContext(),5e3))},document.addEventListener("visibilitychange",this._visibilityHandler)}_stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null),this._visibilityHandler&&(document.removeEventListener("visibilitychange",this._visibilityHandler),this._visibilityHandler=null)}_setTab(t){this._activeTab=t,this.render()}_formatTokens(t){return!t||t===0?"0":t>=1e6?(t/1e6).toFixed(2)+"M":t>=1e3?(t/1e3).toFixed(1)+"K":String(t)}_formatUSD(t){return!t||t===0?"$0.00":t<.01?"<$0.01":"$"+t.toFixed(2)}_escapeHTML(t){return t?String(t).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""):""}_getGaugeColor(t){return t>80?"var(--loki-red)":t>=60?"var(--loki-yellow)":"var(--loki-green)"}_getGaugeColorClass(t){return t>80?"gauge-red":t>=60?"gauge-yellow":"gauge-green"}_renderGaugeTab(){let t=this._data?.current||{},e=this._data?.totals||{},a=t.context_window_pct||0,i=this._getGaugeColor(a),s=this._getGaugeColorClass(a),r=70,o=2*Math.PI*r,l=o-a/100*o;return`
|
|
5694
5694
|
<div class="gauge-tab">
|
|
5695
5695
|
<div class="gauge-container">
|
|
5696
5696
|
<svg class="gauge-svg" viewBox="0 0 180 180" aria-label="Context window usage: ${a.toFixed(1)}%">
|
package/docs/INSTALLATION.md
CHANGED
package/mcp/__init__.py
CHANGED
package/memory/embeddings.py
CHANGED
|
@@ -819,6 +819,7 @@ class EmbeddingEngine:
|
|
|
819
819
|
|
|
820
820
|
# Cache
|
|
821
821
|
self._cache: Dict[str, np.ndarray] = {}
|
|
822
|
+
self._max_cache_size = 10000
|
|
822
823
|
self._quality_cache: Dict[str, EmbeddingQuality] = {}
|
|
823
824
|
|
|
824
825
|
# Metrics
|
|
@@ -1008,6 +1009,10 @@ class EmbeddingEngine:
|
|
|
1008
1009
|
|
|
1009
1010
|
# Cache result
|
|
1010
1011
|
if self.config.cache_enabled:
|
|
1012
|
+
if len(self._cache) >= self._max_cache_size:
|
|
1013
|
+
# Remove oldest entry (first key in dict - insertion order in Python 3.7+)
|
|
1014
|
+
oldest_key = next(iter(self._cache))
|
|
1015
|
+
del self._cache[oldest_key]
|
|
1011
1016
|
self._cache[cache_key] = embedding
|
|
1012
1017
|
|
|
1013
1018
|
# Track latency
|
package/memory/engine.py
CHANGED
|
@@ -820,7 +820,8 @@ class MemoryEngine:
|
|
|
820
820
|
})
|
|
821
821
|
|
|
822
822
|
index["last_updated"] = datetime.now(timezone.utc).isoformat()
|
|
823
|
-
|
|
823
|
+
if not topic_found:
|
|
824
|
+
index["total_memories"] = index.get("total_memories", 0) + 1
|
|
824
825
|
|
|
825
826
|
self.storage.write_json("index.json", index)
|
|
826
827
|
|
package/memory/retrieval.py
CHANGED
|
@@ -912,6 +912,7 @@ class MemoryRetrieval:
|
|
|
912
912
|
|
|
913
913
|
for collection, items in results_by_collection.items():
|
|
914
914
|
for item in items:
|
|
915
|
+
item = dict(item) # shallow copy to avoid mutating original
|
|
915
916
|
# Ensure source is set
|
|
916
917
|
if "_source" not in item:
|
|
917
918
|
item["_source"] = collection
|
|
@@ -568,13 +568,24 @@ class TokenEconomics:
|
|
|
568
568
|
|
|
569
569
|
Writes to {base_path}/token_economics.json
|
|
570
570
|
"""
|
|
571
|
+
import tempfile
|
|
572
|
+
|
|
571
573
|
file_path = Path(self.base_path) / "token_economics.json"
|
|
572
574
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
573
575
|
|
|
574
576
|
data = self.get_summary()
|
|
575
577
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
+
tmp_fd, tmp_path = tempfile.mkstemp(dir=str(file_path.parent), suffix='.tmp')
|
|
579
|
+
try:
|
|
580
|
+
with os.fdopen(tmp_fd, 'w', encoding='utf-8') as f:
|
|
581
|
+
json.dump(data, f, indent=2)
|
|
582
|
+
os.replace(tmp_path, str(file_path))
|
|
583
|
+
except Exception:
|
|
584
|
+
try:
|
|
585
|
+
os.unlink(tmp_path)
|
|
586
|
+
except OSError:
|
|
587
|
+
pass
|
|
588
|
+
raise
|
|
578
589
|
|
|
579
590
|
def load(self) -> None:
|
|
580
591
|
"""
|