loki-mode 6.3.1 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v6.3.1
6
+ # Loki Mode v6.4.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.3.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
266
+ **v6.4.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 6.3.1
1
+ 6.4.0
package/autonomy/loki CHANGED
@@ -806,8 +806,35 @@ cmd_start() {
806
806
  }
807
807
 
808
808
  # Check if session is running
809
+ # Usage: is_session_running [session_id]
810
+ # Without args: returns true if ANY session (global or per-session) is running
811
+ # With session_id: returns true only if that specific session is running
809
812
  is_session_running() {
810
- # Check both possible PID files (loki.pid from run.sh, run.pid legacy)
813
+ local target_session="${1:-}"
814
+
815
+ if [ -n "$target_session" ]; then
816
+ # Check specific session
817
+ local session_pid_file="$LOKI_DIR/sessions/$target_session/loki.pid"
818
+ if [ -f "$session_pid_file" ]; then
819
+ local pid
820
+ pid=$(cat "$session_pid_file" 2>/dev/null)
821
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
822
+ return 0
823
+ fi
824
+ fi
825
+ # Also check legacy per-run PID file (run-<number>.pid)
826
+ local run_pid_file="$LOKI_DIR/run-${target_session}.pid"
827
+ if [ -f "$run_pid_file" ]; then
828
+ local pid
829
+ pid=$(cat "$run_pid_file" 2>/dev/null)
830
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
831
+ return 0
832
+ fi
833
+ fi
834
+ return 1
835
+ fi
836
+
837
+ # Check global PID files (loki.pid from run.sh, run.pid legacy)
811
838
  local pid_files=("$LOKI_DIR/loki.pid" "$LOKI_DIR/run.pid")
812
839
  for pid_file in "${pid_files[@]}"; do
813
840
  if [ -f "$pid_file" ]; then
@@ -818,15 +845,151 @@ is_session_running() {
818
845
  fi
819
846
  fi
820
847
  done
848
+
849
+ # Check per-session PIDs
850
+ if [ -d "$LOKI_DIR/sessions" ]; then
851
+ for session_dir in "$LOKI_DIR/sessions"/*/; do
852
+ [ -d "$session_dir" ] || continue
853
+ local pid_file="${session_dir}loki.pid"
854
+ if [ -f "$pid_file" ]; then
855
+ local pid
856
+ pid=$(cat "$pid_file" 2>/dev/null)
857
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
858
+ return 0
859
+ fi
860
+ fi
861
+ done
862
+ fi
821
863
  return 1
822
864
  }
823
865
 
824
- # Stop execution immediately
866
+ # List all running session IDs
867
+ list_running_sessions() {
868
+ local sessions=()
869
+ # Check global session
870
+ if [ -f "$LOKI_DIR/loki.pid" ]; then
871
+ local pid
872
+ pid=$(cat "$LOKI_DIR/loki.pid" 2>/dev/null)
873
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
874
+ sessions+=("global:$pid")
875
+ fi
876
+ fi
877
+ # Check per-session PIDs
878
+ if [ -d "$LOKI_DIR/sessions" ]; then
879
+ for session_dir in "$LOKI_DIR/sessions"/*/; do
880
+ [ -d "$session_dir" ] || continue
881
+ local sid pid_file pid
882
+ sid=$(basename "$session_dir")
883
+ pid_file="${session_dir}loki.pid"
884
+ if [ -f "$pid_file" ]; then
885
+ pid=$(cat "$pid_file" 2>/dev/null)
886
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
887
+ sessions+=("$sid:$pid")
888
+ fi
889
+ fi
890
+ done
891
+ fi
892
+ # Check legacy run-*.pid files
893
+ for run_pid_file in "$LOKI_DIR"/run-*.pid; do
894
+ [ -f "$run_pid_file" ] || continue
895
+ local sid pid
896
+ sid=$(basename "$run_pid_file" .pid)
897
+ sid="${sid#run-}"
898
+ pid=$(cat "$run_pid_file" 2>/dev/null)
899
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
900
+ # Avoid duplicates (if also in sessions/)
901
+ local dup=false
902
+ for s in "${sessions[@]}"; do
903
+ if [[ "$s" == "$sid:"* ]]; then dup=true; break; fi
904
+ done
905
+ if ! $dup; then
906
+ sessions+=("$sid:$pid")
907
+ fi
908
+ fi
909
+ done
910
+ printf '%s\n' "${sessions[@]}"
911
+ }
912
+
913
+ # Kill a PID with SIGTERM, wait, then SIGKILL if needed
914
+ _kill_pid() {
915
+ local pid="$1"
916
+ pkill -P "$pid" 2>/dev/null || true
917
+ kill "$pid" 2>/dev/null || true
918
+ sleep 1
919
+ if kill -0 "$pid" 2>/dev/null; then
920
+ pkill -9 -P "$pid" 2>/dev/null || true
921
+ kill -9 "$pid" 2>/dev/null || true
922
+ fi
923
+ }
924
+
925
+ # Stop a specific session by its session ID
926
+ _stop_session_by_id() {
927
+ local sid="$1"
928
+ local session_dir="$LOKI_DIR/sessions/$sid"
929
+ local pid_file="$session_dir/loki.pid"
930
+
931
+ if [ -f "$pid_file" ]; then
932
+ local pid
933
+ pid=$(cat "$pid_file" 2>/dev/null)
934
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
935
+ _kill_pid "$pid"
936
+ echo -e "${RED}Stopped session $sid (PID: $pid)${NC}"
937
+ fi
938
+ rm -f "$pid_file"
939
+ fi
940
+
941
+ # Also check legacy run-<id>.pid
942
+ local run_pid_file="$LOKI_DIR/run-${sid}.pid"
943
+ if [ -f "$run_pid_file" ]; then
944
+ local pid
945
+ pid=$(cat "$run_pid_file" 2>/dev/null)
946
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
947
+ _kill_pid "$pid"
948
+ fi
949
+ rm -f "$run_pid_file"
950
+ fi
951
+
952
+ # Clean up session lock file
953
+ rm -f "$session_dir/session.lock" 2>/dev/null
954
+ }
955
+
956
+ # Stop a specific session or all sessions
957
+ # Usage: loki stop [session_id]
958
+ # Without args: stops all running sessions
959
+ # With session_id: stops only that specific session
825
960
  cmd_stop() {
961
+ local target_session=""
962
+
963
+ # Parse arguments
964
+ while [[ $# -gt 0 ]]; do
965
+ case "$1" in
966
+ --help|-h)
967
+ echo -e "${BOLD}loki stop${NC} - Stop running sessions (v6.4.0)"
968
+ echo ""
969
+ echo "Usage: loki stop [session-id]"
970
+ echo ""
971
+ echo " loki stop Stop all running sessions"
972
+ echo " loki stop 52 Stop only session #52"
973
+ echo " loki stop 54 Stop only session #54"
974
+ echo ""
975
+ echo "Use 'loki status' to see all running sessions."
976
+ exit 0
977
+ ;;
978
+ -*)
979
+ echo -e "${RED}Unknown option: $1${NC}"
980
+ echo "Run 'loki stop --help' for usage."
981
+ exit 1
982
+ ;;
983
+ *)
984
+ target_session="$1"
985
+ shift
986
+ ;;
987
+ esac
988
+ done
989
+
826
990
  if [ ! -d "$LOKI_DIR" ]; then
827
991
  echo -e "${YELLOW}No .loki directory found.${NC}"
828
992
  echo "No active session to stop."
829
- # Emit error pattern for stop without session (SYN-018)
830
993
  emit_learning_signal error_pattern \
831
994
  --source cli \
832
995
  --action "cmd_stop" \
@@ -837,28 +1000,54 @@ cmd_stop() {
837
1000
  exit 0
838
1001
  fi
839
1002
 
1003
+ # Stop a specific session by ID
1004
+ if [ -n "$target_session" ]; then
1005
+ if is_session_running "$target_session"; then
1006
+ _stop_session_by_id "$target_session"
1007
+ else
1008
+ echo -e "${YELLOW}Session '$target_session' is not running.${NC}"
1009
+ fi
1010
+ return 0
1011
+ fi
1012
+
1013
+ # No session ID given -- stop all running sessions
840
1014
  if is_session_running; then
1015
+ # Stop per-session PIDs first
1016
+ if [ -d "$LOKI_DIR/sessions" ]; then
1017
+ for session_dir in "$LOKI_DIR/sessions"/*/; do
1018
+ [ -d "$session_dir" ] || continue
1019
+ local sid
1020
+ sid=$(basename "$session_dir")
1021
+ if is_session_running "$sid"; then
1022
+ _stop_session_by_id "$sid"
1023
+ fi
1024
+ done
1025
+ fi
1026
+ # Stop legacy run-*.pid sessions
1027
+ for run_pid_file in "$LOKI_DIR"/run-*.pid; do
1028
+ [ -f "$run_pid_file" ] || continue
1029
+ local pid
1030
+ pid=$(cat "$run_pid_file" 2>/dev/null)
1031
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
1032
+ _kill_pid "$pid"
1033
+ local sid
1034
+ sid=$(basename "$run_pid_file" .pid)
1035
+ echo -e "${RED}Stopped session ${sid#run-} (PID: $pid)${NC}"
1036
+ fi
1037
+ rm -f "$run_pid_file"
1038
+ done
1039
+
1040
+ # Stop global session
841
1041
  touch "$LOKI_DIR/STOP"
842
1042
 
843
- # Kill the background process and all its children
844
1043
  local killed_pid=""
845
1044
  for pid_file in "$LOKI_DIR/loki.pid" "$LOKI_DIR/run.pid"; do
846
1045
  if [ -f "$pid_file" ]; then
847
1046
  local pid
848
1047
  pid=$(cat "$pid_file" 2>/dev/null)
849
1048
  if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
850
- # Kill all child processes first (SIGTERM)
851
- pkill -P "$pid" 2>/dev/null || true
852
- # Then kill the parent (SIGTERM)
853
- kill "$pid" 2>/dev/null || true
1049
+ _kill_pid "$pid"
854
1050
  killed_pid="$pid"
855
-
856
- # Wait briefly, then SIGKILL if still alive
857
- sleep 1
858
- if kill -0 "$pid" 2>/dev/null; then
859
- pkill -9 -P "$pid" 2>/dev/null || true
860
- kill -9 "$pid" 2>/dev/null || true
861
- fi
862
1051
  fi
863
1052
  rm -f "$pid_file"
864
1053
  fi
@@ -871,7 +1060,6 @@ cmd_stop() {
871
1060
 
872
1061
  # Mark session.json as stopped (skill-invoked sessions)
873
1062
  if [ -f "$LOKI_DIR/session.json" ]; then
874
- # Use python for reliable JSON update, fall back to overwrite
875
1063
  python3 -c "
876
1064
  import json, sys, os
877
1065
  try:
@@ -1162,6 +1350,29 @@ cmd_status() {
1162
1350
  echo -e "${DIM} Switch with: loki provider set <claude|codex|gemini>${NC}"
1163
1351
  echo ""
1164
1352
 
1353
+ # Show running sessions (v6.4.0 - concurrent session support)
1354
+ local running_sessions=()
1355
+ while IFS= read -r line; do
1356
+ [ -n "$line" ] && running_sessions+=("$line")
1357
+ done < <(list_running_sessions 2>/dev/null)
1358
+
1359
+ if [ ${#running_sessions[@]} -gt 1 ]; then
1360
+ echo -e "${GREEN}Active Sessions: ${#running_sessions[@]}${NC}"
1361
+ for entry in "${running_sessions[@]}"; do
1362
+ local sid="${entry%%:*}"
1363
+ local spid="${entry#*:}"
1364
+ if [ "$sid" = "global" ]; then
1365
+ echo -e " ${CYAN}[global]${NC} PID $spid"
1366
+ else
1367
+ echo -e " ${CYAN}[#$sid]${NC} PID $spid"
1368
+ fi
1369
+ done
1370
+ echo ""
1371
+ echo -e "${DIM} Stop specific: loki stop <session-id>${NC}"
1372
+ echo -e "${DIM} Stop all: loki stop${NC}"
1373
+ echo ""
1374
+ fi
1375
+
1165
1376
  # Check for signals first (more prominent)
1166
1377
  if [ -f "$LOKI_DIR/PAUSE" ]; then
1167
1378
  echo -e "${YELLOW}Status: PAUSED${NC}"
@@ -2740,6 +2951,12 @@ cmd_run() {
2740
2951
  fi
2741
2952
  echo ""
2742
2953
 
2954
+ # Per-session locking (v6.4.0): export session ID so run.sh uses
2955
+ # per-session PID/lock files instead of the global lock. This allows
2956
+ # multiple concurrent `loki run` sessions (e.g., loki run 52 -d && loki run 54 -d).
2957
+ local session_id="${number:-$(date +%s)}"
2958
+ export LOKI_SESSION_ID="$session_id"
2959
+
2743
2960
  # Progressive isolation: set up worktree branch naming
2744
2961
  if $use_worktree; then
2745
2962
  local branch_name="issue/${issue_provider}-${number:-$(date +%s)}"
@@ -2772,6 +2989,7 @@ cmd_run() {
2772
2989
  cd $(pwd)
2773
2990
  export LOKI_DETACHED=true
2774
2991
  export LOKI_PARALLEL_MODE=true
2992
+ export LOKI_SESSION_ID=\"$session_id\"
2775
2993
  export LOKI_WORKTREE_BRANCH=\"$branch_name\"
2776
2994
  $(command -v loki || echo "$0") start \"$detach_prd\" --parallel ${start_args[*]+"${start_args[*]}"}
2777
2995
 
@@ -7181,7 +7399,7 @@ main() {
7181
7399
  cmd_init "$@"
7182
7400
  ;;
7183
7401
  stop)
7184
- cmd_stop
7402
+ cmd_stop "$@"
7185
7403
  ;;
7186
7404
  cleanup)
7187
7405
  cmd_cleanup "$@"
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
- local lock_file=".loki/session.lock"
2646
- local can_cleanup=false
2647
-
2648
- if command -v flock >/dev/null 2>&1 && [ -f "$lock_file" ]; then
2649
- # Try non-blocking lock - if we get it, no other session is running
2650
- {
2651
- if flock -n 201 2>/dev/null; then
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
- } 201>"$lock_file"
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
- # Fallback: check PID file
2657
- local existing_pid=""
2658
- if [ -f ".loki/loki.pid" ]; then
2659
- existing_pid=$(cat ".loki/loki.pid" 2>/dev/null)
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 [ -z "$existing_pid" ] || ! kill -0 "$existing_pid" 2>/dev/null; then
2662
- can_cleanup=true
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
@@ -8093,6 +8112,10 @@ cleanup() {
8093
8112
  stop_status_monitor
8094
8113
  kill_all_registered
8095
8114
  rm -f "$loki_dir/loki.pid" 2>/dev/null
8115
+ # Clean up per-session PID file if running with session ID
8116
+ if [ -n "${LOKI_SESSION_ID:-}" ]; then
8117
+ rm -f "$loki_dir/sessions/${LOKI_SESSION_ID}/loki.pid" 2>/dev/null
8118
+ fi
8096
8119
  if [ -f "$loki_dir/session.json" ]; then
8097
8120
  _LOKI_SESSION_FILE="$loki_dir/session.json" python3 -c "
8098
8121
  import json, os
@@ -8121,6 +8144,10 @@ except (json.JSONDecodeError, OSError): pass
8121
8144
  stop_status_monitor
8122
8145
  kill_all_registered
8123
8146
  rm -f .loki/loki.pid .loki/PAUSE 2>/dev/null
8147
+ # Clean up per-session PID file if running with session ID
8148
+ if [ -n "${LOKI_SESSION_ID:-}" ]; then
8149
+ rm -f ".loki/sessions/${LOKI_SESSION_ID}/loki.pid" 2>/dev/null
8150
+ fi
8124
8151
  # Mark session.json as stopped
8125
8152
  if [ -f ".loki/session.json" ]; then
8126
8153
  python3 -c "
@@ -8325,7 +8352,13 @@ main() {
8325
8352
  mkdir -p .loki/logs
8326
8353
 
8327
8354
  local log_file=".loki/logs/background-$(date +%Y%m%d-%H%M%S).log"
8328
- local pid_file=".loki/loki.pid"
8355
+ local pid_file
8356
+ if [ -n "${LOKI_SESSION_ID:-}" ]; then
8357
+ mkdir -p ".loki/sessions/${LOKI_SESSION_ID}"
8358
+ pid_file=".loki/sessions/${LOKI_SESSION_ID}/loki.pid"
8359
+ else
8360
+ pid_file=".loki/loki.pid"
8361
+ fi
8329
8362
  local project_path=$(pwd)
8330
8363
  local project_name=$(basename "$project_path")
8331
8364
 
@@ -8419,12 +8452,22 @@ main() {
8419
8452
  # Initialize session continuity file with empty template
8420
8453
  update_continuity
8421
8454
 
8422
- # Session lock: prevent concurrent sessions on same repo
8423
- # Use flock for atomic locking to prevent TOCTOU race conditions
8424
- local pid_file=".loki/loki.pid"
8425
- local lock_file=".loki/session.lock"
8455
+ # Session lock: prevent concurrent sessions
8456
+ # Per-session locking (v6.4.0): LOKI_SESSION_ID enables multiple concurrent
8457
+ # sessions (e.g., loki run 52 -d && loki run 54 -d). Each session gets its
8458
+ # own PID/lock files under .loki/sessions/<id>/.
8459
+ # Without LOKI_SESSION_ID, the global .loki/loki.pid lock is used (single session).
8460
+ local pid_file lock_file
8461
+ if [ -n "${LOKI_SESSION_ID:-}" ]; then
8462
+ mkdir -p ".loki/sessions/${LOKI_SESSION_ID}"
8463
+ pid_file=".loki/sessions/${LOKI_SESSION_ID}/loki.pid"
8464
+ lock_file=".loki/sessions/${LOKI_SESSION_ID}/session.lock"
8465
+ else
8466
+ pid_file=".loki/loki.pid"
8467
+ lock_file=".loki/session.lock"
8468
+ fi
8426
8469
 
8427
- # Try to acquire exclusive lock with flock (if available)
8470
+ # Use flock for atomic locking to prevent TOCTOU race conditions
8428
8471
  if command -v flock >/dev/null 2>&1; then
8429
8472
  # Create lock file
8430
8473
  touch "$lock_file"
@@ -8435,8 +8478,13 @@ main() {
8435
8478
 
8436
8479
  # Try to acquire exclusive lock (non-blocking)
8437
8480
  if ! flock -n 200 2>/dev/null; then
8438
- log_error "Another Loki session is already running (locked)"
8439
- log_error "Stop it first with: loki stop"
8481
+ if [ -n "${LOKI_SESSION_ID:-}" ]; then
8482
+ log_error "Session '${LOKI_SESSION_ID}' is already running (locked)"
8483
+ log_error "Stop it first with: loki stop ${LOKI_SESSION_ID}"
8484
+ else
8485
+ log_error "Another Loki session is already running (locked)"
8486
+ log_error "Stop it first with: loki stop"
8487
+ fi
8440
8488
  exit 1
8441
8489
  fi
8442
8490
 
@@ -8446,8 +8494,13 @@ main() {
8446
8494
  existing_pid=$(cat "$pid_file" 2>/dev/null)
8447
8495
  # Skip if it's our own PID or parent PID (background mode writes PID before child starts)
8448
8496
  if [ -n "$existing_pid" ] && [ "$existing_pid" != "$$" ] && [ "$existing_pid" != "$PPID" ] && kill -0 "$existing_pid" 2>/dev/null; then
8449
- log_error "Another Loki session is already running (PID: $existing_pid)"
8450
- log_error "Stop it first with: loki stop"
8497
+ if [ -n "${LOKI_SESSION_ID:-}" ]; then
8498
+ log_error "Session '${LOKI_SESSION_ID}' is already running (PID: $existing_pid)"
8499
+ log_error "Stop it first with: loki stop ${LOKI_SESSION_ID}"
8500
+ else
8501
+ log_error "Another Loki session is already running (PID: $existing_pid)"
8502
+ log_error "Stop it first with: loki stop"
8503
+ fi
8451
8504
  exit 1
8452
8505
  fi
8453
8506
  fi
@@ -8459,8 +8512,13 @@ main() {
8459
8512
  existing_pid=$(cat "$pid_file" 2>/dev/null)
8460
8513
  # Skip if it's our own PID or parent PID (background mode writes PID before child starts)
8461
8514
  if [ -n "$existing_pid" ] && [ "$existing_pid" != "$$" ] && [ "$existing_pid" != "$PPID" ] && kill -0 "$existing_pid" 2>/dev/null; then
8462
- log_error "Another Loki session is already running (PID: $existing_pid)"
8463
- log_error "Stop it first with: loki stop"
8515
+ if [ -n "${LOKI_SESSION_ID:-}" ]; then
8516
+ log_error "Session '${LOKI_SESSION_ID}' is already running (PID: $existing_pid)"
8517
+ log_error "Stop it first with: loki stop ${LOKI_SESSION_ID}"
8518
+ else
8519
+ log_error "Another Loki session is already running (PID: $existing_pid)"
8520
+ log_error "Stop it first with: loki stop"
8521
+ fi
8464
8522
  exit 1
8465
8523
  fi
8466
8524
  fi
@@ -8468,6 +8526,10 @@ main() {
8468
8526
 
8469
8527
  # Write PID file for ALL modes (foreground + background)
8470
8528
  echo "$$" > "$pid_file"
8529
+ # Store session ID in state for dashboard/status visibility
8530
+ if [ -n "${LOKI_SESSION_ID:-}" ]; then
8531
+ echo "${LOKI_SESSION_ID}" > ".loki/sessions/${LOKI_SESSION_ID}/session_id"
8532
+ fi
8471
8533
 
8472
8534
  # Initialize PID registry and clean up orphans from previous sessions
8473
8535
  init_pid_registry
@@ -8675,6 +8737,10 @@ main() {
8675
8737
  stop_dashboard
8676
8738
  stop_status_monitor
8677
8739
  rm -f .loki/loki.pid 2>/dev/null
8740
+ # Clean up per-session PID file if running with session ID
8741
+ if [ -n "${LOKI_SESSION_ID:-}" ]; then
8742
+ rm -f ".loki/sessions/${LOKI_SESSION_ID}/loki.pid" 2>/dev/null
8743
+ fi
8678
8744
  # Mark session.json as stopped
8679
8745
  if [ -f ".loki/session.json" ]; then
8680
8746
  python3 -c "
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "6.3.1"
10
+ __version__ = "6.4.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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,73 @@ 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
+ active_session_list.append(SessionInfo(
540
+ session_id="global",
541
+ pid=int(pid_str) if pid_str else 0,
542
+ status=status,
543
+ ))
544
+
545
+ # Per-session PIDs under .loki/sessions/<id>/
546
+ sessions_dir = loki_dir / "sessions"
547
+ if sessions_dir.is_dir():
548
+ for session_path in sessions_dir.iterdir():
549
+ if not session_path.is_dir():
550
+ continue
551
+ sid = session_path.name
552
+ spid_file = session_path / "loki.pid"
553
+ if spid_file.exists():
554
+ try:
555
+ spid_str = spid_file.read_text().strip()
556
+ spid = int(spid_str)
557
+ os.kill(spid, 0)
558
+ # Find log file if available
559
+ log_path = ""
560
+ log_candidate = loki_dir / "logs" / f"run-{sid}.log"
561
+ if log_candidate.exists():
562
+ log_path = str(log_candidate)
563
+ active_session_list.append(SessionInfo(
564
+ session_id=sid,
565
+ pid=spid,
566
+ status="running",
567
+ log_file=log_path,
568
+ ))
569
+ except (ValueError, OSError, ProcessLookupError):
570
+ pass
571
+
572
+ # Legacy run-*.pid files
573
+ for rpf in loki_dir.glob("run-*.pid"):
574
+ sid = rpf.stem.removeprefix("run-")
575
+ # Skip if already found in sessions/
576
+ if any(s.session_id == sid for s in active_session_list):
577
+ continue
578
+ try:
579
+ rpid = int(rpf.read_text().strip())
580
+ os.kill(rpid, 0)
581
+ log_path = ""
582
+ log_candidate = loki_dir / "logs" / f"run-{sid}.log"
583
+ if log_candidate.exists():
584
+ log_path = str(log_candidate)
585
+ active_session_list.append(SessionInfo(
586
+ session_id=sid,
587
+ pid=rpid,
588
+ status="running",
589
+ log_file=log_path,
590
+ ))
591
+ except (ValueError, OSError, ProcessLookupError):
592
+ pass
593
+
594
+ total_active = len(active_session_list)
595
+
524
596
  return StatusResponse(
525
597
  status=status,
526
598
  version=version,
527
599
  uptime_seconds=uptime,
528
- active_sessions=1 if running else 0,
600
+ active_sessions=total_active,
529
601
  running_agents=running_agents,
530
602
  pending_tasks=pending_tasks,
531
603
  database_connected=True,
@@ -535,6 +607,7 @@ async def get_status() -> StatusResponse:
535
607
  mode=mode,
536
608
  provider=provider,
537
609
  current_task=current_task,
610
+ sessions=active_session_list,
538
611
  )
539
612
 
540
613
 
@@ -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.3.1
5
+ **Version:** v6.4.0
6
6
 
7
7
  ---
8
8
 
9
- ## What's New in v6.3.1
9
+ ## What's New in v6.4.0
10
10
 
11
11
  ### Dual-Mode Architecture (v6.0.0)
12
12
  - `loki run` command for direct autonomous execution
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '6.3.1'
60
+ __version__ = '6.4.0'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "6.3.1",
3
+ "version": "6.4.0",
4
4
  "description": "Loki Mode by Autonomi - Multi-agent autonomous startup system for Claude Code, Codex CLI, and Gemini CLI",
5
5
  "keywords": [
6
6
  "agent",