loki-mode 6.83.0 → 7.0.1

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
@@ -649,7 +649,23 @@ export LOKI_SESSION_MODEL LOKI_LEGACY_TIER_SWITCHING
649
649
  # Both off (default) => zero behavior change from v6.82.0.
650
650
  LOKI_MANAGED_AGENTS="${LOKI_MANAGED_AGENTS:-false}"
651
651
  LOKI_MANAGED_MEMORY="${LOKI_MANAGED_MEMORY:-false}"
652
- export LOKI_MANAGED_AGENTS LOKI_MANAGED_MEMORY
652
+ # v7.0.0 Phase 2: remote->local hydrate on session boot (grandchild of MEMORY).
653
+ # Pulls semantic patterns + procedural skills once at init_loki_dir time.
654
+ LOKI_MANAGED_MEMORY_HYDRATE="${LOKI_MANAGED_MEMORY_HYDRATE:-false}"
655
+ # v7.0.0 Phase 3+4 foundation: umbrella flag for the multiagent-session path.
656
+ # Gates every providers/managed.py entry point (run_council,
657
+ # run_completion_council). Off by default because the Managed Agents
658
+ # multiagent surface is a research preview.
659
+ LOKI_EXPERIMENTAL_MANAGED_AGENTS="${LOKI_EXPERIMENTAL_MANAGED_AGENTS:-false}"
660
+ # v7.0.0 Phase 3 (T5): managed code-review council. Routes run_code_review
661
+ # through providers/managed.py::run_council when true. Requires parent +
662
+ # umbrella.
663
+ LOKI_EXPERIMENTAL_MANAGED_REVIEW="${LOKI_EXPERIMENTAL_MANAGED_REVIEW:-false}"
664
+ # v7.0.0 Phase 4 (T6): managed completion council. Routes council_should_stop
665
+ # through providers/managed.py::run_completion_council when true. Requires
666
+ # parent + umbrella.
667
+ LOKI_EXPERIMENTAL_MANAGED_COUNCIL="${LOKI_EXPERIMENTAL_MANAGED_COUNCIL:-false}"
668
+ export LOKI_MANAGED_AGENTS LOKI_MANAGED_MEMORY LOKI_MANAGED_MEMORY_HYDRATE LOKI_EXPERIMENTAL_MANAGED_AGENTS LOKI_EXPERIMENTAL_MANAGED_REVIEW LOKI_EXPERIMENTAL_MANAGED_COUNCIL
653
669
 
654
670
  # Fail-fast: child on with parent off is a misconfiguration.
655
671
  if [ "$LOKI_MANAGED_MEMORY" = "true" ] && [ "$LOKI_MANAGED_AGENTS" != "true" ]; then
@@ -657,6 +673,47 @@ if [ "$LOKI_MANAGED_MEMORY" = "true" ] && [ "$LOKI_MANAGED_AGENTS" != "true" ];
657
673
  exit 2
658
674
  fi
659
675
 
