loki-mode 8.1.0 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +45 -4
- package/autonomy/council-v2.sh +63 -1
- package/autonomy/grill.sh +32 -0
- package/autonomy/lib/done-recognition.sh +16 -4
- package/autonomy/lib/fast_verify.py +346 -0
- package/autonomy/lib/no_mock_scan.py +21 -1
- package/autonomy/lib/prd-enrich.sh +7 -1
- package/autonomy/lib/proof-generator.py +146 -6
- package/autonomy/lib/proof-template.html +60 -12
- package/autonomy/lib/proof-verify.py +52 -0
- package/autonomy/loki +209 -2
- package/autonomy/verify.sh +12 -6
- package/dashboard/__init__.py +1 -1
- package/events/emit.sh +66 -6
- package/loki-ts/dist/loki.js +319 -316
- package/mcp/__init__.py +30 -12
- package/mcp/server.py +150 -0
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +72 -0
- package/providers/codex.sh +13 -0
- package/providers/loader.sh +10 -4
- package/providers/model_catalog.json +50 -0
- package/providers/models.sh +26 -3
- package/providers/opencode.sh +145 -0
- package/references/design-archetypes.md +85 -0
package/autonomy/loki
CHANGED
|
@@ -10593,8 +10593,133 @@ EOF
|
|
|
10593
10593
|
}
|
|
10594
10594
|
|
|
10595
10595
|
# Check system prerequisites
|
|
10596
|
+
# _loki_airgap_audit <json?> -- enumerate every network egress this engine can
|
|
10597
|
+
# perform, classify each as REQUIRED or optional, and say how to disable it.
|
|
10598
|
+
#
|
|
10599
|
+
# WHY THIS EXISTS
|
|
10600
|
+
# Air-gapped deployment is a real enterprise requirement and the incumbent
|
|
10601
|
+
# (Tabnine) states it can "be deployed in a completely air-gapped environment"
|
|
10602
|
+
# without publishing WHICH hosts it would otherwise contact. A claim you
|
|
10603
|
+
# cannot audit is a claim you have to take on faith.
|
|
10604
|
+
#
|
|
10605
|
+
# Sourcegraph is the honest counter-example: their own whitepaper states that
|
|
10606
|
+
# self-hosted Cody still "sends requests from the Sourcegraph instance to
|
|
10607
|
+
# Anthropic's API". Self-hosted control plane, egressing inference.
|
|
10608
|
+
#
|
|
10609
|
+
# So the credible position is not "trust us, we are air-gapped" -- it is here
|
|
10610
|
+
# is every host, here is which ones are required, here is how to turn each one
|
|
10611
|
+
# off, now verify it yourself with tcpdump. We would rather be auditable than
|
|
10612
|
+
# assertive.
|
|
10613
|
+
#
|
|
10614
|
+
# Exit 0 when no REQUIRED egress remains (air-gap achievable in this config),
|
|
10615
|
+
# 1 otherwise. Deterministic: reads configuration, performs NO network calls.
|
|
10616
|
+
_loki_airgap_audit() {
|
|
10617
|
+
local as_json="${1:-false}"
|
|
10618
|
+
|
|
10619
|
+
# Provider inference endpoint: the one egress that is genuinely required,
|
|
10620
|
+
# unless the active provider serves weights locally.
|
|
10621
|
+
local provider="${LOKI_PROVIDER:-claude}"
|
|
10622
|
+
local model_host="" model_required="yes" model_note=""
|
|
10623
|
+
case "$provider" in
|
|
10624
|
+
claude)
|
|
10625
|
+
model_host="${ANTHROPIC_BASE_URL:-https://api.anthropic.com}"
|
|
10626
|
+
model_note="Claude Code inference. Set ANTHROPIC_BASE_URL to an in-network gateway, or switch to a local-weights provider."
|
|
10627
|
+
;;
|
|
10628
|
+
codex)
|
|
10629
|
+
model_host="${OPENAI_BASE_URL:-https://api.openai.com}"
|
|
10630
|
+
model_note="Codex inference. Point at an in-network gateway or switch provider."
|
|
10631
|
+
;;
|
|
10632
|
+
opencode|cline|aider)
|
|
10633
|
+
model_host="${OPENAI_BASE_URL:-${OPENROUTER_API_KEY:+https://openrouter.ai}}"
|
|
10634
|
+
if [ -z "$model_host" ] || printf '%s' "${LOKI_OPENCODE_MODEL:-}${LOKI_AIDER_MODEL:-}" | grep -qiE 'ollama|localhost|127\.0\.0\.1|lmstudio'; then
|
|
10635
|
+
model_host="local weights"
|
|
10636
|
+
model_required="no"
|
|
10637
|
+
model_note="Local model runtime (Ollama / LM Studio / llama.cpp). No inference egress."
|
|
10638
|
+
else
|
|
10639
|
+
model_note="Remote model gateway. Use an ollama/ model id for zero-egress inference."
|
|
10640
|
+
fi
|
|
10641
|
+
;;
|
|
10642
|
+
*)
|
|
10643
|
+
model_host="unknown (provider '$provider')"
|
|
10644
|
+
model_note="Unrecognized provider; audit its config manually."
|
|
10645
|
+
;;
|
|
10646
|
+
esac
|
|
10647
|
+
|
|
10648
|
+
# Optional egress, each individually disableable.
|
|
10649
|
+
local tel_state="off"
|
|
10650
|
+
[ "${LOKI_TELEMETRY:-}" = "on" ] && tel_state="on"
|
|
10651
|
+
local upd_state="on"
|
|
10652
|
+
[ "${LOKI_NO_UPDATE_CHECK:-0}" = "1" ] && upd_state="off"
|
|
10653
|
+
|
|
10654
|
+
if [ "$as_json" = "true" ]; then
|
|
10655
|
+
_AG_PROV="$provider" _AG_HOST="$model_host" _AG_REQ="$model_required" \
|
|
10656
|
+
_AG_NOTE="$model_note" _AG_TEL="$tel_state" _AG_UPD="$upd_state" \
|
|
10657
|
+
python3 -c '
|
|
10658
|
+
import json, os
|
|
10659
|
+
req = os.environ["_AG_REQ"] == "yes"
|
|
10660
|
+
egress = [{
|
|
10661
|
+
"name": "model_inference",
|
|
10662
|
+
"host": os.environ["_AG_HOST"],
|
|
10663
|
+
"required": req,
|
|
10664
|
+
"disable": "use a local-weights provider (ollama/, lmstudio/) or an in-network gateway",
|
|
10665
|
+
"note": os.environ["_AG_NOTE"],
|
|
10666
|
+
}, {
|
|
10667
|
+
"name": "telemetry",
|
|
10668
|
+
"host": "telemetry endpoint",
|
|
10669
|
+
"required": False,
|
|
10670
|
+
"enabled": os.environ["_AG_TEL"] == "on",
|
|
10671
|
+
"disable": "loki telemetry off (already the default)",
|
|
10672
|
+
}, {
|
|
10673
|
+
"name": "update_check",
|
|
10674
|
+
"host": "registry.npmjs.org",
|
|
10675
|
+
"required": False,
|
|
10676
|
+
"enabled": os.environ["_AG_UPD"] == "on",
|
|
10677
|
+
"disable": "export LOKI_NO_UPDATE_CHECK=1",
|
|
10678
|
+
}]
|
|
10679
|
+
blockers = [e for e in egress if e.get("required")]
|
|
10680
|
+
print(json.dumps({
|
|
10681
|
+
"provider": os.environ["_AG_PROV"],
|
|
10682
|
+
"airgap_ready": not blockers,
|
|
10683
|
+
"required_egress": [e["name"] for e in blockers],
|
|
10684
|
+
"egress": egress,
|
|
10685
|
+
}, indent=2))
|
|
10686
|
+
'
|
|
10687
|
+
[ "$model_required" = "yes" ] && return 1
|
|
10688
|
+
return 0
|
|
10689
|
+
fi
|
|
10690
|
+
|
|
10691
|
+
echo -e "${BOLD}Network egress audit${NC}"
|
|
10692
|
+
echo ""
|
|
10693
|
+
echo " Active provider: $provider"
|
|
10694
|
+
echo ""
|
|
10695
|
+
echo -e "${BOLD}Egress points${NC}"
|
|
10696
|
+
if [ "$model_required" = "yes" ]; then
|
|
10697
|
+
echo -e " ${YELLOW}REQUIRED${NC} model inference -> $model_host"
|
|
10698
|
+
else
|
|
10699
|
+
echo -e " ${GREEN}none${NC} model inference -> $model_host"
|
|
10700
|
+
fi
|
|
10701
|
+
echo " $model_note"
|
|
10702
|
+
echo -e " optional telemetry [$tel_state] disable: loki telemetry off (default off)"
|
|
10703
|
+
echo -e " optional update check [$upd_state] disable: export LOKI_NO_UPDATE_CHECK=1"
|
|
10704
|
+
echo ""
|
|
10705
|
+
if [ "$model_required" = "yes" ]; then
|
|
10706
|
+
echo -e "${YELLOW}Not air-gap ready in this configuration.${NC}"
|
|
10707
|
+
echo " One required egress remains: model inference."
|
|
10708
|
+
echo " To close it, serve the model in-network:"
|
|
10709
|
+
echo " loki provider set opencode"
|
|
10710
|
+
echo " export LOKI_OPENCODE_MODEL=ollama/qwen2.5-coder"
|
|
10711
|
+
echo ""
|
|
10712
|
+
echo " Then re-run: loki doctor --airgap"
|
|
10713
|
+
return 1
|
|
10714
|
+
fi
|
|
10715
|
+
echo -e "${GREEN}Air-gap ready: no required egress in this configuration.${NC}"
|
|
10716
|
+
echo " Verify independently -- run with the interface down, or watch with tcpdump."
|
|
10717
|
+
return 0
|
|
10718
|
+
}
|
|
10719
|
+
|
|
10596
10720
|
cmd_doctor() {
|
|
10597
10721
|
local json_output=false
|
|
10722
|
+
local airgap_audit=false
|
|
10598
10723
|
|
|
10599
10724
|
while [[ $# -gt 0 ]]; do
|
|
10600
10725
|
case "$1" in
|
|
@@ -10602,13 +10727,21 @@ cmd_doctor() {
|
|
|
10602
10727
|
json_output=true
|
|
10603
10728
|
shift
|
|
10604
10729
|
;;
|
|
10730
|
+
--airgap)
|
|
10731
|
+
airgap_audit=true
|
|
10732
|
+
shift
|
|
10733
|
+
;;
|
|
10605
10734
|
--help|-h)
|
|
10606
10735
|
echo -e "${BOLD}loki doctor${NC} - Check system prerequisites"
|
|
10607
10736
|
echo ""
|
|
10608
|
-
echo "Usage: loki doctor [--json]"
|
|
10737
|
+
echo "Usage: loki doctor [--json] [--airgap]"
|
|
10609
10738
|
echo ""
|
|
10610
10739
|
echo "Options:"
|
|
10611
10740
|
echo " --json Output machine-readable JSON"
|
|
10741
|
+
echo " --airgap Audit network egress: list every host this engine"
|
|
10742
|
+
echo " can contact, whether it is REQUIRED or optional,"
|
|
10743
|
+
echo " and how to disable it. Exits non-zero if any"
|
|
10744
|
+
echo " required egress remains."
|
|
10612
10745
|
echo ""
|
|
10613
10746
|
echo "Checks: node, python3, jq, git, curl, bash version,"
|
|
10614
10747
|
echo " claude/codex CLIs, disk space, and cockpit render capability."
|
|
@@ -10616,12 +10749,17 @@ cmd_doctor() {
|
|
|
10616
10749
|
;;
|
|
10617
10750
|
*)
|
|
10618
10751
|
echo -e "${RED}Unknown option: $1${NC}"
|
|
10619
|
-
echo "Usage: loki doctor [--json]"
|
|
10752
|
+
echo "Usage: loki doctor [--json] [--airgap]"
|
|
10620
10753
|
return 1
|
|
10621
10754
|
;;
|
|
10622
10755
|
esac
|
|
10623
10756
|
done
|
|
10624
10757
|
|
|
10758
|
+
if [ "$airgap_audit" = true ]; then
|
|
10759
|
+
_loki_airgap_audit "$json_output"
|
|
10760
|
+
return $?
|
|
10761
|
+
fi
|
|
10762
|
+
|
|
10625
10763
|
if [ "$json_output" = true ]; then
|
|
10626
10764
|
cmd_doctor_json
|
|
10627
10765
|
return $?
|
|
@@ -11007,6 +11145,24 @@ except Exception:
|
|
|
11007
11145
|
echo -e " ${YELLOW}WARN${NC} sentrux - not installed (optional, brew install sentrux/tap/sentrux)"
|
|
11008
11146
|
warn_count=$((warn_count + 1))
|
|
11009
11147
|
fi
|
|
11148
|
+
# Evidence Receipt signing state (optional). Unsigned receipts are the
|
|
11149
|
+
# default and are still checkable (hash + diff re-derivation); what they
|
|
11150
|
+
# cannot prove is PROVENANCE, so proof-verify.py reports
|
|
11151
|
+
# generator_trusted: true. Surface the state here because it is otherwise
|
|
11152
|
+
# invisible until someone reads a receipt. WARN (never FAIL): unsigned is
|
|
11153
|
+
# a supported, documented mode, not a broken install.
|
|
11154
|
+
if [ -n "${LOKI_PROOF_GPG_KEY:-}" ]; then
|
|
11155
|
+
if command -v gpg &>/dev/null; then
|
|
11156
|
+
echo -e " ${GREEN}PASS${NC} Receipt signing: LOKI_PROOF_GPG_KEY set and gpg available"
|
|
11157
|
+
pass_count=$((pass_count + 1))
|
|
11158
|
+
else
|
|
11159
|
+
echo -e " ${YELLOW}WARN${NC} Receipt signing: LOKI_PROOF_GPG_KEY set but gpg NOT on PATH - receipts will be UNSIGNED"
|
|
11160
|
+
warn_count=$((warn_count + 1))
|
|
11161
|
+
fi
|
|
11162
|
+
else
|
|
11163
|
+
echo -e " ${YELLOW}WARN${NC} Receipt signing: UNSIGNED (set LOKI_PROOF_GPG_KEY to a gpg key id; see docs/SIGNED-RECEIPTS.md)"
|
|
11164
|
+
warn_count=$((warn_count + 1))
|
|
11165
|
+
fi
|
|
11010
11166
|
echo ""
|
|
11011
11167
|
|
|
11012
11168
|
echo -e "${CYAN}System:${NC}"
|
|
@@ -11242,6 +11398,23 @@ sentrux = {
|
|
|
11242
11398
|
'required': 'optional'
|
|
11243
11399
|
}
|
|
11244
11400
|
|
|
11401
|
+
# Evidence Receipt signing state. Unsigned is the DEFAULT and is a supported
|
|
11402
|
+
# mode, so this is never 'fail'. Exposed because signing state is otherwise
|
|
11403
|
+
# invisible until someone reads a receipt, and it is exactly what decides
|
|
11404
|
+
# whether proof-verify.py reports generator_trusted: true. Sibling of
|
|
11405
|
+
# checks/disk/sentrux -- deliberately NOT counted in the summary tally so
|
|
11406
|
+
# existing consumers see unchanged numbers.
|
|
11407
|
+
signing_key_set = bool(os.environ.get('LOKI_PROOF_GPG_KEY', '').strip())
|
|
11408
|
+
signing_gpg_available = shutil.which('gpg') is not None
|
|
11409
|
+
signing_enabled = signing_key_set and signing_gpg_available
|
|
11410
|
+
receipt_signing = {
|
|
11411
|
+
'enabled': signing_enabled,
|
|
11412
|
+
'key_configured': signing_key_set,
|
|
11413
|
+
'gpg_available': signing_gpg_available,
|
|
11414
|
+
'status': 'pass' if signing_enabled else 'warn',
|
|
11415
|
+
'required': 'optional'
|
|
11416
|
+
}
|
|
11417
|
+
|
|
11245
11418
|
# v7.7.17: memory subsystem health surface. Reports the latest entries
|
|
11246
11419
|
# from .loki/memory/.errors.log (rotated by memory/error_log.py) so
|
|
11247
11420
|
# developers see regressions in the previously-silent-fail call sites
|
|
@@ -11295,6 +11468,7 @@ result = {
|
|
|
11295
11468
|
'status': disk_status
|
|
11296
11469
|
},
|
|
11297
11470
|
'sentrux': sentrux,
|
|
11471
|
+
'receipt_signing': receipt_signing,
|
|
11298
11472
|
'memory': memory,
|
|
11299
11473
|
'summary': {
|
|
11300
11474
|
'passed': pass_count,
|
|
@@ -14885,6 +15059,33 @@ with open(manifest_path, 'w') as f:
|
|
|
14885
15059
|
# usable.
|
|
14886
15060
|
# ---------------------------------------------------------------------------
|
|
14887
15061
|
cmd_verify() {
|
|
15062
|
+
# --fast: deterministic checks only, in milliseconds. Measured on this repo
|
|
15063
|
+
# (1,932 tracked files): 298 ms cold, 87 ms warm, 19 ms diff-scoped, against
|
|
15064
|
+
# an 11,040 ms shell-based baseline.
|
|
15065
|
+
#
|
|
15066
|
+
# It runs ONLY exogenous checks -- no model call, no network -- so every
|
|
15067
|
+
# verdict is reproducible by anyone with the same commit. That is the point,
|
|
15068
|
+
# not a limitation: a verdict you can re-derive is a fact, and a fact is what
|
|
15069
|
+
# an IDE, a CI step, or another vendor's agent can safely act on without
|
|
15070
|
+
# trusting our model choices.
|
|
15071
|
+
#
|
|
15072
|
+
# The full `loki verify` remains the deeper, slower path.
|
|
15073
|
+
for _fv_arg in "$@"; do
|
|
15074
|
+
if [ "$_fv_arg" = "--fast" ]; then
|
|
15075
|
+
local _fv_py="$_LOKI_SCRIPT_DIR/lib/fast_verify.py"
|
|
15076
|
+
if [ ! -f "$_fv_py" ]; then
|
|
15077
|
+
echo -e "${RED}Error: fast_verify not found at $_fv_py${NC}" >&2
|
|
15078
|
+
return 3
|
|
15079
|
+
fi
|
|
15080
|
+
local _fv_argv=()
|
|
15081
|
+
for _a in "$@"; do
|
|
15082
|
+
[ "$_a" = "--fast" ] || _fv_argv+=("$_a")
|
|
15083
|
+
done
|
|
15084
|
+
python3 "$_fv_py" "${_fv_argv[@]+"${_fv_argv[@]}"}"
|
|
15085
|
+
return $?
|
|
15086
|
+
fi
|
|
15087
|
+
done
|
|
15088
|
+
|
|
14888
15089
|
local verify_mod="$_LOKI_SCRIPT_DIR/verify.sh"
|
|
14889
15090
|
if [ ! -f "$verify_mod" ]; then
|
|
14890
15091
|
echo -e "${RED}Error: verify module not found at $verify_mod${NC}" >&2
|
|
@@ -32314,6 +32515,12 @@ cmd_proof() {
|
|
|
32314
32515
|
echo " --hosted Publish to LOKI_HOSTED_ENDPOINT (open-core seam; no official backend yet)"
|
|
32315
32516
|
echo ""
|
|
32316
32517
|
echo "Proofs are generated automatically at run completion (LOKI_PROOF=0 to opt out)."
|
|
32518
|
+
echo ""
|
|
32519
|
+
echo "Signing (off by default): receipts are UNSIGNED unless LOKI_PROOF_GPG_KEY is"
|
|
32520
|
+
echo "set to a gpg key id. Unsigned, the integrity hash proves the bytes were not"
|
|
32521
|
+
echo "edited after hashing but NOT who produced them, so verify reports"
|
|
32522
|
+
echo "generator_trusted: true. Export LOKI_PROOF_GPG_KEY to close that gap."
|
|
32523
|
+
echo "See docs/SIGNED-RECEIPTS.md."
|
|
32317
32524
|
[ "$sub" = "" ] && exit 1
|
|
32318
32525
|
exit 0
|
|
32319
32526
|
;;
|
package/autonomy/verify.sh
CHANGED
|
@@ -781,13 +781,19 @@ verify_gate_nomock() {
|
|
|
781
781
|
return 0
|
|
782
782
|
fi
|
|
783
783
|
local status_line
|
|
784
|
+
# The changed-file list goes over STDIN, not an env var. A single env string
|
|
785
|
+
# is capped at MAX_ARG_STRLEN (131071 bytes) on Linux; a large changed set
|
|
786
|
+
# (e.g. a generated source tree) makes execve fail with E2BIG, the fallback
|
|
787
|
+
# below fires, and the gate silently degrades to inconclusive. macOS has no
|
|
788
|
+
# per-string cap, so that failure was Linux-only.
|
|
784
789
|
status_line=$(
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
790
|
+
printf '%s\n' "$changed" | {
|
|
791
|
+
_NM_TREE="$tree" \
|
|
792
|
+
_NM_OUT="$scan_file" \
|
|
793
|
+
_NM_BASE="${VERIFY_MERGE_BASE:-}" \
|
|
794
|
+
python3 -I "$scanner" 2>/dev/null \
|
|
795
|
+
|| echo "INCONCLUSIVE:detector_error"
|
|
796
|
+
}
|
|
791
797
|
)
|
|
792
798
|
|
|
793
799
|
case "$status_line" in
|
package/dashboard/__init__.py
CHANGED
package/events/emit.sh
CHANGED
|
@@ -71,6 +71,26 @@ safe_append_event_jsonl() {
|
|
|
71
71
|
local max_attempts=100 # ~1s at 10ms sleep
|
|
72
72
|
local stale_after=30
|
|
73
73
|
while ! mkdir "$lock_dir" 2>/dev/null; do
|
|
74
|
+
# COUNT EVERY ITERATION, before any `continue` can skip the increment.
|
|
75
|
+
#
|
|
76
|
+
# This was the v8.1.0 fix's blind spot and it kept the P0 alive. Both
|
|
77
|
+
# `continue` paths below (lock vanished mid-stat; stale lock reclaimed)
|
|
78
|
+
# jumped PAST the increment that used to live at the bottom of the loop,
|
|
79
|
+
# so `max_attempts` was unreachable on those paths and the loop spun
|
|
80
|
+
# forever. Under concurrent emits the stale-reclaim path fires over and
|
|
81
|
+
# over, which is precisely the pathological case.
|
|
82
|
+
#
|
|
83
|
+
# MEASURED 2026-07-30, AFTER the v8.1.0 fix shipped: 63 orphaned
|
|
84
|
+
# emit.sh processes, oldest alive 10h51m, each burning ~5% CPU, machine
|
|
85
|
+
# at load 63 on 14 cores. Every orphan started after the fix landed.
|
|
86
|
+
# An unbounded wait whose only exit is a counter must increment that
|
|
87
|
+
# counter on EVERY path, or the bound is decorative.
|
|
88
|
+
attempts=$((attempts + 1))
|
|
89
|
+
if [ "$attempts" -ge "$max_attempts" ]; then
|
|
90
|
+
# Give up fast -- best-effort write so observability never blocks.
|
|
91
|
+
printf '%s\n' "$line" >> "$events_path" 2>/dev/null || true
|
|
92
|
+
return 0
|
|
93
|
+
fi
|
|
74
94
|
# Check staleness EVERY iteration, not only after exhausting attempts.
|
|
75
95
|
# The old code waited for all 500 attempts before its first staleness
|
|
76
96
|
# check, so a stale lock cost ~5s AND ~500 forked sleep helpers per
|
|
@@ -90,12 +110,8 @@ safe_append_event_jsonl() {
|
|
|
90
110
|
rmdir "$lock_dir" 2>/dev/null || rm -rf "$lock_dir" 2>/dev/null || true
|
|
91
111
|
continue
|
|
92
112
|
fi
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# Give up fast -- best-effort write so observability never blocks.
|
|
96
|
-
printf '%s\n' "$line" >> "$events_path" 2>/dev/null || true
|
|
97
|
-
return 0
|
|
98
|
-
fi
|
|
113
|
+
# (attempt counting and the give-up branch moved to the TOP of the loop
|
|
114
|
+
# so no `continue` can bypass them)
|
|
99
115
|
# Sleep ~10ms WITHOUT forking when the shell supports fractional sleep
|
|
100
116
|
# (bash's `read -t` needs no external process). perl/sleep are fallbacks.
|
|
101
117
|
read -r -t 0.01 _unused_ < /dev/null 2>/dev/null \
|
|
@@ -117,6 +133,50 @@ fi
|
|
|
117
133
|
|
|
118
134
|
set -euo pipefail
|
|
119
135
|
|
|
136
|
+
#-----------------------------------------------------------------------------
|
|
137
|
+
# SELF-REAPER: telemetry must never outlive the thing it observes.
|
|
138
|
+
#-----------------------------------------------------------------------------
|
|
139
|
+
# WHY A WATCHDOG AND NOT ANOTHER LOCK FIX. v8.1.0 fixed a specific unbounded
|
|
140
|
+
# wait (bare `flock -x`, and a staleness check that ran only after 500 attempts).
|
|
141
|
+
# It was a real fix and it was not sufficient: MEASURED 2026-07-30, AFTER that
|
|
142
|
+
# release, 63 orphaned emit.sh processes were alive on this machine, the oldest
|
|
143
|
+
# 10h51m, each burning ~5% CPU, contributing to load 63 on 14 cores. Every one
|
|
144
|
+
# of them started AFTER the fix landed, so whatever wedges emit.sh is not (only)
|
|
145
|
+
# the path that was fixed.
|
|
146
|
+
#
|
|
147
|
+
# The lesson is that patching each discovered hang is an arms race we keep
|
|
148
|
+
# losing, because a hang anywhere in this script has the same user-visible cost.
|
|
149
|
+
# emit.sh is FIRE-AND-FORGET telemetry: nothing waits on its result, and a
|
|
150
|
+
# dropped event is strictly cheaper than a wedged process. So instead of proving
|
|
151
|
+
# no path can block, we cap the lifetime of EVERY path.
|
|
152
|
+
#
|
|
153
|
+
# A background timer SIGKILLs this process after LOKI_EMIT_MAX_SECONDS (default
|
|
154
|
+
# 10). It is deliberately blunt: no cleanup hook, no graceful drain, because the
|
|
155
|
+
# failure mode being defended against is precisely "graceful paths did not run".
|
|
156
|
+
# The killer is disowned so it never becomes a job the parent shell waits on,
|
|
157
|
+
# and it exits immediately when the main process finishes normally.
|
|
158
|
+
#
|
|
159
|
+
# Set LOKI_EMIT_MAX_SECONDS=0 to disable (useful only when debugging emit.sh
|
|
160
|
+
# itself -- an operator who disables it is choosing the orphan risk knowingly).
|
|
161
|
+
_emit_max="${LOKI_EMIT_MAX_SECONDS:-10}"
|
|
162
|
+
case "$_emit_max" in ''|*[!0-9]*) _emit_max=10 ;; esac
|
|
163
|
+
if [ "$_emit_max" -gt 0 ]; then
|
|
164
|
+
_emit_target=$$
|
|
165
|
+
(
|
|
166
|
+
# Poll rather than one long sleep so the watchdog exits promptly on the
|
|
167
|
+
# normal path instead of lingering for the full window.
|
|
168
|
+
_waited=0
|
|
169
|
+
while [ "$_waited" -lt "$_emit_max" ]; do
|
|
170
|
+
sleep 1
|
|
171
|
+
kill -0 "$_emit_target" 2>/dev/null || exit 0
|
|
172
|
+
_waited=$((_waited + 1))
|
|
173
|
+
done
|
|
174
|
+
kill -9 "$_emit_target" 2>/dev/null || true
|
|
175
|
+
) >/dev/null 2>&1 &
|
|
176
|
+
# Disown so the watchdog is not a tracked job of the caller's shell.
|
|
177
|
+
disown 2>/dev/null || true
|
|
178
|
+
fi
|
|
179
|
+
|
|
120
180
|
# Configuration
|
|
121
181
|
LOKI_DIR="${LOKI_DIR:-.loki}"
|
|
122
182
|
EVENTS_DIR="$LOKI_DIR/events/pending"
|