Skip to content

feat(console)!: restructure web console + retention-score forgetting model (0.2.0) - #43

Merged
ch-liuzhide merged 2 commits into
mainfrom
feat/web-console-forgetting-overhaul
Jun 23, 2026
Merged

feat(console)!: restructure web console + retention-score forgetting model (0.2.0)#43
ch-liuzhide merged 2 commits into
mainfrom
feat/web-console-forgetting-overhaul

Conversation

@ch-liuzhide

@ch-liuzhide ch-liuzhide commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two coordinated changes that overhaul memory lifecycle management, plus a 0.1.8 → 0.2.0 bump (breaking config change).

1. Web console restructure

Reorganized around the four lifecycle stages, with a unified design language and deep-linkable in-page tabs:

  • Manage — memories / partitions / graph (tabs) + overview stats
  • Activate — recall test + retrieval parameters
  • Consolidate — trigger + run records + config
  • Forget — trigger + records + global defaults + per-partition tuner
  • System — infra config (LLM / embedding / storage / server)
  • lifecycle.js teardown registry (tears down timers/observers on navigation); static served no-cache to survive ES-module upgrades.

2. Retention-score forgetting model (replaces dynamic-TTL crossover)

eff_half_life = half_life_days · (1 + k_importance·(importance/10) + k_access·(access_count/10))
retention(idle) = exp(-idle / eff_half_life)
forget when retention < threshold
  • A true Ebbinghaus curve — monotonic in importance and access (the old model could forget a once-accessed memory sooner than a never-accessed one, and capped lifetimes at days).
  • Per-region defaults (episodic 30 / semantic·procedural 90 / preference 180 day half-lives); user partitions use global defaults; HIPPOCAMPUS never swept.
  • Forget page reworked: retention decay curve + threshold line, importance×access matrix on a 0–100 axis with WCAG-contrast cell text, real-memory impact preview, "how forgetting works" explainer.
  • New forgetting run tracker records each sweep.

Before → after (default config): a max-importance, 100×-accessed memory went from forgotten in 4.2 days → ~3.6 years.

⚠️ BREAKING CHANGE (config)

Global base_ttl_hours / decay_factor and the per-partition override fields of the same name are removed, replaced by half_life_days / k_importance / k_access / forget_threshold / forget_min_retention_days (global) and half_life_days / k_importance / k_access / threshold (per-partition override). Legacy hebb.json keys are ignored; per-partition overrides fall back to region/global defaults — re-tune in the console's Forgetting page if you had custom retention policies.

Verification

  • 719 tests pass; mypy --strict clean (121 files); ruff clean.
  • JS↔Python forgetting mirror: exact across a 40-cell sweep.
  • Console verified in an isolated server: 0 console errors; matrix monotonic; clean-break load path confirmed (legacy keys ignored).
  • Two adversarial multi-agent reviews: 0 confirmed defects (low-severity doc/comment fallout fixed).

Design docs in reports/design/; concept/config/API docs updated EN+ZH; CHANGELOG [0.2.0] added.

Note: the pre-push hook reports collection errors because the system Python has a stale PyPI hebb in site-packages; all checks above were run on the editable .venv install.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes - v0.2.0

  • New Features

    • Upgraded forgetting from TTL-based expiry to a retention-score (Ebbinghaus curve) model.
    • Added per-partition forgetting overrides, including a non-destructive preview of upcoming forgetting impact.
    • Restructured the Web Console into memory-lifecycle pages (Manage, Activate, Consolidate, Forget) and added forgetting run history tracking.
    • Added a tuning UI for retention parameters with live curve visualization.
  • Breaking Changes

    • Renamed forgetting configuration keys (removed base_ttl_hours / decay_factor, replaced with half_life_days, k_*, forget_threshold, forget_min_retention_days).
  • Chores

    • Version bumped to v0.2.0.

…model

Reorganize the web console around the four memory-lifecycle stages —
Manage / Activate / Consolidate / Forget — plus a System page for
infrastructure config, with a unified design language, deep-linkable
in-page tabs, and a lifecycle teardown registry.

Migrate forgetting from the dynamic-TTL crossover to a retention-score
(Ebbinghaus) model: retention(idle) = exp(-idle / eff_half_life),
eff_half_life = half_life_days * (1 + k_importance*(importance/10) +
k_access*(access_count/10)), forgotten when retention < threshold. Monotonic
in importance and access, per-region defaults (30-180d half-lives), long-term
timescales. The Forget page is reworked (retention decay curve + threshold
line, importance x access matrix on a 0-100 axis with WCAG-contrast cell text,
real-memory impact preview), hippocampus is hidden from the picker, and a new
forgetting run tracker records each sweep.

BREAKING CHANGE: the global base_ttl_hours / decay_factor settings and the
per-partition override fields of the same name are removed, replaced by
half_life_days / k_importance / k_access / forget_threshold /
forget_min_retention_days (global) and half_life_days / k_importance /
k_access / threshold (per-partition). Legacy hebb.json keys are ignored;
per-partition overrides fall back to the region/global defaults.

Docs (EN+ZH), CHANGELOG, and tests updated; JS<->Python forgetting mirror
verified exact; 0.1.8 -> 0.2.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e2265d9a-be1b-432c-8619-c71646e1fbff

📥 Commits

Reviewing files that changed from the base of the PR and between af971a3 and 82bec09.

📒 Files selected for processing (1)
  • pyproject.toml

📝 Walkthrough

Walkthrough

Version 0.2.0 replaces the TTL-based forgetting model with an Ebbinghaus retention-score model (eff_half_life, retention, threshold), adds per-partition forgetting overrides with a new /forgetting API router (CRUD + preview), and introduces a lightweight forgetting run tracker. The web console is restructured from six flat tabs into a memory-lifecycle sidebar (Manage/Activate/Consolidate/Forget/System) with #page/sub hash routing, a teardown lifecycle registry, shared config-section primitives, a client-side forgetting-math mirror, and expanded EN/ZH i18n.

Changes

Retention-Score Forgetting Model & Per-Partition API

Layer / File(s) Summary
Settings schema: PartitionForgettingOverride + new global fields
src/hebb/config/settings.py, src/hebb/config/loader.py, src/hebb/retrieval/searcher.py
Removes base_ttl_hours/decay_factor; adds half_life_days, k_importance, k_access, forget_threshold, forget_min_retention_days, forgetting_overrides to Settings. Adds PartitionForgettingOverride Pydantic model with bounds. Adds update_forgetting_overrides for atomic override persistence. Decouples recency searcher comment from forgetting model.
Forgetting formula + per-partition param resolution
src/hebb/scheduler/forgetting_job.py
Replaces TTL-hours logic with eff_half_life_days, retention, forget_idle_days, compute_expires_at (days-based). Introduces REGION_FORGET_DEFAULTS, EffectiveForgettingParams, resolve_forgetting_params, and resolve_inherited_params.
Scheduler manager + admin /forget endpoint
src/hebb/scheduler/manager.py, src/hebb/server/routers/admin.py, src/hebb/server/routers/config.py
Updates _run_forgetting to iterate partitions, resolve per-partition params, skip disabled, and record metrics. Rewrites admin /forget to mirror the scheduler sweep with resolve_forgetting_params and run tracking. Excludes forgetting_overrides from scalar config PUT.
Forgetting run tracker
src/hebb/server/forgetting_tracker.py
New module: ForgettingRun dataclass, init_forgetting_tracker, record_run, list_runs. Best-effort atomic JSON manifest with defensive loading.
Forgetting router: config CRUD, preview, run history
src/hebb/server/routers/forgetting.py, src/hebb/server/app.py
New router with GET config, GET runs, PUT/DELETE partition override, POST preview (non-destructive projection). Wired under /api/v1/admin with NoCacheStaticFiles and tracker init at lifespan.
Backend tests
tests/unit/scheduler/test_forgetting_formula.py, tests/unit/scheduler/test_forgetting_overrides.py, tests/unit/scheduler/test_forgetting_job.py, tests/unit/server/test_forgetting_tracker.py, tests/unit/config/test_forgetting_config.py, tests/unit/test_audit_consolidation.py, tests/integration/server/test_forgetting_router.py
Rewrites formula tests for new primitives. Adds override-resolution, tracker init/record/prune/manifest, config round-trip/atomicity, and integration tests for all forgetting endpoints.

Web Console Restructure

Layer / File(s) Summary
SPA router + lifecycle teardown registry
src/hebb/static/js/lifecycle.js, src/hebb/static/js/app.js, src/hebb/static/index.html
New lifecycle.js with onCleanup/runCleanups. Updates app.js to parse #page/sub, call runCleanups() on navigation/language change. Updates sidebar nav links to new route names.
Shared config-section UI primitives
src/hebb/static/js/components/config-section.js
New module: RESTART_KEYS, SENSITIVE_KEYS, esc, offerRestart (with /health polling), buildGenericSection (save-on-change card), renderInput.
New page modules: Manage, Activate, Consolidate, Forget
src/hebb/static/js/components/manage.js, src/hebb/static/js/components/activate.js, src/hebb/static/js/components/consolidate.js, src/hebb/static/js/components/forget.js
renderManage renders stats + tabbed sub-pages. renderActivate replaces renderSearch, adds recall-param config sections. renderConsolidate replaces renderDashboard with run history and config. renderForget adds run history, trigger with confirm dialog, global config, and per-partition tuning.
System page replaces Settings
src/hebb/static/js/components/system.js
Renames renderSettingsrenderSystem with #system/<tab> routing. Scoped to infrastructure tabs (LLM/Embedding/Storage/Server). Removes inline helpers in favor of imported primitives. Fully internationalizes LLM and embedding UI strings.
Refactored components: memories, partitions, graph + forgetting tuning
src/hebb/static/js/components/memories.js, src/hebb/static/js/components/partitions.js, src/hebb/static/js/components/graph.js, src/hebb/static/js/components/forgetting.js, src/hebb/static/js/lib/forgetting-math.js
Memories switches table → card rendering. Partitions adds renderDistribution and i18n. Graph adds onCleanup teardown and i18n. New renderForgettingTuning with sliders, inherit toggles, SVG retention curve, importance/access matrix, debounced preview. New client-side forgetting-math.js mirrors server formula.
CSS + i18n + API wrappers
src/hebb/static/css/style.css, src/hebb/static/js/i18n.js, src/hebb/static/js/api.js
Adds memory card styles, console tab bar, forgetting-tuning page CSS (curve/matrix/responsive). Adds EN and ZH strings for all new modules. Adds five api.js wrappers for forgetting endpoints.

