loki-mode 7.6.3 → 7.6.5
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 +57 -3
- package/autonomy/run.sh +68 -2
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/memory/engine.py +67 -0
- 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 a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.6.
|
|
6
|
+
# Loki Mode v7.6.5
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -381,4 +381,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
381
381
|
|
|
382
382
|
---
|
|
383
383
|
|
|
384
|
-
**v7.6.
|
|
384
|
+
**v7.6.5 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.6.
|
|
1
|
+
7.6.5
|
package/autonomy/loki
CHANGED
|
@@ -14212,9 +14212,63 @@ except Exception as e:
|
|
|
14212
14212
|
;;
|
|
14213
14213
|
|
|
14214
14214
|
economics)
|
|
14215
|
-
#
|
|
14216
|
-
|
|
14217
|
-
|
|
14215
|
+
# v7.6.6 B-3d fix: previously only read .loki/memory/token_economics.json
|
|
14216
|
+
# which is rarely written by current sessions, so users saw
|
|
14217
|
+
# "No token economics data" while `loki kpis` correctly read
|
|
14218
|
+
# .loki/metrics/efficiency/iter-*.json. Unify: prefer the
|
|
14219
|
+
# canonical kpis source; fall back to the legacy file for
|
|
14220
|
+
# backward compat with pre-v7.6.6 sessions.
|
|
14221
|
+
local _eff_dir=".loki/metrics/efficiency"
|
|
14222
|
+
local _legacy_file=".loki/memory/token_economics.json"
|
|
14223
|
+
local _have_iters=0
|
|
14224
|
+
if [ -d "$_eff_dir" ]; then
|
|
14225
|
+
if compgen -G "$_eff_dir/iter-*.json" >/dev/null 2>&1; then
|
|
14226
|
+
_have_iters=1
|
|
14227
|
+
fi
|
|
14228
|
+
fi
|
|
14229
|
+
if [ "$_have_iters" -eq 1 ]; then
|
|
14230
|
+
PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 - <<'PYEOF' 2>/dev/null
|
|
14231
|
+
import json, glob, os, sys
|
|
14232
|
+
files = sorted(glob.glob('.loki/metrics/efficiency/iter-*.json'))
|
|
14233
|
+
totals = {
|
|
14234
|
+
'source': '.loki/metrics/efficiency/iter-*.json',
|
|
14235
|
+
'iterations': len(files),
|
|
14236
|
+
'total_input_tokens': 0,
|
|
14237
|
+
'total_output_tokens': 0,
|
|
14238
|
+
'total_tokens': 0,
|
|
14239
|
+
'total_cost_usd': 0.0,
|
|
14240
|
+
'total_duration_ms': 0,
|
|
14241
|
+
'by_model': {},
|
|
14242
|
+
'by_phase': {},
|
|
14243
|
+
}
|
|
14244
|
+
def _num(x, cast=float, default=0):
|
|
14245
|
+
try:
|
|
14246
|
+
return cast(x) if x is not None else default
|
|
14247
|
+
except (TypeError, ValueError):
|
|
14248
|
+
return default
|
|
14249
|
+
for f in files:
|
|
14250
|
+
try:
|
|
14251
|
+
d = json.load(open(f))
|
|
14252
|
+
except Exception:
|
|
14253
|
+
continue
|
|
14254
|
+
ti = int(_num(d.get('input_tokens'), int))
|
|
14255
|
+
to = int(_num(d.get('output_tokens'), int))
|
|
14256
|
+
co = float(_num(d.get('cost_usd'), float))
|
|
14257
|
+
du = int(_num(d.get('duration_ms'), int))
|
|
14258
|
+
totals['total_input_tokens'] += ti
|
|
14259
|
+
totals['total_output_tokens'] += to
|
|
14260
|
+
totals['total_tokens'] += ti + to
|
|
14261
|
+
totals['total_cost_usd'] += co
|
|
14262
|
+
totals['total_duration_ms'] += du
|
|
14263
|
+
m = d.get('model') or 'unknown'
|
|
14264
|
+
totals['by_model'][m] = totals['by_model'].get(m, 0) + 1
|
|
14265
|
+
p = d.get('phase') or 'unknown'
|
|
14266
|
+
totals['by_phase'][p] = totals['by_phase'].get(p, 0) + 1
|
|
14267
|
+
print(json.dumps(totals, indent=2))
|
|
14268
|
+
PYEOF
|
|
14269
|
+
elif [ -f "$_legacy_file" ]; then
|
|
14270
|
+
# Backward compat: pre-v7.6.6 sessions wrote here.
|
|
14271
|
+
cat "$_legacy_file" | python3 -m json.tool 2>/dev/null || echo "Error parsing token economics JSON"
|
|
14218
14272
|
else
|
|
14219
14273
|
echo "No token economics data. Run a session first."
|
|
14220
14274
|
fi
|
package/autonomy/run.sh
CHANGED
|
@@ -8749,14 +8749,46 @@ auto_capture_episode() {
|
|
|
8749
8749
|
return
|
|
8750
8750
|
fi
|
|
8751
8751
|
|
|
8752
|
-
#
|
|
8752
|
+
# v7.6.4 B-3a fix: previously `git diff --name-only HEAD` only captured
|
|
8753
|
+
# UNSTAGED changes -- always empty after loki's per-iteration auto-commit
|
|
8754
|
+
# rolled the new files into HEAD. Now diff against the iteration-start
|
|
8755
|
+
# SHA captured at the top of the retry loop. Falls back to HEAD~1 if the
|
|
8756
|
+
# start SHA env is unset (older direct callers).
|
|
8753
8757
|
local files_modified=""
|
|
8754
|
-
|
|
8758
|
+
local _diff_base="${_LOKI_ITER_START_SHA:-}"
|
|
8759
|
+
if [ -z "$_diff_base" ]; then
|
|
8760
|
+
_diff_base=$(cd "$target_dir" && git rev-parse HEAD~1 2>/dev/null || echo "")
|
|
8761
|
+
fi
|
|
8762
|
+
if [ -n "$_diff_base" ]; then
|
|
8763
|
+
files_modified=$(cd "$target_dir" && git diff --name-only "$_diff_base" HEAD 2>/dev/null | head -50 | tr '\n' '|' || true)
|
|
8764
|
+
# Also include unstaged changes (in case auto-commit didn't run)
|
|
8765
|
+
local _unstaged
|
|
8766
|
+
_unstaged=$(cd "$target_dir" && git diff --name-only HEAD 2>/dev/null | head -20 | tr '\n' '|' || true)
|
|
8767
|
+
if [ -n "$_unstaged" ]; then
|
|
8768
|
+
files_modified="${files_modified}${_unstaged}"
|
|
8769
|
+
fi
|
|
8770
|
+
else
|
|
8771
|
+
# No git history yet (initial commit) -- list all tracked + untracked.
|
|
8772
|
+
files_modified=$(cd "$target_dir" && git ls-files --others --exclude-standard 2>/dev/null | head -50 | tr '\n' '|' || true)
|
|
8773
|
+
fi
|
|
8755
8774
|
|
|
8756
8775
|
# Collect last git commit if any
|
|
8757
8776
|
local git_commit=""
|
|
8758
8777
|
git_commit=$(cd "$target_dir" && git rev-parse --short HEAD 2>/dev/null || true)
|
|
8759
8778
|
|
|
8779
|
+
# v7.6.4 B-3a fix: aggregate per-iteration efficiency metrics into the
|
|
8780
|
+
# episode so `tokens_used` / `cost_usd` / `input_tokens` / `output_tokens`
|
|
8781
|
+
# are no longer always 0. Source: `.loki/metrics/efficiency/iter-N.json`
|
|
8782
|
+
# (same source `loki kpis` reads from).
|
|
8783
|
+
local _iter_metrics_file="$target_dir/.loki/metrics/efficiency/iter-${iteration}.json"
|
|
8784
|
+
local _iter_tokens_in=0 _iter_tokens_out=0 _iter_cost=0
|
|
8785
|
+
if [ -f "$_iter_metrics_file" ]; then
|
|
8786
|
+
_iter_tokens_in=$(python3 -c "import json; d=json.load(open('$_iter_metrics_file')); print(int(d.get('input_tokens', 0) or 0))" 2>/dev/null || echo 0)
|
|
8787
|
+
_iter_tokens_out=$(python3 -c "import json; d=json.load(open('$_iter_metrics_file')); print(int(d.get('output_tokens', 0) or 0))" 2>/dev/null || echo 0)
|
|
8788
|
+
_iter_cost=$(python3 -c "import json; d=json.load(open('$_iter_metrics_file')); print(float(d.get('cost_usd', 0) or 0))" 2>/dev/null || echo 0)
|
|
8789
|
+
fi
|
|
8790
|
+
local _iter_tokens_total=$((_iter_tokens_in + _iter_tokens_out))
|
|
8791
|
+
|
|
8760
8792
|
# Determine outcome
|
|
8761
8793
|
local outcome="success"
|
|
8762
8794
|
if [ "$exit_code" -ne 0 ]; then
|
|
@@ -8774,6 +8806,8 @@ auto_capture_episode() {
|
|
|
8774
8806
|
_LOKI_DURATION="$duration" _LOKI_OUTCOME="$outcome" \
|
|
8775
8807
|
_LOKI_FILES_MODIFIED="$files_modified" _LOKI_GIT_COMMIT="$git_commit" \
|
|
8776
8808
|
_LOKI_EPISODE_PATH_FILE="$episode_path_file" \
|
|
8809
|
+
_LOKI_TOKENS_IN="$_iter_tokens_in" _LOKI_TOKENS_OUT="$_iter_tokens_out" \
|
|
8810
|
+
_LOKI_TOKENS_TOTAL="$_iter_tokens_total" _LOKI_COST_USD="$_iter_cost" \
|
|
8777
8811
|
python3 << 'PYEOF' 2>/dev/null || true
|
|
8778
8812
|
import sys
|
|
8779
8813
|
import os
|
|
@@ -8811,6 +8845,32 @@ try:
|
|
|
8811
8845
|
trace.git_commit = git_commit if git_commit else None
|
|
8812
8846
|
trace.files_modified = [f for f in files_modified.split('|') if f] if files_modified else []
|
|
8813
8847
|
|
|
8848
|
+
# v7.6.4 B-3a + B-3b fix: hydrate tokens + cost from the iteration's
|
|
8849
|
+
# efficiency metrics file (same source `loki kpis` reads). Backward
|
|
8850
|
+
# compat: zero stays zero on missing metrics.
|
|
8851
|
+
try:
|
|
8852
|
+
trace.tokens_used = int(os.environ.get('_LOKI_TOKENS_TOTAL', '0') or 0)
|
|
8853
|
+
except (TypeError, ValueError):
|
|
8854
|
+
trace.tokens_used = 0
|
|
8855
|
+
# Try to set the input/output/cost fields if the schema accepts them.
|
|
8856
|
+
for attr, env_key, caster in (
|
|
8857
|
+
('input_tokens', '_LOKI_TOKENS_IN', int),
|
|
8858
|
+
('output_tokens', '_LOKI_TOKENS_OUT', int),
|
|
8859
|
+
('cost_usd', '_LOKI_COST_USD', float),
|
|
8860
|
+
):
|
|
8861
|
+
try:
|
|
8862
|
+
value = caster(os.environ.get(env_key, '0') or 0)
|
|
8863
|
+
setattr(trace, attr, value)
|
|
8864
|
+
except (AttributeError, TypeError, ValueError):
|
|
8865
|
+
pass
|
|
8866
|
+
# files_modified -> artifacts_produced shadow (so .artifacts_produced
|
|
8867
|
+
# reflects what was created if the schema has that field separately).
|
|
8868
|
+
try:
|
|
8869
|
+
if not getattr(trace, 'artifacts_produced', None):
|
|
8870
|
+
trace.artifacts_produced = list(trace.files_modified)
|
|
8871
|
+
except AttributeError:
|
|
8872
|
+
pass
|
|
8873
|
+
|
|
8814
8874
|
engine.store_episode(trace)
|
|
8815
8875
|
|
|
8816
8876
|
# v6.83.0: surface the on-disk episode path + importance so bash can
|
|
@@ -10711,6 +10771,12 @@ except Exception as exc:
|
|
|
10711
10771
|
|
|
10712
10772
|
save_state $retry "running" 0
|
|
10713
10773
|
|
|
10774
|
+
# v7.6.4 B-3a fix: capture iteration-start git SHA so auto_capture_episode
|
|
10775
|
+
# can diff against this baseline (not just HEAD, which is empty after
|
|
10776
|
+
# loki's per-iteration auto-commit makes the new files HEAD).
|
|
10777
|
+
_LOKI_ITER_START_SHA=$(cd "${TARGET_DIR:-.}" && git rev-parse HEAD 2>/dev/null || echo "")
|
|
10778
|
+
export _LOKI_ITER_START_SHA
|
|
10779
|
+
|
|
10714
10780
|
# Run AI provider with live output
|
|
10715
10781
|
local start_time=$(date +%s)
|
|
10716
10782
|
local log_file=".loki/logs/autonomy-$(date +%Y%m%d).log"
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var _7=Object.defineProperty;var I7=(K)=>K;function P7(K,$){this[K]=I7.bind(null,$)}var b=(K,$)=>{for(var z in $)_7(K,z,{get:$[z],enumerable:!0,configurable:!0,set:P7.bind($,z)})};var L=(K,$)=>()=>(K&&($=K(K=0)),$);var V1=import.meta.require;var e1={};b(e1,{lokiDir:()=>P,homeLokiDir:()=>k1,findRepoRootForVersion:()=>N1,REPO_ROOT:()=>u});import{resolve as f,dirname as S1}from"path";import{fileURLToPath as L7}from"url";import{existsSync as J1}from"fs";import{homedir as R7}from"os";function E7(){let K=i1;for(let $=0;$<6;$++){if(J1(f(K,"VERSION"))&&J1(f(K,"autonomy/run.sh")))return K;let z=S1(K);if(z===K)break;K=z}return f(i1,"..","..","..")}function N1(K){let $=K;for(let z=0;z<6;z++){if(J1(f($,"VERSION"))&&J1(f($,"autonomy/run.sh")))return $;let Q=S1($);if(Q===$)break;$=Q}return f(K,"..","..","..")}function P(){return process.env.LOKI_DIR??f(process.cwd(),".loki")}function k1(){return f(R7(),".loki")}var i1,u;var y=L(()=>{i1=S1(L7(import.meta.url));u=E7()});import{readFileSync as x7}from"fs";import{resolve as F7,dirname as w7}from"path";import{fileURLToPath as S7}from"url";function G1(){if(o!==null)return o;let K="7.6.
|
|
2
|
+
var _7=Object.defineProperty;var I7=(K)=>K;function P7(K,$){this[K]=I7.bind(null,$)}var b=(K,$)=>{for(var z in $)_7(K,z,{get:$[z],enumerable:!0,configurable:!0,set:P7.bind($,z)})};var L=(K,$)=>()=>(K&&($=K(K=0)),$);var V1=import.meta.require;var e1={};b(e1,{lokiDir:()=>P,homeLokiDir:()=>k1,findRepoRootForVersion:()=>N1,REPO_ROOT:()=>u});import{resolve as f,dirname as S1}from"path";import{fileURLToPath as L7}from"url";import{existsSync as J1}from"fs";import{homedir as R7}from"os";function E7(){let K=i1;for(let $=0;$<6;$++){if(J1(f(K,"VERSION"))&&J1(f(K,"autonomy/run.sh")))return K;let z=S1(K);if(z===K)break;K=z}return f(i1,"..","..","..")}function N1(K){let $=K;for(let z=0;z<6;z++){if(J1(f($,"VERSION"))&&J1(f($,"autonomy/run.sh")))return $;let Q=S1($);if(Q===$)break;$=Q}return f(K,"..","..","..")}function P(){return process.env.LOKI_DIR??f(process.cwd(),".loki")}function k1(){return f(R7(),".loki")}var i1,u;var y=L(()=>{i1=S1(L7(import.meta.url));u=E7()});import{readFileSync as x7}from"fs";import{resolve as F7,dirname as w7}from"path";import{fileURLToPath as S7}from"url";function G1(){if(o!==null)return o;let K="7.6.5";if(typeof K==="string"&&K.length>0)return o=K,o;try{let $=w7(S7(import.meta.url)),z=N1($);o=x7(F7(z,"VERSION"),"utf-8").trim()}catch{o="unknown"}return o}var o=null;var D1=L(()=>{y()});var $0={};b($0,{runOrThrow:()=>N7,run:()=>w,commandVersion:()=>D7,commandExists:()=>h,ShellError:()=>C1});async function w(K,$={}){let z=Bun.spawn({cmd:[...K],stdout:"pipe",stderr:"pipe",env:$.env?{...process.env,...$.env}:process.env,cwd:$.cwd}),Q,X;if($.timeoutMs&&$.timeoutMs>0)Q=setTimeout(()=>{try{z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{z.kill("SIGKILL")}catch{}},2000)},$.timeoutMs);try{let[H,Z,q]=await Promise.all([new Response(z.stdout).text(),new Response(z.stderr).text(),z.exited]);return{stdout:H,stderr:Z,exitCode:q}}finally{if(Q)clearTimeout(Q);if(X)clearTimeout(X)}}async function N7(K,$={}){let z=await w(K,$);if(z.exitCode!==0)throw new C1(`command failed (${z.exitCode}): ${K.join(" ")}`,z.exitCode,z.stdout,z.stderr);return z}async function h(K){let $=k7(K),z=await w(["sh","-c",`command -v ${$}`],{timeoutMs:5000});if(z.exitCode===0)return z.stdout.trim()||null;return null}function k7(K){if(!/^[A-Za-z0-9._/-]+$/.test(K))throw Error(`refused to shell-escape suspect token: ${K}`);return K}async function D7(K,$="--version"){if(!await h(K))return null;let Q=await w([K,$],{timeoutMs:5000});if(Q.exitCode!==0)return null;return((Q.stdout||Q.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var C1;var p=L(()=>{C1=class C1 extends Error{message;exitCode;stdout;stderr;constructor(K,$,z,Q){super(K);this.message=K;this.exitCode=$;this.stdout=z;this.stderr=Q;this.name="ShellError"}}});function c(K){return C7?"":K}var C7,R,D,E,O6,O,k,x,W;var n=L(()=>{C7=(process.env.NO_COLOR??"").length>0;R=c("\x1B[0;31m"),D=c("\x1B[0;32m"),E=c("\x1B[1;33m"),O6=c("\x1B[0;34m"),O=c("\x1B[0;36m"),k=c("\x1B[1m"),x=c("\x1B[2m"),W=c("\x1B[0m")});import{existsSync as c7}from"fs";async function r(){if(z1!==void 0)return z1;let K="/opt/homebrew/bin/python3.12";if(c7(K))return z1=K,K;let $=await h("python3.12");if($)return z1=$,$;let z=await h("python3");return z1=z,z}async function a(K,$={}){let z=await r();if(!z)return{stdout:"",stderr:"python3 not found",exitCode:127};return w([z,"-c",K],$)}var z1;var Q1=L(()=>{p()});var G0={};b(G0,{runStatus:()=>z5});import{existsSync as S,readFileSync as Z1,readdirSync as H0,statSync as W0}from"fs";import{resolve as F,basename as a7}from"path";async function r7(){if(await h("jq"))return!0;return process.stdout.write(`${R}Error: jq is required but not installed.${W}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -534,4 +534,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
534
534
|
`),2}default:return process.stderr.write(`Unknown command: ${$}
|
|
535
535
|
`),process.stderr.write(j7),2}}process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var z6=await $6(Bun.argv.slice(2));process.exit(z6);
|
|
536
536
|
|
|
537
|
-
//# debugId=
|
|
537
|
+
//# debugId=062EAA592CDADAB764756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/memory/engine.py
CHANGED
|
@@ -308,12 +308,79 @@ class MemoryEngine:
|
|
|
308
308
|
# Update timeline with action summary
|
|
309
309
|
self._update_timeline_with_episode(trace_dict)
|
|
310
310
|
|
|
311
|
+
# v7.6.5 B-3c fix: previously index.json topics were ONLY populated by
|
|
312
|
+
# consolidated patterns, so a real session that wrote 5 episodes left
|
|
313
|
+
# topics:[] until the user manually ran `loki memory consolidate`.
|
|
314
|
+
# Now every episode also stamps a lightweight topic into index.json
|
|
315
|
+
# derived from its phase + goal keywords. Topic count grows
|
|
316
|
+
# monotonically with episodes; consolidation still refines them.
|
|
317
|
+
self._update_index_with_episode(trace_dict)
|
|
318
|
+
|
|
311
319
|
# Queue for embedding if embeddings are enabled
|
|
312
320
|
if self._embedding_func is not None:
|
|
313
321
|
self._queue_for_embedding(episode_id, "episodic", trace_dict)
|
|
314
322
|
|
|
315
323
|
return episode_id
|
|
316
324
|
|
|
325
|
+
def _update_index_with_episode(self, episode: Dict[str, Any]) -> None:
|
|
326
|
+
"""Stamp a lightweight topic into index.json from an episode.
|
|
327
|
+
|
|
328
|
+
v7.6.5 B-3c fix. Keeps the index alive between consolidation cycles
|
|
329
|
+
so the dashboard Memory Files panel and `loki memory index` show
|
|
330
|
+
real topics immediately after a session ends.
|
|
331
|
+
"""
|
|
332
|
+
try:
|
|
333
|
+
index = self.storage.read_json("index.json") or {
|
|
334
|
+
"version": "1.1.0",
|
|
335
|
+
"topics": [],
|
|
336
|
+
"total_memories": 0,
|
|
337
|
+
}
|
|
338
|
+
context = episode.get("context", {}) if isinstance(episode.get("context"), dict) else {}
|
|
339
|
+
phase = (context.get("phase") or episode.get("phase") or "general").lower()
|
|
340
|
+
goal = (context.get("goal") or episode.get("goal") or "")[:200]
|
|
341
|
+
# Topic id = phase. Multiple episodes in the same phase share a topic.
|
|
342
|
+
topic_id = phase or "general"
|
|
343
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
344
|
+
episode_id = episode.get("id")
|
|
345
|
+
cost = float(episode.get("cost_usd", 0) or 0)
|
|
346
|
+
tokens = int(episode.get("tokens_used", 0) or 0)
|
|
347
|
+
files = list(episode.get("files_modified", []) or [])
|
|
348
|
+
|
|
349
|
+
found = None
|
|
350
|
+
for topic in index.get("topics", []):
|
|
351
|
+
if topic.get("id") == topic_id:
|
|
352
|
+
found = topic
|
|
353
|
+
break
|
|
354
|
+
if found is None:
|
|
355
|
+
index.setdefault("topics", []).append({
|
|
356
|
+
"id": topic_id,
|
|
357
|
+
"summary": goal or f"Activity in phase {topic_id}",
|
|
358
|
+
"episode_ids": [episode_id] if episode_id else [],
|
|
359
|
+
"episode_count": 1,
|
|
360
|
+
"total_cost_usd": cost,
|
|
361
|
+
"total_tokens": tokens,
|
|
362
|
+
"files_touched": files[:20],
|
|
363
|
+
"first_seen": now,
|
|
364
|
+
"last_accessed": now,
|
|
365
|
+
"relevance_score": 0.5,
|
|
366
|
+
})
|
|
367
|
+
index["total_memories"] = index.get("total_memories", 0) + 1
|
|
368
|
+
else:
|
|
369
|
+
if episode_id and episode_id not in found.get("episode_ids", []):
|
|
370
|
+
found.setdefault("episode_ids", []).append(episode_id)
|
|
371
|
+
found["episode_count"] = found.get("episode_count", 0) + 1
|
|
372
|
+
found["total_cost_usd"] = float(found.get("total_cost_usd", 0) or 0) + cost
|
|
373
|
+
found["total_tokens"] = int(found.get("total_tokens", 0) or 0) + tokens
|
|
374
|
+
merged = set(found.get("files_touched", []) or []) | set(files[:20])
|
|
375
|
+
found["files_touched"] = sorted(merged)[:50]
|
|
376
|
+
found["last_accessed"] = now
|
|
377
|
+
|
|
378
|
+
index["last_updated"] = now
|
|
379
|
+
self.storage.write_json("index.json", index)
|
|
380
|
+
except Exception: # noqa: BLE001
|
|
381
|
+
# Never let index update break episode storage.
|
|
382
|
+
pass
|
|
383
|
+
|
|
317
384
|
def get_episode(self, episode_id: str) -> Optional[EpisodeTrace]:
|
|
318
385
|
"""
|
|
319
386
|
Retrieve an episode by ID.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
|
-
"version": "7.6.
|
|
3
|
+
"version": "7.6.5",
|
|
4
4
|
"description": "Loki Mode by Autonomi. Multi-agent autonomous SDLC framework. Spec to deployed app: PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief. 4 AI providers (Claude Code, OpenAI Codex, Cline, Aider). 11 quality gates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|