loki-mode 9.17.2 → 9.18.2

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/autonomy/run.sh CHANGED
@@ -1519,6 +1519,22 @@ PYREG
1519
1519
  # if any other project is still running (KEEP) it is left up. NEVER uses a
1520
1520
  # blanket pkill and NEVER touches another folder's pids. Best-effort and
1521
1521
  # failure-swallowed: teardown bookkeeping must never block a clean exit.
1522
+ # Is this pid plausibly OUR dashboard, or a recycled number now naming something
1523
+ # else? Fails OPEN (returns 0) whenever ps cannot tell us, so the only behavior
1524
+ # change is refusing to kill a process that is positively identified as NOT a
1525
+ # dashboard. See the call site for the measured stale-file evidence.
1526
+ _loki_pid_looks_like_dashboard() {
1527
+ local _p="$1" _cmd
1528
+ case "$_p" in ''|*[!0-9]*) return 1 ;; esac
1529
+ [ "$_p" -gt 1 ] 2>/dev/null || return 1 # never signal pid 0/1
1530
+ _cmd="$(ps -o command= -p "$_p" 2>/dev/null)"
1531
+ [ -n "$_cmd" ] || return 0 # ps unavailable: behave as before
1532
+ case "$_cmd" in
1533
+ *dashboard*|*uvicorn*|*loki*) return 0 ;;
1534
+ esac
1535
+ return 1
1536
+ }
1537
+
1522
1538
  loki_mark_project_stopped_and_maybe_kill_shared_dashboard() {
1523
1539
  local _skill="${LOKI_SKILL_DIR:-${PROJECT_DIR:-$SCRIPT_DIR/..}}"
1524
1540
  local _shared_pidfile="${HOME}/.loki/dashboard/dashboard.pid"
@@ -1566,7 +1582,19 @@ PYCHECK
1566
1582
  if [ -f "$_shared_pidfile" ]; then
1567
1583
  local _shared_pid
1568
1584
  _shared_pid=$(cat "$_shared_pidfile" 2>/dev/null)
1569
- if [ -n "$_shared_pid" ]; then
1585
+ # Identity check before signalling. dashboard.pid is removed only on
1586
+ # the explicit stop paths (:16608, :16658, :17399) -- nothing covers
1587
+ # a crash, so the file outlives its dashboard. Measured in this very
1588
+ # checkout: .loki/dashboard/dashboard.pid held a DEAD 87992 with an
1589
+ # 8-day-old mtime. PIDs recycle, so a stale number eventually names
1590
+ # an unrelated LIVE process and this `kill -9` hits it.
1591
+ #
1592
+ # kill -0 is NOT sufficient: a recycled pid is alive by definition,
1593
+ # which is exactly the insufficiency the loki.pgid self-check had.
1594
+ # Mirrors _app_runner_pid_is_ours (app-runner.sh:242) and fails OPEN
1595
+ # the same way -- if `ps` says nothing we signal as before, so a
1596
+ # legitimate dashboard is never left running by this check.
1597
+ if [ -n "$_shared_pid" ] && _loki_pid_looks_like_dashboard "$_shared_pid"; then
1570
1598
  kill "$_shared_pid" 2>/dev/null || true
1571
1599
  sleep 0.5
1572
1600
  kill -9 "$_shared_pid" 2>/dev/null || true
@@ -11467,15 +11495,61 @@ sys.stdout.write(t.strip())
11467
11495
  "${TARGET_DIR:-.}"/apps/*/package.json \
11468
11496
  "${TARGET_DIR:-.}"/services/*/package.json; do
11469
11497
  [ -f "$pkg_json" ] || continue
11470
- if grep -q '"vitest"' "$pkg_json" 2>/dev/null; then
11471
- workspace_runner="vitest"
11472
- break
11473
- elif grep -q '"jest"' "$pkg_json" 2>/dev/null; then
11474
- workspace_runner="jest"
11475
- break
11476
- fi
11498
+ # Read the workspace's DECLARED test script, same rule as the
11499
+ # single-package path above. A bare grep for '"jest"' matches
11500
+ # a devDependency, so a workspace that merely depends on jest
11501
+ # got labelled monorepo-jest.
11502
+ #
11503
+ # LOWER SEVERITY than the single-package case, and worth being
11504
+ # precise about why: all three branches below dispatch the
11505
+ # project's own script (turbo test / pnpm test --recursive /
11506
+ # npm test), so the grep only ever picked the LABEL, never
11507
+ # what ran. This corrects the evidence, not the execution.
11508
+ local _ws_script
11509
+ _ws_script=$(_LOKI_PKG="$pkg_json" python3 -c "
11510
+ import json, os, sys
11511
+ try:
11512
+ with open(os.environ['_LOKI_PKG']) as f:
11513
+ d = json.load(f)
11514
+ except Exception:
11515
+ sys.exit(0)
11516
+ if not isinstance(d, dict):
11517
+ sys.exit(0)
11518
+ t = (d.get('scripts') or {}).get('test') or ''
11519
+ if 'no test specified' in t.lower():
11520
+ sys.exit(0)
11521
+ sys.stdout.write(t.strip())
11522
+ " 2>/dev/null || echo "")
11523
+ case "$_ws_script" in
11524
+ *vitest*) workspace_runner="vitest"; break ;;
11525
+ *jest*) workspace_runner="jest"; break ;;
11526
+ *mocha*) workspace_runner="mocha"; break ;;
11527
+ "") : ;;
11528
+ *) workspace_runner="npm-test"; break ;;
11529
+ esac
11477
11530
  done