Documentation, Design Reports & Version Bump

Layer / File(s) Summary
Version bump to 0.2.0 and CHANGELOG
src/hebb/__init__.py, pyproject.toml, .release-please-manifest.json, .claude-plugin/plugin.json, CHANGELOG.md
Bumps version from 0.1.8 to 0.2.0 across all manifests. Adds 0.2.0 CHANGELOG entry documenting breaking config changes and new features.
Concept and guide docs
repo_pages/concepts/forgetting.md, repo_pages/concepts/memory-lifecycle.md, repo_pages/guide/..., repo_pages/api/config.md, repo_pages/troubleshooting.md, repo_pages/zh/...
Rewrites forgetting docs (EN/ZH) for half-life/retention model. Updates configuration field references, web-console navigation, embedding setup paths, and troubleshooting references in both EN and ZH.
Design and research reports
reports/design/retention-forgetting-model-2026-06-22.md, reports/design/per-partition-forgetting-ui-2026-06-22.md, reports/design/web-console-restructure-2026-06-22.md, reports/design/on-demand-ml-deps-2026-06-22.md, reports/research/brain-recall-integration-2026-06-17.md
Adds four internal design documents and one neuroscience literature research document.
CLI command-drift audit test
tests/unit/test_audit_distribution.py
Expands audit test to scan all documented hebb … invocations across README and VitePress docs and assert they resolve against the Click command tree. Adds KNOWN_UNIMPLEMENTED_COMMANDS allowlist.

Sequence Diagram

sequenceDiagram
  participant Browser
  participant app.js
  participant forgetting.js
  participant forgetting-math.js
  participant ForgettingRouter
  participant forgetting_job.py
  participant ForgettingTracker

  Browser->>app.js: navigate `#forget`
  app.js->>app.js: runCleanups()
  app.js->>forgetting.js: renderForget(root)
  forgetting.js->>ForgettingRouter: GET /forgetting (config)
  ForgettingRouter->>forgetting_job.py: resolve_forgetting_params per partition
  ForgettingRouter-->>forgetting.js: ForgettingConfigResponse
  forgetting.js->>forgetting-math.js: buildCurve(params)
  forgetting.js->>forgetting-math.js: buildMatrix(params)
  forgetting-math.js-->>forgetting.js: SVG curve + matrix data
  Browser->>forgetting.js: adjust slider (debounced)
  forgetting.js->>ForgettingRouter: POST /forgetting/{id}/preview
  ForgettingRouter-->>forgetting.js: would_forget / would_keep
  Browser->>forgetting.js: click "Save override"
  forgetting.js->>ForgettingRouter: PUT /forgetting/{id}
  ForgettingRouter-->>forgetting.js: PartitionForgettingEntry updated
  Browser->>forgetting.js: click "Clean up now"
  forgetting.js->>ForgettingRouter: POST /admin/forget
  ForgettingRouter->>forgetting_job.py: per-partition sweep
  ForgettingRouter->>ForgettingTracker: record_run(trigger=manual)
  ForgettingRouter-->>forgetting.js: deleted count
  forgetting.js->>ForgettingRouter: GET /forgetting/runs
  ForgettingRouter->>ForgettingTracker: list_runs()
  ForgettingRouter-->>forgetting.js: ForgettingRunsResponse
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 Hop, hop — the clock ticks no more for my memories!
Old TTL hours, farewell, you're erased from the store,
Now half-life and threshold decide who shall stay,
Ebbinghaus curves light the forgetting display.
The sidebar's reborn with four lifecycle doors —
Manage, Activate, Consolidate, Forget evermore! 🧠✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the PR's primary changes: a web console restructure and retention-score forgetting model replacement with a breaking version bump to 0.2.0.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/web-console-forgetting-overhaul

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

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

Copy link
Copy Markdown

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 bumps the version to 0.2.0 and introduces a major overhaul of the forgetting mechanism, replacing the legacy dynamic-TTL model with an Ebbinghaus-inspired retention-score model. It adds per-partition forgetting overrides stored in the configuration, introduces a lightweight forgetting run tracker, and restructures the web console around the four memory lifecycle stages (Manage, Activate, Consolidate, and Forget). Additionally, it adds comprehensive unit and integration tests, including a command-drift guard for documentation. The review feedback correctly identifies two critical platform-specific issues where files are opened or written without specifying UTF-8 encoding, which could cause decoding failures on Windows when handling non-ASCII characters.

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 src/hebb/config/loader.py
raise FileNotFoundError(f"Config file not found: {path}")

with _ConfigLock(path):
with open(path) as f:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

On Windows and other platforms where the default system encoding is not UTF-8, opening the configuration file without specifying an encoding can lead to UnicodeDecodeError if the file contains non-ASCII characters (such as Chinese descriptions or partition names). Please specify encoding="utf-8" when opening the file.

Suggested change
with open(path) as f:
with open(path, encoding="utf-8") as f:

path.parent.mkdir(parents=True, exist_ok=True)
fd, tmp_name = tempfile.mkstemp(dir=str(path.parent), prefix=".manifest.", suffix=".tmp")
try:
with os.fdopen(fd, "w") as f:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

When writing the forgetting run manifest, os.fdopen is used without specifying an encoding. On systems where the default encoding is not UTF-8 (e.g., Windows), this can write the file in a different encoding (like CP1252). Since _load_manifest reads the file using encoding="utf-8", this mismatch will cause a UnicodeDecodeError and corrupt the run history if any error messages contain non-ASCII characters. Please specify encoding="utf-8" in os.fdopen.

Suggested change
with os.fdopen(fd, "w") as f:
with os.fdopen(fd, "w", encoding="utf-8") as f:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/hebb/static/js/components/graph.js (1)

163-169: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clear auto-pause timers on unmount to avoid cross-instance interval kills.

setTimeout callbacks are not tracked/cleared, but they call clearInterval(fa2Interval) on a module-scoped variable. After remount, an old timeout can stop the new layout loop unexpectedly.

🔧 Proposed fix
   const FA2 = globalThis.ForceAtlas2;
   let layoutRunning = true;
+  let autoPauseTimer = null;
+
+  function scheduleAutoPause() {
+    if (autoPauseTimer) clearTimeout(autoPauseTimer);
+    autoPauseTimer = setTimeout(() => {
+      if (layoutRunning) {
+        layoutRunning = false;
+        clearInterval(fa2Interval); fa2Interval = null;
+        layoutBtn.textContent = t('graph.resume_layout');
+      }
+    }, 10000);
+  }

   for (let i = 0; i < 50; i++) runFA2Step();
   renderer.getCamera().animatedReset();
   fa2Interval = setInterval(runFA2Step, 50);

-  setTimeout(() => {
-    if (layoutRunning) {
-      layoutRunning = false;
-      clearInterval(fa2Interval); fa2Interval = null;
-      layoutBtn.textContent = t('graph.resume_layout');
-    }
-  }, 10000);
+  scheduleAutoPause();

   layoutBtn.addEventListener('click', () => {
     if (layoutRunning) {
       layoutRunning = false;
       clearInterval(fa2Interval); fa2Interval = null;
       layoutBtn.textContent = t('graph.resume_layout');
     } else {
       layoutRunning = true;
       fa2Interval = setInterval(runFA2Step, 50);
       layoutBtn.textContent = t('graph.pause_layout');
-      setTimeout(() => {
-        if (layoutRunning) {
-          layoutRunning = false;
-          clearInterval(fa2Interval); fa2Interval = null;
-          layoutBtn.textContent = t('graph.resume_layout');
-        }
-      }, 10000);
+      scheduleAutoPause();
     }
   });

   onCleanup(() => {
+    if (autoPauseTimer) { clearTimeout(autoPauseTimer); autoPauseTimer = null; }
     observer.disconnect();
     if (renderer) { renderer.kill(); renderer = null; }
     if (fa2Interval) { clearInterval(fa2Interval); fa2Interval = null; }
   });

Also applies to: 180-187, 390-394

🤖 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 `@src/hebb/static/js/components/graph.js` around lines 163 - 169, The
setTimeout callbacks are not being tracked and cleared, but they reference the
module-scoped variable fa2Interval and call clearInterval on it. When the
component remounts, old timeouts from previous instances can still fire and
unexpectedly stop the new layout loop. Store the setTimeout references in
tracked variables (similar to how fa2Interval is tracked), then ensure these
timeout IDs are cleared during cleanup or unmount to prevent stale timeouts from
interfering with new layout instances. Apply this fix to all setTimeout calls
that interact with fa2Interval throughout the component.
src/hebb/static/js/components/system.js (1)

279-286: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not persist masked LLM key placeholders on save.

The LLM save path writes any non-empty llm_api_key, but the embedding save path already guards against masked placeholders (****). Mirror that guard here to avoid replacing a real secret with masked text.

