loki-mode 7.18.0 → 7.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/crash.sh +164 -0
- package/autonomy/lib/crash_capture.py +286 -0
- package/autonomy/lib/crash_redact.py +509 -0
- package/autonomy/loki +255 -10
- package/autonomy/run.sh +56 -2
- package/autonomy/telemetry.sh +11 -0
- package/bin/loki +3 -1
- package/bin/postinstall.js +15 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/telemetry.py +15 -0
- package/docs/CRASH-REPORTING-PLAN.md +527 -0
- package/docs/INSTALLATION.md +1 -1
- package/docs/PRIVACY.md +145 -0
- package/loki-ts/dist/loki.js +265 -226
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
package/autonomy/loki
CHANGED
|
@@ -61,6 +61,13 @@ if [ -f "$_LOKI_SCRIPT_DIR/tui.sh" ]; then
|
|
|
61
61
|
source "$_LOKI_SCRIPT_DIR/tui.sh"
|
|
62
62
|
fi
|
|
63
63
|
|
|
64
|
+
# Crash-reporting helpers (provides loki_collection_enabled, used by
|
|
65
|
+
# cmd_telemetry status and cmd_crash). Self-guarded against double-source.
|
|
66
|
+
if [ -f "$_LOKI_SCRIPT_DIR/crash.sh" ]; then
|
|
67
|
+
# shellcheck source=crash.sh
|
|
68
|
+
source "$_LOKI_SCRIPT_DIR/crash.sh"
|
|
69
|
+
fi
|
|
70
|
+
|
|
64
71
|
# Resolve the script's real path (handles symlinks)
|
|
65
72
|
resolve_script_path() {
|
|
66
73
|
local script="$1"
|
|
@@ -7627,6 +7634,22 @@ cmd_doctor() {
|
|
|
7627
7634
|
elif command -v codex &>/dev/null; then
|
|
7628
7635
|
echo -e " ${DIM} -- ${NC} OPENAI_API_KEY not set (Codex CLI uses its own login)"
|
|
7629
7636
|
fi
|
|
7637
|
+
# Phase I (v7.5.25): detect ANTHROPIC_BASE_URL alt-provider routing
|
|
7638
|
+
# (OpenRouter, Ollama, LiteLLM, self-hosted). Claude Code reads this env
|
|
7639
|
+
# var natively; Loki passes it through unchanged. Warn when the user sets
|
|
7640
|
+
# an alt endpoint without LOKI_MODEL_OVERRIDE -- the default opus/sonnet/
|
|
7641
|
+
# haiku aliases may not resolve on the alt-provider.
|
|
7642
|
+
if [ -n "${ANTHROPIC_BASE_URL:-}" ]; then
|
|
7643
|
+
echo -e " ${GREEN}PASS${NC} ANTHROPIC_BASE_URL: ${ANTHROPIC_BASE_URL}"
|
|
7644
|
+
pass_count=$((pass_count + 1))
|
|
7645
|
+
if [ -z "${LOKI_MODEL_OVERRIDE:-}" ]; then
|
|
7646
|
+
echo -e " ${YELLOW}WARN${NC} LOKI_MODEL_OVERRIDE not set -- opus/sonnet/haiku aliases may not resolve on alt-provider"
|
|
7647
|
+
warn_count=$((warn_count + 1))
|
|
7648
|
+
else
|
|
7649
|
+
echo -e " ${GREEN}PASS${NC} LOKI_MODEL_OVERRIDE: ${LOKI_MODEL_OVERRIDE}"
|
|
7650
|
+
pass_count=$((pass_count + 1))
|
|
7651
|
+
fi
|
|
7652
|
+
fi
|
|
7630
7653
|
echo ""
|
|
7631
7654
|
|
|
7632
7655
|
echo -e "${CYAN}Skills:${NC}"
|
|
@@ -13456,6 +13479,9 @@ main() {
|
|
|
13456
13479
|
report)
|
|
13457
13480
|
cmd_report "$@"
|
|
13458
13481
|
;;
|
|
13482
|
+
crash)
|
|
13483
|
+
cmd_crash "$@"
|
|
13484
|
+
;;
|
|
13459
13485
|
share)
|
|
13460
13486
|
cmd_share "$@"
|
|
13461
13487
|
;;
|
|
@@ -17927,6 +17953,201 @@ for line in sys.stdin:
|
|
|
17927
17953
|
}
|
|
17928
17954
|
|
|
17929
17955
|
# Telemetry management (v6.7.0)
|
|
17956
|
+
cmd_crash() {
|
|
17957
|
+
# Phase 0: local-only crash report inspection. NO network egress.
|
|
17958
|
+
#
|
|
17959
|
+
# PARITY: the user-facing strings here MUST match the TS canonical command
|
|
17960
|
+
# at loki-ts/src/commands/crash.ts byte-for-byte (ignoring color codes).
|
|
17961
|
+
# The whole subcommand body is implemented in one Python helper so the
|
|
17962
|
+
# behavior (filename-id resolution, "-" for missing fields, JSON shape,
|
|
17963
|
+
# URL encoding, exit codes) cannot drift from the TS route.
|
|
17964
|
+
#
|
|
17965
|
+
# Subcommands:
|
|
17966
|
+
# loki crash list .loki/crash/*.json reports
|
|
17967
|
+
# loki crash show <id> pretty-print one scrubbed report
|
|
17968
|
+
# loki crash submit [<id>] print the scrubbed payload + a prefilled
|
|
17969
|
+
# GitHub issue URL for MANUAL submission
|
|
17970
|
+
_LOKI_CRASH_DIR=".loki/crash" _LOKI_CRASH_SUB="${1-}" _LOKI_CRASH_ARG="${2-}" \
|
|
17971
|
+
python3 -c '
|
|
17972
|
+
import json, os, sys, urllib.parse
|
|
17973
|
+
|
|
17974
|
+
# Colors: honor NO_COLOR (https://no-color.org) exactly like the TS route
|
|
17975
|
+
# (loki-ts/src/util/colors.ts). Codes match autonomy/loki:25-32 byte-for-byte.
|
|
17976
|
+
_no_color = len(os.environ.get("NO_COLOR", "")) > 0
|
|
17977
|
+
def _c(code):
|
|
17978
|
+
return "" if _no_color else code
|
|
17979
|
+
RED = _c("\x1b[0;31m")
|
|
17980
|
+
GREEN = _c("\x1b[0;32m")
|
|
17981
|
+
YELLOW = _c("\x1b[1;33m")
|
|
17982
|
+
CYAN = _c("\x1b[0;36m")
|
|
17983
|
+
BOLD = _c("\x1b[1m")
|
|
17984
|
+
NC = _c("\x1b[0m")
|
|
17985
|
+
|
|
17986
|
+
ISSUE_BASE = "https://github.com/asklokesh/loki-mode/issues/new"
|
|
17987
|
+
CRASH_DIR = os.environ["_LOKI_CRASH_DIR"]
|
|
17988
|
+
SUB = os.environ.get("_LOKI_CRASH_SUB", "")
|
|
17989
|
+
ARG = os.environ.get("_LOKI_CRASH_ARG", "")
|
|
17990
|
+
|
|
17991
|
+
HELP = (
|
|
17992
|
+
BOLD + "loki crash" + NC + " - inspect and manually submit local crash reports\n"
|
|
17993
|
+
"\n"
|
|
17994
|
+
"Usage: loki crash [subcommand] [args]\n"
|
|
17995
|
+
"\n"
|
|
17996
|
+
"Subcommands:\n"
|
|
17997
|
+
" (none) List crash reports in .loki/crash/\n"
|
|
17998
|
+
" show <id> Pretty-print one scrubbed crash report\n"
|
|
17999
|
+
" submit [<id>] Print the scrubbed payload and a prefilled GitHub\n"
|
|
18000
|
+
" issue URL for manual submission\n"
|
|
18001
|
+
"\n"
|
|
18002
|
+
"Crash reports are anonymous, scrubbed, and stored locally only. Nothing is\n"
|
|
18003
|
+
"sent automatically in this version. See docs/PRIVACY.md.\n"
|
|
18004
|
+
)
|
|
18005
|
+
|
|
18006
|
+
def s(v):
|
|
18007
|
+
return "-" if v is None else str(v)
|
|
18008
|
+
|
|
18009
|
+
def pad(text, w):
|
|
18010
|
+
return text if len(text) >= w else text + " " * (w - len(text))
|
|
18011
|
+
|
|
18012
|
+
def report_ids():
|
|
18013
|
+
if not os.path.isdir(CRASH_DIR):
|
|
18014
|
+
return []
|
|
18015
|
+
try:
|
|
18016
|
+
names = [e for e in os.listdir(CRASH_DIR)
|
|
18017
|
+
if e.endswith(".json") and os.path.isfile(os.path.join(CRASH_DIR, e))]
|
|
18018
|
+
except OSError:
|
|
18019
|
+
return []
|
|
18020
|
+
return sorted(n[:-len(".json")] for n in names)
|
|
18021
|
+
|
|
18022
|
+
def _safe_id(rid):
|
|
18023
|
+
# Reject path traversal: ids are bare filenames (no separators, no "..",
|
|
18024
|
+
# no leading separator). This guards the direct filename lookup; the
|
|
18025
|
+
# fingerprint-resolution loop only ever passes real listdir ids, which
|
|
18026
|
+
# always pass. Mirrors the identical rule in loki-ts crash.ts.
|
|
18027
|
+
if rid is None or rid == "":
|
|
18028
|
+
return False
|
|
18029
|
+
if "/" in rid or "\\" in rid or ".." in rid:
|
|
18030
|
+
return False
|
|
18031
|
+
if rid[0] in ("/", "\\"):
|
|
18032
|
+
return False
|
|
18033
|
+
return True
|
|
18034
|
+
|
|
18035
|
+
def read_report(rid):
|
|
18036
|
+
if not _safe_id(rid):
|
|
18037
|
+
return None
|
|
18038
|
+
p = os.path.join(CRASH_DIR, rid + ".json")
|
|
18039
|
+
if not os.path.exists(p):
|
|
18040
|
+
return None
|
|
18041
|
+
try:
|
|
18042
|
+
with open(p) as f:
|
|
18043
|
+
return json.load(f)
|
|
18044
|
+
except Exception:
|
|
18045
|
+
return {}
|
|
18046
|
+
|
|
18047
|
+
def resolve_report(arg):
|
|
18048
|
+
direct = read_report(arg)
|
|
18049
|
+
if direct is not None:
|
|
18050
|
+
return (arg, direct)
|
|
18051
|
+
for rid in report_ids():
|
|
18052
|
+
r = read_report(rid)
|
|
18053
|
+
if r is not None and str(r.get("fingerprint", "")) == arg:
|
|
18054
|
+
return (rid, r)
|
|
18055
|
+
return None
|
|
18056
|
+
|
|
18057
|
+
def dumps(obj):
|
|
18058
|
+
return json.dumps(obj, indent=2, ensure_ascii=False)
|
|
18059
|
+
|
|
18060
|
+
def list_crashes():
|
|
18061
|
+
ids = report_ids()
|
|
18062
|
+
if not ids:
|
|
18063
|
+
sys.stdout.write(YELLOW + "No crash reports found." + NC +
|
|
18064
|
+
" Nothing has been captured in .loki/crash/.\n")
|
|
18065
|
+
return 0
|
|
18066
|
+
sys.stdout.write(pad("ID", 40) + " " + pad("CAPTURED_AT", 22) + " ERROR_CLASS\n")
|
|
18067
|
+
for rid in ids:
|
|
18068
|
+
d = read_report(rid) or {}
|
|
18069
|
+
fp = s(d.get("fingerprint"))
|
|
18070
|
+
captured_at = s(d.get("captured_at"))
|
|
18071
|
+
error_class = s(d.get("error_class"))
|
|
18072
|
+
visible_id = fp if fp != "-" else rid
|
|
18073
|
+
sys.stdout.write(pad(visible_id, 40) + " " + pad(captured_at, 22) + " " + error_class + "\n")
|
|
18074
|
+
sys.stdout.write(
|
|
18075
|
+
"\n" + str(len(ids)) + " report(s). Run \x27loki crash show <id>\x27 to inspect, "
|
|
18076
|
+
"\x27loki crash submit\x27 to get a prefilled GitHub issue URL.\n")
|
|
18077
|
+
return 0
|
|
18078
|
+
|
|
18079
|
+
def show_crash(rid):
|
|
18080
|
+
if not rid:
|
|
18081
|
+
sys.stderr.write(RED + "Missing crash id." + NC + " Use \x27loki crash\x27 to list reports.\n")
|
|
18082
|
+
return 2
|
|
18083
|
+
resolved = resolve_report(rid)
|
|
18084
|
+
if resolved is None:
|
|
18085
|
+
sys.stderr.write(RED + "Crash report not found: " + rid + NC + "\n")
|
|
18086
|
+
sys.stderr.write("Use \x27loki crash\x27 to see available reports.\n")
|
|
18087
|
+
return 1
|
|
18088
|
+
sys.stdout.write(dumps(resolved[1]) + "\n")
|
|
18089
|
+
return 0
|
|
18090
|
+
|
|
18091
|
+
def issue_url(report):
|
|
18092
|
+
error_class = s(report.get("error_class"))
|
|
18093
|
+
fp = s(report.get("fingerprint"))
|
|
18094
|
+
short_fp = fp[:12] if fp != "-" else "unknown"
|
|
18095
|
+
title = "crash: " + error_class + " (" + short_fp + ")"
|
|
18096
|
+
payload = dumps(report)
|
|
18097
|
+
body = "\n".join([
|
|
18098
|
+
"Anonymous crash report captured by Loki Mode (scrubbed, whitelist-only).",
|
|
18099
|
+
"",
|
|
18100
|
+
"Scrubbed payload:",
|
|
18101
|
+
"```json",
|
|
18102
|
+
payload,
|
|
18103
|
+
"```",
|
|
18104
|
+
"",
|
|
18105
|
+
"Nothing was sent automatically. This issue is submitted manually by me.",
|
|
18106
|
+
])
|
|
18107
|
+
q = urllib.parse.urlencode({"title": title, "body": body})
|
|
18108
|
+
return ISSUE_BASE + "?" + q
|
|
18109
|
+
|
|
18110
|
+
def submit_crash(rid):
|
|
18111
|
+
if rid:
|
|
18112
|
+
target = resolve_report(rid)
|
|
18113
|
+
if target is None:
|
|
18114
|
+
sys.stderr.write(RED + "Crash report not found: " + rid + NC + "\n")
|
|
18115
|
+
sys.stderr.write("Use \x27loki crash\x27 to see available reports.\n")
|
|
18116
|
+
return 1
|
|
18117
|
+
else:
|
|
18118
|
+
ids = report_ids()
|
|
18119
|
+
if not ids:
|
|
18120
|
+
sys.stdout.write(YELLOW + "No crash reports found." + NC + " Nothing to submit.\n")
|
|
18121
|
+
return 0
|
|
18122
|
+
last_id = ids[-1]
|
|
18123
|
+
target = (last_id, read_report(last_id) or {})
|
|
18124
|
+
sys.stdout.write(BOLD + "Scrubbed payload (this is the ENTIRE report):" + NC + "\n")
|
|
18125
|
+
sys.stdout.write(dumps(target[1]) + "\n\n")
|
|
18126
|
+
sys.stdout.write(YELLOW + "Nothing is sent automatically in this version." + NC +
|
|
18127
|
+
" Loki Mode never transmits crash data on its own.\n")
|
|
18128
|
+
sys.stdout.write("To submit manually, open this prefilled GitHub issue and review it first:\n\n")
|
|
18129
|
+
sys.stdout.write(" " + CYAN + issue_url(target[1]) + NC + "\n\n")
|
|
18130
|
+
sys.stdout.write(GREEN + "The payload above is exactly what the URL contains." + NC + "\n")
|
|
18131
|
+
sys.stdout.write("See docs/PRIVACY.md for what is and is not collected.\n")
|
|
18132
|
+
return 0
|
|
18133
|
+
|
|
18134
|
+
if SUB == "":
|
|
18135
|
+
sys.exit(list_crashes())
|
|
18136
|
+
elif SUB in ("--help", "-h", "help"):
|
|
18137
|
+
sys.stdout.write(HELP)
|
|
18138
|
+
sys.exit(0)
|
|
18139
|
+
elif SUB == "show":
|
|
18140
|
+
sys.exit(show_crash(ARG))
|
|
18141
|
+
elif SUB == "submit":
|
|
18142
|
+
sys.exit(submit_crash(ARG))
|
|
18143
|
+
else:
|
|
18144
|
+
sys.stderr.write(RED + "Unknown crash subcommand: " + SUB + NC + "\n")
|
|
18145
|
+
sys.stdout.write(HELP)
|
|
18146
|
+
sys.exit(2)
|
|
18147
|
+
'
|
|
18148
|
+
return $?
|
|
18149
|
+
}
|
|
18150
|
+
|
|
17930
18151
|
cmd_telemetry() {
|
|
17931
18152
|
local subcommand="${1:-status}"
|
|
17932
18153
|
shift 2>/dev/null || true
|
|
@@ -17991,6 +18212,25 @@ try {
|
|
|
17991
18212
|
echo -e " Saved: $saved_endpoint"
|
|
17992
18213
|
fi
|
|
17993
18214
|
fi
|
|
18215
|
+
|
|
18216
|
+
# Unified collection state (PostHog usage telemetry + crash reporting).
|
|
18217
|
+
# loki_collection_enabled is the single source of truth (crash.sh).
|
|
18218
|
+
echo ""
|
|
18219
|
+
if type loki_collection_enabled &>/dev/null; then
|
|
18220
|
+
if loki_collection_enabled; then
|
|
18221
|
+
echo -e " Collection: ${GREEN}enabled${NC} (anonymous diagnostics; turn off with: loki telemetry off)"
|
|
18222
|
+
else
|
|
18223
|
+
echo -e " Collection: ${YELLOW}disabled${NC} (re-enable with: loki telemetry on)"
|
|
18224
|
+
fi
|
|
18225
|
+
fi
|
|
18226
|
+
|
|
18227
|
+
# Count of pending local crash reports (.loki/crash/*.json).
|
|
18228
|
+
local crash_dir=".loki/crash"
|
|
18229
|
+
local pending=0
|
|
18230
|
+
if [ -d "$crash_dir" ]; then
|
|
18231
|
+
pending=$(find "$crash_dir" -maxdepth 1 -name '*.json' -type f 2>/dev/null | wc -l | tr -d '[:space:]')
|
|
18232
|
+
fi
|
|
18233
|
+
echo -e " Crash reports pending: ${pending:-0} (see: loki crash)"
|
|
17994
18234
|
echo ""
|
|
17995
18235
|
;;
|
|
17996
18236
|
|
|
@@ -18058,26 +18298,28 @@ TELEM_DISABLE_PY
|
|
|
18058
18298
|
echo " Unset LOKI_OTEL_ENDPOINT in your shell for immediate effect."
|
|
18059
18299
|
;;
|
|
18060
18300
|
|
|
18061
|
-
stop)
|
|
18062
|
-
# Persistent opt-out across all sessions
|
|
18301
|
+
stop|off)
|
|
18302
|
+
# Persistent opt-out across all sessions. This is the unified
|
|
18303
|
+
# opt-out: it gates BOTH PostHog usage telemetry AND crash reporting.
|
|
18063
18304
|
local global_config="${HOME}/.loki/config"
|
|
18064
18305
|
mkdir -p "${HOME}/.loki"
|
|
18065
18306
|
|
|
18066
18307
|
# Remove existing TELEMETRY_DISABLED line if present, then add
|
|
18308
|
+
# (idempotent).
|
|
18067
18309
|
if [ -f "$global_config" ]; then
|
|
18068
18310
|
grep -v "^TELEMETRY_DISABLED=" "$global_config" > "${global_config}.tmp" 2>/dev/null || true
|
|
18069
18311
|
mv "${global_config}.tmp" "$global_config"
|
|
18070
18312
|
fi
|
|
18071
18313
|
echo "TELEMETRY_DISABLED=true" >> "$global_config"
|
|
18072
18314
|
|
|
18073
|
-
echo -e "${BOLD}Telemetry
|
|
18315
|
+
echo -e "${BOLD}Telemetry and crash reporting disabled${NC}"
|
|
18074
18316
|
echo ""
|
|
18075
18317
|
echo " Opt-out saved to: $global_config"
|
|
18076
18318
|
echo " This persists across all sessions and new runs."
|
|
18077
|
-
echo " Run 'loki telemetry
|
|
18319
|
+
echo " Run 'loki telemetry on' to re-enable."
|
|
18078
18320
|
;;
|
|
18079
18321
|
|
|
18080
|
-
start)
|
|
18322
|
+
start|on)
|
|
18081
18323
|
# Remove persistent opt-out
|
|
18082
18324
|
local global_config="${HOME}/.loki/config"
|
|
18083
18325
|
if [ -f "$global_config" ]; then
|
|
@@ -18085,9 +18327,10 @@ TELEM_DISABLE_PY
|
|
|
18085
18327
|
mv "${global_config}.tmp" "$global_config"
|
|
18086
18328
|
fi
|
|
18087
18329
|
|
|
18088
|
-
echo -e "${BOLD}Telemetry
|
|
18330
|
+
echo -e "${BOLD}Telemetry and crash reporting re-enabled${NC}"
|
|
18089
18331
|
echo ""
|
|
18090
|
-
echo "
|
|
18332
|
+
echo " Anonymous diagnostics collection is on again."
|
|
18333
|
+
echo " OTEL tracing can be configured with 'loki telemetry enable [endpoint]'."
|
|
18091
18334
|
;;
|
|
18092
18335
|
|
|
18093
18336
|
--help|-h|help)
|
|
@@ -18096,11 +18339,13 @@ TELEM_DISABLE_PY
|
|
|
18096
18339
|
echo "Usage: loki telemetry <command>"
|
|
18097
18340
|
echo ""
|
|
18098
18341
|
echo "Commands:"
|
|
18099
|
-
echo " status Show
|
|
18342
|
+
echo " status Show OTEL config + collection state + pending crash reports"
|
|
18100
18343
|
echo " enable [endpoint] Enable OTEL (default: http://localhost:4318)"
|
|
18101
18344
|
echo " disable Disable OTEL for current project"
|
|
18102
|
-
echo "
|
|
18103
|
-
echo "
|
|
18345
|
+
echo " off Opt out of all anonymous diagnostics (telemetry + crash)"
|
|
18346
|
+
echo " on Re-enable anonymous diagnostics"
|
|
18347
|
+
echo " stop Alias for 'off' (permanent opt-out across all sessions)"
|
|
18348
|
+
echo " start Alias for 'on' (remove persistent opt-out)"
|
|
18104
18349
|
echo ""
|
|
18105
18350
|
;;
|
|
18106
18351
|
*)
|
package/autonomy/run.sh
CHANGED
|
@@ -651,6 +651,15 @@ if [ -f "$TELEMETRY_SCRIPT" ]; then
|
|
|
651
651
|
source "$TELEMETRY_SCRIPT"
|
|
652
652
|
fi
|
|
653
653
|
|
|
654
|
+
# Crash-reporting helpers (Phase 0: local-only, zero egress).
|
|
655
|
+
# Provides loki_collection_enabled (unified opt-out), loki_crash_capture,
|
|
656
|
+
# loki_crash_friction, loki_show_disclosure_once.
|
|
657
|
+
CRASH_SCRIPT="$SCRIPT_DIR/crash.sh"
|
|
658
|
+
if [ -f "$CRASH_SCRIPT" ]; then
|
|
659
|
+
# shellcheck source=crash.sh
|
|
660
|
+
source "$CRASH_SCRIPT"
|
|
661
|
+
fi
|
|
662
|
+
|
|
654
663
|
|
|
655
664
|
|
|
656
665
|
# 2026 Research Enhancements (minimal additions)
|
|
@@ -1743,6 +1752,11 @@ invoke_with_timeout() {
|
|
|
1743
1752
|
done
|
|
1744
1753
|
|
|
1745
1754
|
log_error "Provider spawn failed after $((max_retries+1)) attempts (timeout=${timeout}s)"
|
|
1755
|
+
# Crash friction (retry_loop): provider spawn exhausted all retries -- a
|
|
1756
|
+
# clear threshold (not a single retry). Best-effort, never blocks.
|
|
1757
|
+
if type loki_crash_friction &>/dev/null; then
|
|
1758
|
+
loki_crash_friction "retry_loop" "provider spawn failed after $((max_retries+1)) attempts" >/dev/null 2>&1 || true
|
|
1759
|
+
fi
|
|
1746
1760
|
return 124
|
|
1747
1761
|
}
|
|
1748
1762
|
|
|
@@ -6107,7 +6121,11 @@ track_gate_failure() {
|
|
|
6107
6121
|
local gate_file="${TARGET_DIR:-.}/.loki/quality/gate-failure-count.json"
|
|
6108
6122
|
mkdir -p "$(dirname "$gate_file")"
|
|
6109
6123
|
|
|
6110
|
-
|
|
6124
|
+
# IMPORTANT: this function's stdout IS its return value (callers do
|
|
6125
|
+
# count=$(track_gate_failure ...)). Capture the count first, then do any
|
|
6126
|
+
# side-effects with their stdout suppressed, then echo ONLY the count.
|
|
6127
|
+
local count
|
|
6128
|
+
count=$(_GATE_FILE="$gate_file" _GATE_NAME="$gate_name" python3 -c "
|
|
6111
6129
|
import json, os
|
|
6112
6130
|
gate_file = os.environ['_GATE_FILE']
|
|
6113
6131
|
gate_name = os.environ['_GATE_NAME']
|
|
@@ -6120,7 +6138,16 @@ counts[gate_name] = counts.get(gate_name, 0) + 1
|
|
|
6120
6138
|
with open(gate_file, 'w') as f:
|
|
6121
6139
|
json.dump(counts, f, indent=2)
|
|
6122
6140
|
print(counts[gate_name])
|
|
6123
|
-
" 2>/dev/null || echo "1"
|
|
6141
|
+
" 2>/dev/null || echo "1")
|
|
6142
|
+
|
|
6143
|
+
# Crash friction (gate_failure): fire exactly once at the threshold (3
|
|
6144
|
+
# consecutive failures) so a sustained failure does not re-fire every
|
|
6145
|
+
# iteration. Best-effort, stdout suppressed so the count stays clean.
|
|
6146
|
+
if [ "${count:-0}" -eq 3 ] 2>/dev/null && type loki_crash_friction &>/dev/null; then
|
|
6147
|
+
loki_crash_friction "gate_failure" "gate=${gate_name} consecutive=${count}" >/dev/null 2>&1 || true
|
|
6148
|
+
fi
|
|
6149
|
+
|
|
6150
|
+
echo "$count"
|
|
6124
6151
|
}
|
|
6125
6152
|
|
|
6126
6153
|
clear_gate_failure() {
|
|
@@ -8196,6 +8223,11 @@ attempt_provider_failover() {
|
|
|
8196
8223
|
done
|
|
8197
8224
|
|
|
8198
8225
|
log_warn "Failover: all providers in chain exhausted, falling back to retry"
|
|
8226
|
+
# Crash friction (rate_limit_loop): a clear threshold -- every provider in
|
|
8227
|
+
# the failover chain is rate-limited/unhealthy. Best-effort, never blocks.
|
|
8228
|
+
if type loki_crash_friction &>/dev/null; then
|
|
8229
|
+
loki_crash_friction "rate_limit_loop" "failover chain exhausted: ${FAILOVER_CHAIN}" >/dev/null 2>&1 || true
|
|
8230
|
+
fi
|
|
8199
8231
|
return 1
|
|
8200
8232
|
}
|
|
8201
8233
|
|
|
@@ -11989,6 +12021,23 @@ if __name__ == "__main__":
|
|
|
11989
12021
|
"status=$([[ $exit_code -eq 0 ]] && echo ok || echo error)"
|
|
11990
12022
|
fi
|
|
11991
12023
|
|
|
12024
|
+
# Crash capture (Phase 0: local-only, best-effort, never blocks).
|
|
12025
|
+
# Conservative: only on a genuine non-zero failure exit. Signal-induced
|
|
12026
|
+
# exits (130 SIGINT / 143 SIGTERM / 137 SIGKILL) are user/operator
|
|
12027
|
+
# interrupts, not crashes, so we skip them. Known conservatism tradeoff:
|
|
12028
|
+
# this fires once per iteration on any nonzero exit, so a long, repeatedly
|
|
12029
|
+
# failing run can accumulate multiple local reports under .loki/crash/.
|
|
12030
|
+
if [ "$exit_code" -ne 0 ] 2>/dev/null && \
|
|
12031
|
+
[ "$exit_code" -ne 130 ] && [ "$exit_code" -ne 143 ] && [ "$exit_code" -ne 137 ] && \
|
|
12032
|
+
type loki_crash_capture &>/dev/null; then
|
|
12033
|
+
loki_crash_capture \
|
|
12034
|
+
"IterationError" \
|
|
12035
|
+
"provider exited non-zero on iteration ${ITERATION_COUNT:-?}" \
|
|
12036
|
+
"$([ -f "$iter_output" ] && tail -c 16384 "$iter_output" 2>/dev/null || true)" \
|
|
12037
|
+
"${rarv_phase:-iteration}" \
|
|
12038
|
+
"$exit_code"
|
|
12039
|
+
fi
|
|
12040
|
+
|
|
11992
12041
|
# PRD Checklist verification on interval (v5.44.0)
|
|
11993
12042
|
if type checklist_should_verify &>/dev/null && checklist_should_verify; then
|
|
11994
12043
|
checklist_verify
|
|
@@ -12914,6 +12963,11 @@ main() {
|
|
|
12914
12963
|
trap cleanup INT TERM
|
|
12915
12964
|
SESSION_START_EPOCH=$(date +%s)
|
|
12916
12965
|
|
|
12966
|
+
# First-run disclosure (shown once, before any work; best-effort).
|
|
12967
|
+
if type loki_show_disclosure_once &>/dev/null; then
|
|
12968
|
+
loki_show_disclosure_once
|
|
12969
|
+
fi
|
|
12970
|
+
|
|
12917
12971
|
echo ""
|
|
12918
12972
|
echo -e "${BOLD}${BLUE}"
|
|
12919
12973
|
echo " ██╗ ██████╗ ██╗ ██╗██╗ ███╗ ███╗ ██████╗ ██████╗ ███████╗"
|
package/autonomy/telemetry.sh
CHANGED
|
@@ -7,8 +7,19 @@ LOKI_POSTHOG_HOST="${LOKI_TELEMETRY_ENDPOINT:-https://us.i.posthog.com}"
|
|
|
7
7
|
LOKI_POSTHOG_KEY="phc_ya0vGBru41AJWtGNfZZ8H9W4yjoZy4KON0nnayS7s87"
|
|
8
8
|
|
|
9
9
|
_loki_telemetry_enabled() {
|
|
10
|
+
# Unified opt-out: these checks must mirror loki_collection_enabled in
|
|
11
|
+
# autonomy/crash.sh so one switch gates BOTH PostHog usage telemetry and
|
|
12
|
+
# crash reporting.
|
|
13
|
+
# LOKI_TELEMETRY=off (case-insensitive)
|
|
14
|
+
local _telem_lower
|
|
15
|
+
_telem_lower="$(printf '%s' "${LOKI_TELEMETRY:-}" | tr '[:upper:]' '[:lower:]')"
|
|
16
|
+
[ "$_telem_lower" = "off" ] && return 1
|
|
10
17
|
[ "${LOKI_TELEMETRY_DISABLED:-}" = "true" ] && return 1
|
|
11
18
|
[ "${DO_NOT_TRACK:-}" = "1" ] && return 1
|
|
19
|
+
# Persistent opt-out in ~/.loki/config
|
|
20
|
+
if [ -f "${HOME}/.loki/config" ] && grep -q "^TELEMETRY_DISABLED=true" "${HOME}/.loki/config" 2>/dev/null; then
|
|
21
|
+
return 1
|
|
22
|
+
fi
|
|
12
23
|
command -v curl >/dev/null 2>&1 || return 1
|
|
13
24
|
return 0
|
|
14
25
|
}
|
package/bin/loki
CHANGED
|
@@ -116,11 +116,13 @@ fi
|
|
|
116
116
|
# Two-token routes (provider show/list, memory list/index) match on the first
|
|
117
117
|
# token only; the Bun dispatcher handles subcommand routing internally.
|
|
118
118
|
case "${1:-}" in
|
|
119
|
-
version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis|trust|proof|wiki)
|
|
119
|
+
version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis|trust|proof|wiki|crash)
|
|
120
120
|
# v7.5.2: rollback added (wires loki-ts/src/commands/rollback.ts).
|
|
121
121
|
# v7.5.3: internal added for autonomy/run.sh phase1-hooks calls.
|
|
122
122
|
# v7.5.28: kpis added (Phase K MVP: read-only KPI snapshot).
|
|
123
123
|
# R4: trust added (visible trust trajectory; Bun route, bash fallback).
|
|
124
|
+
# crash added (Crash Reporting Phase 0: inspect/manually submit local
|
|
125
|
+
# scrubbed crash reports; Bun route, bash fallback).
|
|
124
126
|
#
|
|
125
127
|
# v7.8.2: emit the cli_command product-analytics event for Bun-routed
|
|
126
128
|
# commands. The bash CLI fires this from autonomy/loki main(), but the
|
package/bin/postinstall.js
CHANGED
|
@@ -180,8 +180,22 @@ console.log('New here? Run `loki welcome` for a 30-second tour.');
|
|
|
180
180
|
console.log('');
|
|
181
181
|
|
|
182
182
|
// Anonymous install telemetry (fire-and-forget, silent)
|
|
183
|
+
// Unified opt-out: these checks mirror loki_collection_enabled in
|
|
184
|
+
// autonomy/crash.sh so one switch gates BOTH PostHog usage telemetry and
|
|
185
|
+
// crash reporting.
|
|
186
|
+
function _lokiCollectionDisabled() {
|
|
187
|
+
if ((process.env.LOKI_TELEMETRY || '').toLowerCase() === 'off') return true;
|
|
188
|
+
if (process.env.LOKI_TELEMETRY_DISABLED === 'true') return true;
|
|
189
|
+
if (process.env.DO_NOT_TRACK === '1') return true;
|
|
190
|
+
try {
|
|
191
|
+
const cfg = path.join(homeDir, '.loki', 'config');
|
|
192
|
+
const lines = fs.readFileSync(cfg, 'utf8').split('\n');
|
|
193
|
+
if (lines.some((l) => l.startsWith('TELEMETRY_DISABLED=true'))) return true;
|
|
194
|
+
} catch {}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
183
197
|
try {
|
|
184
|
-
if (
|
|
198
|
+
if (!_lokiCollectionDisabled()) {
|
|
185
199
|
const https = require('https');
|
|
186
200
|
const crypto = require('crypto');
|
|
187
201
|
const idFile = path.join(homeDir, '.loki-telemetry-id');
|
package/dashboard/__init__.py
CHANGED
package/dashboard/telemetry.py
CHANGED
|
@@ -19,10 +19,25 @@ _POSTHOG_KEY = "phc_ya0vGBru41AJWtGNfZZ8H9W4yjoZy4KON0nnayS7s87"
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def _is_enabled():
|
|
22
|
+
# Unified opt-out: these checks must mirror loki_collection_enabled in
|
|
23
|
+
# autonomy/crash.sh so one switch gates BOTH PostHog usage telemetry and
|
|
24
|
+
# crash reporting.
|
|
25
|
+
if os.environ.get("LOKI_TELEMETRY", "").lower() == "off":
|
|
26
|
+
return False
|
|
22
27
|
if os.environ.get("LOKI_TELEMETRY_DISABLED") == "true":
|
|
23
28
|
return False
|
|
24
29
|
if os.environ.get("DO_NOT_TRACK") == "1":
|
|
25
30
|
return False
|
|
31
|
+
# Persistent opt-out in ~/.loki/config (matches the bash grep prefix
|
|
32
|
+
# semantics: any line beginning with TELEMETRY_DISABLED=true).
|
|
33
|
+
try:
|
|
34
|
+
config_path = Path.home() / ".loki" / "config"
|
|
35
|
+
if config_path.is_file():
|
|
36
|
+
for line in config_path.read_text().splitlines():
|
|
37
|
+
if line.startswith("TELEMETRY_DISABLED=true"):
|
|
38
|
+
return False
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
26
41
|
return True
|
|
27
42
|
|
|
28
43
|
|