loki-mode 7.68.0 → 7.70.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 +2 -2
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +14 -2
- package/autonomy/completion-council.sh +34 -3
- package/autonomy/hooks/migration-hooks.sh +1 -1
- package/autonomy/loki +52 -38
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +32 -21
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +141 -141
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +12 -0
- package/memory/engine.py +25 -5
- package/memory/layers/loader.py +8 -4
- package/memory/token_economics.py +9 -0
- package/memory/vector_index.py +13 -0
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/aider.sh +1 -1
- package/providers/claude.sh +9 -1
- package/providers/codex.sh +7 -0
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.
|
|
6
|
+
# Loki Mode v7.70.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -406,4 +406,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
406
406
|
|
|
407
407
|
---
|
|
408
408
|
|
|
409
|
-
**v7.
|
|
409
|
+
**v7.70.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.70.0
|
package/autonomy/app-runner.sh
CHANGED
|
@@ -1799,8 +1799,14 @@ app_runner_watchdog() {
|
|
|
1799
1799
|
log_info "App Runner: auto-restarting in ${backoff}s..."
|
|
1800
1800
|
sleep "$backoff"
|
|
1801
1801
|
|
|
1802
|
-
# Clear PID and restart
|
|
1802
|
+
# Clear PID and restart. Remove the identity token alongside app.pid (LOW-3):
|
|
1803
|
+
# the token belongs to the now-dead process, and if the upcoming start fails
|
|
1804
|
+
# (e.g. the old port is still held) no new token is written, so a leftover
|
|
1805
|
+
# token would outlive its pid and could mislead a later _app_runner_pid_is_ours
|
|
1806
|
+
# check. Every site that removes app.pid removes app.token (cf. stop:1443,
|
|
1807
|
+
# watchdog crash-limit:1789).
|
|
1803
1808
|
rm -f "$_APP_RUNNER_DIR/app.pid"
|
|
1809
|
+
rm -f "$_APP_RUNNER_DIR/app.token"
|
|
1804
1810
|
_APP_RUNNER_PID=""
|
|
1805
1811
|
app_runner_start || log_warn "App Runner: auto-restart failed"
|
|
1806
1812
|
}
|
|
@@ -1829,8 +1835,14 @@ app_runner_cleanup() {
|
|
|
1829
1835
|
fi
|
|
1830
1836
|
fi
|
|
1831
1837
|
|
|
1832
|
-
# Remove PID file
|
|
1838
|
+
# Remove PID file and its paired identity token (LOW-3). app_runner_stop
|
|
1839
|
+
# above removes both when a pid is present, but it early-returns without
|
|
1840
|
+
# touching either when called with no pid (the post-failed-restart leftover
|
|
1841
|
+
# state: token present, app.pid already gone). Removing the token here too
|
|
1842
|
+
# guarantees no stale token survives session end regardless of how cleanup
|
|
1843
|
+
# was reached.
|
|
1833
1844
|
rm -f "$_APP_RUNNER_DIR/app.pid"
|
|
1845
|
+
rm -f "$_APP_RUNNER_DIR/app.token"
|
|
1834
1846
|
|
|
1835
1847
|
# Update state
|
|
1836
1848
|
_write_app_state "stopped"
|
|
@@ -1971,6 +1971,9 @@ council_member_review() {
|
|
|
1971
1971
|
fi
|
|
1972
1972
|
|
|
1973
1973
|
local verdict=""
|
|
1974
|
+
# bash-F4: exit code of the provider subcall (0 when no subcall ran, i.e.
|
|
1975
|
+
# no-provider degraded mode). 124/137/143 => timeout => conservative REJECT.
|
|
1976
|
+
local _provider_rc=0
|
|
1974
1977
|
local role_instruction=""
|
|
1975
1978
|
case "$role" in
|
|
1976
1979
|
requirements_verifier)
|
|
@@ -2059,34 +2062,62 @@ ISSUES: CRITICAL:description (optional, one per line per issue)"
|
|
|
2059
2062
|
# CAVEMAN_DEFAULT_MODE=off suppression is preserved (see above).
|
|
2060
2063
|
# bash-F3: timeout-guard the provider subcall so a hung CLI can
|
|
2061
2064
|
# not stall the whole council. Default 600s matches the Bun route
|
|
2062
|
-
# (council.ts LOKI_COUNCIL_TIMEOUT_MS=600000).
|
|
2063
|
-
#
|
|
2064
|
-
#
|
|
2065
|
+
# (council.ts LOKI_COUNCIL_TIMEOUT_MS=600000).
|
|
2066
|
+
# bash-F4 (WAVE10 SAFE-DEFAULT): capture the subcall exit code so a
|
|
2067
|
+
# timeout (124) is NOT silently routed into council_heuristic_review.
|
|
2068
|
+
# The heuristic fallback defaults to APPROVE on benign evidence, so
|
|
2069
|
+
# a full provider timeout used to let a 2-of-3 heuristic APPROVE mark
|
|
2070
|
+
# the project COMPLETE (force-review path) -- the opposite of the
|
|
2071
|
+
# required safe default. pipefail (run.sh:172) makes the assignment's
|
|
2072
|
+
# $? equal timeout's 124 when the CLI is killed. _provider_rc is read
|
|
2073
|
+
# after the case to force a conservative REJECT on timeout.
|
|
2065
2074
|
verdict=$(echo "$prompt" | timeout "${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}" env CAVEMAN_DEFAULT_MODE=off claude "${_cm_argv[@]}" -p 2>/dev/null)
|
|
2075
|
+
_provider_rc=$?
|
|
2066
2076
|
fi
|
|
2067
2077
|
;;
|
|
2068
2078
|
codex)
|
|
2069
2079
|
if command -v codex &>/dev/null; then
|
|
2070
2080
|
verdict=$(timeout "${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}" codex exec --sandbox workspace-write "$prompt" 2>/dev/null)
|
|
2081
|
+
_provider_rc=$?
|
|
2071
2082
|
fi
|
|
2072
2083
|
;;
|
|
2073
2084
|
gemini)
|
|
2074
2085
|
if command -v gemini &>/dev/null; then
|
|
2075
2086
|
verdict=$(echo "$prompt" | timeout "${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}" gemini 2>/dev/null)
|
|
2087
|
+
_provider_rc=$?
|
|
2076
2088
|
fi
|
|
2077
2089
|
;;
|
|
2078
2090
|
cline)
|
|
2079
2091
|
if command -v cline &>/dev/null; then
|
|
2080
2092
|
verdict=$(timeout "${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}" cline -y "$prompt" 2>/dev/null)
|
|
2093
|
+
_provider_rc=$?
|
|
2081
2094
|
fi
|
|
2082
2095
|
;;
|
|
2083
2096
|
aider)
|
|
2084
2097
|
if command -v aider &>/dev/null; then
|
|
2085
2098
|
verdict=$(timeout "${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}" aider --message "$prompt" --yes-always --no-auto-commits --no-git 2>/dev/null)
|
|
2099
|
+
_provider_rc=$?
|
|
2086
2100
|
fi
|
|
2087
2101
|
;;
|
|
2088
2102
|
esac
|
|
2089
2103
|
|
|
2104
|
+
# bash-F4 (WAVE10 SAFE-DEFAULT): a provider timeout (124, incl. 128+SIGTERM
|
|
2105
|
+
# variants 137/143 if a wrapper kills it) must NEVER fall through to the
|
|
2106
|
+
# APPROVE-leaning heuristic review. A reviewer whose CLI hung produced NO
|
|
2107
|
+
# judgement, so the only safe verdict is REJECT (conservative re-iterate).
|
|
2108
|
+
# Note: when no provider CLI is installed, the command -v guard above means
|
|
2109
|
+
# the subcall never runs and _provider_rc stays 0, so legitimate no-provider
|
|
2110
|
+
# degraded mode still reaches the heuristic fallback unchanged.
|
|
2111
|
+
if [ "$_provider_rc" -eq 124 ] || [ "$_provider_rc" -eq 137 ] || [ "$_provider_rc" -eq 143 ]; then
|
|
2112
|
+
# run.sh's log_warn writes to STDOUT (see bash-F2 note); council_member_review's
|
|
2113
|
+
# stdout is captured as the verdict, so redirect to stderr to keep the
|
|
2114
|
+
# captured verdict clean (the log line carries no VOTE: token, so the
|
|
2115
|
+
# parse stays REJECT regardless, but this avoids polluting the capture).
|
|
2116
|
+
log_warn "Council member $member_id ($role): provider review timed out (rc=$_provider_rc); defaulting to REJECT" >&2
|
|
2117
|
+
verdict="VOTE:REJECT
|
|
2118
|
+
REASON: Provider review timed out (rc=$_provider_rc); no judgement produced, defaulting to conservative REJECT"
|
|
2119
|
+
fi
|
|
2120
|
+
|
|
2090
2121
|
# Fallback: if no AI provider available, use heuristic-based review
|
|
2091
2122
|
if [ -z "$verdict" ]; then
|
|
2092
2123
|
verdict=$(council_heuristic_review "$role" "$evidence_file")
|
|
@@ -393,7 +393,7 @@ for friction in data.get('frictions', []):
|
|
|
393
393
|
print(f'BLOCKED (strict): Friction {friction.get(\"id\", \"?\")} in {loc} - strict mode requires explicit approval')
|
|
394
394
|
sys.exit(0)
|
|
395
395
|
print('OK')
|
|
396
|
-
" "$file_path" "$strict" "$heal_dir/friction-map.json" 2>/dev/null || echo "
|
|
396
|
+
" "$file_path" "$strict" "$heal_dir/friction-map.json" 2>/dev/null || echo "BLOCKED: friction-map check failed (corrupt/unreadable friction-map.json or python3 unavailable) -- failing closed")
|
|
397
397
|
|
|
398
398
|
if [[ "$blocked" == BLOCKED* ]]; then
|
|
399
399
|
echo "HOOK_BLOCKED: $blocked"
|
package/autonomy/loki
CHANGED
|
@@ -7358,8 +7358,8 @@ cmd_config() {
|
|
|
7358
7358
|
echo " issue.provider Default issue provider: github, gitlab, jira, azure_devops"
|
|
7359
7359
|
echo " blind_validation Blind validation mode: true, false (default: true)"
|
|
7360
7360
|
echo " adversarial_testing Adversarial testing: true, false (default: true)"
|
|
7361
|
-
echo " spawn_timeout
|
|
7362
|
-
echo " spawn_retries
|
|
7361
|
+
echo " spawn_timeout [DEPRECATED] No effect since WAVE9 (no consumer); accepted for back-compat"
|
|
7362
|
+
echo " spawn_retries [DEPRECATED] No effect since WAVE9 (no consumer); accepted for back-compat"
|
|
7363
7363
|
echo " notify.slack Slack webhook URL"
|
|
7364
7364
|
echo " notify.discord Discord webhook URL"
|
|
7365
7365
|
echo " budget Cost budget limit in USD"
|
|
@@ -7437,6 +7437,7 @@ cmd_config_set() {
|
|
|
7437
7437
|
if ! echo "$value" | grep -qE '^[0-9]+$'; then
|
|
7438
7438
|
echo -e "${RED}Invalid $key: $value (expected: integer)${NC}"; return 1
|
|
7439
7439
|
fi
|
|
7440
|
+
echo -e "${YELLOW}Note: '$key' is deprecated and has no effect since WAVE9 (no consumer in the runtime). Accepted for back-compat only.${NC}" >&2
|
|
7440
7441
|
;;
|
|
7441
7442
|
budget)
|
|
7442
7443
|
if ! echo "$value" | grep -qE '^[0-9]+(\.[0-9]+)?$'; then
|
|
@@ -7518,8 +7519,8 @@ SET_CONFIG
|
|
|
7518
7519
|
provider) export LOKI_PROVIDER="$value" ;;
|
|
7519
7520
|
blind_validation) export LOKI_BLIND_VALIDATION="$value" ;;
|
|
7520
7521
|
adversarial_testing) export LOKI_ADVERSARIAL_TESTING="$value" ;;
|
|
7521
|
-
spawn_timeout
|
|
7522
|
-
|
|
7522
|
+
# spawn_timeout / spawn_retries: deprecated, no runtime consumer (WAVE9);
|
|
7523
|
+
# intentionally not exported.
|
|
7523
7524
|
budget) export LOKI_BUDGET_LIMIT="$value" ;;
|
|
7524
7525
|
esac
|
|
7525
7526
|
}
|
|
@@ -7626,8 +7627,8 @@ cmd_config_show() {
|
|
|
7626
7627
|
echo " maxTier: ${LOKI_MAX_TIER:-(unlimited)}"
|
|
7627
7628
|
echo " blind_validation: ${LOKI_BLIND_VALIDATION:-true}"
|
|
7628
7629
|
echo " adversarial_testing: ${LOKI_ADVERSARIAL_TESTING:-true}"
|
|
7629
|
-
echo " spawn_timeout:
|
|
7630
|
-
echo " spawn_retries:
|
|
7630
|
+
echo " spawn_timeout: (deprecated, no effect)"
|
|
7631
|
+
echo " spawn_retries: (deprecated, no effect)"
|
|
7631
7632
|
echo " issue_provider: ${LOKI_ISSUE_PROVIDER:-(auto-detect)}"
|
|
7632
7633
|
echo " budget: ${LOKI_BUDGET_LIMIT:-(no limit)}"
|
|
7633
7634
|
echo ""
|
|
@@ -13012,47 +13013,60 @@ else:
|
|
|
13012
13013
|
return 1
|
|
13013
13014
|
fi
|
|
13014
13015
|
|
|
13015
|
-
# Fire lifecycle hooks and record state
|
|
13016
|
-
|
|
13017
|
-
|
|
13018
|
-
|
|
13016
|
+
# Fire lifecycle hooks and record state.
|
|
13017
|
+
# WAVE10: pass cluster_id / template_name / template_file / SKILL_DIR
|
|
13018
|
+
# via os.environ instead of interpolating into the python source.
|
|
13019
|
+
# A --cluster-id containing a single quote (e.g. "o'brien") made the
|
|
13020
|
+
# heredoc body a SyntaxError, skipping all state recording while the
|
|
13021
|
+
# green "Cluster: ..." line still printed (silent false success).
|
|
13022
|
+
_LOKI_SKILL_DIR="${SKILL_DIR:-.}" \
|
|
13023
|
+
_LOKI_TEMPLATE_FILE="${template_file}" \
|
|
13024
|
+
_LOKI_CLUSTER_ID="${cluster_id}" \
|
|
13025
|
+
_LOKI_TEMPLATE_NAME="${template_name}" \
|
|
13026
|
+
_LOKI_DO_RESUME="${do_resume}" \
|
|
13027
|
+
PYTHONPATH="${SKILL_DIR:-.}" python3 -c '
|
|
13028
|
+
import json, sys, os
|
|
13029
|
+
skill_dir = os.environ["_LOKI_SKILL_DIR"]
|
|
13030
|
+
sys.path.insert(0, skill_dir)
|
|
13019
13031
|
from swarm.patterns import ClusterLifecycleHooks
|
|
13020
13032
|
from state.sqlite_backend import SqliteStateBackend
|
|
13021
13033
|
|
|
13022
13034
|
# Load template hooks config
|
|
13023
|
-
with open(
|
|
13035
|
+
with open(os.environ["_LOKI_TEMPLATE_FILE"]) as f:
|
|
13024
13036
|
tpl = json.load(f)
|
|
13025
13037
|
|
|
13026
|
-
hooks = ClusterLifecycleHooks(tpl.get(
|
|
13038
|
+
hooks = ClusterLifecycleHooks(tpl.get("hooks", {}))
|
|
13027
13039
|
db = SqliteStateBackend()
|
|
13028
13040
|
|
|
13029
|
-
cluster_id =
|
|
13030
|
-
|
|
13041
|
+
cluster_id = os.environ["_LOKI_CLUSTER_ID"]
|
|
13042
|
+
template_name = os.environ["_LOKI_TEMPLATE_NAME"]
|
|
13043
|
+
resume = os.environ["_LOKI_DO_RESUME"] == "true"
|
|
13031
13044
|
|
|
13032
13045
|
if resume:
|
|
13033
|
-
events = db.query_events(event_type=
|
|
13046
|
+
events = db.query_events(event_type="cluster_state", migration_id=cluster_id, limit=1)
|
|
13034
13047
|
if events:
|
|
13035
|
-
print(f
|
|
13048
|
+
print(f"Resuming cluster {cluster_id} from last checkpoint")
|
|
13036
13049
|
else:
|
|
13037
|
-
print(f
|
|
13050
|
+
print(f"No previous state found for {cluster_id}. Starting fresh.")
|
|
13038
13051
|
|
|
13039
13052
|
# Fire pre_run hooks
|
|
13040
|
-
results = hooks.fire(
|
|
13053
|
+
results = hooks.fire("pre_run", {"cluster_id": cluster_id, "template": template_name})
|
|
13041
13054
|
for r in results:
|
|
13042
|
-
if not r[
|
|
13043
|
-
print(
|
|
13055
|
+
if not r["success"]:
|
|
13056
|
+
print("Pre-run hook failed: " + str(r.get("output", "")))
|
|
13044
13057
|
|
|
13045
13058
|
# Record cluster start
|
|
13046
|
-
db.record_event(
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
|
|
13059
|
+
db.record_event("cluster_start", {
|
|
13060
|
+
"cluster_id": cluster_id,
|
|
13061
|
+
"template": template_name,
|
|
13062
|
+
"agents": len(tpl.get("agents", [])),
|
|
13063
|
+
"resume": resume,
|
|
13051
13064
|
}, migration_id=cluster_id)
|
|
13052
13065
|
|
|
13053
|
-
|
|
13054
|
-
print(
|
|
13055
|
-
"
|
|
13066
|
+
_n_agents = len(tpl.get("agents", []))
|
|
13067
|
+
print(f"Cluster {cluster_id} initialized with {_n_agents} agents")
|
|
13068
|
+
print("Template validated. Lifecycle hooks active.")
|
|
13069
|
+
' 2>&1
|
|
13056
13070
|
|
|
13057
13071
|
echo -e "${GREEN}Cluster: $cluster_id${NC}"
|
|
13058
13072
|
echo -e "Template: $template_name"
|
|
@@ -23325,10 +23339,10 @@ except: pass
|
|
|
23325
23339
|
output=$(cat <<ENDJSON
|
|
23326
23340
|
{
|
|
23327
23341
|
"project": {
|
|
23328
|
-
"name": "$project_name",
|
|
23342
|
+
"name": $(_VAL="$project_name" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$project_name\""),
|
|
23329
23343
|
"description": $(_DESC="$project_description" python3 -c "import json, os; print(json.dumps(os.environ.get('_DESC','')))" 2>/dev/null || echo "\"$project_description\""),
|
|
23330
|
-
"version": "$project_version",
|
|
23331
|
-
"path": "$target_path"
|
|
23344
|
+
"version": $(_VAL="$project_version" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$project_version\""),
|
|
23345
|
+
"path": $(_VAL="$target_path" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$target_path\"")
|
|
23332
23346
|
},
|
|
23333
23347
|
"languages": "$(echo $languages | sed 's/ */ /g')",
|
|
23334
23348
|
"frameworks": "$(echo $frameworks | sed 's/ */ /g')",
|
|
@@ -23342,9 +23356,9 @@ except: pass
|
|
|
23342
23356
|
"docs": $doc_count
|
|
23343
23357
|
},
|
|
23344
23358
|
"commands": {
|
|
23345
|
-
"build": "$build_cmd",
|
|
23346
|
-
"run": "$run_cmd",
|
|
23347
|
-
"test": "$test_cmd"
|
|
23359
|
+
"build": $(_VAL="$build_cmd" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$build_cmd\""),
|
|
23360
|
+
"run": $(_VAL="$run_cmd" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$run_cmd\""),
|
|
23361
|
+
"test": $(_VAL="$test_cmd" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$test_cmd\"")
|
|
23348
23362
|
},
|
|
23349
23363
|
"depth": $depth
|
|
23350
23364
|
}
|
|
@@ -23354,10 +23368,10 @@ ENDJSON
|
|
|
23354
23368
|
# YAML output
|
|
23355
23369
|
output=$(cat <<ENDYAML
|
|
23356
23370
|
project:
|
|
23357
|
-
name: $project_name
|
|
23358
|
-
description: "$project_description"
|
|
23359
|
-
version: "$project_version"
|
|
23360
|
-
path: $target_path
|
|
23371
|
+
name: $(_VAL="$project_name" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$project_name\"")
|
|
23372
|
+
description: $(_VAL="$project_description" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$project_description\"")
|
|
23373
|
+
version: $(_VAL="$project_version" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$project_version\"")
|
|
23374
|
+
path: $(_VAL="$target_path" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$target_path\"")
|
|
23361
23375
|
languages: $languages
|
|
23362
23376
|
frameworks: $frameworks
|
|
23363
23377
|
build_system: $build_system
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -122,32 +122,43 @@ class _RateLimiter:
|
|
|
122
122
|
self._window = window_seconds
|
|
123
123
|
self._max_keys = max_keys
|
|
124
124
|
self._calls: dict[str, list[float]] = defaultdict(list)
|
|
125
|
+
# Sync route handlers (plain `def`) run in Starlette's threadpool, so
|
|
126
|
+
# check() can be entered by several threads at once against this one
|
|
127
|
+
# shared instance. Without a guard, one thread iterating self._calls
|
|
128
|
+
# (the empty-key prune or the LRU-eviction sort) while another inserts
|
|
129
|
+
# or deletes a key raises "dictionary changed size during iteration",
|
|
130
|
+
# which surfaces to the caller as a 500 on a trivial rate-limit guard.
|
|
131
|
+
# The lock is held only around the in-memory bookkeeping (no I/O, no
|
|
132
|
+
# await), so contention is negligible and it cannot deadlock async
|
|
133
|
+
# callers that reach this via run_in_threadpool.
|
|
134
|
+
self._lock = threading.Lock()
|
|
125
135
|
|
|
126
136
|
def check(self, key: str) -> bool:
|
|
127
137
|
now = time.time()
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
# Evict least-recently-accessed keys if max_keys exceeded
|
|
137
|
-
if len(self._calls) > self._max_keys:
|
|
138
|
-
# Sort by last-access time (most recent timestamp), evict least recent
|
|
139
|
-
sorted_keys = sorted(
|
|
140
|
-
self._calls.items(),
|
|
141
|
-
key=lambda x: max(x[1]) if x[1] else 0
|
|
142
|
-
)
|
|
143
|
-
keys_to_remove = len(self._calls) - self._max_keys
|
|
144
|
-
for k, _ in sorted_keys[:keys_to_remove]:
|
|
138
|
+
with self._lock:
|
|
139
|
+
# Prune old timestamps for this key
|
|
140
|
+
self._calls[key] = [t for t in self._calls[key] if now - t < self._window]
|
|
141
|
+
|
|
142
|
+
# Remove keys with empty timestamp lists
|
|
143
|
+
empty_keys = [k for k, v in self._calls.items() if not v]
|
|
144
|
+
for k in empty_keys:
|
|
145
145
|
del self._calls[k]
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
# Evict least-recently-accessed keys if max_keys exceeded
|
|
148
|
+
if len(self._calls) > self._max_keys:
|
|
149
|
+
# Sort by last-access time (most recent timestamp), evict least recent
|
|
150
|
+
sorted_keys = sorted(
|
|
151
|
+
self._calls.items(),
|
|
152
|
+
key=lambda x: max(x[1]) if x[1] else 0
|
|
153
|
+
)
|
|
154
|
+
keys_to_remove = len(self._calls) - self._max_keys
|
|
155
|
+
for k, _ in sorted_keys[:keys_to_remove]:
|
|
156
|
+
del self._calls[k]
|
|
157
|
+
|
|
158
|
+
if len(self._calls[key]) >= self._max_calls:
|
|
159
|
+
return False
|
|
160
|
+
self._calls[key].append(now)
|
|
161
|
+
return True
|
|
151
162
|
|
|
152
163
|
|
|
153
164
|
_control_limiter = _RateLimiter(max_calls=10, window_seconds=60)
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v7.
|
|
5
|
+
**Version:** v7.70.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -395,7 +395,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
|
|
|
395
395
|
# Run Loki Mode in Docker (Claude provider, API-key auth)
|
|
396
396
|
docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
|
397
397
|
-v $(pwd):/workspace -w /workspace \
|
|
398
|
-
asklokesh/loki-mode:7.
|
|
398
|
+
asklokesh/loki-mode:7.70.0 start ./my-spec.md
|
|
399
399
|
```
|
|
400
400
|
|
|
401
401
|
##### docker compose + .env (no host install)
|