loki-mode 5.32.2 → 5.34.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 +21 -28
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +371 -7
- package/autonomy/hooks/quality-gate.sh +1 -1
- package/autonomy/hooks/track-metrics.sh +2 -2
- package/autonomy/hooks/validate-bash.sh +24 -24
- package/autonomy/loki +301 -32
- package/autonomy/run.sh +213 -18
- package/dashboard/__init__.py +1 -1
- package/dashboard/control.py +7 -3
- package/dashboard/server.py +149 -4
- package/dashboard/static/index.html +5 -5
- package/docs/INSTALLATION.md +10 -11
- package/docs/TOOL-INTEGRATION.md +1 -1
- package/docs/dashboard-guide.md +1 -1
- package/events/emit.sh +16 -9
- package/memory/engine.py +24 -16
- package/memory/retrieval.py +28 -19
- package/memory/storage.py +29 -3
- package/memory/vector_index.py +5 -5
- package/package.json +1 -1
package/autonomy/run.sh
CHANGED
|
@@ -1067,14 +1067,16 @@ import_github_issues() {
|
|
|
1067
1067
|
local pending_file=".loki/queue/pending.json"
|
|
1068
1068
|
local task_count=0
|
|
1069
1069
|
|
|
1070
|
-
#
|
|
1070
|
+
# BUG #14 fix: Normalize to bare [] format (consistent with init_loki_dir
|
|
1071
|
+
# and all other queue consumers). Previously used {"tasks":[]} wrapper here
|
|
1072
|
+
# but bare [] everywhere else, causing format mismatch.
|
|
1071
1073
|
if [ ! -f "$pending_file" ]; then
|
|
1072
|
-
echo '
|
|
1073
|
-
elif jq -e 'type == "
|
|
1074
|
-
# Normalize
|
|
1074
|
+
echo '[]' > "$pending_file"
|
|
1075
|
+
elif jq -e 'type == "object"' "$pending_file" &>/dev/null; then
|
|
1076
|
+
# Normalize {"tasks":[...]} wrapper to bare array
|
|
1075
1077
|
local _tmp_normalize
|
|
1076
1078
|
_tmp_normalize=$(mktemp)
|
|
1077
|
-
jq '
|
|
1079
|
+
jq 'if type == "object" then .tasks // [] else . end' "$pending_file" > "$_tmp_normalize" && mv "$_tmp_normalize" "$pending_file"
|
|
1078
1080
|
rm -f "$_tmp_normalize"
|
|
1079
1081
|
fi
|
|
1080
1082
|
|
|
@@ -1094,8 +1096,8 @@ import_github_issues() {
|
|
|
1094
1096
|
url=$(echo "$issue" | jq -r '.url')
|
|
1095
1097
|
labels=$(echo "$issue" | jq -c '[.labels[].name]')
|
|
1096
1098
|
|
|
1097
|
-
# Check if task already exists
|
|
1098
|
-
if jq -e ".
|
|
1099
|
+
# Check if task already exists (bare array format)
|
|
1100
|
+
if jq -e ".[] | select(.github_issue == $number)" "$pending_file" &>/dev/null; then
|
|
1099
1101
|
log_info "Issue #$number already imported, skipping"
|
|
1100
1102
|
continue
|
|
1101
1103
|
fi
|
|
@@ -1137,10 +1139,10 @@ import_github_issues() {
|
|
|
1137
1139
|
created_at: $created
|
|
1138
1140
|
}')
|
|
1139
1141
|
|
|
1140
|
-
# Append to pending.json with temp file cleanup on error
|
|
1142
|
+
# Append to pending.json (bare array format) with temp file cleanup on error
|
|
1141
1143
|
local temp_file
|
|
1142
1144
|
temp_file=$(mktemp)
|
|
1143
|
-
if jq ".
|
|
1145
|
+
if jq ". += [$task_json]" "$pending_file" > "$temp_file" && mv "$temp_file" "$pending_file"; then
|
|
1144
1146
|
log_info "Imported issue #$number: $title"
|
|
1145
1147
|
task_count=$((task_count + 1))
|
|
1146
1148
|
else
|
|
@@ -1293,8 +1295,8 @@ export_tasks_to_github() {
|
|
|
1293
1295
|
return 0
|
|
1294
1296
|
fi
|
|
1295
1297
|
|
|
1296
|
-
# Export non-GitHub tasks as issues
|
|
1297
|
-
jq -c '.tasks[] | select(.source != "github")' "$pending_file" 2>/dev/null | while read -r task; do
|
|
1298
|
+
# Export non-GitHub tasks as issues (handles both bare array and wrapper formats)
|
|
1299
|
+
jq -c 'if type == "object" then .tasks // [] else . end | .[] | select(.source != "github")' "$pending_file" 2>/dev/null | while read -r task; do
|
|
1298
1300
|
local title desc
|
|
1299
1301
|
title=$(echo "$task" | jq -r '.title')
|
|
1300
1302
|
desc=$(echo "$task" | jq -r '.description // ""')
|
|
@@ -2415,13 +2417,20 @@ write_dashboard_state() {
|
|
|
2415
2417
|
local project_path=$(pwd)
|
|
2416
2418
|
local _tmp_state="${output_file}.tmp"
|
|
2417
2419
|
|
|
2420
|
+
# BUG #49 fix: Escape project path/name for JSON to handle special chars
|
|
2421
|
+
# (spaces, quotes, backslashes in directory names)
|
|
2422
|
+
local project_name_escaped
|
|
2423
|
+
local project_path_escaped
|
|
2424
|
+
project_name_escaped=$(printf '%s' "$project_name" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
|
2425
|
+
project_path_escaped=$(printf '%s' "$project_path" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
|
2426
|
+
|
|
2418
2427
|
cat > "$_tmp_state" << EOF
|
|
2419
2428
|
{
|
|
2420
2429
|
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
2421
2430
|
"version": "$version",
|
|
2422
2431
|
"project": {
|
|
2423
|
-
"name": "$
|
|
2424
|
-
"path": "$
|
|
2432
|
+
"name": "$project_name_escaped",
|
|
2433
|
+
"path": "$project_path_escaped"
|
|
2425
2434
|
},
|
|
2426
2435
|
"mode": "$mode",
|
|
2427
2436
|
"provider": "${PROVIDER_NAME:-claude}",
|
|
@@ -3870,6 +3879,167 @@ if top:
|
|
|
3870
3879
|
SOLUTIONS_SCRIPT
|
|
3871
3880
|
}
|
|
3872
3881
|
|
|
3882
|
+
# ============================================================================
|
|
3883
|
+
# Checkpoint/Snapshot System (v5.34.0)
|
|
3884
|
+
# Git-based checkpoints after task completion with state snapshots
|
|
3885
|
+
# Inspired by Cursor Self-Driving Codebases + Entire.io provenance tracking
|
|
3886
|
+
# ============================================================================
|
|
3887
|
+
|
|
3888
|
+
create_checkpoint() {
|
|
3889
|
+
# Create a git checkpoint after task completion
|
|
3890
|
+
# Args: $1 = task description, $2 = task_id (optional)
|
|
3891
|
+
local task_desc="${1:-task completed}"
|
|
3892
|
+
local task_id="${2:-unknown}"
|
|
3893
|
+
local checkpoint_dir=".loki/state/checkpoints"
|
|
3894
|
+
local iteration="${ITERATION_COUNT:-0}"
|
|
3895
|
+
|
|
3896
|
+
mkdir -p "$checkpoint_dir"
|
|
3897
|
+
|
|
3898
|
+
# Only checkpoint if there are uncommitted changes
|
|
3899
|
+
if ! git diff --quiet 2>/dev/null && ! git diff --cached --quiet 2>/dev/null; then
|
|
3900
|
+
log_info "No uncommitted changes to checkpoint"
|
|
3901
|
+
return 0
|
|
3902
|
+
fi
|
|
3903
|
+
|
|
3904
|
+
# Capture git state
|
|
3905
|
+
local git_sha
|
|
3906
|
+
git_sha=$(git rev-parse HEAD 2>/dev/null || echo "no-git")
|
|
3907
|
+
local git_branch
|
|
3908
|
+
git_branch=$(git branch --show-current 2>/dev/null || echo "unknown")
|
|
3909
|
+
|
|
3910
|
+
# Snapshot .loki state files
|
|
3911
|
+
local timestamp
|
|
3912
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
3913
|
+
local checkpoint_id="cp-${iteration}-$(date +%s)"
|
|
3914
|
+
local cp_dir="${checkpoint_dir}/${checkpoint_id}"
|
|
3915
|
+
|
|
3916
|
+
mkdir -p "$cp_dir"
|
|
3917
|
+
|
|
3918
|
+
# Copy critical state files (lightweight -- not full .loki/)
|
|
3919
|
+
for f in state/orchestrator.json queue/pending.json queue/completed.json queue/in-progress.json queue/current-task.json; do
|
|
3920
|
+
if [ -f ".loki/$f" ]; then
|
|
3921
|
+
local target_dir="$cp_dir/$(dirname "$f")"
|
|
3922
|
+
mkdir -p "$target_dir"
|
|
3923
|
+
cp ".loki/$f" "$cp_dir/$f" 2>/dev/null || true
|
|
3924
|
+
fi
|
|
3925
|
+
done
|
|
3926
|
+
|
|
3927
|
+
# Write checkpoint metadata
|
|
3928
|
+
local safe_desc
|
|
3929
|
+
safe_desc=$(printf '%s' "$task_desc" | sed 's/\\/\\\\/g; s/"/\\"/g' | head -c 200)
|
|
3930
|
+
cat > "$cp_dir/metadata.json" << CPEOF
|
|
3931
|
+
{
|
|
3932
|
+
"id": "${checkpoint_id}",
|
|
3933
|
+
"timestamp": "${timestamp}",
|
|
3934
|
+
"iteration": ${iteration},
|
|
3935
|
+
"task_id": "${task_id}",
|
|
3936
|
+
"task_description": "${safe_desc}",
|
|
3937
|
+
"git_sha": "${git_sha}",
|
|
3938
|
+
"git_branch": "${git_branch}",
|
|
3939
|
+
"provider": "${PROVIDER_NAME:-claude}",
|
|
3940
|
+
"phase": "$(cat .loki/state/orchestrator.json 2>/dev/null | python3 -c 'import sys,json; print(json.load(sys.stdin).get("currentPhase","unknown"))' 2>/dev/null || echo 'unknown')"
|
|
3941
|
+
}
|
|
3942
|
+
CPEOF
|
|
3943
|
+
|
|
3944
|
+
# Maintain checkpoint index for fast listing
|
|
3945
|
+
local index_file="${checkpoint_dir}/index.jsonl"
|
|
3946
|
+
printf '{"id":"%s","ts":"%s","iter":%d,"task":"%s","sha":"%s"}\n' \
|
|
3947
|
+
"$checkpoint_id" "$timestamp" "$iteration" "$safe_desc" "$git_sha" \
|
|
3948
|
+
>> "$index_file"
|
|
3949
|
+
|
|
3950
|
+
# Retention: keep last 50 checkpoints, prune older
|
|
3951
|
+
local cp_count
|
|
3952
|
+
cp_count=$(find "$checkpoint_dir" -maxdepth 1 -type d -name "cp-*" 2>/dev/null | wc -l | tr -d ' ')
|
|
3953
|
+
if [ "$cp_count" -gt 50 ]; then
|
|
3954
|
+
local to_remove=$((cp_count - 50))
|
|
3955
|
+
find "$checkpoint_dir" -maxdepth 1 -type d -name "cp-*" 2>/dev/null | sort | head -n "$to_remove" | while read -r old_cp; do
|
|
3956
|
+
rm -r "$old_cp" 2>/dev/null || true
|
|
3957
|
+
done
|
|
3958
|
+
# Rebuild index from remaining checkpoints
|
|
3959
|
+
: > "$index_file"
|
|
3960
|
+
for remaining in "$checkpoint_dir"/cp-*/metadata.json; do
|
|
3961
|
+
[ -f "$remaining" ] || continue
|
|
3962
|
+
python3 -c "
|
|
3963
|
+
import json,sys
|
|
3964
|
+
m=json.load(open('$remaining'))
|
|
3965
|
+
print(json.dumps({'id':m['id'],'ts':m['timestamp'],'iter':m['iteration'],'task':m.get('task_description',''),'sha':m['git_sha']}))
|
|
3966
|
+
" >> "$index_file" 2>/dev/null || true
|
|
3967
|
+
done
|
|
3968
|
+
fi
|
|
3969
|
+
|
|
3970
|
+
log_info "Checkpoint created: ${checkpoint_id} (git: ${git_sha:0:8})"
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3973
|
+
rollback_to_checkpoint() {
|
|
3974
|
+
# Rollback state files to a specific checkpoint
|
|
3975
|
+
# Args: $1 = checkpoint_id
|
|
3976
|
+
local checkpoint_id="$1"
|
|
3977
|
+
local checkpoint_dir=".loki/state/checkpoints"
|
|
3978
|
+
local cp_dir="${checkpoint_dir}/${checkpoint_id}"
|
|
3979
|
+
|
|
3980
|
+
if [ ! -d "$cp_dir" ]; then
|
|
3981
|
+
log_error "Checkpoint not found: ${checkpoint_id}"
|
|
3982
|
+
return 1
|
|
3983
|
+
fi
|
|
3984
|
+
|
|
3985
|
+
# Read checkpoint metadata
|
|
3986
|
+
local git_sha
|
|
3987
|
+
git_sha=$(python3 -c "import json; print(json.load(open('${cp_dir}/metadata.json'))['git_sha'])" 2>/dev/null || echo "")
|
|
3988
|
+
|
|
3989
|
+
log_warn "Rolling back to checkpoint: ${checkpoint_id}"
|
|
3990
|
+
|
|
3991
|
+
# Create a pre-rollback checkpoint first
|
|
3992
|
+
create_checkpoint "pre-rollback snapshot" "rollback"
|
|
3993
|
+
|
|
3994
|
+
# Restore state files
|
|
3995
|
+
for f in state/orchestrator.json queue/pending.json queue/completed.json queue/in-progress.json queue/current-task.json; do
|
|
3996
|
+
if [ -f "${cp_dir}/${f}" ]; then
|
|
3997
|
+
local target_dir=".loki/$(dirname "$f")"
|
|
3998
|
+
mkdir -p "$target_dir"
|
|
3999
|
+
cp "${cp_dir}/${f}" ".loki/${f}" 2>/dev/null || true
|
|
4000
|
+
fi
|
|
4001
|
+
done
|
|
4002
|
+
|
|
4003
|
+
# Log the rollback
|
|
4004
|
+
local timestamp
|
|
4005
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
4006
|
+
printf '{"event":"rollback","checkpoint":"%s","git_sha":"%s","timestamp":"%s"}\n' \
|
|
4007
|
+
"$checkpoint_id" "$git_sha" "$timestamp" \
|
|
4008
|
+
>> ".loki/events.jsonl" 2>/dev/null || true
|
|
4009
|
+
|
|
4010
|
+
log_info "State files restored from checkpoint: ${checkpoint_id}"
|
|
4011
|
+
|
|
4012
|
+
if [ -n "$git_sha" ] && [ "$git_sha" != "no-git" ]; then
|
|
4013
|
+
log_info "Git SHA at checkpoint: ${git_sha}"
|
|
4014
|
+
log_info "To rollback code: git reset --hard ${git_sha}"
|
|
4015
|
+
fi
|
|
4016
|
+
}
|
|
4017
|
+
|
|
4018
|
+
list_checkpoints() {
|
|
4019
|
+
# List recent checkpoints
|
|
4020
|
+
local checkpoint_dir=".loki/state/checkpoints"
|
|
4021
|
+
local index_file="${checkpoint_dir}/index.jsonl"
|
|
4022
|
+
local limit="${1:-10}"
|
|
4023
|
+
|
|
4024
|
+
if [ ! -f "$index_file" ]; then
|
|
4025
|
+
echo "No checkpoints found."
|
|
4026
|
+
return
|
|
4027
|
+
fi
|
|
4028
|
+
|
|
4029
|
+
tail -n "$limit" "$index_file" | python3 -c "
|
|
4030
|
+
import sys, json
|
|
4031
|
+
lines = sys.stdin.readlines()
|
|
4032
|
+
for line in reversed(lines):
|
|
4033
|
+
try:
|
|
4034
|
+
cp = json.loads(line)
|
|
4035
|
+
sha = cp.get('sha','')[:8]
|
|
4036
|
+
task = cp.get('task','')[:60]
|
|
4037
|
+
print(f\" {cp['id']} {cp['ts']} [{sha}] {task}\")
|
|
4038
|
+
except:
|
|
4039
|
+
continue
|
|
4040
|
+
"
|
|
4041
|
+
}
|
|
4042
|
+
|
|
3873
4043
|
start_dashboard() {
|
|
3874
4044
|
log_header "Starting Loki Dashboard"
|
|
3875
4045
|
|
|
@@ -4665,12 +4835,12 @@ build_prompt() {
|
|
|
4665
4835
|
fi
|
|
4666
4836
|
|
|
4667
4837
|
# Human directive injection (from HUMAN_INPUT.md)
|
|
4838
|
+
# NOTE: Do NOT unset LOKI_HUMAN_INPUT here - build_prompt runs in a subshell
|
|
4839
|
+
# (command substitution) so unset would not affect the parent shell.
|
|
4840
|
+
# The caller (run_autonomous) clears it after consuming the prompt.
|
|
4668
4841
|
local human_directive=""
|
|
4669
4842
|
if [ -n "${LOKI_HUMAN_INPUT:-}" ]; then
|
|
4670
4843
|
human_directive="HUMAN_DIRECTIVE (PRIORITY): $LOKI_HUMAN_INPUT Execute this directive BEFORE continuing normal tasks."
|
|
4671
|
-
# Clear after consumption so it doesn't repeat every iteration
|
|
4672
|
-
unset LOKI_HUMAN_INPUT
|
|
4673
|
-
rm -f "${TARGET_DIR:-.}/.loki/HUMAN_INPUT.md"
|
|
4674
4844
|
fi
|
|
4675
4845
|
|
|
4676
4846
|
# Queue task injection (from dashboard or API)
|
|
@@ -4795,6 +4965,15 @@ run_autonomous() {
|
|
|
4795
4965
|
|
|
4796
4966
|
local prompt=$(build_prompt $retry "$prd_path" $ITERATION_COUNT)
|
|
4797
4967
|
|
|
4968
|
+
# BUG #5 fix: Clear LOKI_HUMAN_INPUT in the parent shell after build_prompt
|
|
4969
|
+
# consumed it. build_prompt runs in a subshell (command substitution), so
|
|
4970
|
+
# any unset inside it does not affect the parent. Clear here to prevent
|
|
4971
|
+
# the same directive from repeating every iteration.
|
|
4972
|
+
if [ -n "${LOKI_HUMAN_INPUT:-}" ]; then
|
|
4973
|
+
unset LOKI_HUMAN_INPUT
|
|
4974
|
+
rm -f "${TARGET_DIR:-.}/.loki/HUMAN_INPUT.md"
|
|
4975
|
+
fi
|
|
4976
|
+
|
|
4798
4977
|
echo ""
|
|
4799
4978
|
log_header "Attempt $((retry + 1)) of $MAX_RETRIES"
|
|
4800
4979
|
log_info "Prompt: $prompt"
|
|
@@ -5234,11 +5413,19 @@ check_human_intervention() {
|
|
|
5234
5413
|
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
5235
5414
|
|
|
5236
5415
|
# Check for PAUSE file
|
|
5416
|
+
# BUG #4 fix: Check handle_pause return value before deleting PAUSE file.
|
|
5417
|
+
# handle_pause returns 1 if STOP was requested during the pause, so we must
|
|
5418
|
+
# propagate that as return 2 (stop) instead of always returning 1 (continue).
|
|
5237
5419
|
if [ -f "$loki_dir/PAUSE" ]; then
|
|
5238
5420
|
log_warn "PAUSE file detected - pausing execution"
|
|
5239
5421
|
notify_intervention_needed "Execution paused via PAUSE file"
|
|
5240
5422
|
handle_pause
|
|
5423
|
+
local pause_result=$?
|
|
5241
5424
|
rm -f "$loki_dir/PAUSE"
|
|
5425
|
+
if [ "$pause_result" -eq 1 ]; then
|
|
5426
|
+
# STOP was requested during pause
|
|
5427
|
+
return 2
|
|
5428
|
+
fi
|
|
5242
5429
|
return 1
|
|
5243
5430
|
fi
|
|
5244
5431
|
|
|
@@ -5287,8 +5474,13 @@ check_human_intervention() {
|
|
|
5287
5474
|
rm -f "$loki_dir/signals/COUNCIL_REVIEW_REQUESTED"
|
|
5288
5475
|
if type council_vote &>/dev/null && council_vote; then
|
|
5289
5476
|
log_header "COMPLETION COUNCIL: FORCE REVIEW - PROJECT COMPLETE"
|
|
5290
|
-
#
|
|
5291
|
-
|
|
5477
|
+
# BUG #17 fix: Write COMPLETED marker, generate council report, and
|
|
5478
|
+
# run memory consolidation (matching the normal council approval path
|
|
5479
|
+
# in council_should_stop).
|
|
5480
|
+
echo "Council force-review approved at iteration $ITERATION_COUNT on $(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$loki_dir/COMPLETED"
|
|
5481
|
+
if type council_write_report &>/dev/null; then
|
|
5482
|
+
council_write_report
|
|
5483
|
+
fi
|
|
5292
5484
|
log_info "Running memory consolidation..."
|
|
5293
5485
|
run_memory_consolidation
|
|
5294
5486
|
notify_all_complete
|
|
@@ -5764,6 +5956,9 @@ main() {
|
|
|
5764
5956
|
# Compound learnings into structured solution files (v5.30.0)
|
|
5765
5957
|
compound_session_to_solutions
|
|
5766
5958
|
|
|
5959
|
+
# Create session-end checkpoint (v5.34.0)
|
|
5960
|
+
create_checkpoint "session end (iterations=$ITERATION_COUNT)" "session-end"
|
|
5961
|
+
|
|
5767
5962
|
# Log session end for audit
|
|
5768
5963
|
audit_log "SESSION_END" "result=$result,prd=$PRD_PATH"
|
|
5769
5964
|
|
package/dashboard/__init__.py
CHANGED
package/dashboard/control.py
CHANGED
|
@@ -56,10 +56,13 @@ app = FastAPI(
|
|
|
56
56
|
version="1.0.0"
|
|
57
57
|
)
|
|
58
58
|
|
|
59
|
-
# CORS middleware for dashboard frontend
|
|
59
|
+
# CORS middleware for dashboard frontend - restricted to localhost by default.
|
|
60
|
+
# Set LOKI_DASHBOARD_CORS to override (comma-separated origins).
|
|
61
|
+
_cors_default = "http://localhost:57374,http://127.0.0.1:57374"
|
|
62
|
+
_cors_origins = os.environ.get("LOKI_DASHBOARD_CORS", _cors_default).split(",")
|
|
60
63
|
app.add_middleware(
|
|
61
64
|
CORSMiddleware,
|
|
62
|
-
allow_origins=[
|
|
65
|
+
allow_origins=[o.strip() for o in _cors_origins if o.strip()],
|
|
63
66
|
allow_credentials=True,
|
|
64
67
|
allow_methods=["*"],
|
|
65
68
|
allow_headers=["*"],
|
|
@@ -511,4 +514,5 @@ async def asyncio_sleep(seconds: float):
|
|
|
511
514
|
if __name__ == "__main__":
|
|
512
515
|
import uvicorn
|
|
513
516
|
port = int(os.environ.get("LOKI_DASHBOARD_PORT", "57374"))
|
|
514
|
-
|
|
517
|
+
host = os.environ.get("LOKI_DASHBOARD_HOST", "127.0.0.1")
|
|
518
|
+
uvicorn.run(app, host=host, port=port)
|
package/dashboard/server.py
CHANGED
|
@@ -209,9 +209,10 @@ app = FastAPI(
|
|
|
209
209
|
lifespan=lifespan,
|
|
210
210
|
)
|
|
211
211
|
|
|
212
|
-
# Add CORS middleware -
|
|
213
|
-
#
|
|
214
|
-
|
|
212
|
+
# Add CORS middleware - restricted to localhost by default.
|
|
213
|
+
# Set LOKI_DASHBOARD_CORS to override (comma-separated origins).
|
|
214
|
+
_cors_default = "http://localhost:57374,http://127.0.0.1:57374"
|
|
215
|
+
_cors_origins = os.environ.get("LOKI_DASHBOARD_CORS", _cors_default).split(",")
|
|
215
216
|
app.add_middleware(
|
|
216
217
|
CORSMiddleware,
|
|
217
218
|
allow_origins=[o.strip() for o in _cors_origins if o.strip()],
|
|
@@ -1983,6 +1984,149 @@ async def force_council_review():
|
|
|
1983
1984
|
return {"success": True, "message": "Council review requested"}
|
|
1984
1985
|
|
|
1985
1986
|
|
|
1987
|
+
# =============================================================================
|
|
1988
|
+
# Checkpoint API (v5.34.0)
|
|
1989
|
+
# =============================================================================
|
|
1990
|
+
|
|
1991
|
+
class CheckpointCreate(BaseModel):
|
|
1992
|
+
"""Schema for creating a checkpoint."""
|
|
1993
|
+
message: Optional[str] = Field(None, description="Optional description for the checkpoint")
|
|
1994
|
+
|
|
1995
|
+
|
|
1996
|
+
def _sanitize_checkpoint_id(checkpoint_id: str) -> str:
|
|
1997
|
+
"""Validate checkpoint_id contains only safe characters for file paths."""
|
|
1998
|
+
if not checkpoint_id or ".." in checkpoint_id or not _SAFE_ID_RE.match(checkpoint_id):
|
|
1999
|
+
raise HTTPException(
|
|
2000
|
+
status_code=400,
|
|
2001
|
+
detail="Invalid checkpoint_id: must contain only alphanumeric characters, hyphens, and underscores",
|
|
2002
|
+
)
|
|
2003
|
+
return checkpoint_id
|
|
2004
|
+
|
|
2005
|
+
|
|
2006
|
+
@app.get("/api/checkpoints")
|
|
2007
|
+
async def list_checkpoints(limit: int = Query(default=20, ge=1, le=200)):
|
|
2008
|
+
"""List recent checkpoints from index.jsonl."""
|
|
2009
|
+
loki_dir = _get_loki_dir()
|
|
2010
|
+
index_file = loki_dir / "state" / "checkpoints" / "index.jsonl"
|
|
2011
|
+
checkpoints = []
|
|
2012
|
+
|
|
2013
|
+
if index_file.exists():
|
|
2014
|
+
try:
|
|
2015
|
+
for line in index_file.read_text().strip().split("\n"):
|
|
2016
|
+
if line.strip():
|
|
2017
|
+
try:
|
|
2018
|
+
checkpoints.append(json.loads(line))
|
|
2019
|
+
except json.JSONDecodeError:
|
|
2020
|
+
pass
|
|
2021
|
+
except Exception:
|
|
2022
|
+
pass
|
|
2023
|
+
|
|
2024
|
+
# Return most recent first, limited
|
|
2025
|
+
checkpoints.reverse()
|
|
2026
|
+
return checkpoints[:limit]
|
|
2027
|
+
|
|
2028
|
+
|
|
2029
|
+
@app.get("/api/checkpoints/{checkpoint_id}")
|
|
2030
|
+
async def get_checkpoint(checkpoint_id: str):
|
|
2031
|
+
"""Get checkpoint details by ID."""
|
|
2032
|
+
checkpoint_id = _sanitize_checkpoint_id(checkpoint_id)
|
|
2033
|
+
loki_dir = _get_loki_dir()
|
|
2034
|
+
metadata_file = loki_dir / "state" / "checkpoints" / checkpoint_id / "metadata.json"
|
|
2035
|
+
|
|
2036
|
+
if not metadata_file.exists():
|
|
2037
|
+
raise HTTPException(status_code=404, detail="Checkpoint not found")
|
|
2038
|
+
|
|
2039
|
+
try:
|
|
2040
|
+
return json.loads(metadata_file.read_text())
|
|
2041
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
2042
|
+
raise HTTPException(status_code=500, detail=f"Failed to read checkpoint: {e}")
|
|
2043
|
+
|
|
2044
|
+
|
|
2045
|
+
@app.post("/api/checkpoints", status_code=201)
|
|
2046
|
+
async def create_checkpoint(body: CheckpointCreate = None):
|
|
2047
|
+
"""Create a new checkpoint capturing current state."""
|
|
2048
|
+
import subprocess
|
|
2049
|
+
import shutil
|
|
2050
|
+
|
|
2051
|
+
loki_dir = _get_loki_dir()
|
|
2052
|
+
checkpoints_dir = loki_dir / "state" / "checkpoints"
|
|
2053
|
+
checkpoints_dir.mkdir(parents=True, exist_ok=True)
|
|
2054
|
+
|
|
2055
|
+
# Generate checkpoint ID from timestamp
|
|
2056
|
+
now = datetime.now(timezone.utc)
|
|
2057
|
+
checkpoint_id = now.strftime("chk-%Y%m%d-%H%M%S")
|
|
2058
|
+
|
|
2059
|
+
# Create checkpoint directory
|
|
2060
|
+
checkpoint_dir = checkpoints_dir / checkpoint_id
|
|
2061
|
+
checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
|
2062
|
+
|
|
2063
|
+
# Capture git SHA
|
|
2064
|
+
git_sha = ""
|
|
2065
|
+
try:
|
|
2066
|
+
result = subprocess.run(
|
|
2067
|
+
["git", "rev-parse", "HEAD"],
|
|
2068
|
+
capture_output=True, text=True, timeout=5,
|
|
2069
|
+
)
|
|
2070
|
+
if result.returncode == 0:
|
|
2071
|
+
git_sha = result.stdout.strip()
|
|
2072
|
+
except Exception:
|
|
2073
|
+
pass
|
|
2074
|
+
|
|
2075
|
+
# Copy key state files into checkpoint
|
|
2076
|
+
state_files = [
|
|
2077
|
+
"dashboard-state.json",
|
|
2078
|
+
"session.json",
|
|
2079
|
+
]
|
|
2080
|
+
for fname in state_files:
|
|
2081
|
+
src = loki_dir / fname
|
|
2082
|
+
if src.exists():
|
|
2083
|
+
try:
|
|
2084
|
+
shutil.copy2(str(src), str(checkpoint_dir / fname))
|
|
2085
|
+
except Exception:
|
|
2086
|
+
pass
|
|
2087
|
+
|
|
2088
|
+
# Copy queue directory if present
|
|
2089
|
+
queue_src = loki_dir / "queue"
|
|
2090
|
+
if queue_src.exists():
|
|
2091
|
+
try:
|
|
2092
|
+
shutil.copytree(str(queue_src), str(checkpoint_dir / "queue"), dirs_exist_ok=True)
|
|
2093
|
+
except Exception:
|
|
2094
|
+
pass
|
|
2095
|
+
|
|
2096
|
+
# Build metadata
|
|
2097
|
+
message = ""
|
|
2098
|
+
if body and body.message:
|
|
2099
|
+
message = body.message
|
|
2100
|
+
|
|
2101
|
+
metadata = {
|
|
2102
|
+
"id": checkpoint_id,
|
|
2103
|
+
"created_at": now.isoformat(),
|
|
2104
|
+
"git_sha": git_sha,
|
|
2105
|
+
"message": message,
|
|
2106
|
+
"files": [f.name for f in checkpoint_dir.iterdir() if f.is_file()],
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
# Write metadata.json
|
|
2110
|
+
(checkpoint_dir / "metadata.json").write_text(json.dumps(metadata, indent=2))
|
|
2111
|
+
|
|
2112
|
+
# Append to index.jsonl
|
|
2113
|
+
index_file = checkpoints_dir / "index.jsonl"
|
|
2114
|
+
with open(str(index_file), "a") as f:
|
|
2115
|
+
f.write(json.dumps(metadata) + "\n")
|
|
2116
|
+
|
|
2117
|
+
# Retention policy: keep last 50 checkpoints
|
|
2118
|
+
MAX_CHECKPOINTS = 50
|
|
2119
|
+
all_dirs = sorted(
|
|
2120
|
+
[d for d in checkpoints_dir.iterdir() if d.is_dir()],
|
|
2121
|
+
key=lambda d: d.name,
|
|
2122
|
+
)
|
|
2123
|
+
while len(all_dirs) > MAX_CHECKPOINTS:
|
|
2124
|
+
oldest = all_dirs.pop(0)
|
|
2125
|
+
shutil.rmtree(str(oldest), ignore_errors=True)
|
|
2126
|
+
|
|
2127
|
+
return metadata
|
|
2128
|
+
|
|
2129
|
+
|
|
1986
2130
|
# =============================================================================
|
|
1987
2131
|
# Agent Management API (v5.25.0)
|
|
1988
2132
|
# =============================================================================
|
|
@@ -2046,6 +2190,7 @@ async def get_agents():
|
|
|
2046
2190
|
@app.post("/api/agents/{agent_id}/kill")
|
|
2047
2191
|
async def kill_agent(agent_id: str):
|
|
2048
2192
|
"""Kill a specific agent by ID."""
|
|
2193
|
+
agent_id = _sanitize_agent_id(agent_id)
|
|
2049
2194
|
agents_file = _get_loki_dir() / "state" / "agents.json"
|
|
2050
2195
|
if not agents_file.exists():
|
|
2051
2196
|
raise HTTPException(404, "No agents file found")
|
|
@@ -2296,7 +2441,7 @@ def run_server(host: str = None, port: int = None) -> None:
|
|
|
2296
2441
|
"""Run the dashboard server."""
|
|
2297
2442
|
import uvicorn
|
|
2298
2443
|
if host is None:
|
|
2299
|
-
# Default to localhost-only
|
|
2444
|
+
# Default to localhost-only for security
|
|
2300
2445
|
host = os.environ.get("LOKI_DASHBOARD_HOST", "127.0.0.1")
|
|
2301
2446
|
if port is None:
|
|
2302
2447
|
port = int(os.environ.get("LOKI_DASHBOARD_PORT", "57374"))
|
|
@@ -3750,7 +3750,7 @@ var LokiDashboard=(()=>{var N=Object.defineProperty;var se=Object.getOwnProperty
|
|
|
3750
3750
|
${t}
|
|
3751
3751
|
</div>
|
|
3752
3752
|
</div>
|
|
3753
|
-
`,this._attachEventListeners()}_attachEventListeners(){let e=this.shadowRoot.getElementById("time-range-select");e&&e.addEventListener("change",r=>this._setFilter("timeRange",r.target.value));let t=this.shadowRoot.getElementById("signal-type-select");t&&t.addEventListener("change",r=>this._setFilter("signalType",r.target.value));let a=this.shadowRoot.getElementById("source-select");a&&a.addEventListener("change",r=>this._setFilter("source",r.target.value));let i=this.shadowRoot.getElementById("refresh-btn");i&&i.addEventListener("click",()=>this._loadData());let s=this.shadowRoot.getElementById("close-detail");s&&s.addEventListener("click",()=>this._closeDetail()),this.shadowRoot.querySelectorAll(".list-item").forEach(r=>{r.addEventListener("click",()=>{let n=r.dataset.type,d=r.dataset.id,p=this._findItemData(n,d);p&&this._selectMetric(n,p)}),r.addEventListener("keydown",n=>{(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),r.click())})})}_findItemData(e,t){if(!this._metrics?.aggregation)return null;switch(e){case"preference":return this._metrics.aggregation.preferences?.find(a=>a.preference_key===t);case"error_pattern":return this._metrics.aggregation.error_patterns?.find(a=>a.error_type===t);case"success_pattern":return this._metrics.aggregation.success_patterns?.find(a=>a.pattern_name===t);case"tool_efficiency":return this._metrics.aggregation.tool_efficiencies?.find(a=>a.tool_name===t);default:return null}}};customElements.get("loki-learning-dashboard")||customElements.define("loki-learning-dashboard",H);var me=[{id:"overview",label:"Overview"},{id:"decisions",label:"Decision Log"},{id:"convergence",label:"Convergence"},{id:"agents",label:"Agents"}],O=class extends c{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._loading=!1,this._error=null,this._api=null,this._activeTab="overview",this._pollInterval=null,this._councilState=null,this._verdicts=[],this._convergence=[],this._agents=[],this._selectedAgent=null,this._lastDataHash=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadData(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling()}attributeChangedCallback(e,t,a){t!==a&&(e==="api-url"&&this._api&&(this._api.baseUrl=a,this._loadData()),e==="theme"&&this._applyTheme())}_setupApi(){let e=this.getAttribute("api-url")||window.location.origin;this._api=g({baseUrl:e})}_startPolling(){this._pollInterval=setInterval(()=>this._loadData(),3e3),this._visibilityHandler=()=>{document.hidden?this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null):this._pollInterval||(this._loadData(),this._pollInterval=setInterval(()=>this._loadData(),3e3))},document.addEventListener("visibilitychange",this._visibilityHandler)}_stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null),this._visibilityHandler&&(document.removeEventListener("visibilitychange",this._visibilityHandler),this._visibilityHandler=null)}async _loadData(){try{let[t,a,i,s]=await Promise.allSettled([this._api._get("/api/council/state"),this._api._get("/api/council/verdicts"),this._api._get("/api/council/convergence"),this._api._get("/api/agents")]);t.status==="fulfilled"&&(this._councilState=t.value),a.status==="fulfilled"&&(this._verdicts=a.value.verdicts||[]),i.status==="fulfilled"&&(this._convergence=i.value.dataPoints||[]),s.status==="fulfilled"&&(this._agents=Array.isArray(s.value)?s.value:[]),this._error=null}catch(t){this._error=t.message}let e=JSON.stringify({s:this._councilState,v:this._verdicts,c:this._convergence,a:this._agents,e:this._error});e!==this._lastDataHash&&(this._lastDataHash=e,this.render())}async _forceReview(){try{await this._api._post("/api/council/force-review"),this.dispatchEvent(new CustomEvent("council-action",{detail:{action:"force-review"},bubbles:!0}))}catch(e){this._error=`Failed to force review: ${e.message}`,this.render()}}async _killAgent(e){if(confirm(`Kill agent ${e}?`))try{await this._api._post(`/api/agents/${e}/kill`),this.dispatchEvent(new CustomEvent("council-action",{detail:{action:"kill-agent",agentId:e},bubbles:!0})),await this._loadData()}catch(t){this._error=`Failed to kill agent: ${t.message}`,this.render()}}async _pauseAgent(e){try{await this._api._post(`/api/agents/${e}/pause`),await this._loadData()}catch(t){this._error=`Failed to pause agent: ${t.message}`,this.render()}}async _resumeAgent(e){try{await this._api._post(`/api/agents/${e}/resume`),await this._loadData()}catch(t){this._error=`Failed to resume agent: ${t.message}`,this.render()}}_setTab(e){this._activeTab=e,this.render()}_selectAgent(e){this._selectedAgent=this._selectedAgent?.id===e.id?null:e,this.render()}render(){let e=this.shadowRoot;e&&(e.innerHTML=`
|
|
3753
|
+
`,this._attachEventListeners()}_attachEventListeners(){let e=this.shadowRoot.getElementById("time-range-select");e&&e.addEventListener("change",r=>this._setFilter("timeRange",r.target.value));let t=this.shadowRoot.getElementById("signal-type-select");t&&t.addEventListener("change",r=>this._setFilter("signalType",r.target.value));let a=this.shadowRoot.getElementById("source-select");a&&a.addEventListener("change",r=>this._setFilter("source",r.target.value));let i=this.shadowRoot.getElementById("refresh-btn");i&&i.addEventListener("click",()=>this._loadData());let s=this.shadowRoot.getElementById("close-detail");s&&s.addEventListener("click",()=>this._closeDetail()),this.shadowRoot.querySelectorAll(".list-item").forEach(r=>{r.addEventListener("click",()=>{let n=r.dataset.type,d=r.dataset.id,p=this._findItemData(n,d);p&&this._selectMetric(n,p)}),r.addEventListener("keydown",n=>{(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),r.click())})})}_findItemData(e,t){if(!this._metrics?.aggregation)return null;switch(e){case"preference":return this._metrics.aggregation.preferences?.find(a=>a.preference_key===t);case"error_pattern":return this._metrics.aggregation.error_patterns?.find(a=>a.error_type===t);case"success_pattern":return this._metrics.aggregation.success_patterns?.find(a=>a.pattern_name===t);case"tool_efficiency":return this._metrics.aggregation.tool_efficiencies?.find(a=>a.tool_name===t);default:return null}}};customElements.get("loki-learning-dashboard")||customElements.define("loki-learning-dashboard",H);var me=[{id:"overview",label:"Overview"},{id:"decisions",label:"Decision Log"},{id:"convergence",label:"Convergence"},{id:"agents",label:"Agents"}],O=class extends c{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._loading=!1,this._error=null,this._api=null,this._activeTab="overview",this._pollInterval=null,this._councilState=null,this._verdicts=[],this._convergence=[],this._agents=[],this._selectedAgent=null,this._lastDataHash=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadData(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling()}attributeChangedCallback(e,t,a){t!==a&&(e==="api-url"&&this._api&&(this._api.baseUrl=a,this._loadData()),e==="theme"&&this._applyTheme())}_setupApi(){let e=this.getAttribute("api-url")||window.location.origin;this._api=g({baseUrl:e})}_startPolling(){this._pollInterval=setInterval(()=>this._loadData(),3e3),this._visibilityHandler=()=>{document.hidden?this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null):this._pollInterval||(this._loadData(),this._pollInterval=setInterval(()=>this._loadData(),3e3))},document.addEventListener("visibilitychange",this._visibilityHandler)}_stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null),this._visibilityHandler&&(document.removeEventListener("visibilitychange",this._visibilityHandler),this._visibilityHandler=null),this._pendingRaf&&(cancelAnimationFrame(this._pendingRaf),this._pendingRaf=null)}async _loadData(){try{let[t,a,i,s]=await Promise.allSettled([this._api._get("/api/council/state"),this._api._get("/api/council/verdicts"),this._api._get("/api/council/convergence"),this._api._get("/api/agents")]);t.status==="fulfilled"&&(this._councilState=t.value),a.status==="fulfilled"&&(this._verdicts=a.value.verdicts||[]),i.status==="fulfilled"&&(this._convergence=i.value.dataPoints||[]),s.status==="fulfilled"&&(this._agents=Array.isArray(s.value)?s.value:[]),this._error=null}catch(t){this._error=t.message}let e=JSON.stringify({s:this._councilState,v:this._verdicts,c:this._convergence,a:this._agents,e:this._error});e!==this._lastDataHash&&(this._lastDataHash=e,this.render())}async _forceReview(){try{await this._api._post("/api/council/force-review"),this.dispatchEvent(new CustomEvent("council-action",{detail:{action:"force-review"},bubbles:!0}))}catch(e){this._error=`Failed to force review: ${e.message}`,this.render()}}async _killAgent(e){if(confirm(`Kill agent ${e}?`))try{await this._api._post(`/api/agents/${e}/kill`),this.dispatchEvent(new CustomEvent("council-action",{detail:{action:"kill-agent",agentId:e},bubbles:!0})),await this._loadData()}catch(t){this._error=`Failed to kill agent: ${t.message}`,this.render()}}async _pauseAgent(e){try{await this._api._post(`/api/agents/${e}/pause`),await this._loadData()}catch(t){this._error=`Failed to pause agent: ${t.message}`,this.render()}}async _resumeAgent(e){try{await this._api._post(`/api/agents/${e}/resume`),await this._loadData()}catch(t){this._error=`Failed to resume agent: ${t.message}`,this.render()}}_setTab(e){this._activeTab=e,this.render()}_selectAgent(e){this._selectedAgent=this._selectedAgent?.id===e.id?null:e,this.render()}render(){let e=this.shadowRoot;e&&(this._pendingRaf&&(cancelAnimationFrame(this._pendingRaf),this._pendingRaf=null),e.innerHTML=`
|
|
3754
3754
|
<style>${this.getBaseStyles()}${this._getStyles()}</style>
|
|
3755
3755
|
<div class="council-dashboard">
|
|
3756
3756
|
<div class="council-header">
|
|
@@ -3758,7 +3758,7 @@ var LokiDashboard=(()=>{var N=Object.defineProperty;var se=Object.getOwnProperty
|
|
|
3758
3758
|
<h2 class="title">Completion Council</h2>
|
|
3759
3759
|
${this._councilState?.enabled!==!1?'<span class="badge badge-active">Active</span>':'<span class="badge badge-inactive">Disabled</span>'}
|
|
3760
3760
|
</div>
|
|
3761
|
-
<button class="btn btn-primary"
|
|
3761
|
+
<button class="btn btn-primary" id="force-review-btn">
|
|
3762
3762
|
Force Review
|
|
3763
3763
|
</button>
|
|
3764
3764
|
</div>
|
|
@@ -3767,7 +3767,7 @@ var LokiDashboard=(()=>{var N=Object.defineProperty;var se=Object.getOwnProperty
|
|
|
3767
3767
|
${me.map(t=>`
|
|
3768
3768
|
<button
|
|
3769
3769
|
class="tab ${this._activeTab===t.id?"active":""}"
|
|
3770
|
-
|
|
3770
|
+
data-tab="${t.id}"
|
|
3771
3771
|
>${t.label}</button>
|
|
3772
3772
|
`).join("")}
|
|
3773
3773
|
</div>
|
|
@@ -3778,7 +3778,7 @@ var LokiDashboard=(()=>{var N=Object.defineProperty;var se=Object.getOwnProperty
|
|
|
3778
3778
|
|
|
3779
3779
|
${this._error?`<div class="error-banner">${this._error}</div>`:""}
|
|
3780
3780
|
</div>
|
|
3781
|
-
|
|
3781
|
+
`,this._attachEventListeners())}_attachEventListeners(){let e=this.shadowRoot;if(!e)return;let t=e.getElementById("force-review-btn");t&&t.addEventListener("click",()=>this._forceReview()),e.querySelectorAll(".tab[data-tab]").forEach(a=>{a.addEventListener("click",()=>this._setTab(a.dataset.tab))})}_renderTabContent(){switch(this._activeTab){case"overview":return this._renderOverview();case"decisions":return this._renderDecisions();case"convergence":return this._renderConvergence();case"agents":return this._renderAgents();default:return""}}_renderOverview(){let e=this._councilState||{},t=e.consecutive_no_change||0,a=e.done_signals||0,i=e.total_votes||0,s=e.approve_votes||0,r=this._verdicts.length>0?this._verdicts[this._verdicts.length-1]:null,n=this._agents.filter(d=>d.alive).length;return`
|
|
3782
3782
|
<div class="overview-grid">
|
|
3783
3783
|
<div class="stat-card">
|
|
3784
3784
|
<div class="stat-label">Council Status</div>
|
|
@@ -3918,7 +3918,7 @@ var LokiDashboard=(()=>{var N=Object.defineProperty;var se=Object.getOwnProperty
|
|
|
3918
3918
|
</div>
|
|
3919
3919
|
`).join("")}
|
|
3920
3920
|
</div>
|
|
3921
|
-
`;return requestAnimationFrame(()=>{let t=this.shadowRoot;t&&t.querySelectorAll(".agent-card[data-agent-index]").forEach(a=>{let i=parseInt(a.dataset.agentIndex,10),s=this._agents[i];s&&(a.addEventListener("click",()=>this._selectAgent(s)),a.querySelectorAll("[data-action]").forEach(r=>{r.addEventListener("click",n=>{n.stopPropagation();let d=r.dataset.action,p=r.dataset.agentId;d==="pause"?this._pauseAgent(p):d==="kill"?this._killAgent(p):d==="resume"&&this._resumeAgent(p)})}))})}),e}_formatTime(e){if(!e)return"";try{return new Date(e).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}catch{return e}}_getStyles(){return`
|
|
3921
|
+
`;return this._pendingRaf=requestAnimationFrame(()=>{this._pendingRaf=null;let t=this.shadowRoot;t&&t.querySelectorAll(".agent-card[data-agent-index]").forEach(a=>{let i=parseInt(a.dataset.agentIndex,10),s=this._agents[i];s&&(a.addEventListener("click",()=>this._selectAgent(s)),a.querySelectorAll("[data-action]").forEach(r=>{r.addEventListener("click",n=>{n.stopPropagation();let d=r.dataset.action,p=r.dataset.agentId;d==="pause"?this._pauseAgent(p):d==="kill"?this._killAgent(p):d==="resume"&&this._resumeAgent(p)})}))})}),e}_formatTime(e){if(!e)return"";try{return new Date(e).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}catch{return e}}_getStyles(){return`
|
|
3922
3922
|
:host {
|
|
3923
3923
|
display: block;
|
|
3924
3924
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|