loki-mode 5.47.0 → 5.48.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/loki +9 -1
- package/autonomy/run.sh +8 -6
- 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.0
|
|
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.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.
|
|
1
|
+
5.48.0
|
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
|
|
@@ -3123,7 +3131,7 @@ cmd_api() {
|
|
|
3123
3131
|
if [ -n "${LOKI_TLS_CERT:-}" ] && [ -n "${LOKI_TLS_KEY:-}" ]; then
|
|
3124
3132
|
uvicorn_args="$uvicorn_args --ssl-certfile ${LOKI_TLS_CERT} --ssl-keyfile ${LOKI_TLS_KEY}"
|
|
3125
3133
|
fi
|
|
3126
|
-
LOKI_DIR="$LOKI_DIR" nohup python3 -m uvicorn dashboard.server:app $uvicorn_args > "$LOKI_DIR/logs/api.log" 2>&1 &
|
|
3134
|
+
LOKI_DIR="$LOKI_DIR" PYTHONPATH="$SKILL_DIR" nohup python3 -m uvicorn dashboard.server:app $uvicorn_args > "$LOKI_DIR/logs/api.log" 2>&1 &
|
|
3127
3135
|
local new_pid=$!
|
|
3128
3136
|
echo "$new_pid" > "$pid_file"
|
|
3129
3137
|
|
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
|
#===============================================================================
|
|
@@ -5874,7 +5876,7 @@ save_state() {
|
|
|
5874
5876
|
"status": "$status",
|
|
5875
5877
|
"lastExitCode": $exit_code,
|
|
5876
5878
|
"lastRun": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
5877
|
-
"prdPath": "${PRD_PATH:-}",
|
|
5879
|
+
"prdPath": "$(printf '%s' "${PRD_PATH:-}" | sed 's/\\/\\\\/g; s/"/\\"/g')",
|
|
5878
5880
|
"pid": $$,
|
|
5879
5881
|
"maxRetries": $MAX_RETRIES,
|
|
5880
5882
|
"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
|
"""
|