Skip to content

chore: PR 206 review feedback and declarative PGDATA version-gating#207

Closed
sheepdestroyer wants to merge 22 commits into
masterfrom
chore/pr206-review-fixes
Closed

chore: PR 206 review feedback and declarative PGDATA version-gating#207
sheepdestroyer wants to merge 22 commits into
masterfrom
chore/pr206-review-fixes

Conversation

@sheepdestroyer

@sheepdestroyer sheepdestroyer commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Address unaddressed CodeRabbit and inline review feedback for PR #206 on LLM-Routing, ensuring backward compatibility, proper test assertion reporting, shell script robustness, documentation accuracy, and declarative database version-gating.

Summary by CodeRabbit

  • New Features

    • Added live stack verification and routing benchmark scripts, plus updated setup instructions in the README.
    • Improved startup handling for environment values and generated credentials, with clearer failure messages if placeholders remain unresolved.
  • Bug Fixes

    • Improved routing accuracy and responsiveness, including better token estimation, safer quota checks, and more reliable cache loading.
    • Updated service configuration to use explicit placeholders and pinned images for more predictable deployments.
  • Tests

    • Expanded automated coverage for routing, status reporting, memory handling, annotations, and stack verification.

…am limits and add early arg parsing for --help in start-stack.sh

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @sheepdestroyer, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR restores and hardens several regressed behaviors: async quota detection and file I/O (aiofiles), v2 memory keys with concurrent deletion, Valkey URL init, regex-based token estimation, lazy AA score loading, annotation caching, deployment secret placeholderization, test-path consolidation, and new verification/benchmark scripts, plus extensive new test coverage.

Changes

Agent Process Documentation

Layer / File(s) Summary
Agent conflict-resolution policy and regression audit docs
.agents/AGENTS.md, .agents/branch_audit_plan.md, .agents/jules_regression_analysis.md
Documents Git rebase/conflict policy, a branch audit report, and a detailed regression root-cause analysis with recommended guardrails.

Test Infrastructure and CI Consolidation

Layer / File(s) Summary
CI workflow, pytest config, and scripts docs updates
.github/workflows/test.yml, pytest.ini, scripts/README.md
Expands installed dependencies, updates pytest invocation with PYTHONPATH and ignored targets, and re-points test docs to new paths.
Test path resolution via conftest.py
tests/conftest.py, router/tests/conftest.py, tests/test_a2_verify.py, tests/test_circuit_breaker.py, tests/test_models_proxy.py, tests/test_agy_behavior.py, tests/test_antigravity.py
Adds conftest-based sys.path/CONFIG_PATH setup, removes redundant per-test sys.path manipulation, and adds CI-skip/harness guards.

Router Core Behavior Updates

Layer / File(s) Summary
Valkey URL init and regex-based token estimation
router/main.py, router/tests/test_get_redis.py, router/tests/test_estimate_prompt_tokens.py, scripts/benchmark_tokens.py
Prefers VALKEY_URL for client init and replaces character-count token estimation with a regex-weighted heuristic, validated by new tests and a benchmark script.
AA score cache lazy loading
router/main.py, tests/test_compute_free_model_score.py
Defers AA score cache loading to roster sync/get_best_free_model call sites via asyncio.to_thread instead of loading inside compute_free_model_score.
Classifier input truncation and agy routing override
router/main.py
Truncates classifier prompt via CLASSIFIER_INPUT_MAX_CHARS and forces target_model override for direct llm-routing-agy requests.
Annotation cache and persisted stats coverage
router/main.py, tests/test_read_annotations_async.py, router/tests/test_load_persisted_stats.py
Adds mtime-based _annotations_cache with async reader/invalidation and adds tests for annotation reads and load_persisted_stats.
Additional router unit test coverage
router/tests/test_detect_active_tool.py, router/tests/test_get_gemini_oauth_status.py, router/tests/test_get_goose_sessions.py
Adds new pytest modules covering existing router functions for tool detection, OAuth status, and Goose session retrieval.

Async Quota Detection in agy_proxy

Layer / File(s) Summary
Async quota check via aiofiles
router/Dockerfile, router/agy_proxy.py, router/tests/test_agy_proxy.py
Converts _is_quota_exhausted to async using aiofiles for log tail reads, updates streaming/non-streaming call sites, and rewrites tests for async mocking.