676
+ # Phase 2 fail-fast: HYDRATE is a grandchild of MEMORY.
677
+ if [ "$LOKI_MANAGED_MEMORY_HYDRATE" = "true" ] && [ "$LOKI_MANAGED_MEMORY" != "true" ]; then
678
+ echo "ERROR: LOKI_MANAGED_MEMORY_HYDRATE=true requires LOKI_MANAGED_MEMORY=true" >&2
679
+ exit 2
680
+ fi
681
+
682
+ # Same fail-fast for the experimental multiagent session path.
683
+ if [ "$LOKI_EXPERIMENTAL_MANAGED_AGENTS" = "true" ] && [ "$LOKI_MANAGED_AGENTS" != "true" ]; then
684
+ echo "ERROR: LOKI_EXPERIMENTAL_MANAGED_AGENTS=true requires LOKI_MANAGED_AGENTS=true" >&2
685
+ exit 2
686
+ fi
687
+
688
+ # Phase 3 fail-fast: REVIEW requires parent AND umbrella.
689
+ if [ "$LOKI_EXPERIMENTAL_MANAGED_REVIEW" = "true" ]; then
690
+ if [ "$LOKI_MANAGED_AGENTS" != "true" ]; then
691
+ echo "ERROR: LOKI_EXPERIMENTAL_MANAGED_REVIEW=true requires LOKI_MANAGED_AGENTS=true" >&2
692
+ exit 2
693
+ fi
694
+ if [ "$LOKI_EXPERIMENTAL_MANAGED_AGENTS" != "true" ]; then
695
+ echo "ERROR: LOKI_EXPERIMENTAL_MANAGED_REVIEW=true requires LOKI_EXPERIMENTAL_MANAGED_AGENTS=true" >&2
696
+ exit 2
697
+ fi
698
+ fi
699
+
700
+ # Phase 4 fail-fast: COUNCIL requires parent AND umbrella.
701
+ if [ "$LOKI_EXPERIMENTAL_MANAGED_COUNCIL" = "true" ]; then
702
+ if [ "$LOKI_MANAGED_AGENTS" != "true" ]; then
703
+ echo "ERROR: LOKI_EXPERIMENTAL_MANAGED_COUNCIL=true requires LOKI_MANAGED_AGENTS=true" >&2
704
+ exit 2
705
+ fi
706
+ if [ "$LOKI_EXPERIMENTAL_MANAGED_AGENTS" != "true" ]; then
707
+ echo "ERROR: LOKI_EXPERIMENTAL_MANAGED_COUNCIL=true requires LOKI_EXPERIMENTAL_MANAGED_AGENTS=true" >&2
708
+ exit 2
709
+ fi
710
+ fi
711
+
712
+ # Research-preview warning banner.
713
+ if [ "$LOKI_EXPERIMENTAL_MANAGED_AGENTS" = "true" ]; then
714
+ echo "WARN: LOKI_EXPERIMENTAL_MANAGED_AGENTS uses Managed Agents research preview; expect beta churn." >&2
715
+ fi
716
+
660
717
  # Parallel Workflows (Git Worktrees)
661
718
  PARALLEL_MODE=${LOKI_PARALLEL_MODE:-false}
662
719
  MAX_WORKTREES=${LOKI_MAX_WORKTREES:-5}
@@ -3021,6 +3078,30 @@ BUDGET_EOF
3021
3078
  log_info "Budget limit set: \$$BUDGET_LIMIT"
3022
3079
  fi
3023
3080
 
3081
+ # v7.0.0 Phase 2: remote->local hydrate. Runs ONCE at session boot (not
3082
+ # per iteration) to pull semantic patterns + procedural skills from the
3083
+ # managed store into .loki/memory/semantic/patterns.json and
3084
+ # .loki/memory/skills/*.json. Gated on parent + MEMORY + HYDRATE; all
3085
+ # three must be "true". 10s hard timeout so a slow remote never blocks
3086
+ # startup. Idempotent (sentinel: .loki/managed/hydrate.lock).
3087
+ if [ "$LOKI_MANAGED_AGENTS" = "true" ] \
3088
+ && [ "$LOKI_MANAGED_MEMORY" = "true" ] \
3089
+ && [ "$LOKI_MANAGED_MEMORY_HYDRATE" = "true" ]; then
3090
+ local _hydrate_target="${TARGET_DIR:-$(pwd)}"
3091
+ local _hydrate_out
3092
+ _hydrate_out=$(
3093
+ cd "$PROJECT_DIR" 2>/dev/null && \
3094
+ LOKI_TARGET_DIR="$_hydrate_target" \
3095
+ timeout 10 python3 -m memory.managed_memory.retrieve --hydrate 2>/dev/null || true
3096
+ )
3097
+ if [ -n "$_hydrate_out" ]; then
3098
+ log_info "Managed hydrate: $_hydrate_out"
3099
+ else
3100
+ LOKI_TARGET_DIR="$_hydrate_target" \
3101
+ python3 -c "from memory.managed_memory.events import emit_managed_event; emit_managed_event('managed_memory_hydrate_timeout', {'phase': 'init'})" 2>/dev/null || true
3102
+ fi
3103
+ fi
3104
+
3024
3105
  log_info "Loki directory initialized: .loki/"
3025
3106
  }
3026
3107
 