11478
11531
 
11532
+ # FAIL-SAFE. If no workspace DECLARES a test script but one has a
11533
+ # runner installed, still run the monorepo suite -- labelled
11534
+ # "installed" so the evidence does not claim a declaration that
11535
+ # does not exist.
11536
+ #
11537
+ # Without this the change would trade a wrong label for a SKIPPED
11538
+ # GATE, which is the dangerous direction: the old grep at least
11539
+ # dispatched `npm test`. Reporting a runner inaccurately is a
11540
+ # documentation bug; silently not testing a monorepo is not.
11541
+ if [ -z "$workspace_runner" ]; then
11542
+ for pkg_json in "${TARGET_DIR:-.}"/packages/*/package.json \
11543
+ "${TARGET_DIR:-.}"/apps/*/package.json \
11544
+ "${TARGET_DIR:-.}"/services/*/package.json; do
11545
+ [ -f "$pkg_json" ] || continue
11546
+ if grep -qE '"(vitest|jest|mocha)"' "$pkg_json" 2>/dev/null; then
11547
+ workspace_runner="installed"
11548
+ break
11549
+ fi
11550
+ done
11551
+ fi
11552
+
11479
11553
  if [ -n "$workspace_runner" ]; then
11480
11554
  test_runner="monorepo-$workspace_runner"
11481
11555
  local output
@@ -24622,6 +24696,58 @@ kill_provider_child() {
24622
24696
  # ~run.sh:15034); in interactive foreground we share the user's shell group,
24623
24697
  # leave the pgid absent, and skip this reap entirely -- so Ctrl+C semantics
24624
24698
  # and the user's shell are untouched.
24699
+ # --- pgid stamp helpers (P0: stale-pgid session killer) ---------------------
24700
+ # The pgid file is written as `pgid=<n> boot=<id> started=<epoch>` so a reader
24701
+ # can tell a live record from a week-old orphan. See reap_own_process_group.
24702
+
24703
+ # Extract one field from a stamp. A bare number (legacy file) yields the pgid
24704
+ # and nothing else, so the reader's fail-closed branch rejects it.
24705
+ _loki_pgid_field() {
24706
+ local _s="$1" _k="$2"
24707
+ case "$_s" in
24708
+ *=*) ;;
24709
+ *) [ "$_k" = "pgid" ] && printf '%s' "$(printf '%s' "$_s" | tr -d ' ')"; return 0 ;;
24710
+ esac
24711
+ # Prefix/suffix trimming, not `for _tok in $_s`. Two reasons:
24712
+ # - `while read` in a pipeline runs in a SUBSHELL and drops a final field
24713
+ # with no trailing newline, silently returning an empty `started`.
24714
+ # - unquoted `$_s` in a `for` relies on word splitting, which bash does
24715
+ # and ZSH DOES NOT. run.sh is bash, but this file gets sourced and
24716
+ # probed from other shells, and a helper whose correctness depends on
24717
+ # the caller's shell is a trap. This form needs no word splitting.
24718
+ local _rest="$_s" _tok
24719
+ while [ -n "$_rest" ]; do
24720
+ _tok="${_rest%% *}" # first space-delimited token
24721
+ case "$_tok" in
24722
+ "$_k"=*) printf '%s' "${_tok#*=}"; return 0 ;;
24723
+ esac
24724
+ case "$_rest" in
24725
+ *" "*) _rest="${_rest#* }" ;; # advance past this token
24726
+ *) _rest="" ;; # last token: stop
24727
+ esac
24728
+ done
24729
+ }
24730
+
24731
+ # A value that changes on every reboot, so a pgid recorded before a reboot can
24732
+ # never validate afterwards. Empty when unavailable -- callers fail closed.
24733
+ _loki_boot_id() {
24734
+ if [ -r /proc/stat ]; then
24735
+ awk '/^btime /{print $2; exit}' /proc/stat 2>/dev/null && return 0
24736
+ fi
24737
+ # macOS: `kern.boottime` prints `{ sec = 1234567890, usec = 0 } ...`
24738
+ sysctl -n kern.boottime 2>/dev/null | sed -n 's/.*sec *= *\([0-9][0-9]*\).*/\1/p'
24739
+ }
24740
+
24741
+ # Start time of a pid as a unix epoch, or empty when it cannot be determined.
24742
+ _loki_proc_start_epoch() {
24743
+ local _p="${1:-$$}" _lstart
24744
+ _lstart="$(ps -o lstart= -p "$_p" 2>/dev/null)"
24745
+ [ -n "$_lstart" ] || return 0
24746
+ date -j -f '%a %b %e %T %Y' "$_lstart" '+%s' 2>/dev/null \
24747
+ || date -d "$_lstart" '+%s' 2>/dev/null \
24748
+ || true
24749
+ }
24750
+
24625
24751
  reap_own_process_group() {
24626
24752
  local loki_dir="${TARGET_DIR:-.}/.loki"
24627
24753
  # Resolve the pgid file the same way main() recorded it (global or per-session).
@@ -24632,8 +24758,13 @@ reap_own_process_group() {
24632
24758
  _reap_pgid_file="${_reap_pgid_file%.pid}.pgid"
24633
24759
  [ -f "$_reap_pgid_file" ] || return 0 # interactive / no own session: skip
24634
24760
 
24635
- local _pgid
24636
- _pgid=$(cat "$_reap_pgid_file" 2>/dev/null | tr -d ' ')
24761
+ # Parse the stamped format `pgid=<n> boot=<id> started=<epoch>`, falling back
24762
+ # to a bare number for a file written by an older version.
24763
+ local _stamp _pgid _boot _started
24764
+ _stamp=$(cat "$_reap_pgid_file" 2>/dev/null | tr -d '\n')
24765
+ _pgid=$(_loki_pgid_field "$_stamp" pgid)
24766
+ _boot=$(_loki_pgid_field "$_stamp" boot)
24767
+ _started=$(_loki_pgid_field "$_stamp" started)
24637
24768
  case "$_pgid" in ''|*[!0-9]*) return 0 ;; esac
24638
24769
  [ "$_pgid" -gt 1 ] 2>/dev/null || return 0 # never touch pgid 0/1
24639
24770
 
@@ -24644,6 +24775,40 @@ reap_own_process_group() {
24644
24775
  _my_pgid=$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ')
24645
24776
  [ -n "$_my_pgid" ] && [ "$_pgid" = "$_my_pgid" ] || return 0
24646
24777
 
24778
+ # BOOT + AGE GUARD (the fix for the session-killer).
24779
+ #
24780
+ # The self-check above is NOT sufficient on its own. loki.pgid was removed
24781
+ # only on the normal exit path, so a Ctrl+C'd or crashed run left the file
24782
+ # behind indefinitely -- measured on a real machine: two orphans aged 155h
24783
+ # and 202h. PIDs recycle (macOS wraps near 99999; max observed 99762), so a
24784
+ # week-old pgid eventually matches a LIVE, unrelated shell group. The
24785
+ # self-check then passes and every sibling in the user's terminal -- their
24786
+ # editor, another agent session -- is TERM'd then KILL'd.
24787
+ #
24788
+ # A recycled pgid cannot forge both of these:
24789
+ # - boot id: a file from a previous boot can never match this boot.
24790
+ # - started: the recording run must not predate THIS process.
24791
+ #
24792
+ # Fails CLOSED. A missing or malformed stamp means we do nothing: an
24793
+ # orphaned agent (the v7.41.x problem this reap was added for) is strictly
24794
+ # less harmful than killing a user's editor.
24795
+ local _now_boot
24796
+ _now_boot="$(_loki_boot_id)"
24797
+ if [ -z "$_boot" ] || [ -z "$_started" ]; then
24798
+ return 0 # unstamped legacy file: never trust it
24799
+ fi
24800
+ if [ -z "$_now_boot" ] || [ "$_boot" != "$_now_boot" ]; then
24801
+ return 0 # different boot (or boot id unavailable): refuse
24802
+ fi
24803
+ case "$_started" in ''|*[!0-9]*) return 0 ;; esac
24804
+ local _self_started
24805
+ _self_started="$(_loki_proc_start_epoch $$)"
24806
+ # The stamp must not predate this process by more than a small clock skew.
24807
+ # A stale file is always OLDER than the process now reading it.
24808
+ if [ -n "$_self_started" ] && [ "$_started" -lt "$(( _self_started - 5 ))" ]; then
24809
+ return 0
24810
+ fi
24811
+
24647
24812
  # Collect protected pids (dashboard, app-runner, registered children) so the
24648
24813
  # reap never takes down the shared dashboard if it happens to share our
24649
24814
  # group. Mirrors the `loki stop` / dashboard reaper protection set.
@@ -25045,6 +25210,20 @@ _loki_remove_temp_self_copy() {
25045
25210
  fi
25046
25211
  }
25047
25212
 
25213
+ # P0 (stale-pgid session killer): the pgid file must never outlive its run.
25214
+ # Recorded by the writer (see _LOKI_PGID_FILE below) rather than re-derived, so
25215
+ # the removal hits the exact path written -- global OR per-session -- and can
25216
+ # never delete a concurrent run's file.
25217
+ _loki_remove_pgid_file() {
25218
+ [ -n "${_LOKI_PGID_FILE:-}" ] || return 0
25219
+ # A `trap ... EXIT` fires in SUBSHELLS too, and a subshell exiting must not
25220
+ # delete a LIVE parent's file (repo scar: a bare trap deleted a parent's
25221
+ # lock). $$ does NOT change in a subshell -- BASHPID does, so it is the only
25222
+ # real discriminator here.
25223
+ [ "${BASHPID:-$$}" = "${_LOKI_PGID_OWNER:-$$}" ] || return 0
25224
+ rm -f "$_LOKI_PGID_FILE" 2>/dev/null || true
25225
+ }
25226
+
25048
25227
  _loki_session_exit_cleanup() {
25049
25228
  local exit_code=$?
25050
25229
  _loki_terminal_record 2>/dev/null || true
@@ -25052,6 +25231,12 @@ _loki_session_exit_cleanup() {
25052
25231
  && type safe_release_lock >/dev/null 2>&1; then
25053
25232
  safe_release_lock "$_LOKI_SESSION_LOCK_FILE" 2>/dev/null || true
25054
25233
  fi
25234
+ # Covers EXIT plus INT/TERM: every cleanup() path that actually terminates
25235
+ # does so via `exit`, which fires this trap. Deliberately NOT called from
25236
+ # cleanup() itself -- its perpetual/pause branches RETURN and the run
25237
+ # continues, so removing the file there would silently disable the
25238
+ # completion-path reap for the rest of the run.
25239
+ _loki_remove_pgid_file
25055
25240
  _loki_remove_temp_self_copy
25056
25241
  return "$exit_code"
25057
25242
  }
@@ -25720,6 +25905,10 @@ main() {
25720
25905
  fi
25721
25906
  else
25722
25907
  # Lock helper not loaded (lib/lock.sh missing). PID-only fallback.
25908
+ # This branch still falls through to the pgid writer below, so it needs
25909
+ # the same EXIT cleanup or the pgid file leaks on every interrupt here.
25910
+ # An empty lock file is safe: the release is [ -n ]-guarded.
25911
+ _loki_arm_session_exit_cleanup ""
25723
25912
  if [ -f "$pid_file" ]; then
25724
25913
  local existing_pid
25725
25914
  existing_pid=$(cat "$pid_file" 2>/dev/null)
@@ -25750,7 +25939,22 @@ main() {
25750
25939
  if [ "${LOKI_OWN_SESSION:-}" = "1" ]; then
25751
25940
  _loki_pgid="$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ')"
25752
25941
  if [ -n "$_loki_pgid" ]; then
25753
- echo "$_loki_pgid" > "${pid_file%.pid}.pgid" 2>/dev/null || true
25942
+ # STAMPED, not a bare number. The reader requires boot id + start
25943
+ # epoch to match before it will signal anything, because this file
25944
+ # outlived its run (measured: 155h and 202h orphans) and PIDs
25945
+ # recycle -- a stale bare pgid eventually matches a live shell group
25946
+ # and the reap killed the user's other terminal sessions.
25947
+ _loki_pgid_started="$(_loki_proc_start_epoch $$)"
25948
+ [ -n "$_loki_pgid_started" ] || _loki_pgid_started="$(date +%s 2>/dev/null)"
25949
+ # Record the EXACT path written (global or per-session) plus the
25950
+ # owning shell, so the EXIT trap removes THIS file and only from
25951
+ # this process. Without removal on the interrupt/crash paths the
25952
+ # file outlived its run -- measured orphans aged 155h and 202h.
25953
+ _LOKI_PGID_FILE="${pid_file%.pid}.pgid"
25954
+ _LOKI_PGID_OWNER="${BASHPID:-$$}"
25955
+ printf 'pgid=%s boot=%s started=%s\n' \
25956
+ "$_loki_pgid" "$(_loki_boot_id)" "$_loki_pgid_started" \
25957
+ > "$_LOKI_PGID_FILE" 2>/dev/null || true
25754
25958
  fi
25755
25959
  fi
25756
25960
  # Store session ID in state for dashboard/status visibility