Memory MCP v2 Key Scheme and Concurrent Deletion

Layer / File(s) Summary
v2 key format, value normalization, and concurrent deletes
router/memory_mcp.py, router/tests/test_memory_mcp.py
Introduces memory:v2 URL-encoded keys, hardens value/key parsing, and reworks deletion to use semaphore-limited concurrent DELETE calls with URL-quoted keys, backed by a new comprehensive test suite.

Deployment Secret Placeholderization

Layer / File(s) Summary
pod.yaml placeholder-based secrets and image bumps
pod.yaml
Replaces embedded secrets with *_PLACEHOLDER tokens across LiteLLM, Postgres, ClickHouse, Valkey, Langfuse, and Minio containers, and bumps several image versions.
start-stack.sh secret generation and template rendering
start-stack.sh
Adds UUID/secret generation helpers, expands required-variable checks and interactive key capture, and rewrites render_pod_yaml to substitute and validate placeholders.

Verification and Benchmark Scripts

Layer / File(s) Summary
Concurrent classifier benchmarking
scripts/benchmark_classifier.py
Replaces sequential classification with a ThreadPoolExecutor-based pipeline using a new process_item helper and rate-limited concurrency.
scripts/get_pr_status.py and tests
scripts/get_pr_status.py, tests/test_get_pr_status.py
Adds a standalone gh-CLI-based PR status script with run_cmd/get_pr_status/main functions and full test coverage.
Verification script relocation and live tier testing
scripts/verification/verify_breaker.py, scripts/watch_quota.sh, scripts/verification/test_reasoning_tiers.py, README.md
Adjusts script path resolution, adds a live 5-tier routing smoke test, and documents version pins and the new verification step.

Prisma Datetime Serializer Registration

Layer / File(s) Summary
Prisma datetime serializer registration
litellm/entrypoint.py
Registers a UTC ISO-8601 datetime serializer with Prisma for both the original datetime class and RobustDatetime.

Host Agy Daemon Test Coverage

Layer / File(s) Summary
host_agy_daemon endpoint and caching tests
tests/test_host_agy_daemon.py
Adds a large new test suite covering conversation-id caching and streaming/non-streaming /run endpoint behavior including timeouts and error paths.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is relevant and specific to the PR’s main themes: review-feedback cleanup and PGDATA version-gating.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/pr206-review-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request restores asynchronous AA scores loading optimizations, introduces merge conflict resolution guidelines, transitions file operations to asynchronous aiofiles reads, secures credentials with placeholders, and expands test coverage. Feedback on the changes highlights a critical missing timezone import in litellm/entrypoint.py causing a runtime NameError, a metric tracking issue in router/main.py for llm-routing-agy requests, a concurrency bottleneck in scripts/benchmark_classifier.py due to holding a lock during sleep, and a recommendation to simplify project root discovery in tests/conftest.py.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread litellm/entrypoint.py
Comment on lines +114 to +121
if serializer is not None:
def _serialize_dt(dt):
"""Serialize datetime to ISO8601 with timezone (UTC if naive)."""
if dt.utcoffset() is None:
dt = dt.replace(tzinfo=timezone.utc)
else:
dt = dt.astimezone(timezone.utc)
return dt.isoformat().replace("+00:00", "Z")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The timezone name is used here but it is not imported in this file, which will raise a NameError at runtime when serializing datetimes. Please import timezone from datetime.

Suggested change
if serializer is not None:
def _serialize_dt(dt):
"""Serialize datetime to ISO8601 with timezone (UTC if naive)."""
if dt.utcoffset() is None:
dt = dt.replace(tzinfo=timezone.utc)
else:
dt = dt.astimezone(timezone.utc)
return dt.isoformat().replace("+00:00", "Z")
if serializer is not None:
from datetime import timezone
def _serialize_dt(dt):
"""Serialize datetime to ISO8601 with timezone (UTC if naive)."""
if dt.utcoffset() is None:
dt = dt.replace(tzinfo=timezone.utc)
else:
dt = dt.astimezone(timezone.utc)
return dt.isoformat().replace("+00:00", "Z")

Comment thread router/main.py
Comment on lines +2177 to +2178
if target_model == "llm-routing-agy":
target_model = "agent-advanced-core"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Mapping target_model to agent-advanced-core here (after the statistics are updated) causes direct requests to llm-routing-agy to not be counted in the advanced_requests metric. Consider moving this mapping to the top of the function (where DIRECT_TIERS is handled) so that stats["advanced_requests"] is correctly incremented.