@@ -5869,6 +5950,236 @@ run_magic_debate_gate() {
5869
5950
  # architecture-strategist always included, 2 more selected by keyword scoring
5870
5951
  # ============================================================================
5871
5952
 
5953
+ # Write managed-council verdicts into the legacy per-reviewer .txt layout so
5954
+ # the dashboard quality panel (which only reads .loki/quality/reviews/$id/*.txt)
5955
+ # stays functional. Called from the managed branch of run_code_review().
5956
+ # Single-writer invariant: either this helper writes the files, or the legacy
5957
+ # CLI fan-out does -- never both for the same review_id.
5958
+ council_verdicts_to_txt_files() {
5959
+ local review_id="$1"
5960
+ local verdicts_json="$2"
5961
+ local loki_dir="${TARGET_DIR:-.}/.loki"
5962
+ local review_dir="$loki_dir/quality/reviews/$review_id"
5963
+ mkdir -p "$review_dir"
5964
+
5965
+ # Use python3 to fan the JSON verdict list out to individual .txt files
5966
+ # in the same VERDICT/FINDINGS format the legacy parser expects.
5967
+ local out_dir_env="$review_dir"
5968
+ export LOKI_COUNCIL_OUT_DIR="$out_dir_env"
5969
+ export LOKI_COUNCIL_VERDICTS_JSON="$verdicts_json"
5970
+ python3 << 'COUNCIL_WRITE'
5971
+ import json
5972
+ import os
5973
+ import re
5974
+
5975
+ out_dir = os.environ["LOKI_COUNCIL_OUT_DIR"]
5976
+ raw = os.environ.get("LOKI_COUNCIL_VERDICTS_JSON", "").strip()
5977
+ if not raw:
5978
+ raise SystemExit(0)
5979
+
5980
+ try:
5981
+ payload = json.loads(raw)
5982
+ except json.JSONDecodeError:
5983
+ raise SystemExit("council_verdicts_to_txt_files: invalid JSON")
5984
+
5985
+ if isinstance(payload, dict):
5986
+ verdicts = payload.get("verdicts") or []
5987
+ else:
5988
+ verdicts = payload or []
5989
+
5990
+ SAFE_NAME = re.compile(r"[^A-Za-z0-9._-]+")
5991
+ DOT_RUN = re.compile(r"\.{2,}")
5992
+
5993
+ def _pool_name(v):
5994
+ name = v.get("pool_name") or v.get("name") or v.get("agent_id") or "reviewer"
5995
+ cleaned = SAFE_NAME.sub("-", str(name))
5996
+ # Defend against path-traversal via ".." in pool names.
5997
+ cleaned = DOT_RUN.sub("-", cleaned).strip("-.")
5998
+ return cleaned[:80] or "reviewer"
5999
+
6000
+ def _verdict_token(v):
6001
+ token = str(v.get("verdict") or "").strip().upper()
6002
+ if token in ("APPROVE", "PASS"):
6003
+ return "PASS"
6004
+ if token in ("REQUEST_CHANGES", "REJECT", "FAIL"):
6005
+ return "FAIL"
6006
+ return "PASS" # ABSTAIN => PASS per legacy behavior
6007
+
6008
+ def _findings(v):
6009
+ rationale = (v.get("rationale") or "").strip()
6010
+ sev = v.get("severity")
6011
+ if not rationale:
6012
+ return "- None"
6013
+ lines = []
6014
+ for line in rationale.splitlines():
6015
+ line = line.strip()
6016
+ if not line:
6017
+ continue
6018
+ if line.lstrip().startswith("- ["):
6019
+ lines.append(line)
6020
+ else:
6021
+ tag = f"[{sev.capitalize()}]" if sev else "[Medium]"
6022
+ lines.append(f"- {tag} {line}")
6023
+ return "\n".join(lines) if lines else "- None"
6024
+
6025
+ for v in verdicts:
6026
+ if not isinstance(v, dict):
6027
+ continue
6028
+ name = _pool_name(v)
6029
+ path = os.path.join(out_dir, f"{name}.txt")
6030
+ body = f"VERDICT: {_verdict_token(v)}\nFINDINGS:\n{_findings(v)}\n"
6031
+ with open(path, "w", encoding="utf-8") as f:
6032
+ f.write(body)
6033
+ COUNCIL_WRITE
6034
+ local rc=$?
6035
+ unset LOKI_COUNCIL_OUT_DIR LOKI_COUNCIL_VERDICTS_JSON
6036
+ return $rc
6037
+ }
6038
+
6039
+ # Execute the managed-agents multiagent council path. Writes legacy .txt
6040
+ # files via council_verdicts_to_txt_files() on success so the existing
6041
+ # aggregation loop below can read them exactly like the CLI path.
6042
+ # Returns 0 on success, 1 on ManagedUnavailable (caller should fall back).
6043
+ _run_managed_review_council() {
6044
+ local review_id="$1"
6045
+ local diff_file="$2"
6046
+ local files_file="$3"
6047
+ local review_dir="${TARGET_DIR:-.}/.loki/quality/reviews/$review_id"
6048
+ mkdir -p "$review_dir"
6049
+
6050
+ export LOKI_MANAGED_REVIEW_ID="$review_id"
6051
+ export LOKI_MANAGED_REVIEW_DIFF_FILE="$diff_file"
6052
+ export LOKI_MANAGED_REVIEW_FILES_FILE="$files_file"
6053
+ export LOKI_MANAGED_REVIEW_OUT_JSON="$review_dir/managed_result.json"
6054
+ local project_dir_env="${PROJECT_DIR:-.}"
6055
+ export LOKI_MANAGED_REVIEW_PROJECT_DIR="$project_dir_env"
6056
+
6057
+ local result_json
6058
+ result_json=$(python3 << 'MANAGED_REVIEW' 2>&1
6059
+ import json
6060
+ import os
6061
+ import sys
6062
+
6063
+ project_dir = os.environ.get("LOKI_MANAGED_REVIEW_PROJECT_DIR", ".")
6064
+ if project_dir and project_dir not in sys.path:
6065
+ sys.path.insert(0, project_dir)
6066
+
6067
+ try:
6068
+ from providers import managed as managed_mod
6069
+ except Exception as e:
6070
+ print(json.dumps({"status": "unavailable", "reason": f"import_failed: {e}"}))
6071
+ sys.exit(0)
6072
+
6073
+ # Test hook: allow tests to inject a fake run_council by setting
6074
+ # LOKI_MANAGED_REVIEW_FAKE_MODULE to a dotted path exposing run_council.
6075
+ fake_mod = os.environ.get("LOKI_MANAGED_REVIEW_FAKE_MODULE", "").strip()
6076
+ if fake_mod:
6077
+ try:
6078
+ import importlib
6079
+ fm = importlib.import_module(fake_mod)
6080
+ if hasattr(fm, "install"):
6081
+ fm.install(managed_mod)
6082
+ except Exception as e:
6083
+ print(json.dumps({"status": "unavailable", "reason": f"fake_install_failed: {e}"}))
6084
+ sys.exit(0)
6085
+
6086
+ if not managed_mod.is_enabled():
6087
+ print(json.dumps({"status": "unavailable", "reason": "is_enabled_false"}))
6088
+ sys.exit(0)
6089
+
6090
+ diff_path = os.environ.get("LOKI_MANAGED_REVIEW_DIFF_FILE", "")
6091
+ files_path = os.environ.get("LOKI_MANAGED_REVIEW_FILES_FILE", "")
6092
+ diff_text = ""
6093
+ files_text = ""
6094
+ if diff_path and os.path.exists(diff_path):
6095
+ with open(diff_path, "r", encoding="utf-8", errors="replace") as f:
6096
+ diff_text = f.read()
6097
+ if files_path and os.path.exists(files_path):
6098
+ with open(files_path, "r", encoding="utf-8", errors="replace") as f:
6099
+ files_text = f.read()
6100
+
6101
+ target_paths = [p.strip() for p in files_text.splitlines() if p.strip()]
6102
+
6103
+ pool = ["security-sentinel", "test-coverage-auditor", "performance-oracle"]
6104
+ context = {
6105
+ "diff": diff_text,
6106
+ "files": target_paths,
6107
+ "target_paths": target_paths,
6108
+ }
6109
+
6110
+ try:
6111
+ result = managed_mod.run_council(pool, context, timeout_s=300)
6112
+ except managed_mod.ManagedUnavailable as e:
6113
+ print(json.dumps({"status": "unavailable", "reason": str(e)}))
6114
+ sys.exit(0)
6115
+ except Exception as e:
6116
+ # Anything else is unexpected; bubble up as unavailable so the caller
6117
+ # falls back rather than aborting the iteration.
6118
+ print(json.dumps({"status": "unavailable", "reason": f"unexpected: {e}"}))
6119
+ sys.exit(0)
6120
+
6121
+ verdicts_out = []
6122
+ for v in (result.verdicts or []):
6123
+ verdicts_out.append({
6124
+ "agent_id": getattr(v, "agent_id", ""),
6125
+ "pool_name": getattr(v, "pool_name", ""),
6126
+ "verdict": getattr(v, "verdict", ""),
6127
+ "rationale": getattr(v, "rationale", ""),
6128
+ "severity": getattr(v, "severity", None),
6129
+ })
6130
+ out = {
6131
+ "status": "ok",
6132
+ "verdicts": verdicts_out,
6133
+ "session_id": getattr(result, "session_id", None),
6134
+ "elapsed_ms": getattr(result, "elapsed_ms", 0),
6135
+ "partial": getattr(result, "partial", False),
6136
+ }
6137
+ print(json.dumps(out))
6138
+ MANAGED_REVIEW
6139
+ )
6140
+ local py_rc=$?
6141
+ unset LOKI_MANAGED_REVIEW_ID LOKI_MANAGED_REVIEW_DIFF_FILE LOKI_MANAGED_REVIEW_FILES_FILE
6142
+ unset LOKI_MANAGED_REVIEW_OUT_JSON LOKI_MANAGED_REVIEW_PROJECT_DIR
6143
+
6144
+ if [ $py_rc -ne 0 ] || [ -z "$result_json" ]; then
6145
+ emit_event_json "managed_agents_fallback" \
6146
+ "op=run_code_review" \
6147
+ "reason=subprocess_failed" \
6148
+ "review_id=$review_id"
6149
+ return 1
6150
+ fi
6151
+
6152
+ local status
6153
+ status=$(printf '%s' "$result_json" | python3 -c "import json,sys; d=json.loads(sys.stdin.read() or '{}'); print(d.get('status',''))" 2>/dev/null || echo "")
6154
+
6155
+ if [ "$status" != "ok" ]; then
6156
+ local reason
6157
+ reason=$(printf '%s' "$result_json" | python3 -c "import json,sys; d=json.loads(sys.stdin.read() or '{}'); print(d.get('reason',''))" 2>/dev/null || echo "")
6158
+ emit_event_json "managed_agents_fallback" \
6159
+ "op=run_code_review" \
6160
+ "reason=managed_unavailable" \
6161
+ "detail=${reason//\"/}" \
6162
+ "review_id=$review_id"
6163
+ return 1
6164
+ fi
6165
+
6166
+ # Persist the raw managed result for observability and write legacy .txt
6167
+ # files for the dashboard panel / aggregation loop.
6168
+ printf '%s\n' "$result_json" > "$review_dir/managed_result.json"
6169
+ if ! council_verdicts_to_txt_files "$review_id" "$result_json"; then
6170
+ emit_event_json "managed_agents_fallback" \
6171
+ "op=run_code_review" \
6172
+ "reason=verdict_write_failed" \
6173
+ "review_id=$review_id"
6174
+ return 1
6175
+ fi
6176
+
6177
+ emit_event_json "managed_review_council_ok" \
6178
+ "review_id=$review_id" \
6179
+ "iteration=${ITERATION_COUNT:-0}"
6180
+ return 0
6181
+ }
6182
+
5872
6183
  run_code_review() {
5873
6184
  local loki_dir="${TARGET_DIR:-.}/.loki"
5874
6185
  local review_dir="$loki_dir/quality/reviews"
@@ -5888,6 +6199,51 @@ run_code_review() {
5888
6199
  changed_files=$(git -C "${TARGET_DIR:-.}" diff --name-only HEAD~1 2>/dev/null || git -C "${TARGET_DIR:-.}" diff --name-only --cached 2>/dev/null || echo "")
5889
6200
 
5890
6201
  log_header "CODE REVIEW: $review_id"
6202
+
6203
+ # Phase 3 (v7.0.0): managed code-review council. When the flag is on,
6204
+ # route to providers/managed.py::run_council. On ManagedUnavailable,
6205
+ # emit a fallback event and drop through to the legacy CLI fan-out
6206
+ # below -- the existing v6.83.1 behavior is preserved.
6207
+ if [ "${LOKI_EXPERIMENTAL_MANAGED_REVIEW:-false}" = "true" ]; then
6208
+ local managed_diff_file="$review_dir/$review_id/diff.txt"
6209
+ local managed_files_file="$review_dir/$review_id/files.txt"
6210
+ printf '%s\n' "$diff_content" > "$managed_diff_file"
6211
+ printf '%s\n' "$changed_files" > "$managed_files_file"
6212
+ log_info "Managed review council: attempting multiagent session (Phase 3)"
6213
+ if _run_managed_review_council "$review_id" "$managed_diff_file" "$managed_files_file"; then
6214
+ log_info "Managed review council: verdicts written, skipping CLI fan-out"
6215
+ # Managed path wrote legacy .txt files; skip CLI fan-out but let
6216
+ # the aggregation step run by setting a minimal selection.json
6217
+ # the downstream loop can read.
6218
+ emit_event_json "code_review_complete" \
6219
+ "review_id=$review_id" \
6220
+ "source=managed" \
6221
+ "iteration=${ITERATION_COUNT:-0}"
6222
+ # Build a selection.json so any downstream consumer can find the
6223
+ # reviewer list. Mirrors the shape the CLI path writes below.
6224
+ python3 - "$review_dir/$review_id/selection.json" << 'MANAGED_SELECTION'
6225
+ import json
6226
+ import sys
6227
+
6228
+ path = sys.argv[1]
6229
+ selection = {
6230
+ "reviewers": [
6231
+ {"name": "security-sentinel", "focus": "managed", "checks": "managed council"},
6232
+ {"name": "test-coverage-auditor", "focus": "managed", "checks": "managed council"},
6233
+ {"name": "performance-oracle", "focus": "managed", "checks": "managed council"},
6234
+ ],
6235
+ "scores": {},
6236
+ "pool_size": 3,
6237
+ "source": "managed",
6238
+ }
6239
+ with open(path, "w", encoding="utf-8") as f:
6240
+ json.dump(selection, f)
6241
+ MANAGED_SELECTION
6242
+ return 0
6243
+ fi
6244
+ log_warn "Managed review council unavailable; falling back to CLI fan-out"
6245
+ fi
6246
+
5891
6247
  log_info "Selecting 3 specialist reviewers from pool..."
5892
6248
 
5893
6249
  # Write diff/files to temp files for python to read (avoid env var size limits)
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "6.83.0"
10
+ __version__ = "7.0.1"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -5559,6 +5559,241 @@ def start_migration_phase(migration_id: str, request_body: dict):
5559
5559
  raise HTTPException(status_code=500, detail="Failed to start phase")
5560
5560
 
5561
5561
 
5562
+ # ---------------------------------------------------------------------------
5563
+ # Managed Agents Memory bridge (Phase 5, read-only)
5564
+ #
5565
+ # These endpoints expose the contents of .loki/managed/events.ndjson plus a
5566
+ # thin proxy to beta.memory_stores.memory_versions.list(). All endpoints are
5567
+ # safe to call when the managed-agents flags are off: they return empty
5568
+ # lists / {enabled: false} rather than 500s. No endpoint writes to the
5569
+ # managed store -- the only writer in the codebase remains
5570
+ # memory/managed_memory/shadow_write.py.
5571
+ # ---------------------------------------------------------------------------
5572
+
5573
+ _MANAGED_EVENTS_TAIL_MAX = 10000 # Safety ceiling on tail reads.
5574
+
5575
+
5576
+ def _managed_events_path() -> _Path:
5577
+ """Return the absolute path to .loki/managed/events.ndjson."""
5578
+ return _get_loki_dir() / "managed" / "events.ndjson"
5579
+
5580
+
5581
+ def _managed_flags_snapshot() -> dict[str, Any]:
5582
+ """Read managed-agents flags without importing the SDK path."""
5583
+ parent = os.environ.get("LOKI_MANAGED_AGENTS", "").strip().lower() == "true"
5584
+ child = os.environ.get("LOKI_MANAGED_MEMORY", "").strip().lower() == "true"
5585
+ try:
5586
+ from memory.managed_memory._beta import BETA_HEADER as _beta_header
5587
+ except Exception:
5588
+ _beta_header = "managed-agents-2026-04-01"
5589
+ return {
5590
+ "enabled": parent and child,
5591
+ "parent_flag": parent,
5592
+ "child_flags": {"LOKI_MANAGED_MEMORY": child},
5593
+ "beta_header": _beta_header,
5594
+ }
5595
+
5596
+
5597
+ def _tail_ndjson(
5598
+ path: _Path,
5599
+ limit: int,
5600
+ since_iso: Optional[str],
5601
+ event_type: Optional[str],
5602
+ ) -> list[dict[str, Any]]:
5603
+ """
5604
+ Return the last *limit* records from an ndjson file, optionally filtered
5605
+ by ts >= since_iso and/or type == event_type. The file is streamed line
5606
+ by line; malformed lines are skipped rather than raising.
5607
+ """
5608
+ if not path.exists():
5609
+ return []
5610
+ try:
5611
+ # Read lines (file is small: rotation at 10MB per the writer).
5612
+ with open(path, "r", encoding="utf-8") as f:
5613
+ lines = f.readlines()
5614
+ except OSError:
5615
+ return []
5616
+
5617
+ results: list[dict[str, Any]] = []
5618
+ # Scan from newest to oldest so we can early-exit once we have enough.
5619
+ for raw in reversed(lines):
5620
+ line = raw.strip()
5621
+ if not line:
5622
+ continue
5623
+ try:
5624
+ record = json.loads(line)
5625
+ except (json.JSONDecodeError, ValueError):
5626
+ continue
5627
+ if not isinstance(record, dict):
5628
+ continue
5629
+ if event_type and record.get("type") != event_type:
5630
+ continue
5631
+ if since_iso:
5632
+ ts = record.get("ts", "")
5633
+ if isinstance(ts, str) and ts < since_iso:
5634
+ # ISO-8601 strings sort lexicographically with Z suffix; once
5635
+ # we pass the floor we can stop scanning.
5636
+ break
5637
+ results.append(record)
5638
+ if len(results) >= limit:
5639
+ break
5640
+ # Return in chronological order (oldest first) for UI convenience.
5641
+ results.reverse()
5642
+ return results
5643
+
5644
+
5645
+ def _last_fallback_ts(events: list[dict[str, Any]]) -> Optional[str]:
5646
+ """Return the ts of the most recent managed_agents_fallback event, if any."""
5647
+ for rec in reversed(events):
5648
+ if rec.get("type") == "managed_agents_fallback":
5649
+ ts = rec.get("ts")
5650
+ return ts if isinstance(ts, str) else None
5651
+ return None
5652
+
5653
+
5654
+ @app.get("/api/managed/events")
5655
+ async def get_managed_events(
5656
+ limit: int = Query(default=100, ge=1, le=_MANAGED_EVENTS_TAIL_MAX),
5657
+ since: Optional[str] = Query(default=None),
5658
+ type: Optional[str] = Query(default=None, alias="type"),
5659
+ ):
5660
+ """
5661
+ Return the tail of .loki/managed/events.ndjson.
5662
+
5663
+ Works regardless of flag state. When the flags are off or the file does
5664
+ not exist yet, returns an empty list. Never raises on I/O error.
5665
+ """
5666
+ try:
5667
+ path = _managed_events_path()
5668
+ records = _tail_ndjson(path, limit=limit, since_iso=since, event_type=type)
5669
+ return {
5670
+ "events": records,
5671
+ "count": len(records),
5672
+ "source": str(path),
5673
+ }
5674
+ except Exception as exc: # defensive: never 500 on read-only tail.
5675
+ logger.warning("managed events tail failed: %s", exc)
5676
+ return {"events": [], "count": 0, "error": str(exc)}
5677
+
5678
+
5679
+ @app.get("/api/managed/status")
5680
+ async def get_managed_status():
5681
+ """
5682
+ Return the managed-agents flag snapshot plus last_fallback_ts.
5683
+
5684
+ When flags are off, returns {enabled: false, ...} rather than 503. This
5685
+ endpoint is meant to be polled by the UI to decide whether to surface
5686
+ the managed-memory panel at all.
5687
+ """
5688
+ snapshot = _managed_flags_snapshot()
5689
+ # last_fallback_ts is best-effort from the local events file.
5690
+ try:
5691
+ events = _tail_ndjson(
5692
+ _managed_events_path(),
5693
+ limit=500,
5694
+ since_iso=None,
5695
+ event_type="managed_agents_fallback",
5696
+ )
5697
+ snapshot["last_fallback_ts"] = _last_fallback_ts(events)
5698
+ except Exception:
5699
+ snapshot["last_fallback_ts"] = None
5700
+ return snapshot
5701
+
5702
+
5703
+ @app.get("/api/managed/memory_versions/{memory_id}")
5704
+ async def list_managed_memory_versions(memory_id: str):
5705
+ """
5706
+ Proxy to beta.memory_stores.memory_versions.list(memory_id=...).
5707
+
5708
+ Returns 503 with a helpful JSON body when flags are off or the SDK does
5709
+ not expose the expected attribute path. On any SDK / transport error the
5710
+ endpoint returns 502 with the error detail -- the managed store owns the
5711
+ source of truth, so we do NOT silently return an empty list here.
5712
+ """
5713
+ # Validate memory_id early so we don't leak path-traversal attempts into
5714
+ # the SDK payload. The managed API uses opaque identifiers; alphanumerics,
5715
+ # hyphens, underscores only.
5716
+ if (
5717
+ not memory_id
5718
+ or len(memory_id) > 256
5719
+ or ".." in memory_id
5720
+ or not re.match(r"^[a-zA-Z0-9_\-]+$", memory_id)
5721
+ ):
5722
+ raise HTTPException(status_code=400, detail="Invalid memory_id")
5723
+
5724
+ snapshot = _managed_flags_snapshot()
5725
+ if not snapshot["enabled"]:
5726
+ raise HTTPException(
5727
+ status_code=503,
5728
+ detail=(
5729
+ "managed memory disabled: set LOKI_MANAGED_AGENTS=true and "
5730
+ "LOKI_MANAGED_MEMORY=true to enable"
5731
+ ),
5732
+ )
5733
+
5734
+ try:
5735
+ from memory.managed_memory import ManagedDisabled
5736
+ from memory.managed_memory.client import get_client
5737
+ except Exception as exc:
5738
+ raise HTTPException(status_code=503, detail=f"managed client unavailable: {exc}")
5739
+
5740
+ try:
5741
+ client = get_client()
5742
+ except ManagedDisabled as exc:
5743
+ raise HTTPException(status_code=503, detail=str(exc))
5744
+
5745
+ # Resolve beta.memory_stores.memory_versions.list(...) defensively. Some
5746
+ # SDK versions may not expose this path yet; treat missing attributes as
5747
+ # 503 (flag state prevents us from guaranteeing anything else).
5748
+ try:
5749
+ beta = getattr(client._client, "beta", None) # type: ignore[attr-defined]
5750
+ memory_stores = getattr(beta, "memory_stores", None) if beta is not None else None
5751
+ memory_versions = (
5752
+ getattr(memory_stores, "memory_versions", None)
5753
+ if memory_stores is not None
5754
+ else None
5755
+ )
5756
+ list_fn = getattr(memory_versions, "list", None) if memory_versions is not None else None
5757
+ if list_fn is None:
5758
+ raise HTTPException(
5759
+ status_code=503,
5760
+ detail="memory_versions.list not available in installed SDK",
5761
+ )
5762
+ except HTTPException:
5763
+ raise
5764
+ except Exception as exc:
5765
+ raise HTTPException(status_code=503, detail=f"SDK introspection failed: {exc}")
5766
+
5767
+ try:
5768
+ result = list_fn(memory_id=memory_id)
5769
+ except Exception as exc:
5770
+ # Distinguish "not found" from transport errors when we can.
5771
+ status = getattr(exc, "status_code", None) or getattr(exc, "status", None)
5772
+ if status == 404:
5773
+ raise HTTPException(status_code=404, detail=f"memory_id not found: {memory_id}")
5774
+ logger.warning("memory_versions.list failed: %s", exc)
5775
+ raise HTTPException(status_code=502, detail=f"managed API error: {exc}")
5776
+
5777
+ # Normalize to a list of dicts.
5778
+ data = getattr(result, "data", result)
5779
+ if data is None:
5780
+ data = []
5781
+ items: list[dict[str, Any]] = []
5782
+ for entry in data:
5783
+ if isinstance(entry, dict):
5784
+ items.append(entry)
5785
+ continue
5786
+ to_dict = getattr(entry, "model_dump", None) or getattr(entry, "dict", None)
5787
+ if callable(to_dict):
5788
+ try:
5789
+ items.append(to_dict())
5790
+ continue
5791
+ except Exception:
5792
+ pass
5793
+ items.append({"raw": str(entry)})
5794
+ return {"memory_id": memory_id, "versions": items, "count": len(items)}
5795
+
5796
+
5562
5797
  # ---------------------------------------------------------------------------
5563
5798
  # SPA catch-all: serve index.html for any path not matched by API routes
5564
5799
  # or static asset mounts. This lets the dashboard UI handle client-side routing.
@@ -2,7 +2,7 @@
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.83.0
5
+ **Version:** v7.0.1
6
6
 
7
7
  ---
8
8
 
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '6.83.0'
60
+ __version__ = '7.0.1'