loki-mode 6.3.1 → 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 +299 -55
- package/autonomy/run.sh +119 -49
- 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 +97 -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/autonomy/run.sh
CHANGED
|
@@ -2642,33 +2642,52 @@ init_loki_dir() {
|
|
|
2642
2642
|
# Clean up stale control files ONLY if no other session is running
|
|
2643
2643
|
# Deleting these while another session is active would destroy its signals
|
|
2644
2644
|
# Use flock if available to avoid TOCTOU race
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2645
|
+
#
|
|
2646
|
+
# Per-session locking (v6.4.0): When LOKI_SESSION_ID is set, only clean up
|
|
2647
|
+
# that session's files. Global control files (PAUSE/STOP) are only cleaned
|
|
2648
|
+
# when NO sessions are active.
|
|
2649
|
+
local lock_file can_cleanup=false
|
|
2650
|
+
|
|
2651
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
2652
|
+
# Per-session: check only this session's lock
|
|
2653
|
+
lock_file=".loki/sessions/${LOKI_SESSION_ID}/session.lock"
|
|
2654
|
+
local session_pid_file=".loki/sessions/${LOKI_SESSION_ID}/loki.pid"
|
|
2655
|
+
if command -v flock >/dev/null 2>&1 && [ -f "$lock_file" ]; then
|
|
2656
|
+
{ if flock -n 201 2>/dev/null; then can_cleanup=true; fi } 201>"$lock_file"
|
|
2657
|
+
else
|
|
2658
|
+
local existing_pid=""
|
|
2659
|
+
if [ -f "$session_pid_file" ]; then
|
|
2660
|
+
existing_pid=$(cat "$session_pid_file" 2>/dev/null)
|
|
2661
|
+
fi
|
|
2662
|
+
if [ -z "$existing_pid" ] || ! kill -0 "$existing_pid" 2>/dev/null; then
|
|
2652
2663
|
can_cleanup=true
|
|
2653
2664
|
fi
|
|
2654
|
-
|
|
2665
|
+
fi
|
|
2666
|
+
if [ "$can_cleanup" = "true" ]; then
|
|
2667
|
+
rm -f "$session_pid_file" 2>/dev/null
|
|
2668
|
+
rm -f "$lock_file" 2>/dev/null
|
|
2669
|
+
fi
|
|
2655
2670
|
else
|
|
2656
|
-
#
|
|
2657
|
-
|
|
2658
|
-
if [ -f "
|
|
2659
|
-
|
|
2671
|
+
# Global: original behavior
|
|
2672
|
+
lock_file=".loki/session.lock"
|
|
2673
|
+
if command -v flock >/dev/null 2>&1 && [ -f "$lock_file" ]; then
|
|
2674
|
+
{ if flock -n 201 2>/dev/null; then can_cleanup=true; fi } 201>"$lock_file"
|
|
2675
|
+
else
|
|
2676
|
+
local existing_pid=""
|
|
2677
|
+
if [ -f ".loki/loki.pid" ]; then
|
|
2678
|
+
existing_pid=$(cat ".loki/loki.pid" 2>/dev/null)
|
|
2679
|
+
fi
|
|
2680
|
+
if [ -z "$existing_pid" ] || ! kill -0 "$existing_pid" 2>/dev/null; then
|
|
2681
|
+
can_cleanup=true
|
|
2682
|
+
fi
|
|
2660
2683
|
fi
|
|
2661
|
-
if [
|
|
2662
|
-
|
|
2684
|
+
if [ "$can_cleanup" = "true" ]; then
|
|
2685
|
+
rm -f .loki/PAUSE .loki/STOP .loki/HUMAN_INPUT.md 2>/dev/null
|
|
2686
|
+
rm -f .loki/loki.pid 2>/dev/null
|
|
2687
|
+
rm -f .loki/session.lock 2>/dev/null
|
|
2663
2688
|
fi
|
|
2664
2689
|
fi
|
|
2665
2690
|
|
|
2666
|
-
if [ "$can_cleanup" = "true" ]; then
|
|
2667
|
-
rm -f .loki/PAUSE .loki/STOP .loki/HUMAN_INPUT.md 2>/dev/null
|
|
2668
|
-
rm -f .loki/loki.pid 2>/dev/null
|
|
2669
|
-
rm -f .loki/session.lock 2>/dev/null
|
|
2670
|
-
fi
|
|
2671
|
-
|
|
2672
2691
|
mkdir -p .loki/{state,queue,messages,logs,config,prompts,artifacts,scripts}
|
|
2673
2692
|
mkdir -p .loki/queue
|
|
2674
2693
|
mkdir -p .loki/state/checkpoints
|
|
@@ -7415,12 +7434,13 @@ run_autonomous() {
|
|
|
7415
7434
|
audit_agent_action "cli_invoke" "Starting iteration $ITERATION_COUNT" "provider=${PROVIDER_NAME:-claude},tier=$CURRENT_TIER"
|
|
7416
7435
|
|
|
7417
7436
|
# Provider-specific invocation with dynamic tier selection
|
|
7437
|
+
local exit_code=0
|
|
7418
7438
|
case "${PROVIDER_NAME:-claude}" in
|
|
7419
7439
|
claude)
|
|
7420
7440
|
# Claude: Full features with stream-json output and agent tracking
|
|
7421
7441
|
# Uses dynamic tier for model selection based on RARV phase
|
|
7422
7442
|
# Pass tier to Python via environment for dashboard display
|
|
7423
|
-
LOKI_CURRENT_MODEL="$tier_param" \
|
|
7443
|
+
{ LOKI_CURRENT_MODEL="$tier_param" \
|
|
7424
7444
|
claude --dangerously-skip-permissions --model "$tier_param" -p "$prompt" \
|
|
7425
7445
|
--output-format stream-json --verbose 2>&1 | \
|
|
7426
7446
|
tee -a "$log_file" "$agent_log" | \
|
|
@@ -7628,7 +7648,7 @@ if __name__ == "__main__":
|
|
|
7628
7648
|
except BrokenPipeError:
|
|
7629
7649
|
sys.exit(0)
|
|
7630
7650
|
'
|
|
7631
|
-
|
|
7651
|
+
} && exit_code=0 || exit_code=$?
|
|
7632
7652
|
;;
|
|
7633
7653
|
|
|
7634
7654
|
codex)
|
|
@@ -7636,10 +7656,10 @@ if __name__ == "__main__":
|
|
|
7636
7656
|
# Uses positional prompt after exec subcommand
|
|
7637
7657
|
# Note: Effort is set via env var, not CLI flag
|
|
7638
7658
|
# Uses dynamic tier from RARV phase (tier_param already set above)
|
|
7639
|
-
CODEX_MODEL_REASONING_EFFORT="$tier_param" \
|
|
7659
|
+
{ CODEX_MODEL_REASONING_EFFORT="$tier_param" \
|
|
7640
7660
|
codex exec --full-auto \
|
|
7641
|
-
"$prompt" 2>&1 | tee -a "$log_file" "$agent_log"
|
|
7642
|
-
|
|
7661
|
+
"$prompt" 2>&1 | tee -a "$log_file" "$agent_log"; \
|
|
7662
|
+
} && exit_code=0 || exit_code=$?
|
|
7643
7663
|
;;
|
|
7644
7664
|
|
|
7645
7665
|
gemini)
|
|
@@ -7653,8 +7673,8 @@ if __name__ == "__main__":
|
|
|
7653
7673
|
# Try primary model, fallback on rate limit
|
|
7654
7674
|
local tmp_output
|
|
7655
7675
|
tmp_output=$(mktemp)
|
|
7656
|
-
gemini --approval-mode=yolo --model "$model" "$prompt" < /dev/null 2>&1 | tee "$tmp_output" | tee -a "$log_file" "$agent_log"
|
|
7657
|
-
|
|
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=$?
|
|
7658
7678
|
|
|
7659
7679
|
if [[ $exit_code -ne 0 ]] && grep -qiE "(rate.?limit|429|quota|resource.?exhausted)" "$tmp_output"; then
|
|
7660
7680
|
log_warn "Rate limit hit on $model, falling back to $fallback"
|
|
@@ -8093,6 +8113,10 @@ cleanup() {
|
|
|
8093
8113
|
stop_status_monitor
|
|
8094
8114
|
kill_all_registered
|
|
8095
8115
|
rm -f "$loki_dir/loki.pid" 2>/dev/null
|
|
8116
|
+
# Clean up per-session PID file if running with session ID
|
|
8117
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
8118
|
+
rm -f "$loki_dir/sessions/${LOKI_SESSION_ID}/loki.pid" 2>/dev/null
|
|
8119
|
+
fi
|
|
8096
8120
|
if [ -f "$loki_dir/session.json" ]; then
|
|
8097
8121
|
_LOKI_SESSION_FILE="$loki_dir/session.json" python3 -c "
|
|
8098
8122
|
import json, os
|
|
@@ -8120,13 +8144,18 @@ except (json.JSONDecodeError, OSError): pass
|
|
|
8120
8144
|
stop_dashboard
|
|
8121
8145
|
stop_status_monitor
|
|
8122
8146
|
kill_all_registered
|
|
8123
|
-
rm -f
|
|
8147
|
+
rm -f "$loki_dir/loki.pid" "$loki_dir/PAUSE" 2>/dev/null
|
|
8148
|
+
# Clean up per-session PID file if running with session ID
|
|
8149
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
8150
|
+
rm -f "$loki_dir/sessions/${LOKI_SESSION_ID}/loki.pid" 2>/dev/null
|
|
8151
|
+
fi
|
|
8124
8152
|
# Mark session.json as stopped
|
|
8125
|
-
if [ -f "
|
|
8126
|
-
python3 -c "
|
|
8127
|
-
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']
|
|
8128
8157
|
try:
|
|
8129
|
-
with open(
|
|
8158
|
+
with open(sf, 'r+') as f:
|
|
8130
8159
|
d = json.load(f); d['status'] = 'stopped'
|
|
8131
8160
|
f.seek(0); f.truncate(); json.dump(d, f)
|
|
8132
8161
|
except (json.JSONDecodeError, OSError): pass
|
|
@@ -8325,7 +8354,13 @@ main() {
|
|
|
8325
8354
|
mkdir -p .loki/logs
|
|
8326
8355
|
|
|
8327
8356
|
local log_file=".loki/logs/background-$(date +%Y%m%d-%H%M%S).log"
|
|
8328
|
-
local pid_file
|
|
8357
|
+
local pid_file
|
|
8358
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
8359
|
+
mkdir -p ".loki/sessions/${LOKI_SESSION_ID}"
|
|
8360
|
+
pid_file=".loki/sessions/${LOKI_SESSION_ID}/loki.pid"
|
|
8361
|
+
else
|
|
8362
|
+
pid_file=".loki/loki.pid"
|
|
8363
|
+
fi
|
|
8329
8364
|
local project_path=$(pwd)
|
|
8330
8365
|
local project_name=$(basename "$project_path")
|
|
8331
8366
|
|
|
@@ -8419,12 +8454,22 @@ main() {
|
|
|
8419
8454
|
# Initialize session continuity file with empty template
|
|
8420
8455
|
update_continuity
|
|
8421
8456
|
|
|
8422
|
-
# Session lock: prevent concurrent sessions
|
|
8423
|
-
#
|
|
8424
|
-
|
|
8425
|
-
|
|
8457
|
+
# Session lock: prevent concurrent sessions
|
|
8458
|
+
# Per-session locking (v6.4.0): LOKI_SESSION_ID enables multiple concurrent
|
|
8459
|
+
# sessions (e.g., loki run 52 -d && loki run 54 -d). Each session gets its
|
|
8460
|
+
# own PID/lock files under .loki/sessions/<id>/.
|
|
8461
|
+
# Without LOKI_SESSION_ID, the global .loki/loki.pid lock is used (single session).
|
|
8462
|
+
local pid_file lock_file
|
|
8463
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
8464
|
+
mkdir -p ".loki/sessions/${LOKI_SESSION_ID}"
|
|
8465
|
+
pid_file=".loki/sessions/${LOKI_SESSION_ID}/loki.pid"
|
|
8466
|
+
lock_file=".loki/sessions/${LOKI_SESSION_ID}/session.lock"
|
|
8467
|
+
else
|
|
8468
|
+
pid_file=".loki/loki.pid"
|
|
8469
|
+
lock_file=".loki/session.lock"
|
|
8470
|
+
fi
|
|
8426
8471
|
|
|
8427
|
-
#
|
|
8472
|
+
# Use flock for atomic locking to prevent TOCTOU race conditions
|
|
8428
8473
|
if command -v flock >/dev/null 2>&1; then
|
|
8429
8474
|
# Create lock file
|
|
8430
8475
|
touch "$lock_file"
|
|
@@ -8435,8 +8480,13 @@ main() {
|
|
|
8435
8480
|
|
|
8436
8481
|
# Try to acquire exclusive lock (non-blocking)
|
|
8437
8482
|
if ! flock -n 200 2>/dev/null; then
|
|
8438
|
-
|
|
8439
|
-
|
|
8483
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
8484
|
+
log_error "Session '${LOKI_SESSION_ID}' is already running (locked)"
|
|
8485
|
+
log_error "Stop it first with: loki stop ${LOKI_SESSION_ID}"
|
|
8486
|
+
else
|
|
8487
|
+
log_error "Another Loki session is already running (locked)"
|
|
8488
|
+
log_error "Stop it first with: loki stop"
|
|
8489
|
+
fi
|
|
8440
8490
|
exit 1
|
|
8441
8491
|
fi
|
|
8442
8492
|
|
|
@@ -8446,8 +8496,13 @@ main() {
|
|
|
8446
8496
|
existing_pid=$(cat "$pid_file" 2>/dev/null)
|
|
8447
8497
|
# Skip if it's our own PID or parent PID (background mode writes PID before child starts)
|
|
8448
8498
|
if [ -n "$existing_pid" ] && [ "$existing_pid" != "$$" ] && [ "$existing_pid" != "$PPID" ] && kill -0 "$existing_pid" 2>/dev/null; then
|
|
8449
|
-
|
|
8450
|
-
|
|
8499
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
8500
|
+
log_error "Session '${LOKI_SESSION_ID}' is already running (PID: $existing_pid)"
|
|
8501
|
+
log_error "Stop it first with: loki stop ${LOKI_SESSION_ID}"
|
|
8502
|
+
else
|
|
8503
|
+
log_error "Another Loki session is already running (PID: $existing_pid)"
|
|
8504
|
+
log_error "Stop it first with: loki stop"
|
|
8505
|
+
fi
|
|
8451
8506
|
exit 1
|
|
8452
8507
|
fi
|
|
8453
8508
|
fi
|
|
@@ -8459,8 +8514,13 @@ main() {
|
|
|
8459
8514
|
existing_pid=$(cat "$pid_file" 2>/dev/null)
|
|
8460
8515
|
# Skip if it's our own PID or parent PID (background mode writes PID before child starts)
|
|
8461
8516
|
if [ -n "$existing_pid" ] && [ "$existing_pid" != "$$" ] && [ "$existing_pid" != "$PPID" ] && kill -0 "$existing_pid" 2>/dev/null; then
|
|
8462
|
-
|
|
8463
|
-
|
|
8517
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
8518
|
+
log_error "Session '${LOKI_SESSION_ID}' is already running (PID: $existing_pid)"
|
|
8519
|
+
log_error "Stop it first with: loki stop ${LOKI_SESSION_ID}"
|
|
8520
|
+
else
|
|
8521
|
+
log_error "Another Loki session is already running (PID: $existing_pid)"
|
|
8522
|
+
log_error "Stop it first with: loki stop"
|
|
8523
|
+
fi
|
|
8464
8524
|
exit 1
|
|
8465
8525
|
fi
|
|
8466
8526
|
fi
|
|
@@ -8468,6 +8528,10 @@ main() {
|
|
|
8468
8528
|
|
|
8469
8529
|
# Write PID file for ALL modes (foreground + background)
|
|
8470
8530
|
echo "$$" > "$pid_file"
|
|
8531
|
+
# Store session ID in state for dashboard/status visibility
|
|
8532
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
8533
|
+
echo "${LOKI_SESSION_ID}" > ".loki/sessions/${LOKI_SESSION_ID}/session_id"
|
|
8534
|
+
fi
|
|
8471
8535
|
|
|
8472
8536
|
# Initialize PID registry and clean up orphans from previous sessions
|
|
8473
8537
|
init_pid_registry
|
|
@@ -8674,13 +8738,19 @@ main() {
|
|
|
8674
8738
|
fi
|
|
8675
8739
|
stop_dashboard
|
|
8676
8740
|
stop_status_monitor
|
|
8677
|
-
|
|
8741
|
+
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
8742
|
+
rm -f "$loki_dir/loki.pid" 2>/dev/null
|
|
8743
|
+
# Clean up per-session PID file if running with session ID
|
|
8744
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
8745
|
+
rm -f "$loki_dir/sessions/${LOKI_SESSION_ID}/loki.pid" 2>/dev/null
|
|
8746
|
+
fi
|
|
8678
8747
|
# Mark session.json as stopped
|
|
8679
|
-
if [ -f "
|
|
8680
|
-
python3 -c "
|
|
8681
|
-
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']
|
|
8682
8752
|
try:
|
|
8683
|
-
with open(
|
|
8753
|
+
with open(sf, 'r+') as f:
|
|
8684
8754
|
d = json.load(f); d['status'] = 'stopped'
|
|
8685
8755
|
f.seek(0); f.truncate(); json.dump(d, f)
|
|
8686
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
|
@@ -203,6 +203,14 @@ class TaskResponse(BaseModel):
|
|
|
203
203
|
from_attributes = True
|
|
204
204
|
|
|
205
205
|
|
|
206
|
+
class SessionInfo(BaseModel):
|
|
207
|
+
"""Info about a single running session."""
|
|
208
|
+
session_id: str
|
|
209
|
+
pid: int
|
|
210
|
+
status: str = "running"
|
|
211
|
+
log_file: str = ""
|
|
212
|
+
|
|
213
|
+
|
|
206
214
|
class StatusResponse(BaseModel):
|
|
207
215
|
"""Schema for system status response."""
|
|
208
216
|
status: str
|
|
@@ -219,6 +227,8 @@ class StatusResponse(BaseModel):
|
|
|
219
227
|
mode: str = ""
|
|
220
228
|
provider: str = "claude"
|
|
221
229
|
current_task: str = ""
|
|
230
|
+
# Concurrent sessions (v6.4.0)
|
|
231
|
+
sessions: list[SessionInfo] = []
|
|
222
232
|
|
|
223
233
|
|
|
224
234
|
# WebSocket connection manager
|
|
@@ -521,11 +531,77 @@ async def get_status() -> StatusResponse:
|
|
|
521
531
|
except Exception:
|
|
522
532
|
pass
|
|
523
533
|
|
|
534
|
+
# Discover all running sessions (v6.4.0 - concurrent session support)
|
|
535
|
+
active_session_list: list[SessionInfo] = []
|
|
536
|
+
|
|
537
|
+
# Global session
|
|
538
|
+
if running:
|
|
539
|
+
try:
|
|
540
|
+
_global_pid = int(pid_str) if pid_str else 0
|
|
541
|
+
except (ValueError, TypeError):
|
|
542
|
+
_global_pid = 0
|
|
543
|
+
active_session_list.append(SessionInfo(
|
|
544
|
+
session_id="global",
|
|
545
|
+
pid=_global_pid,
|
|
546
|
+
status=status,
|
|
547
|
+
))
|
|
548
|
+
|
|
549
|
+
# Per-session PIDs under .loki/sessions/<id>/
|
|
550
|
+
sessions_dir = loki_dir / "sessions"
|
|
551
|
+
if sessions_dir.is_dir():
|
|
552
|
+
for session_path in sessions_dir.iterdir():
|
|
553
|
+
if not session_path.is_dir():
|
|
554
|
+
continue
|
|
555
|
+
sid = session_path.name
|
|
556
|
+
spid_file = session_path / "loki.pid"
|
|
557
|
+
if spid_file.exists():
|
|
558
|
+
try:
|
|
559
|
+
spid_str = spid_file.read_text().strip()
|
|
560
|
+
spid = int(spid_str)
|
|
561
|
+
os.kill(spid, 0)
|
|
562
|
+
# Find log file if available
|
|
563
|
+
log_path = ""
|
|
564
|
+
log_candidate = loki_dir / "logs" / f"run-{sid}.log"
|
|
565
|
+
if log_candidate.exists():
|
|
566
|
+
log_path = str(log_candidate)
|
|
567
|
+
active_session_list.append(SessionInfo(
|
|
568
|
+
session_id=sid,
|
|
569
|
+
pid=spid,
|
|
570
|
+
status="running",
|
|
571
|
+
log_file=log_path,
|
|
572
|
+
))
|
|
573
|
+
except (ValueError, OSError, ProcessLookupError):
|
|
574
|
+
pass
|
|
575
|
+
|
|
576
|
+
# Legacy run-*.pid files
|
|
577
|
+
for rpf in loki_dir.glob("run-*.pid"):
|
|
578
|
+
sid = rpf.stem.removeprefix("run-")
|
|
579
|
+
# Skip if already found in sessions/
|
|
580
|
+
if any(s.session_id == sid for s in active_session_list):
|
|
581
|
+
continue
|
|
582
|
+
try:
|
|
583
|
+
rpid = int(rpf.read_text().strip())
|
|
584
|
+
os.kill(rpid, 0)
|
|
585
|
+
log_path = ""
|
|
586
|
+
log_candidate = loki_dir / "logs" / f"run-{sid}.log"
|
|
587
|
+
if log_candidate.exists():
|
|
588
|
+
log_path = str(log_candidate)
|
|
589
|
+
active_session_list.append(SessionInfo(
|
|
590
|
+
session_id=sid,
|
|
591
|
+
pid=rpid,
|
|
592
|
+
status="running",
|
|
593
|
+
log_file=log_path,
|
|
594
|
+
))
|
|
595
|
+
except (ValueError, OSError, ProcessLookupError):
|
|
596
|
+
pass
|
|
597
|
+
|
|
598
|
+
total_active = len(active_session_list)
|
|
599
|
+
|
|
524
600
|
return StatusResponse(
|
|
525
601
|
status=status,
|
|
526
602
|
version=version,
|
|
527
603
|
uptime_seconds=uptime,
|
|
528
|
-
active_sessions=
|
|
604
|
+
active_sessions=total_active,
|
|
529
605
|
running_agents=running_agents,
|
|
530
606
|
pending_tasks=pending_tasks,
|
|
531
607
|
database_connected=True,
|
|
@@ -535,6 +611,7 @@ async def get_status() -> StatusResponse:
|
|
|
535
611
|
mode=mode,
|
|
536
612
|
provider=provider,
|
|
537
613
|
current_task=current_task,
|
|
614
|
+
sessions=active_session_list,
|
|
538
615
|
)
|
|
539
616
|
|
|
540
617
|
|
|
@@ -1368,7 +1445,7 @@ async def query_audit_logs(
|
|
|
1368
1445
|
action: Optional[str] = None,
|
|
1369
1446
|
resource_type: Optional[str] = None,
|
|
1370
1447
|
resource_id: Optional[str] = None,
|
|
1371
|
-
limit: int = 100,
|
|
1448
|
+
limit: int = Query(default=100, ge=1, le=1000),
|
|
1372
1449
|
offset: int = 0,
|
|
1373
1450
|
):
|
|
1374
1451
|
"""
|
|
@@ -1488,7 +1565,7 @@ async def get_memory_summary():
|
|
|
1488
1565
|
|
|
1489
1566
|
|
|
1490
1567
|
@app.get("/api/memory/episodes")
|
|
1491
|
-
async def list_episodes(limit: int = 50):
|
|
1568
|
+
async def list_episodes(limit: int = Query(default=50, ge=1, le=1000)):
|
|
1492
1569
|
"""List episodic memory entries."""
|
|
1493
1570
|
ep_dir = _get_loki_dir() / "memory" / "episodic"
|
|
1494
1571
|
episodes = []
|
|
@@ -1505,11 +1582,15 @@ async def list_episodes(limit: int = 50):
|
|
|
1505
1582
|
@app.get("/api/memory/episodes/{episode_id}")
|
|
1506
1583
|
async def get_episode(episode_id: str):
|
|
1507
1584
|
"""Get a specific episodic memory entry."""
|
|
1508
|
-
|
|
1585
|
+
loki_dir = _get_loki_dir()
|
|
1586
|
+
ep_dir = loki_dir / "memory" / "episodic"
|
|
1509
1587
|
if not ep_dir.exists():
|
|
1510
1588
|
raise HTTPException(status_code=404, detail="Episode not found")
|
|
1511
1589
|
# Try direct filename match
|
|
1512
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")
|
|
1513
1594
|
try:
|
|
1514
1595
|
data = json.loads(f.read_text())
|
|
1515
1596
|
if data.get("id") == episode_id or f.stem == episode_id:
|
|
@@ -1560,10 +1641,14 @@ async def list_skills():
|
|
|
1560
1641
|
@app.get("/api/memory/skills/{skill_id}")
|
|
1561
1642
|
async def get_skill(skill_id: str):
|
|
1562
1643
|
"""Get a specific procedural skill."""
|
|
1563
|
-
|
|
1644
|
+
loki_dir = _get_loki_dir()
|
|
1645
|
+
skills_dir = loki_dir / "memory" / "skills"
|
|
1564
1646
|
if not skills_dir.exists():
|
|
1565
1647
|
raise HTTPException(status_code=404, detail="Skill not found")
|
|
1566
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")
|
|
1567
1652
|
try:
|
|
1568
1653
|
data = json.loads(f.read_text())
|
|
1569
1654
|
if data.get("id") == skill_id or f.stem == skill_id:
|
|
@@ -1633,6 +1718,7 @@ def _read_learning_signals(signal_type: Optional[str] = None, limit: int = 50) -
|
|
|
1633
1718
|
(learning/emitter.py). Each file contains a single signal object with fields:
|
|
1634
1719
|
id, type, source, action, timestamp, confidence, outcome, data, context.
|
|
1635
1720
|
"""
|
|
1721
|
+
limit = min(limit, 1000)
|
|
1636
1722
|
signals_dir = _get_loki_dir() / "learning" / "signals"
|
|
1637
1723
|
if not signals_dir.exists() or not signals_dir.is_dir():
|
|
1638
1724
|
return []
|
|
@@ -1752,7 +1838,7 @@ async def get_learning_signals(
|
|
|
1752
1838
|
timeRange: str = "7d",
|
|
1753
1839
|
signalType: Optional[str] = None,
|
|
1754
1840
|
source: Optional[str] = None,
|
|
1755
|
-
limit: int = 50,
|
|
1841
|
+
limit: int = Query(default=50, ge=1, le=1000),
|
|
1756
1842
|
offset: int = 0,
|
|
1757
1843
|
):
|
|
1758
1844
|
"""Get raw learning signals from both events.jsonl and learning signals directory."""
|
|
@@ -1957,7 +2043,7 @@ async def trigger_aggregation():
|
|
|
1957
2043
|
|
|
1958
2044
|
|
|
1959
2045
|
@app.get("/api/learning/preferences")
|
|
1960
|
-
async def get_learning_preferences(limit: int = 50):
|
|
2046
|
+
async def get_learning_preferences(limit: int = Query(default=50, ge=1, le=1000)):
|
|
1961
2047
|
"""Get aggregated user preferences from events and learning signals directory."""
|
|
1962
2048
|
events = _read_events("30d")
|
|
1963
2049
|
prefs = [e for e in events if e.get("type") == "user_preference"]
|
|
@@ -1969,7 +2055,7 @@ async def get_learning_preferences(limit: int = 50):
|
|
|
1969
2055
|
|
|
1970
2056
|
|
|
1971
2057
|
@app.get("/api/learning/errors")
|
|
1972
|
-
async def get_learning_errors(limit: int = 50):
|
|
2058
|
+
async def get_learning_errors(limit: int = Query(default=50, ge=1, le=1000)):
|
|
1973
2059
|
"""Get aggregated error patterns from events and learning signals directory."""
|
|
1974
2060
|
events = _read_events("30d")
|
|
1975
2061
|
errors = [e for e in events if e.get("type") == "error_pattern"]
|
|
@@ -1981,7 +2067,7 @@ async def get_learning_errors(limit: int = 50):
|
|
|
1981
2067
|
|
|
1982
2068
|
|
|
1983
2069
|
@app.get("/api/learning/success")
|
|
1984
|
-
async def get_learning_success(limit: int = 50):
|
|
2070
|
+
async def get_learning_success(limit: int = Query(default=50, ge=1, le=1000)):
|
|
1985
2071
|
"""Get aggregated success patterns from events and learning signals directory."""
|
|
1986
2072
|
events = _read_events("30d")
|
|
1987
2073
|
successes = [e for e in events if e.get("type") == "success_pattern"]
|
|
@@ -1993,7 +2079,7 @@ async def get_learning_success(limit: int = 50):
|
|
|
1993
2079
|
|
|
1994
2080
|
|
|
1995
2081
|
@app.get("/api/learning/tools")
|
|
1996
|
-
async def get_tool_efficiency(limit: int = 50):
|
|
2082
|
+
async def get_tool_efficiency(limit: int = Query(default=50, ge=1, le=1000)):
|
|
1997
2083
|
"""Get tool efficiency rankings from events and learning signals directory."""
|
|
1998
2084
|
events = _read_events("30d")
|
|
1999
2085
|
tools = [e for e in events if e.get("type") == "tool_efficiency"]
|
|
@@ -2432,7 +2518,7 @@ async def get_council_state():
|
|
|
2432
2518
|
|
|
2433
2519
|
|
|
2434
2520
|
@app.get("/api/council/verdicts")
|
|
2435
|
-
async def get_council_verdicts(limit: int = 20):
|
|
2521
|
+
async def get_council_verdicts(limit: int = Query(default=20, ge=1, le=1000)):
|
|
2436
2522
|
"""Get council vote history (decision log)."""
|
|
2437
2523
|
state_file = _get_loki_dir() / "council" / "state.json"
|
|
2438
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
|