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 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.47.0
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.47.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
265
+ **v5.48.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 5.47.0
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=$(echo "$event_data" | sed 's/"/\\"/g' | tr -d '\n')
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=$(echo "$value" | sed 's/"/\\"/g')
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
- echo "$agents_json" > "$output_file"
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
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "5.47.0"
10
+ __version__ = "5.48.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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
- MAX_CONNECTIONS = int(os.environ.get("LOKI_MAX_WS_CONNECTIONS", "100"))
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
- for raw_line in events_file.read_text().strip().split("\n"):
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
- try:
2400
- for line in convergence_file.read_text().strip().split("\n"):
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
- except Exception:
2411
- pass
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": int(os.environ.get("LOKI_GITHUB_LIMIT", "100")),
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 = int(os.environ.get("LOKI_MAX_ITERATIONS", "1000"))
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 = int(os.environ.get("LOKI_DASHBOARD_PORT", "57374"))
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"):""}};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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"):""}};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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"):""}_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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"):""}_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)}%">
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v5.47.0
5
+ **Version:** v5.48.0
6
6
 
7
7
  ---
8
8
 
package/mcp/__init__.py CHANGED
@@ -21,4 +21,4 @@ try:
21
21
  except ImportError:
22
22
  __all__ = ['mcp']
23
23
 
24
- __version__ = '5.47.0'
24
+ __version__ = '5.48.0'
@@ -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
- index["total_memories"] = index.get("total_memories", 0) + 1
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
 
@@ -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
- with open(file_path, "w", encoding="utf-8") as f:
577
- json.dump(data, f, indent=2)
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
  """
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "5.47.0",
3
+ "version": "5.48.0",
4
4
  "description": "Loki Mode by Autonomi - Multi-agent autonomous startup system for Claude Code, Codex CLI, and Gemini CLI",
5
5
  "keywords": [
6
6
  "autonomi",