Comment thread tests/conftest.py
Comment on lines +5 to +11
# Dynamic project root discovery
root = Path(__file__).resolve()
while root.parent != root and not (root / ".git").exists():
root = root.parent

if not (root / ".git").exists():
raise RuntimeError("Could not find project root: .git directory not found in parent paths.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since tests/conftest.py is located exactly one level below the project root, we can resolve the root directory directly using Path(__file__).resolve().parent.parent. This is simpler and avoids relying on the .git directory, which may be absent in certain environments (like Docker containers or release archives).

Suggested change
# Dynamic project root discovery
root = Path(__file__).resolve()
while root.parent != root and not (root / ".git").exists():
root = root.parent
if not (root / ".git").exists():
raise RuntimeError("Could not find project root: .git directory not found in parent paths.")
# Dynamic project root discovery
root = Path(__file__).resolve().parent.parent

Comment on lines +84 to +88
with rate_lock:
now = time.monotonic()
if now < next_start_time[0]:
time.sleep(next_start_time[0] - now)
next_start_time[0] = time.monotonic() + 0.05

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Holding a lock (rate_lock) during time.sleep blocks other threads from entering the rate-limiting block to schedule their own sleeps. It is a better practice to calculate the required sleep duration inside the lock, release the lock, and then sleep outside of it.

Suggested change
with rate_lock:
now = time.monotonic()
if now < next_start_time[0]:
time.sleep(next_start_time[0] - now)
next_start_time[0] = time.monotonic() + 0.05
with rate_lock:
now = time.monotonic()
sleep_dur = 0.0
if now < next_start_time[0]:
sleep_dur = next_start_time[0] - now
next_start_time[0] += 0.05
else:
next_start_time[0] = now + 0.05
if sleep_dur > 0:
time.sleep(sleep_dur)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
.github/workflows/test.yml (1)

29-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

--ignore=scripts/verification is redundant given pytest.ini.

pytest.ini's norecursedirs already lists scripts, which excludes the entire scripts/ tree (including scripts/verification) from collection. The explicit --ignore=scripts/verification flag duplicates that exclusion. Harmless but unnecessary; consider dropping it to avoid config drift between the two settings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 29 - 30, The pytest command in the
workflow redundantly ignores scripts/verification even though pytest.ini already
excludes scripts via norecursedirs. Remove the explicit
--ignore=scripts/verification from the test invocation and keep the rest of the
pytest arguments unchanged, so the workflow relies on the existing pytest.ini
setting instead of duplicating it.
router/tests/conftest.py (1)

6-6: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unconditional CONFIG_PATH override vs. setdefault in tests/conftest.py.

tests/conftest.py uses os.environ.setdefault("CONFIG_PATH", ...) (Line 19 there), but this file force-overwrites os.environ["CONFIG_PATH"] unconditionally. Both currently resolve to the same absolute path (router/config.yaml), so there's no functional bug today, but the inconsistent precedence pattern means an externally-set CONFIG_PATH (e.g., from the CI workflow's CONFIG_PATH=router/config.yaml env var) is silently overridden here while respected in the other conftest. Worth aligning both to setdefault for consistency.

♻️ Proposed fix for consistency
-os.environ["CONFIG_PATH"] = str(Path(__file__).resolve().parent.parent / "config.yaml")
+os.environ.setdefault("CONFIG_PATH", str(Path(__file__).resolve().parent.parent / "config.yaml"))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router/tests/conftest.py` at line 6, The CONFIG_PATH assignment in the router
conftest is overriding any externally provided value, which is inconsistent with
the other conftest’s setdefault behavior. Update the environment setup around
the top-level CONFIG_PATH assignment to use the same precedence pattern as
tests/conftest.py so external configuration is preserved. Use the existing
Path-based config.yaml resolution in this module and keep the change localized
to the CONFIG_PATH initialization.
router/tests/test_get_gemini_oauth_status.py (1)

21-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: parametrize to reduce duplication.

Six tests differ only in the time-delta and expected string; a @pytest.mark.parametrize table would cut repetition without losing clarity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router/tests/test_get_gemini_oauth_status.py` around lines 21 - 85, Refactor
the duplicated Gemini OAuth status tests in test_get_gemini_oauth_status by
replacing the six near-identical async test cases with a single parametrized
test using `@pytest.mark.parametrize`. Keep the existing patching of
os.path.exists, builtins.open, and time.time, but feed each delta/expected
status/detail/expiry_ms case through the table so get_gemini_oauth_status is
still exercised with the same scenarios and assertions.
tests/test_host_agy_daemon.py (1)

