loki-mode 6.4.0 → 6.5.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/README.md +1 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/hooks/migration-hooks.sh +6 -2
- package/autonomy/loki +66 -40
- package/autonomy/run.sh +23 -19
- package/autonomy/sandbox.sh +28 -22
- package/autonomy/telemetry.sh +20 -7
- package/dashboard/__init__.py +1 -1
- package/dashboard/migration_engine.py +1 -1
- package/dashboard/server.py +24 -11
- package/docs/INSTALLATION.md +2 -2
- package/events/emit.sh +1 -1
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +11 -6
- package/memory/embeddings.py +15 -6
- package/memory/engine.py +56 -0
- package/memory/schemas.py +6 -2
- package/memory/storage.py +5 -2
- package/memory/token_economics.py +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
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 minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v6.
|
|
6
|
+
# Loki Mode v6.5.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -263,4 +263,4 @@ The following features are documented in skill modules but not yet fully automat
|
|
|
263
263
|
| Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
|
|
264
264
|
| Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
|
|
265
265
|
|
|
266
|
-
**v6.
|
|
266
|
+
**v6.5.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.
|
|
1
|
+
6.5.0
|
|
@@ -52,7 +52,7 @@ load_migration_hook_config() {
|
|
|
52
52
|
# Parse YAML config safely using read/declare instead of eval
|
|
53
53
|
while IFS='=' read -r key val; do
|
|
54
54
|
case "$key" in
|
|
55
|
-
HOOK_*)
|
|
55
|
+
HOOK_*) printf -v "$key" '%s' "$val" ;;
|
|
56
56
|
esac
|
|
57
57
|
done < <(python3 -c "
|
|
58
58
|
import sys
|
|
@@ -224,7 +224,7 @@ try:
|
|
|
224
224
|
print(len([s for s in steps if s.get('status') != 'completed']))
|
|
225
225
|
except: print(-1)
|
|
226
226
|
" 2>/dev/null || echo -1)
|
|
227
|
-
[[ "$pending" -
|
|
227
|
+
[[ "$pending" -ne 0 ]] && echo "GATE_BLOCKED: ${pending} steps still pending (or plan missing)" && return 1
|
|
228
228
|
;;
|
|
229
229
|
esac
|
|
230
230
|
|
|
@@ -236,6 +236,10 @@ hook_on_agent_stop() {
|
|
|
236
236
|
local features_path="${LOKI_FEATURES_PATH:-}"
|
|
237
237
|
|
|
238
238
|
[[ "$HOOK_ON_AGENT_STOP_ENABLED" != "true" ]] && return 0
|
|
239
|
+
if [[ -z "$features_path" ]]; then
|
|
240
|
+
echo "HOOK_BLOCKED: LOKI_FEATURES_PATH not set. Cannot verify features."
|
|
241
|
+
return 1
|
|
242
|
+
fi
|
|
239
243
|
[[ ! -f "$features_path" ]] && return 0
|
|
240
244
|
|
|
241
245
|
local failing
|
package/autonomy/loki
CHANGED
|
@@ -1356,7 +1356,7 @@ cmd_status() {
|
|
|
1356
1356
|
[ -n "$line" ] && running_sessions+=("$line")
|
|
1357
1357
|
done < <(list_running_sessions 2>/dev/null)
|
|
1358
1358
|
|
|
1359
|
-
if [ ${#running_sessions[@]} -gt
|
|
1359
|
+
if [ ${#running_sessions[@]} -gt 0 ]; then
|
|
1360
1360
|
echo -e "${GREEN}Active Sessions: ${#running_sessions[@]}${NC}"
|
|
1361
1361
|
for entry in "${running_sessions[@]}"; do
|
|
1362
1362
|
local sid="${entry%%:*}"
|
|
@@ -2092,7 +2092,7 @@ cmd_dashboard_stop() {
|
|
|
2092
2092
|
local wait_count=0
|
|
2093
2093
|
while kill -0 "$pid" 2>/dev/null && [ $wait_count -lt 10 ]; do
|
|
2094
2094
|
sleep 0.5
|
|
2095
|
-
((wait_count
|
|
2095
|
+
wait_count=$((wait_count + 1))
|
|
2096
2096
|
done
|
|
2097
2097
|
|
|
2098
2098
|
# Force kill if still running
|
|
@@ -2874,20 +2874,17 @@ cmd_run() {
|
|
|
2874
2874
|
--pr)
|
|
2875
2875
|
use_worktree=true
|
|
2876
2876
|
create_pr=true
|
|
2877
|
-
start_args+=("--parallel")
|
|
2878
2877
|
shift
|
|
2879
2878
|
;;
|
|
2880
2879
|
--ship)
|
|
2881
2880
|
use_worktree=true
|
|
2882
2881
|
create_pr=true
|
|
2883
2882
|
auto_merge=true
|
|
2884
|
-
start_args+=("--parallel")
|
|
2885
2883
|
shift
|
|
2886
2884
|
;;
|
|
2887
2885
|
--detach|-d)
|
|
2888
2886
|
use_worktree=true
|
|
2889
2887
|
run_detached=true
|
|
2890
|
-
start_args+=("--parallel")
|
|
2891
2888
|
shift
|
|
2892
2889
|
;;
|
|
2893
2890
|
-*)
|
|
@@ -2904,6 +2901,11 @@ cmd_run() {
|
|
|
2904
2901
|
esac
|
|
2905
2902
|
done
|
|
2906
2903
|
|
|
2904
|
+
# Add --parallel once if worktree mode is enabled (not per-flag)
|
|
2905
|
+
if $use_worktree; then
|
|
2906
|
+
start_args+=("--parallel")
|
|
2907
|
+
fi
|
|
2908
|
+
|
|
2907
2909
|
if [[ -z "$issue_ref" ]]; then
|
|
2908
2910
|
echo -e "${RED}Error: Issue reference required${NC}"
|
|
2909
2911
|
echo ""
|
|
@@ -2971,6 +2973,13 @@ cmd_run() {
|
|
|
2971
2973
|
|
|
2972
2974
|
# Detached mode: fork to background
|
|
2973
2975
|
if $run_detached; then
|
|
2976
|
+
# Guard: prevent launching duplicate session
|
|
2977
|
+
if is_session_running "$session_id"; then
|
|
2978
|
+
echo -e "${RED}Error: Session '$session_id' is already running.${NC}"
|
|
2979
|
+
echo -e "Stop it first with: ${CYAN}loki stop $session_id${NC}"
|
|
2980
|
+
exit 1
|
|
2981
|
+
fi
|
|
2982
|
+
|
|
2974
2983
|
local log_file="$LOKI_DIR/logs/run-${number:-$(date +%s)}.log"
|
|
2975
2984
|
mkdir -p "$(dirname "$log_file")"
|
|
2976
2985
|
echo -e "${GREEN}Running detached. Logs: $log_file${NC}"
|
|
@@ -2985,32 +2994,51 @@ cmd_run() {
|
|
|
2985
2994
|
branch_name="issue/detach-$(date +%s)"
|
|
2986
2995
|
fi
|
|
2987
2996
|
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
2997
|
+
# Write a temp script to avoid shell injection via variable interpolation
|
|
2998
|
+
local run_script="$LOKI_DIR/scripts/run-${number:-detached}.sh"
|
|
2999
|
+
mkdir -p "$LOKI_DIR/scripts"
|
|
3000
|
+
local loki_cmd
|
|
3001
|
+
loki_cmd="$(command -v loki || echo "$0")"
|
|
3002
|
+
cat > "$run_script" << 'INNER_SCRIPT_EOF'
|
|
3003
|
+
#!/usr/bin/env bash
|
|
3004
|
+
set -euo pipefail
|
|
3005
|
+
cd "$LOKI_RUN_DIR"
|
|
3006
|
+
export LOKI_DETACHED=true
|
|
3007
|
+
export LOKI_PARALLEL_MODE=true
|
|
3008
|
+
"$LOKI_CMD" start "$LOKI_PRD_PATH" ${LOKI_START_ARGS:-}
|
|
3009
|
+
|
|
3010
|
+
# Post-completion: create PR if requested
|
|
3011
|
+
if [[ "$LOKI_CREATE_PR" == "true" ]]; then
|
|
3012
|
+
branch_current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
3013
|
+
if [[ -n "$branch_current" && "$branch_current" != "main" && "$branch_current" != "master" ]]; then
|
|
3014
|
+
git push origin "$branch_current" 2>/dev/null || true
|
|
3015
|
+
gh pr create --title "$LOKI_PR_TITLE" --body "Implemented by Loki Mode" --head "$branch_current" 2>/dev/null || true
|
|
3016
|
+
fi
|
|
3017
|
+
fi
|
|
3018
|
+
# Post-completion: auto-merge if requested
|
|
3019
|
+
if [[ "$LOKI_AUTO_MERGE" == "true" ]]; then
|
|
3020
|
+
branch_current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
3021
|
+
if gh pr merge "$branch_current" --squash --delete-branch 2>/dev/null; then
|
|
3022
|
+
if [[ -n "${LOKI_ISSUE_NUMBER:-}" ]]; then
|
|
3023
|
+
gh issue close "$LOKI_ISSUE_NUMBER" --comment "Resolved by Loki Mode" 2>/dev/null || true
|
|
3024
|
+
fi
|
|
3025
|
+
fi
|
|
3026
|
+
fi
|
|
3027
|
+
INNER_SCRIPT_EOF
|
|
3028
|
+
chmod +x "$run_script"
|
|
3029
|
+
|
|
3030
|
+
# Pass all variables safely via environment
|
|
3031
|
+
LOKI_RUN_DIR="$(pwd)" \
|
|
3032
|
+
LOKI_CMD="$loki_cmd" \
|
|
3033
|
+
LOKI_SESSION_ID="$session_id" \
|
|
3034
|
+
LOKI_WORKTREE_BRANCH="$branch_name" \
|
|
3035
|
+
LOKI_PRD_PATH="$detach_prd" \
|
|
3036
|
+
LOKI_START_ARGS="${start_args[*]+"${start_args[*]}"}" \
|
|
3037
|
+
LOKI_CREATE_PR="$create_pr" \
|
|
3038
|
+
LOKI_AUTO_MERGE="$auto_merge" \
|
|
3039
|
+
LOKI_PR_TITLE="${title:-Implementation for issue ${issue_ref}}" \
|
|
3040
|
+
LOKI_ISSUE_NUMBER="${number:-}" \
|
|
3041
|
+
nohup bash "$run_script" > "$log_file" 2>&1 &
|
|
3014
3042
|
|
|
3015
3043
|
local bg_pid=$!
|
|
3016
3044
|
echo "$bg_pid" > "$LOKI_DIR/run-${number:-detached}.pid"
|
|
@@ -4946,7 +4974,7 @@ cmd_notify_test() {
|
|
|
4946
4974
|
echo -n " Slack... "
|
|
4947
4975
|
if send_slack_notification "$message" "Test"; then
|
|
4948
4976
|
echo -e "${GREEN}OK${NC}"
|
|
4949
|
-
((channels_notified
|
|
4977
|
+
channels_notified=$((channels_notified + 1))
|
|
4950
4978
|
else
|
|
4951
4979
|
echo -e "${RED}FAILED${NC}"
|
|
4952
4980
|
fi
|
|
@@ -4959,7 +4987,7 @@ cmd_notify_test() {
|
|
|
4959
4987
|
echo -n " Discord... "
|
|
4960
4988
|
if send_discord_notification "$message" "Test"; then
|
|
4961
4989
|
echo -e "${GREEN}OK${NC}"
|
|
4962
|
-
((channels_notified
|
|
4990
|
+
channels_notified=$((channels_notified + 1))
|
|
4963
4991
|
else
|
|
4964
4992
|
echo -e "${RED}FAILED${NC}"
|
|
4965
4993
|
fi
|
|
@@ -4972,7 +5000,7 @@ cmd_notify_test() {
|
|
|
4972
5000
|
echo -n " Webhook... "
|
|
4973
5001
|
if send_webhook_notification "$message" "Test"; then
|
|
4974
5002
|
echo -e "${GREEN}OK${NC}"
|
|
4975
|
-
((channels_notified
|
|
5003
|
+
channels_notified=$((channels_notified + 1))
|
|
4976
5004
|
else
|
|
4977
5005
|
echo -e "${RED}FAILED${NC}"
|
|
4978
5006
|
fi
|
|
@@ -6691,7 +6719,7 @@ Tasks:
|
|
|
6691
6719
|
local provider_name="${LOKI_PROVIDER:-claude}"
|
|
6692
6720
|
case "$provider_name" in
|
|
6693
6721
|
claude)
|
|
6694
|
-
(cd "$codebase_path" && claude --dangerously-skip-permissions -p "$phase_prompt" --output-format stream-json --verbose 2>&1) | \
|
|
6722
|
+
{ (cd "$codebase_path" && claude --dangerously-skip-permissions -p "$phase_prompt" --output-format stream-json --verbose 2>&1) | \
|
|
6695
6723
|
while IFS= read -r line; do
|
|
6696
6724
|
# Extract text from stream-json
|
|
6697
6725
|
if echo "$line" | python3 -c "
|
|
@@ -6706,8 +6734,7 @@ except Exception: pass
|
|
|
6706
6734
|
" 2>/dev/null; then
|
|
6707
6735
|
true
|
|
6708
6736
|
fi
|
|
6709
|
-
done
|
|
6710
|
-
phase_exit=${PIPESTATUS[0]}
|
|
6737
|
+
done; } && phase_exit=0 || phase_exit=$?
|
|
6711
6738
|
;;
|
|
6712
6739
|
codex)
|
|
6713
6740
|
(cd "$codebase_path" && codex exec --full-auto "$phase_prompt" 2>&1) || phase_exit=$?
|
|
@@ -6854,7 +6881,7 @@ IMPORTANT RULES:
|
|
|
6854
6881
|
local provider_name="${LOKI_PROVIDER:-claude}"
|
|
6855
6882
|
case "$provider_name" in
|
|
6856
6883
|
claude)
|
|
6857
|
-
(cd "$codebase_path" && claude --dangerously-skip-permissions -p "$doc_prompt" --output-format stream-json --verbose 2>&1) | \
|
|
6884
|
+
{ (cd "$codebase_path" && claude --dangerously-skip-permissions -p "$doc_prompt" --output-format stream-json --verbose 2>&1) | \
|
|
6858
6885
|
while IFS= read -r line; do
|
|
6859
6886
|
if echo "$line" | python3 -c "
|
|
6860
6887
|
import sys, json
|
|
@@ -6868,8 +6895,7 @@ except Exception: pass
|
|
|
6868
6895
|
" 2>/dev/null; then
|
|
6869
6896
|
true
|
|
6870
6897
|
fi
|
|
6871
|
-
done
|
|
6872
|
-
doc_exit=${PIPESTATUS[0]}
|
|
6898
|
+
done; } && doc_exit=0 || doc_exit=$?
|
|
6873
6899
|
;;
|
|
6874
6900
|
codex)
|
|
6875
6901
|
(cd "$codebase_path" && codex exec --full-auto "$doc_prompt" 2>&1) || doc_exit=$?
|
package/autonomy/run.sh
CHANGED
|
@@ -7434,12 +7434,13 @@ run_autonomous() {
|
|
|
7434
7434
|
audit_agent_action "cli_invoke" "Starting iteration $ITERATION_COUNT" "provider=${PROVIDER_NAME:-claude},tier=$CURRENT_TIER"
|
|
7435
7435
|
|
|
7436
7436
|
# Provider-specific invocation with dynamic tier selection
|
|
7437
|
+
local exit_code=0
|
|
7437
7438
|
case "${PROVIDER_NAME:-claude}" in
|
|
7438
7439
|
claude)
|
|
7439
7440
|
# Claude: Full features with stream-json output and agent tracking
|
|
7440
7441
|
# Uses dynamic tier for model selection based on RARV phase
|
|
7441
7442
|
# Pass tier to Python via environment for dashboard display
|
|
7442
|
-
LOKI_CURRENT_MODEL="$tier_param" \
|
|
7443
|
+
{ LOKI_CURRENT_MODEL="$tier_param" \
|
|
7443
7444
|
claude --dangerously-skip-permissions --model "$tier_param" -p "$prompt" \
|
|
7444
7445
|
--output-format stream-json --verbose 2>&1 | \
|
|
7445
7446
|
tee -a "$log_file" "$agent_log" | \
|
|
@@ -7647,7 +7648,7 @@ if __name__ == "__main__":
|
|
|
7647
7648
|
except BrokenPipeError:
|
|
7648
7649
|
sys.exit(0)
|
|
7649
7650
|
'
|
|
7650
|
-
|
|
7651
|
+
} && exit_code=0 || exit_code=$?
|
|
7651
7652
|
;;
|
|
7652
7653
|
|
|
7653
7654
|
codex)
|
|
@@ -7655,10 +7656,10 @@ if __name__ == "__main__":
|
|
|
7655
7656
|
# Uses positional prompt after exec subcommand
|
|
7656
7657
|
# Note: Effort is set via env var, not CLI flag
|
|
7657
7658
|
# Uses dynamic tier from RARV phase (tier_param already set above)
|
|
7658
|
-
CODEX_MODEL_REASONING_EFFORT="$tier_param" \
|
|
7659
|
+
{ CODEX_MODEL_REASONING_EFFORT="$tier_param" \
|
|
7659
7660
|
codex exec --full-auto \
|
|
7660
|
-
"$prompt" 2>&1 | tee -a "$log_file" "$agent_log"
|
|
7661
|
-
|
|
7661
|
+
"$prompt" 2>&1 | tee -a "$log_file" "$agent_log"; \
|
|
7662
|
+
} && exit_code=0 || exit_code=$?
|
|
7662
7663
|
;;
|
|
7663
7664
|
|
|
7664
7665
|
gemini)
|
|
@@ -7672,8 +7673,8 @@ if __name__ == "__main__":
|
|
|
7672
7673
|
# Try primary model, fallback on rate limit
|
|
7673
7674
|
local tmp_output
|
|
7674
7675
|
tmp_output=$(mktemp)
|
|
7675
|
-
gemini --approval-mode=yolo --model "$model" "$prompt" < /dev/null 2>&1 | tee "$tmp_output" | tee -a "$log_file" "$agent_log"
|
|
7676
|
-
|
|
7676
|
+
{ gemini --approval-mode=yolo --model "$model" "$prompt" < /dev/null 2>&1 | tee "$tmp_output" | tee -a "$log_file" "$agent_log"; \
|
|
7677
|
+
} && exit_code=0 || exit_code=$?
|
|
7677
7678
|
|
|
7678
7679
|
if [[ $exit_code -ne 0 ]] && grep -qiE "(rate.?limit|429|quota|resource.?exhausted)" "$tmp_output"; then
|
|
7679
7680
|
log_warn "Rate limit hit on $model, falling back to $fallback"
|
|
@@ -8143,17 +8144,18 @@ except (json.JSONDecodeError, OSError): pass
|
|
|
8143
8144
|
stop_dashboard
|
|
8144
8145
|
stop_status_monitor
|
|
8145
8146
|
kill_all_registered
|
|
8146
|
-
rm -f
|
|
8147
|
+
rm -f "$loki_dir/loki.pid" "$loki_dir/PAUSE" 2>/dev/null
|
|
8147
8148
|
# Clean up per-session PID file if running with session ID
|
|
8148
8149
|
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
8149
|
-
rm -f "
|
|
8150
|
+
rm -f "$loki_dir/sessions/${LOKI_SESSION_ID}/loki.pid" 2>/dev/null
|
|
8150
8151
|
fi
|
|
8151
8152
|
# Mark session.json as stopped
|
|
8152
|
-
if [ -f "
|
|
8153
|
-
python3 -c "
|
|
8154
|
-
import json
|
|
8153
|
+
if [ -f "$loki_dir/session.json" ]; then
|
|
8154
|
+
_LOKI_SESSION_FILE="$loki_dir/session.json" python3 -c "
|
|
8155
|
+
import json, os
|
|
8156
|
+
sf = os.environ['_LOKI_SESSION_FILE']
|
|
8155
8157
|
try:
|
|
8156
|
-
with open(
|
|
8158
|
+
with open(sf, 'r+') as f:
|
|
8157
8159
|
d = json.load(f); d['status'] = 'stopped'
|
|
8158
8160
|
f.seek(0); f.truncate(); json.dump(d, f)
|
|
8159
8161
|
except (json.JSONDecodeError, OSError): pass
|
|
@@ -8736,17 +8738,19 @@ main() {
|
|
|
8736
8738
|
fi
|
|
8737
8739
|
stop_dashboard
|
|
8738
8740
|
stop_status_monitor
|
|
8739
|
-
|
|
8741
|
+
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
8742
|
+
rm -f "$loki_dir/loki.pid" 2>/dev/null
|
|
8740
8743
|
# Clean up per-session PID file if running with session ID
|
|
8741
8744
|
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
8742
|
-
rm -f "
|
|
8745
|
+
rm -f "$loki_dir/sessions/${LOKI_SESSION_ID}/loki.pid" 2>/dev/null
|
|
8743
8746
|
fi
|
|
8744
8747
|
# Mark session.json as stopped
|
|
8745
|
-
if [ -f "
|
|
8746
|
-
python3 -c "
|
|
8747
|
-
import json
|
|
8748
|
+
if [ -f "$loki_dir/session.json" ]; then
|
|
8749
|
+
_LOKI_SESSION_FILE="$loki_dir/session.json" python3 -c "
|
|
8750
|
+
import json, os
|
|
8751
|
+
sf = os.environ['_LOKI_SESSION_FILE']
|
|
8748
8752
|
try:
|
|
8749
|
-
with open(
|
|
8753
|
+
with open(sf, 'r+') as f:
|
|
8750
8754
|
d = json.load(f); d['status'] = 'stopped'
|
|
8751
8755
|
f.seek(0); f.truncate(); json.dump(d, f)
|
|
8752
8756
|
except (json.JSONDecodeError, OSError): pass
|
package/autonomy/sandbox.sh
CHANGED
|
@@ -68,28 +68,32 @@ PROMPT_INJECTION_ENABLED="${LOKI_PROMPT_INJECTION:-false}"
|
|
|
68
68
|
|
|
69
69
|
# Preset definitions: "host_path:container_path:mode"
|
|
70
70
|
# Container runs as user 'loki' (UID 1000), so mount to /home/loki/
|
|
71
|
-
declare -A
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
)
|
|
71
|
+
# Uses functions instead of declare -A for bash 3.2 compatibility (macOS default)
|
|
72
|
+
_get_mount_preset() {
|
|
73
|
+
case "$1" in
|
|
74
|
+
gh) echo "$HOME/.config/gh:/home/loki/.config/gh:ro" ;;
|
|
75
|
+
git) echo "$HOME/.gitconfig:/home/loki/.gitconfig:ro" ;;
|
|
76
|
+
ssh) echo "$HOME/.ssh/known_hosts:/home/loki/.ssh/known_hosts:ro" ;;
|
|
77
|
+
aws) echo "$HOME/.aws:/home/loki/.aws:ro" ;;
|
|
78
|
+
azure) echo "$HOME/.azure:/home/loki/.azure:ro" ;;
|
|
79
|
+
kube) echo "$HOME/.kube:/home/loki/.kube:ro" ;;
|
|
80
|
+
terraform) echo "$HOME/.terraform.d:/home/loki/.terraform.d:ro" ;;
|
|
81
|
+
gcloud) echo "$HOME/.config/gcloud:/home/loki/.config/gcloud:ro" ;;
|
|
82
|
+
npm) echo "$HOME/.npmrc:/home/loki/.npmrc:ro" ;;
|
|
83
|
+
*) echo "" ;;
|
|
84
|
+
esac
|
|
85
|
+
}
|
|
84
86
|
|
|
85
87
|
# Environment variables auto-passed per preset (comma-separated)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
)
|
|
88
|
+
_get_env_preset() {
|
|
89
|
+
case "$1" in
|
|
90
|
+
aws) echo "AWS_REGION,AWS_PROFILE,AWS_DEFAULT_REGION" ;;
|
|
91
|
+
azure) echo "AZURE_SUBSCRIPTION_ID,AZURE_TENANT_ID" ;;
|
|
92
|
+
gcloud) echo "GOOGLE_PROJECT,GOOGLE_REGION,GCLOUD_PROJECT" ;;
|
|
93
|
+
terraform) echo "TF_VAR_*" ;;
|
|
94
|
+
*) echo "" ;;
|
|
95
|
+
esac
|
|
96
|
+
}
|
|
93
97
|
|
|
94
98
|
#===============================================================================
|
|
95
99
|
# Utility Functions
|
|
@@ -836,7 +840,8 @@ except: pass
|
|
|
836
840
|
while IFS= read -r name; do
|
|
837
841
|
[[ -z "$name" ]] && continue
|
|
838
842
|
|
|
839
|
-
local preset_value
|
|
843
|
+
local preset_value
|
|
844
|
+
preset_value="$(_get_mount_preset "$name")"
|
|
840
845
|
if [[ -z "$preset_value" ]]; then
|
|
841
846
|
log_warn "Unknown Docker mount preset: $name"
|
|
842
847
|
continue
|
|
@@ -861,7 +866,8 @@ except: pass
|
|
|
861
866
|
fi
|
|
862
867
|
|
|
863
868
|
# Add associated env vars
|
|
864
|
-
local env_list
|
|
869
|
+
local env_list
|
|
870
|
+
env_list="$(_get_env_preset "$name")"
|
|
865
871
|
if [[ -n "$env_list" ]]; then
|
|
866
872
|
IFS=',' read -ra env_names <<< "$env_list"
|
|
867
873
|
local env_name
|
package/autonomy/telemetry.sh
CHANGED
|
@@ -52,17 +52,30 @@ loki_telemetry() {
|
|
|
52
52
|
os_name=$(uname -s 2>/dev/null || echo "unknown")
|
|
53
53
|
arch=$(uname -m 2>/dev/null || echo "unknown")
|
|
54
54
|
|
|
55
|
-
# Build
|
|
56
|
-
local
|
|
55
|
+
# Build JSON payload safely using Python to prevent injection
|
|
56
|
+
local extra_args=""
|
|
57
57
|
for arg in "$@"; do
|
|
58
|
-
|
|
59
|
-
local val="${arg#*=}"
|
|
60
|
-
extra_props="${extra_props},\"${key}\":\"${val}\""
|
|
58
|
+
extra_args="${extra_args}${extra_args:+ }${arg}"
|
|
61
59
|
done
|
|
62
60
|
|
|
63
61
|
local payload
|
|
64
|
-
payload=$(
|
|
65
|
-
|
|
62
|
+
payload=$(python3 -c "
|
|
63
|
+
import json, sys
|
|
64
|
+
props = {'os': sys.argv[1], 'arch': sys.argv[2], 'version': sys.argv[3], 'channel': sys.argv[4]}
|
|
65
|
+
for arg in sys.argv[5:]:
|
|
66
|
+
if '=' in arg:
|
|
67
|
+
k, v = arg.split('=', 1)
|
|
68
|
+
props[k] = v
|
|
69
|
+
print(json.dumps({'api_key': '$LOKI_POSTHOG_KEY', 'event': sys.argv[5] if len(sys.argv) > 5 else '', 'distinct_id': '$distinct_id', 'properties': props}))
|
|
70
|
+
" "$os_name" "$arch" "$version" "$channel" $extra_args 2>/dev/null) || return 0
|
|
71
|
+
# Re-inject event and distinct_id properly
|
|
72
|
+
payload=$(python3 -c "
|
|
73
|
+
import json, sys
|
|
74
|
+
d = json.loads(sys.argv[1])
|
|
75
|
+
d['event'] = sys.argv[2]
|
|
76
|
+
d['distinct_id'] = sys.argv[3]
|
|
77
|
+
print(json.dumps(d))
|
|
78
|
+
" "$payload" "$event" "$distinct_id" 2>/dev/null) || return 0
|
|
66
79
|
|
|
67
80
|
(curl -sS --max-time 3 -X POST "${LOKI_POSTHOG_HOST}/capture/" \
|
|
68
81
|
-H "Content-Type: application/json" \
|
package/dashboard/__init__.py
CHANGED
|
@@ -886,7 +886,7 @@ Summary: {summary}
|
|
|
886
886
|
if len(entries) > 50:
|
|
887
887
|
header = entries[0]
|
|
888
888
|
recent = entries[-50:]
|
|
889
|
-
content = header + "\n## Session:".join(recent)
|
|
889
|
+
content = header + "\n## Session:" + "\n## Session:".join(recent)
|
|
890
890
|
else:
|
|
891
891
|
content = existing
|
|
892
892
|
content += entry
|
package/dashboard/server.py
CHANGED
|
@@ -536,9 +536,13 @@ async def get_status() -> StatusResponse:
|
|
|
536
536
|
|
|
537
537
|
# Global session
|
|
538
538
|
if running:
|
|
539
|
+
try:
|
|
540
|
+
_global_pid = int(pid_str) if pid_str else 0
|
|
541
|
+
except (ValueError, TypeError):
|
|
542
|
+
_global_pid = 0
|
|
539
543
|
active_session_list.append(SessionInfo(
|
|
540
544
|
session_id="global",
|
|
541
|
-
pid=
|
|
545
|
+
pid=_global_pid,
|
|
542
546
|
status=status,
|
|
543
547
|
))
|
|
544
548
|
|
|
@@ -1441,7 +1445,7 @@ async def query_audit_logs(
|
|
|
1441
1445
|
action: Optional[str] = None,
|
|
1442
1446
|
resource_type: Optional[str] = None,
|
|
1443
1447
|
resource_id: Optional[str] = None,
|
|
1444
|
-
limit: int = 100,
|
|
1448
|
+
limit: int = Query(default=100, ge=1, le=1000),
|
|
1445
1449
|
offset: int = 0,
|
|
1446
1450
|
):
|
|
1447
1451
|
"""
|
|
@@ -1561,7 +1565,7 @@ async def get_memory_summary():
|
|
|
1561
1565
|
|
|
1562
1566
|
|
|
1563
1567
|
@app.get("/api/memory/episodes")
|
|
1564
|
-
async def list_episodes(limit: int = 50):
|
|
1568
|
+
async def list_episodes(limit: int = Query(default=50, ge=1, le=1000)):
|
|
1565
1569
|
"""List episodic memory entries."""
|
|
1566
1570
|
ep_dir = _get_loki_dir() / "memory" / "episodic"
|
|
1567
1571
|
episodes = []
|
|
@@ -1578,11 +1582,15 @@ async def list_episodes(limit: int = 50):
|
|
|
1578
1582
|
@app.get("/api/memory/episodes/{episode_id}")
|
|
1579
1583
|
async def get_episode(episode_id: str):
|
|
1580
1584
|
"""Get a specific episodic memory entry."""
|
|
1581
|
-
|
|
1585
|
+
loki_dir = _get_loki_dir()
|
|
1586
|
+
ep_dir = loki_dir / "memory" / "episodic"
|
|
1582
1587
|
if not ep_dir.exists():
|
|
1583
1588
|
raise HTTPException(status_code=404, detail="Episode not found")
|
|
1584
1589
|
# Try direct filename match
|
|
1585
1590
|
for f in ep_dir.glob("*.json"):
|
|
1591
|
+
resolved = os.path.realpath(f)
|
|
1592
|
+
if not resolved.startswith(os.path.realpath(str(loki_dir))):
|
|
1593
|
+
raise HTTPException(status_code=403, detail="Access denied")
|
|
1586
1594
|
try:
|
|
1587
1595
|
data = json.loads(f.read_text())
|
|
1588
1596
|
if data.get("id") == episode_id or f.stem == episode_id:
|
|
@@ -1633,10 +1641,14 @@ async def list_skills():
|
|
|
1633
1641
|
@app.get("/api/memory/skills/{skill_id}")
|
|
1634
1642
|
async def get_skill(skill_id: str):
|
|
1635
1643
|
"""Get a specific procedural skill."""
|
|
1636
|
-
|
|
1644
|
+
loki_dir = _get_loki_dir()
|
|
1645
|
+
skills_dir = loki_dir / "memory" / "skills"
|
|
1637
1646
|
if not skills_dir.exists():
|
|
1638
1647
|
raise HTTPException(status_code=404, detail="Skill not found")
|
|
1639
1648
|
for f in skills_dir.glob("*.json"):
|
|
1649
|
+
resolved = os.path.realpath(f)
|
|
1650
|
+
if not resolved.startswith(os.path.realpath(str(loki_dir))):
|
|
1651
|
+
raise HTTPException(status_code=403, detail="Access denied")
|
|
1640
1652
|
try:
|
|
1641
1653
|
data = json.loads(f.read_text())
|
|
1642
1654
|
if data.get("id") == skill_id or f.stem == skill_id:
|
|
@@ -1706,6 +1718,7 @@ def _read_learning_signals(signal_type: Optional[str] = None, limit: int = 50) -
|
|
|
1706
1718
|
(learning/emitter.py). Each file contains a single signal object with fields:
|
|
1707
1719
|
id, type, source, action, timestamp, confidence, outcome, data, context.
|
|
1708
1720
|
"""
|
|
1721
|
+
limit = min(limit, 1000)
|
|
1709
1722
|
signals_dir = _get_loki_dir() / "learning" / "signals"
|
|
1710
1723
|
if not signals_dir.exists() or not signals_dir.is_dir():
|
|
1711
1724
|
return []
|
|
@@ -1825,7 +1838,7 @@ async def get_learning_signals(
|
|
|
1825
1838
|
timeRange: str = "7d",
|
|
1826
1839
|
signalType: Optional[str] = None,
|
|
1827
1840
|
source: Optional[str] = None,
|
|
1828
|
-
limit: int = 50,
|
|
1841
|
+
limit: int = Query(default=50, ge=1, le=1000),
|
|
1829
1842
|
offset: int = 0,
|
|
1830
1843
|
):
|
|
1831
1844
|
"""Get raw learning signals from both events.jsonl and learning signals directory."""
|
|
@@ -2030,7 +2043,7 @@ async def trigger_aggregation():
|
|
|
2030
2043
|
|
|
2031
2044
|
|
|
2032
2045
|
@app.get("/api/learning/preferences")
|
|
2033
|
-
async def get_learning_preferences(limit: int = 50):
|
|
2046
|
+
async def get_learning_preferences(limit: int = Query(default=50, ge=1, le=1000)):
|
|
2034
2047
|
"""Get aggregated user preferences from events and learning signals directory."""
|
|
2035
2048
|
events = _read_events("30d")
|
|
2036
2049
|
prefs = [e for e in events if e.get("type") == "user_preference"]
|
|
@@ -2042,7 +2055,7 @@ async def get_learning_preferences(limit: int = 50):
|
|
|
2042
2055
|
|
|
2043
2056
|
|
|
2044
2057
|
@app.get("/api/learning/errors")
|
|
2045
|
-
async def get_learning_errors(limit: int = 50):
|
|
2058
|
+
async def get_learning_errors(limit: int = Query(default=50, ge=1, le=1000)):
|
|
2046
2059
|
"""Get aggregated error patterns from events and learning signals directory."""
|
|
2047
2060
|
events = _read_events("30d")
|
|
2048
2061
|
errors = [e for e in events if e.get("type") == "error_pattern"]
|
|
@@ -2054,7 +2067,7 @@ async def get_learning_errors(limit: int = 50):
|
|
|
2054
2067
|
|
|
2055
2068
|
|
|
2056
2069
|
@app.get("/api/learning/success")
|
|
2057
|
-
async def get_learning_success(limit: int = 50):
|
|
2070
|
+
async def get_learning_success(limit: int = Query(default=50, ge=1, le=1000)):
|
|
2058
2071
|
"""Get aggregated success patterns from events and learning signals directory."""
|
|
2059
2072
|
events = _read_events("30d")
|
|
2060
2073
|
successes = [e for e in events if e.get("type") == "success_pattern"]
|
|
@@ -2066,7 +2079,7 @@ async def get_learning_success(limit: int = 50):
|
|
|
2066
2079
|
|
|
2067
2080
|
|
|
2068
2081
|
@app.get("/api/learning/tools")
|
|
2069
|
-
async def get_tool_efficiency(limit: int = 50):
|
|
2082
|
+
async def get_tool_efficiency(limit: int = Query(default=50, ge=1, le=1000)):
|
|
2070
2083
|
"""Get tool efficiency rankings from events and learning signals directory."""
|
|
2071
2084
|
events = _read_events("30d")
|
|
2072
2085
|
tools = [e for e in events if e.get("type") == "tool_efficiency"]
|
|
@@ -2505,7 +2518,7 @@ async def get_council_state():
|
|
|
2505
2518
|
|
|
2506
2519
|
|
|
2507
2520
|
@app.get("/api/council/verdicts")
|
|
2508
|
-
async def get_council_verdicts(limit: int = 20):
|
|
2521
|
+
async def get_council_verdicts(limit: int = Query(default=20, ge=1, le=1000)):
|
|
2509
2522
|
"""Get council vote history (decision log)."""
|
|
2510
2523
|
state_file = _get_loki_dir() / "council" / "state.json"
|
|
2511
2524
|
verdicts = []
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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:** v6.
|
|
5
|
+
**Version:** v6.5.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
## What's New in v6.
|
|
9
|
+
## What's New in v6.5.0
|
|
10
10
|
|
|
11
11
|
### Dual-Mode Architecture (v6.0.0)
|
|
12
12
|
- `loki run` command for direct autonomous execution
|
package/events/emit.sh
CHANGED
|
@@ -43,7 +43,7 @@ fi
|
|
|
43
43
|
|
|
44
44
|
# JSON escape helper: handles \, ", and control characters
|
|
45
45
|
json_escape() {
|
|
46
|
-
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g; s/\r/\\r/g' |
|
|
46
|
+
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g; s/\r/\\r/g' | awk '{if(NR>1) printf "\\n"; printf "%s", $0}'
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
# Build payload JSON
|
package/mcp/__init__.py
CHANGED
package/mcp/server.py
CHANGED
|
@@ -20,6 +20,7 @@ import os
|
|
|
20
20
|
import json
|
|
21
21
|
import logging
|
|
22
22
|
import threading
|
|
23
|
+
import uuid
|
|
23
24
|
from datetime import datetime, timezone
|
|
24
25
|
from typing import Optional, List, Dict, Any
|
|
25
26
|
|
|
@@ -563,11 +564,11 @@ async def loki_memory_store_pattern(
|
|
|
563
564
|
from memory.schemas import SemanticPattern
|
|
564
565
|
|
|
565
566
|
base_path = safe_path_join('.loki', 'memory')
|
|
566
|
-
engine = MemoryEngine(base_path)
|
|
567
|
+
engine = MemoryEngine(base_path=base_path)
|
|
567
568
|
engine.initialize()
|
|
568
569
|
|
|
569
570
|
pattern_obj = SemanticPattern(
|
|
570
|
-
id=f"pattern-{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}",
|
|
571
|
+
id=f"pattern-{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:8]}",
|
|
571
572
|
pattern=pattern,
|
|
572
573
|
category=category,
|
|
573
574
|
conditions=[],
|
|
@@ -821,7 +822,7 @@ async def loki_state_get() -> str:
|
|
|
821
822
|
try:
|
|
822
823
|
from memory.engine import MemoryEngine
|
|
823
824
|
memory_path = safe_path_join('.loki', 'memory')
|
|
824
|
-
engine = MemoryEngine(memory_path)
|
|
825
|
+
engine = MemoryEngine(base_path=memory_path)
|
|
825
826
|
state["memory_stats"] = engine.get_stats()
|
|
826
827
|
except Exception:
|
|
827
828
|
state["memory_stats"] = None
|
|
@@ -1004,9 +1005,13 @@ async def loki_start_project(prd_content: str = "", prd_path: str = "") -> str:
|
|
|
1004
1005
|
try:
|
|
1005
1006
|
content = prd_content
|
|
1006
1007
|
if not content and prd_path:
|
|
1007
|
-
|
|
1008
|
-
if os.path.
|
|
1009
|
-
|
|
1008
|
+
# Resolve relative paths against project root, absolute paths used as-is
|
|
1009
|
+
if os.path.isabs(prd_path):
|
|
1010
|
+
resolved = os.path.realpath(prd_path)
|
|
1011
|
+
else:
|
|
1012
|
+
resolved = os.path.realpath(os.path.join(get_project_root(), prd_path))
|
|
1013
|
+
if os.path.exists(resolved) and os.path.isfile(resolved):
|
|
1014
|
+
with open(resolved, 'r', encoding='utf-8') as f:
|
|
1010
1015
|
content = f.read()
|
|
1011
1016
|
else:
|
|
1012
1017
|
return json.dumps({"error": f"PRD file not found: {prd_path}"})
|
package/memory/embeddings.py
CHANGED
|
@@ -307,6 +307,10 @@ class TextChunker:
|
|
|
307
307
|
if len(text) <= max_size:
|
|
308
308
|
return [text]
|
|
309
309
|
|
|
310
|
+
# Guard against infinite loop when overlap >= max_size
|
|
311
|
+
if overlap >= max_size:
|
|
312
|
+
overlap = 0
|
|
313
|
+
|
|
310
314
|
chunks = []
|
|
311
315
|
start = 0
|
|
312
316
|
while start < len(text):
|
|
@@ -1062,12 +1066,17 @@ class EmbeddingEngine:
|
|
|
1062
1066
|
# Find texts that need computing
|
|
1063
1067
|
texts_to_compute = []
|
|
1064
1068
|
indices_to_compute = []
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1069
|
+
if not self.config.cache_enabled:
|
|
1070
|
+
# No cache - all texts need computing
|
|
1071
|
+
texts_to_compute = list(texts)
|
|
1072
|
+
indices_to_compute = list(range(len(texts)))
|
|
1073
|
+
else:
|
|
1074
|
+
for i, (text, key) in enumerate(zip(texts, cache_keys)):
|
|
1075
|
+
if cached_results.get(key) is None:
|
|
1076
|
+
texts_to_compute.append(text)
|
|
1077
|
+
indices_to_compute.append(i)
|
|
1078
|
+
else:
|
|
1079
|
+
self._metrics["cache_hits"] += 1
|
|
1071
1080
|
|
|
1072
1081
|
# Compute missing embeddings
|
|
1073
1082
|
new_embeddings = None
|
package/memory/engine.py
CHANGED
|
@@ -881,6 +881,21 @@ class MemoryEngine:
|
|
|
881
881
|
for e in errors_raw
|
|
882
882
|
]
|
|
883
883
|
|
|
884
|
+
# Parse last_accessed datetime
|
|
885
|
+
last_accessed = None
|
|
886
|
+
last_accessed_raw = data.get("last_accessed")
|
|
887
|
+
if last_accessed_raw:
|
|
888
|
+
if isinstance(last_accessed_raw, str):
|
|
889
|
+
if last_accessed_raw.endswith("Z"):
|
|
890
|
+
last_accessed_raw = last_accessed_raw[:-1]
|
|
891
|
+
last_accessed = datetime.fromisoformat(last_accessed_raw)
|
|
892
|
+
if last_accessed.tzinfo is None:
|
|
893
|
+
last_accessed = last_accessed.replace(tzinfo=timezone.utc)
|
|
894
|
+
elif isinstance(last_accessed_raw, datetime):
|
|
895
|
+
last_accessed = last_accessed_raw
|
|
896
|
+
if last_accessed.tzinfo is None:
|
|
897
|
+
last_accessed = last_accessed.replace(tzinfo=timezone.utc)
|
|
898
|
+
|
|
884
899
|
return EpisodeTrace(
|
|
885
900
|
id=data.get("id", ""),
|
|
886
901
|
task_id=data.get("task_id", ""),
|
|
@@ -897,6 +912,9 @@ class MemoryEngine:
|
|
|
897
912
|
tokens_used=data.get("tokens_used", 0),
|
|
898
913
|
files_read=data.get("files_read", context.get("files_involved", [])),
|
|
899
914
|
files_modified=data.get("files_modified", []),
|
|
915
|
+
importance=data.get("importance", 0.5),
|
|
916
|
+
last_accessed=last_accessed,
|
|
917
|
+
access_count=data.get("access_count", 0),
|
|
900
918
|
)
|
|
901
919
|
|
|
902
920
|
def _dict_to_pattern(self, data: Dict[str, Any]) -> SemanticPattern:
|
|
@@ -924,6 +942,21 @@ class MemoryEngine:
|
|
|
924
942
|
for link in links_raw
|
|
925
943
|
]
|
|
926
944
|
|
|
945
|
+
# Parse last_accessed datetime
|
|
946
|
+
last_accessed = None
|
|
947
|
+
last_accessed_raw = data.get("last_accessed")
|
|
948
|
+
if last_accessed_raw:
|
|
949
|
+
if isinstance(last_accessed_raw, str):
|
|
950
|
+
if last_accessed_raw.endswith("Z"):
|
|
951
|
+
last_accessed_raw = last_accessed_raw[:-1]
|
|
952
|
+
last_accessed = datetime.fromisoformat(last_accessed_raw)
|
|
953
|
+
if last_accessed.tzinfo is None:
|
|
954
|
+
last_accessed = last_accessed.replace(tzinfo=timezone.utc)
|
|
955
|
+
elif isinstance(last_accessed_raw, datetime):
|
|
956
|
+
last_accessed = last_accessed_raw
|
|
957
|
+
if last_accessed.tzinfo is None:
|
|
958
|
+
last_accessed = last_accessed.replace(tzinfo=timezone.utc)
|
|
959
|
+
|
|
927
960
|
return SemanticPattern(
|
|
928
961
|
id=data.get("id", ""),
|
|
929
962
|
pattern=data.get("pattern", ""),
|
|
@@ -936,6 +969,9 @@ class MemoryEngine:
|
|
|
936
969
|
usage_count=data.get("usage_count", 0),
|
|
937
970
|
last_used=last_used,
|
|
938
971
|
links=links,
|
|
972
|
+
importance=data.get("importance", 0.5),
|
|
973
|
+
last_accessed=last_accessed,
|
|
974
|
+
access_count=data.get("access_count", 0),
|
|
939
975
|
)
|
|
940
976
|
|
|
941
977
|
def _dict_to_skill(self, data: Dict[str, Any]) -> ProceduralSkill:
|
|
@@ -945,6 +981,22 @@ class MemoryEngine:
|
|
|
945
981
|
ErrorFix.from_dict(e) if isinstance(e, dict) else e
|
|
946
982
|
for e in raw_errors
|
|
947
983
|
]
|
|
984
|
+
|
|
985
|
+
# Parse last_accessed datetime
|
|
986
|
+
last_accessed = None
|
|
987
|
+
last_accessed_raw = data.get("last_accessed")
|
|
988
|
+
if last_accessed_raw:
|
|
989
|
+
if isinstance(last_accessed_raw, str):
|
|
990
|
+
if last_accessed_raw.endswith("Z"):
|
|
991
|
+
last_accessed_raw = last_accessed_raw[:-1]
|
|
992
|
+
last_accessed = datetime.fromisoformat(last_accessed_raw)
|
|
993
|
+
if last_accessed.tzinfo is None:
|
|
994
|
+
last_accessed = last_accessed.replace(tzinfo=timezone.utc)
|
|
995
|
+
elif isinstance(last_accessed_raw, datetime):
|
|
996
|
+
last_accessed = last_accessed_raw
|
|
997
|
+
if last_accessed.tzinfo is None:
|
|
998
|
+
last_accessed = last_accessed.replace(tzinfo=timezone.utc)
|
|
999
|
+
|
|
948
1000
|
return ProceduralSkill(
|
|
949
1001
|
id=data.get("id", ""),
|
|
950
1002
|
name=data.get("name", ""),
|
|
@@ -953,6 +1005,10 @@ class MemoryEngine:
|
|
|
953
1005
|
steps=data.get("steps", []),
|
|
954
1006
|
common_errors=common_errors,
|
|
955
1007
|
exit_criteria=data.get("exit_criteria", []),
|
|
1008
|
+
example_usage=data.get("example_usage"),
|
|
1009
|
+
importance=data.get("importance", 0.5),
|
|
1010
|
+
last_accessed=last_accessed,
|
|
1011
|
+
access_count=data.get("access_count", 0),
|
|
956
1012
|
)
|
|
957
1013
|
|
|
958
1014
|
def _skill_to_markdown(self, skill: Dict[str, Any]) -> str:
|
package/memory/schemas.py
CHANGED
|
@@ -20,9 +20,13 @@ from typing import Optional, List, Dict, Any
|
|
|
20
20
|
def _to_utc_isoformat(dt: datetime) -> str:
|
|
21
21
|
"""Convert datetime to UTC ISO 8601 string with Z suffix.
|
|
22
22
|
|
|
23
|
-
Handles both timezone-aware and timezone-naive datetimes
|
|
24
|
-
|
|
23
|
+
Handles both timezone-aware and timezone-naive datetimes.
|
|
24
|
+
If dt has a non-UTC timezone, converts to UTC first.
|
|
25
25
|
"""
|
|
26
|
+
# If timezone-aware and not UTC, convert to UTC
|
|
27
|
+
if dt.tzinfo is not None and dt.utcoffset() != timezone.utc.utcoffset(None):
|
|
28
|
+
dt = dt.astimezone(timezone.utc)
|
|
29
|
+
|
|
26
30
|
iso = dt.isoformat()
|
|
27
31
|
# If already has timezone offset like +00:00, replace with Z
|
|
28
32
|
if iso.endswith("+00:00"):
|
package/memory/storage.py
CHANGED
|
@@ -233,7 +233,7 @@ class MemoryStorage:
|
|
|
233
233
|
path: Path to JSON file
|
|
234
234
|
|
|
235
235
|
Returns:
|
|
236
|
-
Parsed JSON as dictionary, or None if file doesn't exist
|
|
236
|
+
Parsed JSON as dictionary, or None if file doesn't exist or is corrupted
|
|
237
237
|
"""
|
|
238
238
|
path = Path(path)
|
|
239
239
|
if not path.exists():
|
|
@@ -241,7 +241,10 @@ class MemoryStorage:
|
|
|
241
241
|
|
|
242
242
|
with self._file_lock(path, exclusive=False):
|
|
243
243
|
with open(path, "r") as f:
|
|
244
|
-
|
|
244
|
+
try:
|
|
245
|
+
return json.load(f)
|
|
246
|
+
except json.JSONDecodeError:
|
|
247
|
+
return None
|
|
245
248
|
|
|
246
249
|
def _generate_id(self, prefix: str) -> str:
|
|
247
250
|
"""
|
|
@@ -505,9 +505,12 @@ class TokenEconomics:
|
|
|
505
505
|
Ratio of discovery_tokens / read_tokens (0.0 if no reads)
|
|
506
506
|
"""
|
|
507
507
|
read_tokens = self.metrics["read_tokens"]
|
|
508
|
+
discovery_tokens = self.metrics["discovery_tokens"]
|
|
508
509
|
if read_tokens == 0:
|
|
510
|
+
if discovery_tokens > 0:
|
|
511
|
+
return 999.99 # Sentinel: all discovery, no productive reads
|
|
509
512
|
return 0.0
|
|
510
|
-
return
|
|
513
|
+
return discovery_tokens / read_tokens
|
|
511
514
|
|
|
512
515
|
def get_savings_percent(self) -> float:
|
|
513
516
|
"""
|