🔧 Proposed fix
   saveBtn.addEventListener('click', async () => {
     try {
       const model = modelInput.value.trim();
       const base_url = urlInput.value.trim();
       const api_key = keyInput.value.trim();

       if (model) await api.updateConfig('llm_model', model);
       await api.updateConfig('llm_base_url', base_url || 'null');
-      if (api_key) await api.updateConfig('llm_api_key', api_key);
+      if (api_key && !api_key.includes('****')) {
+        await api.updateConfig('llm_api_key', api_key);
+      }
       success(t('system.toast.llm_saved'));

Also applies to: 804-807

🤖 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 `@src/hebb/static/js/components/system.js` around lines 279 - 286, The
llm_api_key configuration is being saved without checking if it contains a
masked placeholder value like the embedding save path already does. In the
updateConfig call for llm_api_key (around line 285), add a guard condition to
check that api_key is not equal to a masked placeholder (such as '****') before
persisting it, similar to how the embedding save path handles this. This same
guard should also be applied at lines 804-807 where similar configuration
updates occur.
🟡 Minor comments (12)
reports/design/retention-forgetting-model-2026-06-22.md-110-110 (1)

110-110: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the malformed table row.

mem_hippocampus only provides 2 cells here, so the table won’t render cleanly. Add the missing placeholder columns or move that note outside the table.

🤖 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 `@reports/design/retention-forgetting-model-2026-06-22.md` at line 110, The
table row for mem_hippocampus has fewer columns than the other rows in the
table, causing rendering issues. Either add placeholder cells (using | —) to
match the column count of the surrounding table rows, or remove the
mem_hippocampus row entirely and add the note about it never being swept and
being drained by consolidation as separate text outside the table.

Source: Linters/SAST tools

repo_pages/concepts/forgetting.md-148-152 (1)

148-152: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Tag the example formula blocks too.

Both scenario snippets are missing a fence language, so markdownlint will keep warning until these openings are labeled.

Suggested fix
-```
+```text
 eff_half_life = 90 * (1 + 3*(8/10) + 1.5*(10/10)) = 90 * 4.9 = 441 days
 retention(2)  = exp(-2 / 441) = 0.995    (≫ 0.3 threshold)
 forget at     = 441 * ln(1/0.3) ≈ 531 days

</details>






Also applies to: 160-164

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In @repo_pages/concepts/forgetting.md around lines 148 - 152, The markdown code
fence blocks for the formula calculation examples are missing language tags,
which causes markdownlint warnings. Add the language identifier "text" to the
opening fence markers (change totext) for both code blocks containing
the formula calculations with eff_half_life, retention(2), and forget at values.
This needs to be applied to two separate example blocks in the file.


</details>

<!-- cr-comment:v1:edf07a42818b57c6ca62c4d1 -->

_Source: Linters/SAST tools_

</blockquote></details>
<details>
<summary>repo_pages/concepts/memory-lifecycle.md-103-109 (1)</summary><blockquote>

`103-109`: _📐 Maintainability & Code Quality_ | _🟡 Minor_ | _⚡ Quick win_

**Tag this formula fence too.**

Markdownlint flags this block for the same reason: no language tag on the opening fence. `text` is enough if you don't want highlighting.

<details>
<summary>Suggested fix</summary>

```diff
-```
+```text
 eff_half_life  = half_life_days * (1 + k_importance*(importance/10) + k_access*(access_count/10))
 retention(idle) = exp(-idle_days / eff_half_life)
 forget when retention < threshold
 ```
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In `@repo_pages/concepts/memory-lifecycle.md` around lines 103 - 109, The formula
fence block displaying the effective half-life calculation (eff_half_life,
retention, and forget condition) is missing a language tag on the opening triple
backticks, which causes markdownlint to flag it. Add the language identifier
`text` immediately after the opening triple backticks (before the line break) to
properly tag this code fence, just like the suggested fix shows, so the block
complies with markdown linting standards.
```

</details>

<!-- cr-comment:v1:0b374a329a2a9b935782b04b -->

_Source: Linters/SAST tools_

</blockquote></details>
<details>
<summary>repo_pages/zh/guide/web-console.md-66-68 (1)</summary><blockquote>

`66-68`: _🎯 Functional Correctness_ | _🟡 Minor_ | _⚡ Quick win_

**Update this to the new retention terminology.**

`基础 TTL、衰减` is stale in the v0.2.0 docs. This should match the half-life / threshold model used elsewhere so readers don't carry over the old config shape.

<details>
<summary>Suggested fix</summary>

```diff
-下方是**遗忘运行记录**、**全局遗忘默认值**(基础 TTL、衰减、扫描间隔),以及一个**按分区遗忘调参器**
+下方是**遗忘运行记录**、**全局遗忘默认值**(half_life_days、k_importance、k_access、forget_threshold、forget_min_retention_days、扫描间隔),以及一个**按分区遗忘调参器**
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In `@repo_pages/zh/guide/web-console.md` around lines 66 - 68, Update the
retention configuration terminology in the "记忆遗忘(`#forget`)" section to align
with v0.2.0 documentation standards. Replace the outdated "基础 TTL、衰减" language
with the new "half-life / threshold" model terminology to ensure consistency
with the rest of the documentation and prevent readers from applying deprecated
configuration patterns. Review other relevant sections in the documentation to
understand the exact terminology used for the new model and apply it
consistently throughout this section.
```

</details>

<!-- cr-comment:v1:febd05ada3ecf2fea3791bc7 -->

</blockquote></details>
<details>
<summary>repo_pages/concepts/forgetting.md-11-15 (1)</summary><blockquote>

`11-15`: _📐 Maintainability & Code Quality_ | _🟡 Minor_ | _⚡ Quick win_

**Add a language tag to this formula fence.**

Markdownlint is flagging the untyped fence here. `text` is enough if you don't want syntax highlighting.

<details>
<summary>Suggested fix</summary>

```diff
-```
+```text
 eff_half_life = half_life_days * (1 + k_importance * (importance / 10) + k_access * (access_count / 10))
 retention(idle_days) = exp(-idle_days / eff_half_life)
 forget when retention < threshold   ⇔   idle_days > eff_half_life * ln(1 / threshold)
 ```
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In @repo_pages/concepts/forgetting.md around lines 11 - 15, The markdown code
fence containing the formulas for eff_half_life, retention, and forget threshold
is missing a language tag, which is causing markdownlint to flag it. Add the
language tag text to the opening backticks of this code fence (change ``` to

and plain text is sufficient.

Source: Linters/SAST tools

repo_pages/zh/concepts/forgetting.md-11-15 (1)

11-15: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add language tags to the fenced examples.

These bare fences trigger MD040 in markdownlint. Please label them consistently as text so the docs stay lint-clean.

Suggested fix
-```
+```text

Also applies to: 148-152, 160-164

🤖 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 `@repo_pages/zh/concepts/forgetting.md` around lines 11 - 15, Add language tags
to all bare fenced code blocks in the file to comply with markdownlint MD040
rules. For each code fence that currently has no language identifier after the
triple backticks (at lines 11-15, 148-152, and 160-164), append the word `text`
immediately after the opening triple backticks. This will label all mathematical
formulas and similar content blocks consistently as plain text examples and
eliminate the linting violations.

Source: Linters/SAST tools

repo_pages/guide/web-console.md-66-69 (1)

66-69: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Replace the legacy TTL wording here.

This paragraph still describes the Forget page defaults in base TTL/decay terms, which no longer matches the retention-score model exposed by the new console and config docs.

Suggested fix
- the **global forgetting defaults** (base TTL, decay, sweep interval),
+ the **global forgetting defaults** (half-life, importance/access weights, threshold, minimum retained lifetime, sweep interval),
🤖 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 `@repo_pages/guide/web-console.md` around lines 66 - 69, The Forget (`#forget`)
section still uses legacy terminology with references to "base TTL" and "decay"
which does not align with the new retention-score model used in the current
console and config documentation. Replace the outdated TTL and decay terminology
in this paragraph with appropriate retention-score model terminology to
accurately describe how the forgetting defaults and per-partition tuning work in
the new console implementation.
src/hebb/static/js/components/memories.js-226-229 (1)

226-229: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle create-modal partition fetch failures.

Lines 226-229 can reject without user feedback if api.listPartitions() fails. Wrap this handler in try/catch and toast the error.

🔧 Proposed fix
   root.querySelector('`#btn-create`').onclick = async () => {
-    const partitions = await api.listPartitions();
-    showCreateModal(root, partitions);
+    try {
+      const partitions = await api.listPartitions();
+      showCreateModal(root, partitions);
+    } catch (e) {
+      error(e.message);
+    }
   };
🤖 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 `@src/hebb/static/js/components/memories.js` around lines 226 - 229, The async
onclick handler for root.querySelector('`#btn-create`') lacks error handling for
the api.listPartitions() call, which means if the API request fails, the user
receives no feedback. Wrap the api.listPartitions() await call in a try/catch
block, keeping the showCreateModal() call in the try block and adding error
handling in the catch block that toasts the error message to inform the user of
the failure.
src/hebb/static/js/app.js-103-107 (1)

103-107: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Resync currentSub from location.hash before language rerender.

Line [107] rerenders with cached currentSub, but sub-tabs are updated via history.replaceState(...) in page modules (no hashchange), so currentSub can be stale and restore the wrong tab on language switch.

Proposed fix
 function applyLang(lang) {
   setLang(lang);
   langLabel.textContent = lang.toUpperCase();
   updateNavLabels();
   // Re-render current page (and its active sub-tab) to apply translations
   if (currentPage && pages[currentPage]) {
+    const raw = location.hash.replace(/^`#/`, '');
+    const [hashPage, hashSub] = raw.split('/');
+    if (pages[hashPage]) {
+      currentPage = hashPage;
+      currentSub = hashSub || null;
+    }
     runCleanups();
     content.innerHTML = '';
     pages[currentPage](content, currentSub);
   }
 }
🤖 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 `@src/hebb/static/js/app.js` around lines 103 - 107, The rerender logic at the
pages[currentPage] call is using a potentially stale currentSub variable because
sub-tabs are updated via history.replaceState() which does not trigger
hashchange events, leaving currentSub out of sync with location.hash. Before
calling pages[currentPage](content, currentSub), extract the current sub-tab
identifier from location.hash to ensure currentSub is resynchronized with the
actual current state before rerendering the page content.
src/hebb/static/js/components/forgetting.js-437-439 (1)

437-439: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Localize the access-count table header.

The hardcoded acc string bypasses i18n and leaks English text in non-English UI.

Suggested patch
-        <th>acc</th><th>${t('forgetting.col_age')}</th><th></th></tr></thead><tbody>`;
+        <th>${t('forgetting.access_count')}</th><th>${t('forgetting.col_age')}</th><th></th></tr></thead><tbody>`;
🤖 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 `@src/hebb/static/js/components/forgetting.js` around lines 437 - 439, The
table header in the forgetting impact table contains a hardcoded acc string that
is not localized, while all other column headers use the i18n translation
function t(). Replace the hardcoded acc string in the table header row with a
properly localized translation call using t() function with an appropriate key
(such as t('forgetting.col_access_count') or similar) to match the pattern of
the other headers like col_content, importance, and col_age.
src/hebb/static/css/style.css-340-343 (1)

340-343: 📐 Maintainability & Code Quality | 🟡 Minor

Replace deprecated word-break: break-word with standards-compliant overflow-wrap: anywhere.

The word-break: break-word property is deprecated per CSS standards (MDN, W3C). Use overflow-wrap: anywhere instead for better standards compliance and consistent behavior across browsers.

Suggested patch
 .mem-card-body {
   font-size: 13.5px; line-height: 1.65; color: var(--text-primary);
-  white-space: pre-wrap; word-break: break-word;
+  white-space: pre-wrap;
+  overflow-wrap: anywhere;
+  word-break: normal;
 }
🤖 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 `@src/hebb/static/css/style.css` around lines 340 - 343, In the .mem-card-body
CSS class, replace the deprecated property `word-break: break-word` with the
standards-compliant `overflow-wrap: anywhere`. This ensures better compatibility
and aligns with current CSS standards. Remove the word-break line and add
overflow-wrap with the value anywhere to maintain the same text wrapping
behavior while using a non-deprecated approach.

Source: Linters/SAST tools

src/hebb/retrieval/searcher.py-31-36 (1)

31-36: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the half-life math note in the recency comment.

Line 31 currently states that score = decay ** hours with 0.693 is “≈ a 1-day half-life”, but that base yields ~1.9-hour half-life. Please align either the formula text or the half-life statement to avoid mis-tuning later.

🤖 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 `@src/hebb/retrieval/searcher.py` around lines 31 - 36, The comment for the
recency decay base constant contains a mathematical inconsistency: it states
that decay=0.693 yields approximately a 1-day half-life in the formula
score=decay**hours, but this base actually produces approximately a 1.9-hour
half-life. Correct this discrepancy by either updating the decay value to
achieve the intended 1-day half-life (approximately 0.967 for 24-hour half-life)
or correcting the stated half-life duration in the comment to reflect what 0.693
actually produces. Ensure the comment accurately documents the recency weighting
behavior so future developers tune it correctly.
🧹 Nitpick comments (1)
src/hebb/server/routers/admin.py (1)

176-187: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use batched expiry persistence in manual sweep for parity with scheduler performance.

This endpoint still writes expiry one row at a time. On large partitions that recreates the O(N) write/commit pressure the scheduler path already avoided.

Suggested refactor
+    expiry_updates: list[tuple[str, str]] = []
     ...
             for memory in memories:
                 expires_at = compute_expires_at(...)
-                await memory_store.update_expiry(memory.id, expires_at.isoformat())
                 if expires_at < now:
-                    await purge_memory(memory_store, kg, memory.id, save=False)
-                    deleted += 1
+                    to_delete.append(memory.id)
+                else:
+                    expiry_updates.append((memory.id, expires_at.isoformat()))
+
+        if expiry_updates:
+            batch = getattr(memory_store, "update_expiry_batch", None)
+            if callable(batch):
+                await batch(expiry_updates)
+            else:
+                for mid, iso in expiry_updates:
+                    await memory_store.update_expiry(mid, iso)
🤖 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 `@src/hebb/server/routers/admin.py` around lines 176 - 187, The current
implementation in the admin router calls update_expiry one memory at a time
within the for loop over memories, creating O(N) individual database writes.
Instead of calling await memory_store.update_expiry for each memory
individually, collect all the expiry updates (pairing each memory.id with its
computed expires_at) into a single data structure and make a single batched
update call to memory_store, similar to how the scheduler path handles this, to
avoid the write/commit pressure of individual row updates.
🤖 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 `@reports/design/per-partition-forgetting-ui-2026-06-22.md`:
- Around line 23-25: The design document describes per-partition parameters
using the old TTL model (base_ttl_hours and decay_factor), but the actual live
API contract at /api/v1/admin/forgetting exposes different retention model
parameters (half_life_days, k_importance, k_access, threshold, and enabled).
Update all references to the old parameter names throughout the document
(including sections at lines 23-25, 32-33, 53-63, and 78-88) to use the current
retention model field names so the UI specification matches the actual contract
the frontend will consume.

In `@reports/design/web-console-restructure-2026-06-22.md`:
- Line 25: Update the Forget feature description to replace the outdated
retention field names with the new retention model fields. In the table row
describing the forget functionality, replace the references to `base TTL`,
`decay`, and `sweep interval` with the correct field names: `half_life_days`,
`k_importance`, `k_access`, `threshold`, and per-partition overrides. This
applies to all mentions of these fields throughout the document (lines 25,
30-32) to ensure consistency with the current admin UI contract for the
retention model.

In `@src/hebb/scheduler/forgetting_job.py`:
- Around line 117-119: The `compute_expires_at` function can raise an
`OverflowError` when `idle` from `forget_idle_days` is too large to fit in a
timedelta. Instead of using fractional seconds for capping, implement a
try/except block around the final return statement that catches `OverflowError`
and returns `datetime.max` as a safe fallback, or alternatively cap the `idle`
value to an integer-day limit (such as timedelta's maximum days of 999999999)
before creating the timedelta from `memory.last_accessed_at +
timedelta(days=idle)` to prevent the overflow from occurring in the first place.

In `@src/hebb/server/routers/admin.py`:
- Around line 189-193: The manual purge operation in the `/forget` endpoint
mutates and saves the knowledge graph without acquiring the shared graph lock,
creating a race condition with concurrent scheduler writes. Wrap the critical
section containing the purge_memory calls and the subsequent kg.save() operation
with the knowledge_graph.lock context manager (using async with) to ensure
mutual exclusion. This mirrors the locking pattern used in the scheduler path
and prevents interleaved graph writes from persisting inconsistent state.

In `@src/hebb/server/routers/forgetting.py`:
- Around line 234-236: The code at lines 234-236 and 252-253 reconstructs the
full forgetting_overrides map from an in-memory snapshot of
settings.forgetting_overrides and then persists it entirely via
_persist_and_sync, creating a lost-update vulnerability where concurrent
requests overwriting different partitions can lose changes. Replace this
full-map rebuild approach with an atomic per-partition mutation pattern: read
the current on-disk map while holding a lock, apply only the single partition
key's add or remove operation (for the set path at line 234-236 and the clear
path at lines 252-253), validate the change, and write back only the updated
map. This ensures that each concurrent modification updates from the current
disk state rather than a potentially stale in-memory snapshot.

In `@src/hebb/static/js/app.js`:
- Around line 46-57: The navigate function invokes the page renderer via
pages[page](content, currentSub) without any mechanism to cancel or guard
against stale async operations from the previous page. To fix this, introduce a
navigation epoch or cancellation token (such as a simple counter variable that
increments on each navigate call), pass this token to each page renderer, and
ensure that any async operations within those renderers (like the
api.getConfig() call mentioned) check whether the navigation token is still
current before mutating the content element. This prevents stale renders from
previous pages from modifying the DOM after navigation has already cleared and
reset the view.

In `@src/hebb/static/js/components/consolidate.js`:
- Around line 300-301: The catch block at line 301 is directly injecting the
error message into innerHTML using string interpolation, which creates an XSS
vulnerability since error messages can contain untrusted content. Replace the
innerHTML assignment with a safer approach: either create a div element using
createElement, set the error message using textContent property (which treats
content as plain text rather than HTML), and append it to configRoot, or
alternatively use textContent to set the div content directly without HTML
parsing.

In `@src/hebb/static/js/components/forget.js`:
- Around line 192-193: The catch block at the end of the file is inserting
e.message directly into globalRoot.innerHTML without escaping, creating an XSS
vulnerability if the error contains malicious HTML. Instead of using e.message
raw in the template literal, escape or sanitize the error message before
injecting it into the innerHTML string. You can use a utility function like
textContent assignment or an HTML escaping function to safely convert the error
message to plain text before embedding it in the HTML template.

In `@src/hebb/static/js/components/forgetting.js`:
- Around line 463-469: The refreshImpact function makes async preview API calls
that can overlap when rapid changes occur, allowing older responses to overwrite
newer ones. Guard the renderImpact call by implementing a request tracking
mechanism such as a counter or timestamp that increments before each
api.previewForgetting call, then verify that the completed response corresponds
to the most recent request before calling renderImpact to ensure only the latest
preview response gets rendered.

In `@src/hebb/static/js/components/memories.js`:
- Around line 31-35: The loadList function caches DOM references at the start
(list, info, pag elements) but these can become detached if the user switches
tabs while the api.listMemories await is pending on line 39. Add a mount check
before the await to verify the root element is still in the document, and add
another mount check immediately after the await completes before performing any
DOM operations (innerHTML updates, event listeners, etc.). This ensures that
stale DOM references are not manipulated after tab unmounting. Apply the same
guard pattern to the other async operations mentioned at lines 72-74.

In `@src/hebb/static/js/components/partitions.js`:
- Around line 34-40: The loadList function has a race condition where
api.listPartitions() can resolve after the DOM has changed (tab replaced),
causing stale updates to the container element. After awaiting the
api.listPartitions() call, add a check to verify that the container element is
still present in the root element before proceeding with innerHTML updates and
handler binding. This check should validate that the container reference is
still valid in the DOM to prevent mutations on detached or replaced elements.

In `@src/hebb/static/js/components/system.js`:
- Around line 89-91: The error message from the catch block is being directly
injected into innerHTML without proper escaping, which creates a potential XSS
vulnerability. In the catch block where
root.querySelector('`#system-panel`').innerHTML is set with e.message, escape the
error message text before inserting it into the DOM. You can create a helper
function or use a method like creating a temporary text element and extracting
its textContent, or use a library utility for HTML escaping, to ensure that any
HTML special characters in the error message are properly encoded before being
included in the innerHTML assignment.

In `@tests/integration/server/test_forgetting_router.py`:
- Around line 44-49: The patch target on line 45 is incorrect for the
`create_app` function's usage. Instead of patching
`hebb.embedding.factory.create_embedder`, patch the symbol where it is imported
and used within the `hebb.server.app` module. Change the patch target to
`hebb.server.app.create_embedder` to ensure the patch intercepts the actual
reference used by the `create_app` function, preventing issues with cached
module-level imports from earlier test runs.

In `@tests/unit/test_audit_distribution.py`:
- Around line 195-211: The _doc_files() function uses check=True in the
subprocess.run() call for the git ls-files command, which causes it to raise an
exception and hard-fail when Git is unavailable (such as in sdist/exported trees
without .git or environments without git installed). Remove the check=True
parameter from the subprocess.run() call and instead check the returncode after
execution. If the git command fails (returncode is non-zero), return an empty
list instead of raising an exception, allowing the test to continue with its
actual drift assertions.

---

Outside diff comments:
In `@src/hebb/static/js/components/graph.js`:
- Around line 163-169: The setTimeout callbacks are not being tracked and
cleared, but they reference the module-scoped variable fa2Interval and call
clearInterval on it. When the component remounts, old timeouts from previous
instances can still fire and unexpectedly stop the new layout loop. Store the
setTimeout references in tracked variables (similar to how fa2Interval is
tracked), then ensure these timeout IDs are cleared during cleanup or unmount to
prevent stale timeouts from interfering with new layout instances. Apply this
fix to all setTimeout calls that interact with fa2Interval throughout the
component.

In `@src/hebb/static/js/components/system.js`:
- Around line 279-286: The llm_api_key configuration is being saved without
checking if it contains a masked placeholder value like the embedding save path
already does. In the updateConfig call for llm_api_key (around line 285), add a
guard condition to check that api_key is not equal to a masked placeholder (such
as '****') before persisting it, similar to how the embedding save path handles
this. This same guard should also be applied at lines 804-807 where similar
configuration updates occur.

---

Minor comments:
In `@repo_pages/concepts/forgetting.md`:
- Around line 148-152: The markdown code fence blocks for the formula
calculation examples are missing language tags, which causes markdownlint
warnings. Add the language identifier "text" to the opening fence markers
(change ``` to ```text) for both code blocks containing the formula calculations
with eff_half_life, retention(2), and forget at values. This needs to be applied
to two separate example blocks in the file.
- Around line 11-15: The markdown code fence containing the formulas for
eff_half_life, retention, and forget threshold is missing a language tag, which
is causing markdownlint to flag it. Add the language tag `text` to the opening
backticks of this code fence (change ``` to ```text) since these mathematical
formulas do not require syntax highlighting and plain text is sufficient.

In `@repo_pages/concepts/memory-lifecycle.md`:
- Around line 103-109: The formula fence block displaying the effective
half-life calculation (eff_half_life, retention, and forget condition) is
missing a language tag on the opening triple backticks, which causes
markdownlint to flag it. Add the language identifier `text` immediately after
the opening triple backticks (before the line break) to properly tag this code
fence, just like the suggested fix shows, so the block complies with markdown
linting standards.

In `@repo_pages/guide/web-console.md`:
- Around line 66-69: The Forget (`#forget`) section still uses legacy
terminology with references to "base TTL" and "decay" which does not align with
the new retention-score model used in the current console and config
documentation. Replace the outdated TTL and decay terminology in this paragraph
with appropriate retention-score model terminology to accurately describe how
the forgetting defaults and per-partition tuning work in the new console
implementation.

In `@repo_pages/zh/concepts/forgetting.md`:
- Around line 11-15: Add language tags to all bare fenced code blocks in the
file to comply with markdownlint MD040 rules. For each code fence that currently
has no language identifier after the triple backticks (at lines 11-15, 148-152,
and 160-164), append the word `text` immediately after the opening triple
backticks. This will label all mathematical formulas and similar content blocks
consistently as plain text examples and eliminate the linting violations.

In `@repo_pages/zh/guide/web-console.md`:
- Around line 66-68: Update the retention configuration terminology in the
"记忆遗忘(`#forget`)" section to align with v0.2.0 documentation standards. Replace
the outdated "基础 TTL、衰减" language with the new "half-life / threshold" model
terminology to ensure consistency with the rest of the documentation and prevent
readers from applying deprecated configuration patterns. Review other relevant
sections in the documentation to understand the exact terminology used for the
new model and apply it consistently throughout this section.

In `@reports/design/retention-forgetting-model-2026-06-22.md`:
- Line 110: The table row for mem_hippocampus has fewer columns than the other
rows in the table, causing rendering issues. Either add placeholder cells (using
| —) to match the column count of the surrounding table rows, or remove the
mem_hippocampus row entirely and add the note about it never being swept and
being drained by consolidation as separate text outside the table.

In `@src/hebb/retrieval/searcher.py`:
- Around line 31-36: The comment for the recency decay base constant contains a
mathematical inconsistency: it states that decay=0.693 yields approximately a
1-day half-life in the formula score=decay**hours, but this base actually
produces approximately a 1.9-hour half-life. Correct this discrepancy by either
updating the decay value to achieve the intended 1-day half-life (approximately
0.967 for 24-hour half-life) or correcting the stated half-life duration in the
comment to reflect what 0.693 actually produces. Ensure the comment accurately
documents the recency weighting behavior so future developers tune it correctly.

In `@src/hebb/static/css/style.css`:
- Around line 340-343: In the .mem-card-body CSS class, replace the deprecated
property `word-break: break-word` with the standards-compliant `overflow-wrap:
anywhere`. This ensures better compatibility and aligns with current CSS
standards. Remove the word-break line and add overflow-wrap with the value
anywhere to maintain the same text wrapping behavior while using a
non-deprecated approach.

In `@src/hebb/static/js/app.js`:
- Around line 103-107: The rerender logic at the pages[currentPage] call is
using a potentially stale currentSub variable because sub-tabs are updated via
history.replaceState() which does not trigger hashchange events, leaving
currentSub out of sync with location.hash. Before calling
pages[currentPage](content, currentSub), extract the current sub-tab identifier
from location.hash to ensure currentSub is resynchronized with the actual
current state before rerendering the page content.

In `@src/hebb/static/js/components/forgetting.js`:
- Around line 437-439: The table header in the forgetting impact table contains
a hardcoded acc string that is not localized, while all other column headers use
the i18n translation function t(). Replace the hardcoded acc string in the table
header row with a properly localized translation call using t() function with an
appropriate key (such as t('forgetting.col_access_count') or similar) to match
the pattern of the other headers like col_content, importance, and col_age.

In `@src/hebb/static/js/components/memories.js`:
- Around line 226-229: The async onclick handler for
root.querySelector('`#btn-create`') lacks error handling for the
api.listPartitions() call, which means if the API request fails, the user
receives no feedback. Wrap the api.listPartitions() await call in a try/catch
block, keeping the showCreateModal() call in the try block and adding error
handling in the catch block that toasts the error message to inform the user of
the failure.

---

Nitpick comments:
In `@src/hebb/server/routers/admin.py`:
- Around line 176-187: The current implementation in the admin router calls
update_expiry one memory at a time within the for loop over memories, creating
O(N) individual database writes. Instead of calling await
memory_store.update_expiry for each memory individually, collect all the expiry
updates (pairing each memory.id with its computed expires_at) into a single data
structure and make a single batched update call to memory_store, similar to how
the scheduler path handles this, to avoid the write/commit pressure of
individual row updates.
🪄 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 Plus

Run ID: ea0e1f00-bfd6-4527-bfea-437f148b7dcb

📥 Commits

Reviewing files that changed from the base of the PR and between 7af92c6 and af971a3.

📒 Files selected for processing (59)
  • .claude-plugin/plugin.json
  • .release-please-manifest.json
  • CHANGELOG.md
  • pyproject.toml
  • repo_pages/api/config.md
  • repo_pages/concepts/forgetting.md
  • repo_pages/concepts/memory-lifecycle.md
  • repo_pages/guide/configuration.md
  • repo_pages/guide/switch-embedding-model.md
  • repo_pages/guide/web-console.md
  • repo_pages/troubleshooting.md
  • repo_pages/zh/api/config.md
  • repo_pages/zh/concepts/forgetting.md
  • repo_pages/zh/concepts/memory-lifecycle.md
  • repo_pages/zh/guide/configuration.md
  • repo_pages/zh/guide/switch-embedding-model.md
  • repo_pages/zh/guide/web-console.md
  • repo_pages/zh/troubleshooting.md
  • reports/design/on-demand-ml-deps-2026-06-22.md
  • reports/design/per-partition-forgetting-ui-2026-06-22.md
  • reports/design/retention-forgetting-model-2026-06-22.md
  • reports/design/web-console-restructure-2026-06-22.md
  • reports/research/brain-recall-integration-2026-06-17.md
  • src/hebb/__init__.py
  • src/hebb/config/loader.py
  • src/hebb/config/settings.py
  • src/hebb/retrieval/searcher.py
  • src/hebb/scheduler/forgetting_job.py
  • src/hebb/scheduler/manager.py
  • src/hebb/server/app.py
  • src/hebb/server/forgetting_tracker.py
  • src/hebb/server/routers/admin.py
  • src/hebb/server/routers/config.py
  • src/hebb/server/routers/forgetting.py
  • src/hebb/static/css/style.css
  • src/hebb/static/index.html
  • src/hebb/static/js/api.js
  • src/hebb/static/js/app.js
  • src/hebb/static/js/components/activate.js
  • src/hebb/static/js/components/config-section.js
  • src/hebb/static/js/components/consolidate.js
  • src/hebb/static/js/components/forget.js
  • src/hebb/static/js/components/forgetting.js
  • src/hebb/static/js/components/graph.js
  • src/hebb/static/js/components/manage.js
  • src/hebb/static/js/components/memories.js
  • src/hebb/static/js/components/partitions.js
  • src/hebb/static/js/components/system.js
  • src/hebb/static/js/i18n.js
  • src/hebb/static/js/lib/forgetting-math.js
  • src/hebb/static/js/lifecycle.js
  • tests/integration/server/test_forgetting_router.py
  • tests/unit/config/test_forgetting_config.py
  • tests/unit/scheduler/test_forgetting_formula.py
  • tests/unit/scheduler/test_forgetting_job.py
  • tests/unit/scheduler/test_forgetting_overrides.py
  • tests/unit/server/test_forgetting_tracker.py
  • tests/unit/test_audit_consolidation.py
  • tests/unit/test_audit_distribution.py

Comment on lines +23 to +25
1. **Per-partition params = `base_ttl_hours`, `decay_factor`, `enabled`.**
The two formula knobs plus an on/off switch (off = never forget that
partition). `min_ttl`/grace stay global module constants (out of scope).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Align this spec with the retention-model contract.

These sections still describe the removed TTL model (base_ttl_hours / decay_factor), but the live /api/v1/admin/forgetting contract now exposes half_life_days, k_importance, k_access, threshold, and enabled. As per PR objectives, this UI needs to follow the retention fields so frontend work doesn’t target a stale schema.

Also applies to: 32-33, 53-63, 78-88

🤖 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 `@reports/design/per-partition-forgetting-ui-2026-06-22.md` around lines 23 -
25, The design document describes per-partition parameters using the old TTL
model (base_ttl_hours and decay_factor), but the actual live API contract at
/api/v1/admin/forgetting exposes different retention model parameters
(half_life_days, k_importance, k_access, threshold, and enabled). Update all
references to the old parameter names throughout the document (including
sections at lines 23-25, 32-33, 53-63, and 78-88) to use the current retention
model field names so the UI specification matches the actual contract the
frontend will consume.

| `manage` | Manage / 记忆管理 | **Overview stat band** (total memories · partitions · graph nodes/edges) on top, then in-page **tabs**: 记忆 (list + add) · 分区 (distribution chart + list + add + edit) · 图谱 (knowledge graph) |
| `activate` | Activate / 记忆激活 | **Recall test** (search w/ weight sliders + results) on top, then **Recall parameters** (recall pipeline toggles · rerank · scoring weights) |
| `consolidate` | Consolidate / 记忆巩固 | **Trigger** (Organize now + pending/auto-next meta + interrupted note) · **Run records** (history w/ live log) · **Consolidation config** (time, concurrency, max tokens, drain-empty) |
| `forget` | Forget / 记忆遗忘 | **Trigger** (Clean up now) · **Forget records** (NEW tracker) · **Global config** (base TTL, decay, sweep interval) · **Per-partition tuning** (curve + matrix + impact + override) |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Update the Forget page copy to the new retention fields.

base TTL / decay / sweep interval no longer matches the admin UI contract. The Forget page should describe half_life_days, k_importance, k_access, threshold, and per-partition overrides instead. As per PR objectives, the new Forget workflow is wired to the retention model.

Also applies to: 30-32

🤖 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 `@reports/design/web-console-restructure-2026-06-22.md` at line 25, Update the
Forget feature description to replace the outdated retention field names with
the new retention model fields. In the table row describing the forget
functionality, replace the references to `base TTL`, `decay`, and `sweep
interval` with the correct field names: `half_life_days`, `k_importance`,
`k_access`, `threshold`, and per-partition overrides. This applies to all
mentions of these fields throughout the document (lines 25, 30-32) to ensure
consistency with the current admin UI contract for the retention model.

Comment on lines +117 to +119
eff = eff_half_life_days(half_life_days, k_importance, k_access, memory.importance_score, memory.access_count)
idle = forget_idle_days(eff, threshold, min_retention_days)
return memory.last_accessed_at + timedelta(days=idle)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
import math
from datetime import datetime, timezone, timedelta

half_life_days = 3650.0
k_access = 10.0
threshold = 0.3
access_count = 300000  # large but plausible over long-lived hot memories

eff = half_life_days * (1 + k_access * (access_count / 10.0))
idle = eff * math.log(1.0 / threshold)
print("idle_days =", idle)

now = datetime.now(timezone.utc)
try:
    _ = now + timedelta(days=idle)
    print("No overflow")
except OverflowError as e:
    print("OverflowError:", e)
PY

Repository: afx-team/hebb-mind

Length of output: 252


🏁 Script executed:

cd /tmp && find . -name "forgetting_job.py" -type f 2>/dev/null | head -5

Repository: afx-team/hebb-mind

Length of output: 156


🏁 Script executed:

git ls-files | grep -E "(forgetting|scheduler)" | head -20

Repository: afx-team/hebb-mind

Length of output: 976


🏁 Script executed:

cat -n src/hebb/scheduler/forgetting_job.py

Repository: afx-team/hebb-mind

Length of output: 9332


🏁 Script executed:

#!/bin/bash
python - <<'PY'
import math
from datetime import datetime, timezone, timedelta

# Test the proposed fix logic
half_life_days = 3650.0
k_access = 10.0
threshold = 0.3
access_count = 300000

eff = half_life_days * (1 + k_access * (access_count / 10.0))
idle = eff * math.log(1.0 / threshold)
print(f"idle_days (uncapped) = {idle}")

# Simulate the proposed fix
now = datetime.now(timezone.utc)
print(f"current datetime: {now}")
print(f"datetime.max: {datetime.max}")

max_dt = datetime.max.replace(tzinfo=now.tzinfo)
print(f"max_dt with same tzinfo: {max_dt}")

max_idle_days = max((max_dt - now).total_seconds() / 86400.0, 0.0)
print(f"max_idle_days = {max_idle_days}")

idle_capped = min(idle, max_idle_days)
print(f"idle_capped = {idle_capped}")

# Try adding the capped idle
try:
    result = now + timedelta(days=idle_capped)
    print(f"Success: {result}")
except OverflowError as e:
    print(f"OverflowError: {e}")
PY

Repository: afx-team/hebb-mind

Length of output: 456


🏁 Script executed:

#!/bin/bash
python - <<'PY'
import math
from datetime import datetime, timezone, timedelta

# Edge case: test with a datetime far in the past
old_date = datetime(2000, 1, 1, tzinfo=timezone.utc)
print(f"old_date: {old_date}")

half_life_days = 3650.0
k_access = 10.0
threshold = 0.3
access_count = 300000

eff = half_life_days * (1 + k_access * (access_count / 10.0))
idle = eff * math.log(1.0 / threshold)
print(f"idle_days (uncapped) = {idle}")

# Proposed fix with old date
max_dt = datetime.max.replace(tzinfo=old_date.tzinfo)
max_idle_days = max((max_dt - old_date).total_seconds() / 86400.0, 0.0)
print(f"max_idle_days for old date = {max_idle_days}")

idle_capped = min(idle, max_idle_days)
print(f"idle_capped = {idle_capped}")

# Try adding the capped idle
try:
    result = old_date + timedelta(days=idle_capped)
    print(f"Success: {result}")
except OverflowError as e:
    print(f"OverflowError: {e}")
PY

Repository: afx-team/hebb-mind

Length of output: 336


🏁 Script executed:

#!/bin/bash
python - <<'PY'
import math
from datetime import datetime, timezone, timedelta

# The issue with the proposed fix: it doesn't account for old dates properly
old_date = datetime(2000, 1, 1, tzinfo=timezone.utc)

half_life_days = 3650.0
k_access = 10.0
threshold = 0.3
access_count = 300000

eff = half_life_days * (1 + k_access * (access_count / 10.0))
idle = eff * math.log(1.0 / threshold)
print(f"idle_days (uncapped) = {idle}")

# The CORRECT fix: cap the result to datetime.max, not the idle days
max_dt = datetime.max.replace(tzinfo=old_date.tzinfo)
max_idle_days = max((max_dt - old_date).total_seconds() / 86400.0, 0.0)
print(f"max_idle_days (from old_date to max): {max_idle_days}")

idle_capped = min(idle, max_idle_days)
print(f"idle_capped = {idle_capped}")

result_before = old_date + timedelta(days=idle_capped)
print(f"result before capping: {result_before}")

# Cap the result itself to datetime.max
result = min(result_before, max_dt)
print(f"result after capping to datetime.max: {result}")

# Verify no overflow
try:
    _ = old_date + timedelta(days=idle_capped)
except OverflowError as e:
    print(f"OverflowError still occurs: {e}")
PY

Repository: afx-team/hebb-mind

Length of output: 383


🏁 Script executed:

#!/bin/bash
python - <<'PY'
import math
from datetime import datetime, timezone, timedelta

# The correct approach: cap the RESULT, not the days
old_date = datetime(2000, 1, 1, tzinfo=timezone.utc)

half_life_days = 3650.0
k_access = 10.0
threshold = 0.3
access_count = 300000

eff = half_life_days * (1 + k_access * (access_count / 10.0))
idle = eff * math.log(1.0 / threshold)
print(f"idle_days (uncapped) = {idle}")

max_dt = datetime.max.replace(tzinfo=old_date.tzinfo)

# Better approach: use a reasonable max timedelta instead
max_timedelta_days = 999999999.0
idle_capped = min(idle, max_timedelta_days)
print(f"idle_capped = {idle_capped}")

try:
    result = old_date + timedelta(days=idle_capped)
    print(f"Success with max_timedelta: {result}")
except OverflowError as e:
    print(f"OverflowError: {e}")
    # Fallback: cap to datetime.max
    result = min(old_date + timedelta(days=idle_capped), max_dt)
    print(f"After fallback: {result}")
PY

Repository: afx-team/hebb-mind

Length of output: 559


🏁 Script executed:

#!/bin/bash
python - <<'PY'
import math
from datetime import datetime, timezone, timedelta

# Test what timedelta max actually is
try:
    td = timedelta(days=999999999)
    print(f"timedelta(days=999999999) works: {td}")
except OverflowError as e:
    print(f"OverflowError: {e}")

try:
    td = timedelta(days=999999999.0)
    print(f"timedelta(days=999999999.0) works: {td}")
except OverflowError as e:
    print(f"OverflowError: {e}")

try:
    td = timedelta(days=1000000000)
    print(f"timedelta(days=1000000000) works: {td}")
except OverflowError as e:
    print(f"OverflowError: {e}")
    
# Test the actual computation with a reasonable bound
old_date = datetime(2000, 1, 1, tzinfo=timezone.utc)
max_dt = datetime.max.replace(tzinfo=old_date.tzinfo)

idle = 1318354615.2376359
idle_capped = min(idle, 999999999)
print(f"\nidle_capped to 999999999: {idle_capped}")

try:
    result = old_date + timedelta(days=idle_capped)
    print(f"Result: {result}")
except OverflowError as e:
    print(f"OverflowError: {e}")
PY

Repository: afx-team/hebb-mind

Length of output: 413


🏁 Script executed:

#!/bin/bash
python - <<'PY'
import math
from datetime import datetime, timezone, timedelta

# The KEY insight: the max_idle_days calculation depends on the starting date
# For a date in 2000, there are fewer days left to 9999
# For a date in 2026, there are fewer days left to 9999

old_date = datetime(2000, 1, 1, tzinfo=timezone.utc)
recent_date = datetime(2026, 6, 23, tzinfo=timezone.utc)
max_dt = datetime.max.replace(tzinfo=timezone.utc)

print(f"From 2000-01-01 to 9999-12-31: {(max_dt - old_date).days} days")
print(f"From 2026-06-23 to 9999-12-31: {(max_dt - recent_date).days} days")

idle = 1318354615.2376359

# The proposed fix tries to cap idle to max_idle_days, but it doesn't work
# for dates in the past because you can't add that many days
max_idle_old = (max_dt - old_date).total_seconds() / 86400.0
idle_capped = min(idle, max_idle_old)
print(f"\nidle_capped for old_date: {idle_capped}")

try:
    result = old_date + timedelta(days=idle_capped)
    print(f"Success: {result}")
except OverflowError as e:
    print(f"OverflowError: {e}")

# For a recent date, the calculation works
max_idle_recent = (max_dt - recent_date).total_seconds() / 86400.0
idle_capped_recent = min(idle, max_idle_recent)
print(f"\nidle_capped for recent_date: {idle_capped_recent}")

try:
    result = recent_date + timedelta(days=idle_capped_recent)
    print(f"Success: {result}")
except OverflowError as e:
    print(f"OverflowError: {e}")
PY

Repository: afx-team/hebb-mind

Length of output: 399


🏁 Script executed:

#!/bin/bash
python - <<'PY'
import math
from datetime import datetime, timezone, timedelta

old_date = datetime(2000, 1, 1, tzinfo=timezone.utc)
max_dt = datetime.max.replace(tzinfo=timezone.utc)

print(f"max_dt: {max_dt}")
print(f"(max_dt - old_date).days: {(max_dt - old_date).days}")
print(f"(max_dt - old_date).total_seconds() / 86400.0: {(max_dt - old_date).total_seconds() / 86400.0}")

# The issue: max_dt is 9999-12-31 23:59:59.999999
# When we compute total_seconds() / 86400, we get fractional days
# But when we add a timedelta with integer+fractional days, Python tries to 
# construct a datetime that goes beyond max

# Let's try capping at slightly less than max
slightly_less = (max_dt - old_date).total_seconds() / 86400.0 - 1.0

try:
    result = old_date + timedelta(days=slightly_less)
    print(f"Success with slightly_less: {result}")
except OverflowError as e:
    print(f"OverflowError: {e}")

# Or try a different approach: directly return datetime.max if we would exceed it
idle = 1318354615.2376359
proposed_result = old_date + timedelta(days=idle)  # This will fail
# Instead: compute and cap the result
try:
    # Clamp to the valid range
    result = old_date + timedelta(days=min(idle, 999999999))
except OverflowError:
    result = max_dt
print(f"\nDirect capping to max_dt: {result}")
PY

Repository: afx-team/hebb-mind

Length of output: 481


🏁 Script executed:

#!/bin/bash
python - <<'PY'
import math
from datetime import datetime, timezone, timedelta

# Test alternative approaches to handle the overflow

old_date = datetime(2000, 1, 1, tzinfo=timezone.utc)
idle = 1318354615.2376359

# Approach 1: Try/except to catch overflow
print("Approach 1: Try/except")
try:
    result = old_date + timedelta(days=idle)
except OverflowError:
    max_dt = datetime.max.replace(tzinfo=old_date.tzinfo)
    result = max_dt
print(f"Result: {result}")

# Approach 2: Compute max_idle_days with safety margin
print("\nApproach 2: Compute max_idle_days with margin")
max_dt = datetime.max.replace(tzinfo=old_date.tzinfo)
max_idle_days = (max_dt - old_date).days - 1  # Use .days instead of total_seconds
idle_capped = min(idle, float(max_idle_days))
try:
    result = old_date + timedelta(days=idle_capped)
    print(f"Result: {result}")
except OverflowError as e:
    print(f"OverflowError: {e}")

# Approach 3: Use days property instead of total_seconds
print("\nApproach 3: Use .days property")
max_dt = datetime.max.replace(tzinfo=old_date.tzinfo)
# The difference between datetimes gives a timedelta
# We want the maximum days we can safely add
delta = max_dt - old_date
max_safe_days = delta.days  # This is an integer
idle_capped = min(idle, float(max_safe_days))
try:
    result = old_date + timedelta(days=idle_capped)
    print(f"Result: {result}")
except OverflowError as e:
    print(f"OverflowError: {e}")
PY

Repository: afx-team/hebb-mind

Length of output: 367


🏁 Script executed:

git ls-files | grep -E "test.*forgetting" | head -10

Repository: afx-team/hebb-mind

Length of output: 438


🏁 Script executed:

cat -n tests/unit/scheduler/test_forgetting_job.py | head -100

Repository: afx-team/hebb-mind

Length of output: 3709


Guard compute_expires_at against timedelta overflow from high access_count.

With uncapped access_count, idle can exceed timedelta limits and raise OverflowError. This is confirmed to occur with realistic parameters (e.g., access_count=300000 produces idle≈1.3 billion days vs. timedelta's 999999999-day max).

However, the proposed fix has a critical flaw: it uses total_seconds() / 86400.0 for the maximum idle calculation, which can still produce overflow when last_accessed_at is an older date. A more robust solution uses either:

  • A try/except pattern to catch overflow and cap to datetime.max, or
  • Conservative integer-day capping via .days property instead of fractional seconds
🛠️ Recommended fix
 def compute_expires_at(
@@
 ) -> datetime:
@@
     eff = eff_half_life_days(half_life_days, k_importance, k_access, memory.importance_score, memory.access_count)
     idle = forget_idle_days(eff, threshold, min_retention_days)
-    return memory.last_accessed_at + timedelta(days=idle)
+    try:
+        return memory.last_accessed_at + timedelta(days=idle)
+    except OverflowError:
+        return datetime.max.replace(tzinfo=memory.last_accessed_at.tzinfo)
🤖 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 `@src/hebb/scheduler/forgetting_job.py` around lines 117 - 119, The
`compute_expires_at` function can raise an `OverflowError` when `idle` from
`forget_idle_days` is too large to fit in a timedelta. Instead of using
fractional seconds for capping, implement a try/except block around the final
return statement that catches `OverflowError` and returns `datetime.max` as a
safe fallback, or alternatively cap the `idle` value to an integer-day limit
(such as timedelta's maximum days of 999999999) before creating the timedelta
from `memory.last_accessed_at + timedelta(days=idle)` to prevent the overflow
from occurring in the first place.

Comment on lines +189 to +193
await purge_memory(memory_store, kg, memory.id, save=False)
deleted += 1
if deleted > 0:
kg.save()
except Exception as exc:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Wrap manual purge/save in knowledge_graph.lock to avoid graph-write races.

The manual /forget path mutates and saves the graph without the shared graph lock, while the scheduler path does lock this critical section. This can interleave concurrent graph writes and persist inconsistent state.

Suggested fix
+    to_delete: list[str] = []
     try:
         partitions = await partition_store.list()
         for partition in partitions:
             ...
             for memory in memories:
                 ...
                 if expires_at < now:
-                    await purge_memory(memory_store, kg, memory.id, save=False)
-                    deleted += 1
-        if deleted > 0:
-            kg.save()
+                    to_delete.append(memory.id)
+        if to_delete:
+            async with kg.lock:
+                for mid in to_delete:
+                    await purge_memory(memory_store, kg, mid, save=False)
+                    deleted += 1
+                kg.save()
📝 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
await purge_memory(memory_store, kg, memory.id, save=False)
deleted += 1
if deleted > 0:
kg.save()
except Exception as exc:
to_delete.append(memory.id)
if to_delete:
async with kg.lock:
for mid in to_delete:
await purge_memory(memory_store, kg, mid, save=False)
deleted += 1
kg.save()
except Exception as exc:
🤖 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 `@src/hebb/server/routers/admin.py` around lines 189 - 193, The manual purge
operation in the `/forget` endpoint mutates and saves the knowledge graph
without acquiring the shared graph lock, creating a race condition with
concurrent scheduler writes. Wrap the critical section containing the
purge_memory calls and the subsequent kg.save() operation with the
knowledge_graph.lock context manager (using async with) to ensure mutual
exclusion. This mirrors the locking pattern used in the scheduler path and
prevents interleaved graph writes from persisting inconsistent state.

Comment on lines +234 to +236
new_map = {pid: o.model_dump() for pid, o in settings.forgetting_overrides.items()}
new_map[partition_id] = override.model_dump()
_persist_and_sync(settings, new_map)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Avoid full-map stale-snapshot writes for overrides (lost-update risk).

Both set/clear paths rebuild new_map from in-memory settings.forgetting_overrides and then replace the entire persisted map. Concurrent requests (or multi-process writers) can overwrite each other’s partition changes.

This needs an atomic per-partition mutate-in-loader flow (read current on-disk map under lock, apply one key add/remove, validate, write), rather than replacing from a potentially stale snapshot.

Also applies to: 252-253

🤖 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 `@src/hebb/server/routers/forgetting.py` around lines 234 - 236, The code at
lines 234-236 and 252-253 reconstructs the full forgetting_overrides map from an
in-memory snapshot of settings.forgetting_overrides and then persists it
entirely via _persist_and_sync, creating a lost-update vulnerability where
concurrent requests overwriting different partitions can lose changes. Replace
this full-map rebuild approach with an atomic per-partition mutation pattern:
read the current on-disk map while holding a lock, apply only the single
partition key's add or remove operation (for the set path at line 234-236 and
the clear path at lines 252-253), validate the change, and write back only the
updated map. This ensures that each concurrent modification updates from the
current disk state rather than a potentially stale in-memory snapshot.

Comment on lines +31 to +35
async function loadList(root) {
const list = root.querySelector('#mem-list');
const info = root.querySelector('#mem-info');
tbody.innerHTML = '<tr><td colspan="6" class="text-muted" style="text-align:center">Loading...</td></tr>';
const pag = root.querySelector('#mem-pagination');
list.innerHTML = `<div class="text-muted" style="padding:28px;text-align:center">${t('common.loading')}</div>`;

Copy link
Copy Markdown

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

Guard loadList against tab unmount during async fetch.

After Line 39 awaits api.listMemories, the Manage tab can be switched, leaving cached DOM refs detached; subsequent DOM writes/event wiring can fail on stale nodes. Add mount checks before and after the await.

🔧 Proposed fix
 async function loadList(root) {
   const list = root.querySelector('`#mem-list`');
   const info = root.querySelector('`#mem-info`');
   const pag = root.querySelector('`#mem-pagination`');
+  if (!list || !info || !pag) return;
   list.innerHTML = `<div class="text-muted" style="padding:28px;text-align:center">${t('common.loading')}</div>`;
   try {
     const params = { offset, limit: PAGE_SIZE };
     if (currentPartition) params.partition_id = currentPartition;
     const data = await api.listMemories(params);
+    if (!list.isConnected || !info.isConnected || !pag.isConnected) return;
     info.textContent = t('memories.count', { n: data.total });

Also applies to: 39-44, 72-74

🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 34-34: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: list.innerHTML = <div class="text-muted" style="padding:28px;text-align:center">${t('common.loading')}</div>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(inner-outer-html)

🤖 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 `@src/hebb/static/js/components/memories.js` around lines 31 - 35, The loadList
function caches DOM references at the start (list, info, pag elements) but these
can become detached if the user switches tabs while the api.listMemories await
is pending on line 39. Add a mount check before the await to verify the root
element is still in the document, and add another mount check immediately after
the await completes before performing any DOM operations (innerHTML updates,
event listeners, etc.). This ensures that stale DOM references are not
manipulated after tab unmounting. Apply the same guard pattern to the other
async operations mentioned at lines 72-74.

Comment on lines 34 to 40
async function loadList(root) {
const container = root.querySelector('#part-list');
container.innerHTML = '<div class="text-muted" style="padding:20px;text-align:center">Loading...</div>';
container.innerHTML = `<div class="text-muted" style="padding:20px;text-align:center">${t('common.loading')}</div>`;
try {
const list = await api.listPartitions();
renderDistribution(root, list);
container.innerHTML = list.map(p => {

Copy link
Copy Markdown

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

Prevent stale async DOM updates in loadList.

At Line 38, api.listPartitions() can resolve after the tab has been replaced, but Line 40+ still mutates the old container and binds handlers. Add presence/connection checks around the await.

🔧 Proposed fix
 async function loadList(root) {
   const container = root.querySelector('`#part-list`');
+  if (!container) return;
   container.innerHTML = `<div class="text-muted" style="padding:20px;text-align:center">${t('common.loading')}</div>`;
   try {
     const list = await api.listPartitions();
+    if (!container.isConnected) return;
     renderDistribution(root, list);
     container.innerHTML = list.map(p => {

Also applies to: 74-83

🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 35-35: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: container.innerHTML = <div class="text-muted" style="padding:20px;text-align:center">${t('common.loading')}</div>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(inner-outer-html)

🤖 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 `@src/hebb/static/js/components/partitions.js` around lines 34 - 40, The
loadList function has a race condition where api.listPartitions() can resolve
after the DOM has changed (tab replaced), causing stale updates to the container
element. After awaiting the api.listPartitions() call, add a check to verify
that the container element is still present in the root element before
proceeding with innerHTML updates and handler binding. This check should
validate that the container reference is still valid in the DOM to prevent
mutations on detached or replaced elements.

Comment on lines 89 to 91
} catch (e) {
root.querySelector('#settings-panel').innerHTML = `<div class="empty-state">${e.message}</div>`;
root.querySelector('#system-panel').innerHTML = `<div class="empty-state">${e.message}</div>`;
return;

Copy link
Copy Markdown

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

Escape config-load error text before injecting into HTML.

Line 90 injects e.message directly into innerHTML. Rendered error strings should be escaped to prevent script/markup injection.

🔧 Proposed fix
   } catch (e) {
-    root.querySelector('`#system-panel`').innerHTML = `<div class="empty-state">${e.message}</div>`;
+    root.querySelector('`#system-panel`').innerHTML = `<div class="empty-state">${esc(e.message)}</div>`;
     return;
   }
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 89-89: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: root.querySelector('#system-panel').innerHTML = <div class="empty-state">${e.message}</div>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(inner-outer-html)

🤖 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 `@src/hebb/static/js/components/system.js` around lines 89 - 91, The error
message from the catch block is being directly injected into innerHTML without
proper escaping, which creates a potential XSS vulnerability. In the catch block
where root.querySelector('`#system-panel`').innerHTML is set with e.message,
escape the error message text before inserting it into the DOM. You can create a
helper function or use a method like creating a temporary text element and
extracting its textContent, or use a library utility for HTML escaping, to
ensure that any HTML special characters in the error message are properly
encoded before being included in the innerHTML assignment.

Source: Linters/SAST tools

Comment on lines +44 to +49
patch("hebb.config.loader.find_config_file", return_value=config_path),
patch("hebb.embedding.factory.create_embedder", side_effect=_mock_create_embedder),
):
from hebb.server.app import create_app

app = create_app()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify possible import-order hazard and patch target alignment.
rg -n -C2 'from\s+hebb\.server\.app\s+import\s+create_app|import\s+hebb\.server\.app' --type=py tests src
rg -n -C2 'patch\("hebb\.embedding\.factory\.create_embedder"|patch\("hebb\.server\.app\.create_embedder"' --type=py tests
rg -n -C2 'from hebb\.embedding\.factory import create_embedder' src/hebb/server/app.py

Repository: afx-team/hebb-mind

Length of output: 7861


🏁 Script executed:

cat src/hebb/server/app.py | head -100

Repository: afx-team/hebb-mind

Length of output: 4264


🏁 Script executed:

cat src/hebb/server/app.py | wc -l

Repository: afx-team/hebb-mind

Length of output: 160


🏁 Script executed:

cat tests/integration/server/test_forgetting_router.py

Repository: afx-team/hebb-mind

Length of output: 7587


Patch the symbol used by create_app to avoid order-dependent test behavior.

Line 45 patches hebb.embedding.factory.create_embedder, but create_app calls the symbol imported into hebb.server.app. If hebb.server.app was imported earlier in the session (e.g., by another test file), the module-level import in that module will have already executed with the real embedder, and the later patch will not affect the cached reference.

Suggested fix
     with (
         patch("hebb.config.loader.find_config_file", return_value=config_path),
-        patch("hebb.embedding.factory.create_embedder", side_effect=_mock_create_embedder),
+        patch("hebb.server.app.create_embedder", side_effect=_mock_create_embedder),
     ):
         from hebb.server.app import create_app
📝 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
patch("hebb.config.loader.find_config_file", return_value=config_path),
patch("hebb.embedding.factory.create_embedder", side_effect=_mock_create_embedder),
):
from hebb.server.app import create_app
app = create_app()
patch("hebb.config.loader.find_config_file", return_value=config_path),
patch("hebb.server.app.create_embedder", side_effect=_mock_create_embedder),
):
from hebb.server.app import create_app
app = create_app()
🤖 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/integration/server/test_forgetting_router.py` around lines 44 - 49, The
patch target on line 45 is incorrect for the `create_app` function's usage.
Instead of patching `hebb.embedding.factory.create_embedder`, patch the symbol
where it is imported and used within the `hebb.server.app` module. Change the
patch target to `hebb.server.app.create_embedder` to ensure the patch intercepts
the actual reference used by the `create_app` function, preventing issues with
cached module-level imports from earlier test runs.

Comment on lines +195 to +211
result = subprocess.run(
["git", "ls-files", "-z", "--", *_DOC_ROOTS],
cwd=REPO_ROOT,
capture_output=True,
text=True,
check=True,
)
files: list[Path] = []
for rel in result.stdout.split("\0"):
if not rel:
continue
path = REPO_ROOT / rel
if ".vitepress" in path.parts:
continue # build output / cache / theme, not authored docs
if path.suffix == ".md" or (path.suffix == ".py" and "examples" in path.parts):
files.append(path)
return files

Copy link
Copy Markdown

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

Make doc-file discovery resilient when Git metadata is unavailable.

_doc_files() hard-fails on git ls-files (check=True). In sdist/exported trees (no .git) or environments without git, this test fails before running the actual drift assertions.

Suggested patch
 def _doc_files() -> list[Path]:
@@
-    result = subprocess.run(
-        ["git", "ls-files", "-z", "--", *_DOC_ROOTS],
-        cwd=REPO_ROOT,
-        capture_output=True,
-        text=True,
-        check=True,
-    )
+    try:
+        result = subprocess.run(
+            ["git", "ls-files", "-z", "--", *_DOC_ROOTS],
+            cwd=REPO_ROOT,
+            capture_output=True,
+            text=True,
+            check=True,
+        )
+        rel_paths = [p for p in result.stdout.split("\0") if p]
+    except (FileNotFoundError, subprocess.CalledProcessError):
+        rel_paths: list[str] = []
+        for root in _DOC_ROOTS:
+            abs_root = REPO_ROOT / root
+            if abs_root.is_file():
+                rel_paths.append(root)
+            elif abs_root.is_dir():
+                rel_paths.extend(str(p.relative_to(REPO_ROOT)) for p in abs_root.rglob("*"))
@@
-    for rel in result.stdout.split("\0"):
-        if not rel:
-            continue
+    for rel in rel_paths:
         path = REPO_ROOT / rel
📝 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
result = subprocess.run(
["git", "ls-files", "-z", "--", *_DOC_ROOTS],
cwd=REPO_ROOT,
capture_output=True,
text=True,
check=True,
)
files: list[Path] = []
for rel in result.stdout.split("\0"):
if not rel:
continue
path = REPO_ROOT / rel
if ".vitepress" in path.parts:
continue # build output / cache / theme, not authored docs
if path.suffix == ".md" or (path.suffix == ".py" and "examples" in path.parts):
files.append(path)
return files
try:
result = subprocess.run(
["git", "ls-files", "-z", "--", *_DOC_ROOTS],
cwd=REPO_ROOT,
capture_output=True,
text=True,
check=True,
)
rel_paths = [p for p in result.stdout.split("\0") if p]
except (FileNotFoundError, subprocess.CalledProcessError):
rel_paths: list[str] = []
for root in _DOC_ROOTS:
abs_root = REPO_ROOT / root
if abs_root.is_file():
rel_paths.append(root)
elif abs_root.is_dir():
rel_paths.extend(str(p.relative_to(REPO_ROOT)) for p in abs_root.rglob("*"))
files: list[Path] = []
for rel in rel_paths:
path = REPO_ROOT / rel
if ".vitepress" in path.parts:
continue # build output / cache / theme, not authored docs
if path.suffix == ".md" or (path.suffix == ".py" and "examples" in path.parts):
files.append(path)
return files
🤖 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/unit/test_audit_distribution.py` around lines 195 - 211, The
_doc_files() function uses check=True in the subprocess.run() call for the git
ls-files command, which causes it to raise an exception and hard-fail when Git
is unavailable (such as in sdist/exported trees without .git or environments
without git installed). Remove the check=True parameter from the
subprocess.run() call and instead check the returncode after execution. If the
git command fails (returncode is non-zero), return an empty list instead of
raising an exception, allowing the test to continue with its actual drift
assertions.

…get)

numpy 2.5.0 ships PEP 695 `type` statements in its stubs, which mypy (run on
Python 3.12 in CI) rejects under our deliberate `python_version = "3.10"`
target — a stub-syntax/target mismatch, not our code. Add a numpy mypy override
with follow_imports=skip + follow_imports_for_stubs, rather than dropping the
3.10 target or pinning numpy down. numpy is shallowly typed here anyway
(disallow_any_generics is off), so no meaningful checking is lost.

Verified against numpy 2.5.0 locally: mypy clean, 719 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ch-liuzhide
ch-liuzhide merged commit 7bc035a into main Jun 23, 2026
35 of 36 checks passed
@ch-liuzhide
ch-liuzhide deleted the feat/web-console-forgetting-overhaul branch June 23, 2026 07:58
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