26-39: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

test_run_server_interrupt binds on a nondeterministic port.

test_run_server_interrupt calls host_agy_daemon.run_server() without first pinning host_agy_daemon.PORT to a known-free port. It relies on whichever value PORT currently holds — either the module's real default (if this test happens to run without a prior daemon_server-based test) or a leftover ephemeral port from daemon_server (line 29 mutates the global directly instead of via monkeypatch.setattr, so it isn't reverted between tests). This makes the test's bind behavior coupled to execution order and, in the worst case, could attempt to bind the module's real production port.

♻️ Proposed fix
 def test_run_server_interrupt(monkeypatch):
+    monkeypatch.setattr(host_agy_daemon, "PORT", find_free_port())
     # Mock serve_forever to raise KeyboardInterrupt
     def mock_serve_forever(self):
         raise KeyboardInterrupt()

And in the daemon_server fixture, prefer monkeypatch.setattr for the port to avoid cross-test leakage:

-def daemon_server():
+def daemon_server(monkeypatch):
     port = find_free_port()
-    host_agy_daemon.PORT = port
+    monkeypatch.setattr(host_agy_daemon, "PORT", port)

Also applies to: 227-246

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_host_agy_daemon.py` around lines 26 - 39,
`test_run_server_interrupt` is using whatever value `host_agy_daemon.PORT`
happens to have, so the bind target is order-dependent and can leak from
`daemon_server`. Update the `daemon_server` fixture to set `PORT` with
`monkeypatch.setattr` instead of assigning it directly, and in
`test_run_server_interrupt` explicitly pin `host_agy_daemon.PORT` to a
known-free port before calling `host_agy_daemon.run_server()`. Keep the fix
localized to the `daemon_server` fixture and `test_run_server_interrupt` so the
server bind behavior is deterministic across tests.
router/main.py (1)

986-991: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider adding test coverage for classifier truncation and agy→advanced-core fallback mapping.

CLASSIFIER_INPUT_MAX_CHARS truncation (lines 986-991) and the llm-routing-agyagent-advanced-core rewrite (lines 2177-2178) are new routing-affecting behaviors without corresponding test files in this review batch (only Valkey/token/AA-score/annotation layers have tests attached). Since the mapping directly determines which LiteLLM backend a failed/unavailable agy request falls back to, a regression here would silently break requests.

Also applies to: 2177-2178

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router/main.py` around lines 986 - 991, Add test coverage for the new routing
behavior in router/main.py by covering both the classifier prompt truncation
path around max_chars/truncated_prompt and the llm-routing-agy to
agent-advanced-core fallback rewrite in the routing logic. Create or extend
tests that exercise the classifier payload construction with
CLASSIFIER_INPUT_MAX_CHARS set and verify the prompt is truncated before being
sent, and add a routing test that confirms failed/unavailable llm-routing-agy
requests are remapped to agent-advanced-core via the relevant routing function
or rewrite branch. Focus on the behavior in the classifier payload builder and
the agy fallback mapping so regressions are caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@router/memory_mcp.py`:
- Around line 92-102: The JSON normalization in memory_mcp.py does not guarantee
that decoded data is always a string, which can later break membership checks in
handle_remove_specific_memory. Update the JSON parsing/normalization logic
around the decode helper so every returned shape produces a string-valued data
field, including non-dict JSON values and {"data": null}; keep tags normalized
as before. Use the existing decode path and handle_remove_specific_memory as the
places to verify the fix.

In `@start-stack.sh`:
- Around line 23-29: The escape_env_val helper currently uses echo to emit
escaped values, which can mis-handle secret strings that look like options.
Update escape_env_val to use printf instead of echo so arbitrary API keys and
other values are preserved reliably, keeping the existing escaping logic intact.
- Around line 82-97: The OPENROUTER_API_KEY prompt flow in start-stack.sh can
append a secret to ENV_FILE before its permissions are tightened, so initialize
the .env file with the correct restrictive mode before the first write. Update
the key handling path around the OPENROUTER_API_KEY read/append logic to ensure
the file is created and chmod’d prior to any echo redirection, and keep the fix
localized to the ENV_FILE write sequence and related permission setup.

---

Nitpick comments:
In @.github/workflows/test.yml:
- Around line 29-30: The pytest command in the workflow redundantly ignores
scripts/verification even though pytest.ini already excludes scripts via
norecursedirs. Remove the explicit --ignore=scripts/verification from the test
invocation and keep the rest of the pytest arguments unchanged, so the workflow
relies on the existing pytest.ini setting instead of duplicating it.

In `@router/main.py`:
- Around line 986-991: Add test coverage for the new routing behavior in
router/main.py by covering both the classifier prompt truncation path around
max_chars/truncated_prompt and the llm-routing-agy to agent-advanced-core
fallback rewrite in the routing logic. Create or extend tests that exercise the
classifier payload construction with CLASSIFIER_INPUT_MAX_CHARS set and verify
the prompt is truncated before being sent, and add a routing test that confirms
failed/unavailable llm-routing-agy requests are remapped to agent-advanced-core
via the relevant routing function or rewrite branch. Focus on the behavior in
the classifier payload builder and the agy fallback mapping so regressions are
caught.

In `@router/tests/conftest.py`:
- Line 6: The CONFIG_PATH assignment in the router conftest is overriding any
externally provided value, which is inconsistent with the other conftest’s
setdefault behavior. Update the environment setup around the top-level
CONFIG_PATH assignment to use the same precedence pattern as tests/conftest.py
so external configuration is preserved. Use the existing Path-based config.yaml
resolution in this module and keep the change localized to the CONFIG_PATH
initialization.

In `@router/tests/test_get_gemini_oauth_status.py`:
- Around line 21-85: Refactor the duplicated Gemini OAuth status tests in
test_get_gemini_oauth_status by replacing the six near-identical async test
cases with a single parametrized test using `@pytest.mark.parametrize`. Keep the
existing patching of os.path.exists, builtins.open, and time.time, but feed each
delta/expected status/detail/expiry_ms case through the table so
get_gemini_oauth_status is still exercised with the same scenarios and
assertions.

In `@tests/test_host_agy_daemon.py`:
- Around line 26-39: `test_run_server_interrupt` is using whatever value
`host_agy_daemon.PORT` happens to have, so the bind target is order-dependent
and can leak from `daemon_server`. Update the `daemon_server` fixture to set
`PORT` with `monkeypatch.setattr` instead of assigning it directly, and in
`test_run_server_interrupt` explicitly pin `host_agy_daemon.PORT` to a
known-free port before calling `host_agy_daemon.run_server()`. Keep the fix
localized to the `daemon_server` fixture and `test_run_server_interrupt` so the
server bind behavior is deterministic across tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f00266c0-cab9-46d8-85fc-73ff6837fb00

📥 Commits

Reviewing files that changed from the base of the PR and between c304f3d and af6489c.

⛔ Files ignored due to path filters (1)
  • debug-homepage.png is excluded by !**/*.png
📒 Files selected for processing (55)
  • .agents/AGENTS.md
  • .agents/branch_audit_plan.md
  • .agents/jules_regression_analysis.md
  • .github/workflows/test.yml
  • README.md
  • get_pr_status.py
  • litellm/entrypoint.py
  • pod.yaml
  • pytest.ini
  • router/Dockerfile
  • router/agy_proxy.py
  • router/main.py
  • router/memory_mcp.py
  • router/test_memory_mcp.py
  • router/tests/conftest.py
  • router/tests/test_agy_proxy.py
  • router/tests/test_detect_active_tool.py
  • router/tests/test_estimate_prompt_tokens.py
  • router/tests/test_get_gemini_oauth_status.py
  • router/tests/test_get_goose_sessions.py
  • router/tests/test_get_redis.py
  • router/tests/test_load_persisted_stats.py
  • router/tests/test_memory_mcp.py
  • scripts/README.md
  • scripts/benchmark_classifier.py
  • scripts/benchmark_tokens.py
  • scripts/get_pr_status.py
  • scripts/host_agy_daemon.py
  • scripts/sync_gemini_token.py
  • scripts/test_quota_reset.sh
  • scripts/verification/test_reasoning_tiers.py
  • scripts/verification/verify_breaker.py
  • scripts/watch_quota.sh
  • start-stack.sh
  • test_memory_mcp.py
  • tests/conftest.py
  • tests/test_a2_verify.py
  • tests/test_agy_behavior.py
  • tests/test_agy_tiers.py
  • tests/test_antigravity.py
  • tests/test_atomic_write.py
  • tests/test_check_http_endpoint.py
  • tests/test_circuit_breaker.py
  • tests/test_classifier_accuracy.py
  • tests/test_compute_free_model_score.py
  • tests/test_get_pr_status.py
  • tests/test_host_agy_daemon.py
  • tests/test_map_tool_to_category.py
  • tests/test_models_proxy.py
  • tests/test_pie_chart_gradient.py
  • tests/test_read_annotations_async.py
  • tests/test_record_tool_usage.py
  • tests/test_src_badge.py
  • tests/test_stream_latency.py
  • tests/test_sync_gemini_token.py
💤 Files with no reviewable changes (6)
  • test_memory_mcp.py
  • tests/test_models_proxy.py
  • router/test_memory_mcp.py
  • get_pr_status.py
  • tests/test_a2_verify.py
  • tests/test_circuit_breaker.py

Comment thread router/memory_mcp.py
Comment on lines +92 to +102
val = json.loads(raw)
if not isinstance(val, dict):
return {"data": val, "tags": []}
if "data" not in val:
val["data"] = str(val)
else:
if val["data"] is not None:
val["data"] = str(val["data"])
if "tags" not in val or not isinstance(val["tags"], list):
val["tags"] = []
return val

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Normalize decoded data to a string for every JSON shape.

Line 98 leaves {"data": null} as None, and Line 94 returns valid non-dict JSON values unchanged. Those values can still reach memory_content in entry["data"] in handle_remove_specific_memory, causing a runtime TypeError.

Proposed fix
 def _parse_memory_value(raw: str) -> dict:
     """Decode stored value back into {data, tags}."""
     try:
         val = json.loads(raw)
         if not isinstance(val, dict):
-            return {"data": val, "tags": []}
+            return {"data": "" if val is None else str(val), "tags": []}
         if "data" not in val:
             val["data"] = str(val)
         else:
-            if val["data"] is not None:
-                val["data"] = str(val["data"])
+            val["data"] = "" if val["data"] is None else str(val["data"])
         if "tags" not in val or not isinstance(val["tags"], list):
             val["tags"] = []
         return val
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val = json.loads(raw)
if not isinstance(val, dict):
return {"data": val, "tags": []}
if "data" not in val:
val["data"] = str(val)
else:
if val["data"] is not None:
val["data"] = str(val["data"])
if "tags" not in val or not isinstance(val["tags"], list):
val["tags"] = []
return val
val = json.loads(raw)
if not isinstance(val, dict):
return {"data": "" if val is None else str(val), "tags": []}
if "data" not in val:
val["data"] = str(val)
else:
val["data"] = "" if val["data"] is None else str(val["data"])
if "tags" not in val or not isinstance(val["tags"], list):
val["tags"] = []
return val
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router/memory_mcp.py` around lines 92 - 102, The JSON normalization in
memory_mcp.py does not guarantee that decoded data is always a string, which can
later break membership checks in handle_remove_specific_memory. Update the JSON
parsing/normalization logic around the decode helper so every returned shape
produces a string-valued data field, including non-dict JSON values and {"data":
null}; keep tags normalized as before. Use the existing decode path and
handle_remove_specific_memory as the places to verify the fix.

Comment thread start-stack.sh
Comment on lines +23 to +29
escape_env_val() {
local val="$1"
val="${val//\\/\\\\}"
val="${val//\"/\\\"}"
val="${val//\$/\\\$}"
val="${val//\`/\\\`}"
echo "$val"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use printf to avoid corrupting API keys.

echo "$val" can treat values like -n... as options. printf preserves arbitrary secret values reliably.

Proposed fix
-    echo "$val"
+    printf '%s\n' "$val"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
escape_env_val() {
local val="$1"
val="${val//\\/\\\\}"
val="${val//\"/\\\"}"
val="${val//\$/\\\$}"
val="${val//\`/\\\`}"
echo "$val"
escape_env_val() {
local val="$1"
val="${val//\\/\\\\}"
val="${val//\"/\\\"}"
val="${val//\$/\\\$}"
val="${val//\`/\\\`}"
printf '%s\n' "$val"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@start-stack.sh` around lines 23 - 29, The escape_env_val helper currently
uses echo to emit escaped values, which can mis-handle secret strings that look
like options. Update escape_env_val to use printf instead of echo so arbitrary
API keys and other values are preserved reliably, keeping the existing escaping
logic intact.

Comment thread start-stack.sh
Comment on lines 82 to +97
if [ -z "$OPENROUTER_API_KEY" ]; then
if [ -t 0 ]; then
echo "🔑 OpenRouter API Key not found."
echo -n "Please enter your OpenRouter API Key (input will be hidden): "
read -rs OPENROUTER_API_KEY
echo ""
echo "OPENROUTER_API_KEY=\"$OPENROUTER_API_KEY\"" >> "$ENV_FILE"
while [ -z "$OPENROUTER_API_KEY" ]; do
echo -n "Please enter your OpenRouter API Key (input will be hidden): "
if ! read -rs OPENROUTER_API_KEY; then
echo -e "\n❌ Error: Failed to read OpenRouter API Key (EOF reached). Aborting." >&2
exit 1
fi
echo ""
if [ -z "$OPENROUTER_API_KEY" ]; then
echo "❌ Error: API key cannot be empty. Please try again."
fi
done
escaped_key=$(escape_env_val "$OPENROUTER_API_KEY")
echo "OPENROUTER_API_KEY=\"$escaped_key\"" >> "$ENV_FILE"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Create and chmod .env before the first secret write.

Line 97 can create $ENV_FILE before the later chmod path shown in this file, briefly using the caller’s umask for a secret-bearing file. Initialize permissions before prompting/appending any key.

Proposed fix
+touch "$ENV_FILE"
+chmod 600 "$ENV_FILE"
+
 if [ -z "$OPENROUTER_API_KEY" ]; then
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if [ -z "$OPENROUTER_API_KEY" ]; then
if [ -t 0 ]; then
echo "🔑 OpenRouter API Key not found."
echo -n "Please enter your OpenRouter API Key (input will be hidden): "
read -rs OPENROUTER_API_KEY
echo ""
echo "OPENROUTER_API_KEY=\"$OPENROUTER_API_KEY\"" >> "$ENV_FILE"
while [ -z "$OPENROUTER_API_KEY" ]; do
echo -n "Please enter your OpenRouter API Key (input will be hidden): "
if ! read -rs OPENROUTER_API_KEY; then
echo -e "\n❌ Error: Failed to read OpenRouter API Key (EOF reached). Aborting." >&2
exit 1
fi
echo ""
if [ -z "$OPENROUTER_API_KEY" ]; then
echo "❌ Error: API key cannot be empty. Please try again."
fi
done
escaped_key=$(escape_env_val "$OPENROUTER_API_KEY")
echo "OPENROUTER_API_KEY=\"$escaped_key\"" >> "$ENV_FILE"
touch "$ENV_FILE"
chmod 600 "$ENV_FILE"
if [ -z "$OPENROUTER_API_KEY" ]; then
if [ -t 0 ]; then
echo "🔑 OpenRouter API Key not found."
while [ -z "$OPENROUTER_API_KEY" ]; do
echo -n "Please enter your OpenRouter API Key (input will be hidden): "
if ! read -rs OPENROUTER_API_KEY; then
echo -e "\n❌ Error: Failed to read OpenRouter API Key (EOF reached). Aborting." >&2
exit 1
fi
echo ""
if [ -z "$OPENROUTER_API_KEY" ]; then
echo "❌ Error: API key cannot be empty. Please try again."
fi
done
escaped_key=$(escape_env_val "$OPENROUTER_API_KEY")
echo "OPENROUTER_API_KEY=\"$escaped_key\"" >> "$ENV_FILE"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@start-stack.sh` around lines 82 - 97, The OPENROUTER_API_KEY prompt flow in
start-stack.sh can append a secret to ENV_FILE before its permissions are
tightened, so initialize the .env file with the correct restrictive mode before
the first write. Update the key handling path around the OPENROUTER_API_KEY
read/append logic to ensure the file is created and chmod’d prior to any echo
redirection, and keep the fix localized to the ENV_FILE write sequence and
related permission setup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant