diff --git a/.scheduler-memory/latest/daily/memories.md b/.scheduler-memory/latest/daily/memories.md index 8dcfff20..ce4fae87 100644 --- a/.scheduler-memory/latest/daily/memories.md +++ b/.scheduler-memory/latest/daily/memories.md @@ -1,45 +1,3 @@ -### 1. [2026-02-26] (scheduler, daily, store, insight) +### 1. [2026-02-27] (scheduler, daily, retrieve) -# Memory Update (2026-02-25) - -## Docs Cleanup -- Replaced `docs/README.md` content that referenced Archivox with a SeedPass-focused docs index. -- Updated docs-site default title in `docs/src/config/loadConfig.js` from `Archivox` to `SeedPass Docs`. - -## Outcome -- Removed stale Arc… - ---- - -### 2. [2026-02-25] (scheduler, daily, store, insight) - -### 1. [2026-02-25] (torch, init, reinstall) -If `npx --no-install torch-lock init --help` prints initialization prompts, it may be interactive; use `printf '\n' | npx --no-install torch-lock init --force` to accept defaults non-interactively in CI/agent runs. - -### 2. [2026-02-25… - ---- - -### 3. [2026-02-25] (scheduler, daily, store, insight) - -### 1. [2026-02-25] (torch, scheduler, host-mode) -When running `npm run --prefix torch scheduler:daily`, the scheduler resolves config from `torch/torch-config.json` (cwd-scoped), not the repo-root `torch-config.json`. - -### 2. [2026-02-25] (torch, scheduler, handoff) -A non-inter… - ---- - -### 4. [2026-02-25] (scheduler, daily, store, insight) - -### 1. [2026-02-25] (torch, scheduler, host-mode) -When running `npm run --prefix torch scheduler:daily`, the scheduler resolves config from `torch/torch-config.json` (cwd-scoped), not the repo-root `torch-config.json`. - -### 2. [2026-02-25] (torch, scheduler, handoff) -A non-inter… - ---- - -### 5. [2026-02-26] (scheduler, daily, retrieve) - -Memory retrieval seed for daily :: scheduler memory retrieval \ No newline at end of file +Memory retrieval seed for daily :: Required startup + artifacts + memory + issue capture \ No newline at end of file diff --git a/.scheduler-memory/memory-store.json b/.scheduler-memory/memory-store.json index 0c946347..3c8b1e8f 100644 --- a/.scheduler-memory/memory-store.json +++ b/.scheduler-memory/memory-store.json @@ -507,5 +507,56 @@ "merged_into": null, "pinned": false } + ], + [ + "094aab2a-4a25-4322-a198-1638752acc77", + { + "schema_version": 1, + "id": "094aab2a-4a25-4322-a198-1638752acc77", + "agent_id": "const-refactor-agent", + "session_id": "session-mm5d7amf", + "type": "batch_window", + "content": "Memory retrieval seed for daily :: Required startup + artifacts + memory + issue capture", + "summary": "Memory retrieval seed for daily :: Required startup + artifacts + memory + issue capture", + "tags": [ + "scheduler", + "daily", + "retrieve" + ], + "importance": 0.4, + "embedding_id": null, + "created_at": 1772225327752, + "last_seen": 1772225327764, + "source": "scheduler-retrieve", + "ttl_days": null, + "merged_into": null, + "pinned": false + } + ], + [ + "cef905d0-7a66-459c-8937-41310afc934f", + { + "schema_version": 1, + "id": "cef905d0-7a66-459c-8937-41310afc934f", + "agent_id": "const-refactor-agent", + "session_id": "session-mm5dpefy", + "type": "batch_window", + "content": "# Memory Update — bug-reproducer-agent — 2026-02-27\n\n## Key findings\n- The `scheduler-flow.md` script execution failed due to a missing runner (`codex`).\n- The automated `bug-reproducer-agent` prompt execution failed.\n- Fallback manual investigation identified one active issue: `BIP85 Non-Standard Derivation Path` (using `app_no=2` instead of standard). This is documented in `KNOWN_ISSUES.md`.\n\n## Patterns / reusable knowledge\n- Future runs should verify the runner environment variables (`SCHEDULER_PROMPT_PATH`, `SCHEDULER_AGENT`, `SCHEDULER_CADENCE`) are set correctly before invoking `run-selected-prompt.mjs`.\n- If the runner fails, manual fallback or alternative automation (e.g., Python script) is required.\n\n## Warnings / gotchas\n- `torch/scripts/agent/run-selected-prompt.mjs` depends on an external runner (`codex` or similar) which may not be available in all environments.", + "summary": "# Memory Update — bug-reproducer-agent — 2026-02-27\n\n## Key findings\n- The `scheduler-flow.md` script execution failed due to a missing runner (`codex`).\n- The automated `bug-reproducer-agent` prompt execution failed.\n- Fallback manual investigation identified one active issue: …", + "tags": [ + "scheduler", + "daily", + "store", + "insight" + ], + "importance": 0.8, + "embedding_id": null, + "created_at": 1772226172512, + "last_seen": 1772226172512, + "source": "agent-output", + "ttl_days": null, + "merged_into": null, + "pinned": false + } ] ] \ No newline at end of file diff --git a/.scheduler-memory/session-mm5d7amf/retrieve.json b/.scheduler-memory/session-mm5d7amf/retrieve.json new file mode 100644 index 00000000..8248ea0e --- /dev/null +++ b/.scheduler-memory/session-mm5d7amf/retrieve.json @@ -0,0 +1,18 @@ +{ + "cadence": "daily", + "operation": "retrieve", + "runId": "session-mm5d7amf", + "servicePath": "src/services/memory/index.js#getRelevantMemories", + "inputs": { + "agentId": "const-refactor-agent", + "promptPath": "torch/src/prompts/daily/const-refactor-agent.md", + "promptIntent": "Required startup + artifacts + memory + issue capture", + "query": "scheduler | daily | agent | const-refactor-agent | intent | Required startup + artifacts + memory + issue capture", + "events": 1 + }, + "outputs": { + "ingestedCount": 1, + "retrievedCount": 1 + }, + "status": "ok" +} \ No newline at end of file diff --git a/.scheduler-memory/session-mm5d7amf/retrieve.ok b/.scheduler-memory/session-mm5d7amf/retrieve.ok new file mode 100644 index 00000000..84b17199 --- /dev/null +++ b/.scheduler-memory/session-mm5d7amf/retrieve.ok @@ -0,0 +1 @@ +MEMORY_RETRIEVED diff --git a/.scheduler-memory/session-mm5dpefy/store.json b/.scheduler-memory/session-mm5dpefy/store.json new file mode 100644 index 00000000..c37e77b1 --- /dev/null +++ b/.scheduler-memory/session-mm5dpefy/store.json @@ -0,0 +1,20 @@ +{ + "cadence": "daily", + "operation": "store", + "runId": "session-mm5dpefy", + "servicePath": "src/services/memory/index.js#ingestEvents", + "inputs": { + "agentId": "const-refactor-agent", + "promptPath": "torch/src/prompts/daily/const-refactor-agent.md", + "promptIntent": "Required startup + artifacts + memory + issue capture", + "events": 1, + "sourceFile": "memory-update.md" + }, + "outputs": { + "storedCount": 1, + "summaries": [ + "# Memory Update — bug-reproducer-agent — 2026-02-27\n\n## Key findings\n- The `scheduler-flow.md` script execution failed due to a missing runner (`codex`).\n- The automated `bug-reproducer-agent` prompt execution failed.\n- Fallback manual investigation identified one active issue: …" + ] + }, + "status": "ok" +} \ No newline at end of file diff --git a/.scheduler-memory/session-mm5dpefy/store.ok b/.scheduler-memory/session-mm5dpefy/store.ok new file mode 100644 index 00000000..1e40bb67 --- /dev/null +++ b/.scheduler-memory/session-mm5dpefy/store.ok @@ -0,0 +1 @@ +MEMORY_STORED diff --git a/reports/performance/constants-refactor/candidates.json b/reports/performance/constants-refactor/candidates.json new file mode 100644 index 00000000..c8632faf --- /dev/null +++ b/reports/performance/constants-refactor/candidates.json @@ -0,0 +1,58 @@ +[ + { + "value": 1000, + "semantic": "one second in ms", + "occurrences": [ + {"file": "torch/src/services/memory/index.js", "line": 45, "context": "const MIN_SAVE_INTERVAL_MS = 1000;"}, + {"file": "torch/src/services/memory/pruner.js", "line": 3, "context": "const DAY_MS = 24 * 60 * 60 * 1000;"}, + {"file": "torch/src/services/memory/retriever.js", "line": 8, "context": "const DEFAULT_RECENCY_HALF_LIFE_MS = 1000 * 60 * 60 * 24 * 7;"}, + {"file": "torch/src/services/memory/formatter.js", "line": 3, "context": "const DEFAULT_TOKEN_BUDGET = 1000;"} + ] + }, + { + "value": 60000, + "semantic": "one minute in ms (or 60_000)", + "occurrences": [ + {"file": "torch/src/services/memory/index.js", "line": 548, "context": "windowMs / 60_000"}, + {"file": "torch/src/services/memory/ingestor.js", "line": 6, "context": "const WINDOW_BUCKET_MS = 60_000;"}, + {"file": "torch/src/services/memory/cache.js", "line": 153, "context": "setRuntimeEvent(scope, event, ttlMs = 60_000) {"}, + {"file": "torch/src/services/memory/cache.js", "line": 157, "context": "const safeTtlMs = Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : 60_000;"}, + {"file": "torch/src/services/memory/scheduler.js", "line": 2, "context": "const MINUTE_MS = 60_000;"} + ] + }, + { + "value": "24 * 60 * 60 * 1000", + "semantic": "one day in ms", + "occurrences": [ + {"file": "torch/src/services/memory/pruner.js", "line": 3, "context": "const DAY_MS = 24 * 60 * 60 * 1000;"}, + {"file": "torch/src/services/memory/index.js", "line": 329, "context": "const retentionMs = options.retentionMs ?? (1000 * 60 * 60 * 24 * 30);"}, + {"file": "torch/src/services/memory/index.js", "line": 495, "context": "const retentionMs = options.retentionMs ?? (1000 * 60 * 60 * 24 * 30);"} + ] + }, + { + "value": 280, + "semantic": "max summary chars length", + "occurrences": [ + {"file": "torch/src/services/memory/pruner.js", "line": 116, "context": ".slice(0, 280)"}, + {"file": "torch/src/services/memory/summarizer.js", "line": 116, "context": "const maxSummaryLength = options.maxSummaryLength ?? 280;"}, + {"file": "torch/src/services/memory/ingestor.js", "line": 92, "context": "function compactContextExcerpt(text, maxChars = 280) {"} + ] + }, + { + "value": 0.25, + "semantic": "default importance score", + "occurrences": [ + {"file": "torch/src/services/memory/summarizer.js", "line": 64, "context": "Math.min(1, Math.max(0, rawImportance)) : 0.25;"}, + {"file": "torch/src/services/memory/summarizer.js", "line": 72, "context": "return { summary: bounded, importance: 0.25 };"}, + {"file": "torch/src/services/memory/retriever.js", "line": 3, "context": "w_imp: 0.25,"} + ] + }, + { + "value": 0.35, + "semantic": "low importance / semantic overlap threshold", + "occurrences": [ + {"file": "torch/src/services/memory/summarizer.js", "line": 89, "context": "return overlap / summaryTokens.length >= 0.35;"}, + {"file": "torch/src/services/memory/pruner.js", "line": 184, "context": "lowImportanceThreshold: options.lowImportanceThreshold ?? 0.35,"} + ] + } +] diff --git a/reports/performance/constants-refactor/raw-numeric-hits.txt b/reports/performance/constants-refactor/raw-numeric-hits.txt new file mode 100644 index 00000000..6294caa5 --- /dev/null +++ b/reports/performance/constants-refactor/raw-numeric-hits.txt @@ -0,0 +1,37 @@ +torch/src/services/memory/index.js:45:const MIN_SAVE_INTERVAL_MS = 1000; +torch/src/services/memory/index.js:158:const ESTIMATED_INDEX_BYTES_PER_RECORD = 512; +torch/src/services/memory/index.js:164: if (typeof value === 'string' && value.length > 300) { +torch/src/services/memory/index.js:165: safe[key] = `${value.slice(0, 300)}…`; +torch/src/services/memory/index.js:225: if (samples.length > 10_000) { +torch/src/services/memory/index.js:226: samples.splice(0, samples.length - 10_000); +torch/src/services/memory/index.js:324: * const result = await runPruneCycle({ retentionMs: 30 * 24 * 60 * 60 * 1000 }); +torch/src/services/memory/index.js:329: const retentionMs = options.retentionMs ?? (1000 * 60 * 60 * 24 * 30); +torch/src/services/memory/index.js:491: * const preview = await triggerPruneDryRun({ retentionMs: 7 * 24 * 60 * 60 * 1000 }); +torch/src/services/memory/index.js:495: const retentionMs = options.retentionMs ?? (1000 * 60 * 60 * 24 * 30); +torch/src/services/memory/index.js:530: * const stats = await memoryStats({ windowMs: 60 * 60 * 1000 }); +torch/src/services/memory/index.js:535: const windowMs = Number.isFinite(options.windowMs) ? Math.max(1, Number(options.windowMs)) : (60 * 60 * 1000); +torch/src/services/memory/index.js:548: const ingestThroughputPerMinute = ingestedInWindow / Math.max(1, (windowMs / 60_000)); +torch/src/services/memory/pruner.js:3:const DAY_MS = 24 * 60 * 60 * 1000; +torch/src/services/memory/pruner.js:116: const fallbackSummary = group.map((item) => item.summary || item.content).filter(Boolean).join(' ').slice(0, 280); +torch/src/services/memory/pruner.js:184: lowImportanceThreshold: options.lowImportanceThreshold ?? 0.35, +torch/src/services/memory/pruner.js:185: deleteImportanceThreshold: options.deleteImportanceThreshold ?? 0.12, +torch/src/services/memory/summarizer.js:64: : 0.25; +torch/src/services/memory/summarizer.js:72: return { summary: bounded, importance: 0.25 }; +torch/src/services/memory/summarizer.js:89: return overlap / summaryTokens.length >= 0.35; +torch/src/services/memory/summarizer.js:116: const maxSummaryLength = options.maxSummaryLength ?? 280; +torch/src/services/memory/ingestor.js:6:const WINDOW_BUCKET_MS = 60_000; +torch/src/services/memory/ingestor.js:37: : 300; +torch/src/services/memory/ingestor.js:92:function compactContextExcerpt(text, maxChars = 280) { +torch/src/services/memory/retriever.js:2: w_sim: 0.6, +torch/src/services/memory/retriever.js:3: w_imp: 0.25, +torch/src/services/memory/retriever.js:4: w_rec: 0.15, +torch/src/services/memory/retriever.js:5: pinBoost: 0.15, +torch/src/services/memory/retriever.js:8:const DEFAULT_RECENCY_HALF_LIFE_MS = 1000 * 60 * 60 * 24 * 7; +torch/src/services/memory/formatter.js:3:const DEFAULT_TOKEN_BUDGET = 1000; +torch/src/services/memory/cache.js:2:const DEFAULT_MAX_EVENTS_PER_NAMESPACE = 200; +torch/src/services/memory/cache.js:3:const DEFAULT_MAX_TOTAL_EVENTS = 5000; +torch/src/services/memory/cache.js:153: setRuntimeEvent(scope, event, ttlMs = 60_000) { +torch/src/services/memory/cache.js:157: const safeTtlMs = Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : 60_000; +torch/src/services/memory/scheduler.js:2:const MINUTE_MS = 60_000; +torch/src/services/memory/scheduler.js:3:const HOUR_MS = 60 * MINUTE_MS; +torch/src/services/memory/scheduler.js:4:const DAY_MS = 24 * HOUR_MS; diff --git a/src/context/CONTEXT_1772225357.md b/src/context/CONTEXT_1772225357.md new file mode 100644 index 00000000..07750a86 --- /dev/null +++ b/src/context/CONTEXT_1772225357.md @@ -0,0 +1,2 @@ +Fri Feb 27 20:49:17 UTC 2026 +Exploring duplicated constants in js/ diff --git a/src/decisions/DECISIONS_1772226155.md b/src/decisions/DECISIONS_1772226155.md new file mode 100644 index 00000000..159427bf --- /dev/null +++ b/src/decisions/DECISIONS_1772226155.md @@ -0,0 +1,7 @@ +## Decisions + +- **Domain-specific constants vs Global**: The constants were found specifically within `torch/src/services/memory/`. Therefore, I created `torch/src/services/memory/constants/` rather than a global `src/constants.js`. +- **Time constants**: Created `time.js` for `SECOND_MS`, `MINUTE_MS`, `HOUR_MS`, `DAY_MS`, `WEEK_MS`, `MONTH_MS`. +- **Text constants**: Created `text.js` for `MAX_SUMMARY_LENGTH`. +- **Scoring constants**: Created `scoring.js` for `DEFAULT_IMPORTANCE_SCORE` and `LOW_IMPORTANCE_THRESHOLD`. +- Replaced literals in `index.js`, `pruner.js`, `retriever.js`, `ingestor.js`, `summarizer.js`, `cache.js`, `scheduler.js`. diff --git a/src/test_logs/TEST_LOG_1772226155.md b/src/test_logs/TEST_LOG_1772226155.md new file mode 100644 index 00000000..6b059532 --- /dev/null +++ b/src/test_logs/TEST_LOG_1772226155.md @@ -0,0 +1 @@ +Ran `npm run --prefix torch lint` and verified that there are no linter errors. diff --git a/src/todo/TODO_1772226155.md b/src/todo/TODO_1772226155.md new file mode 100644 index 00000000..53ec2fad --- /dev/null +++ b/src/todo/TODO_1772226155.md @@ -0,0 +1,10 @@ +## Constants Refactoring TODOs + +1. `1000` (second in ms): Replaced with `SECOND_MS` +2. `60000` (minute in ms): Replaced with `MINUTE_MS` +3. `24 * 60 * 60 * 1000` (day in ms): Replaced with `DAY_MS` +4. `280` (max summary length): Replaced with `MAX_SUMMARY_LENGTH` +5. `0.25` (default importance): Replaced with `DEFAULT_IMPORTANCE_SCORE` +6. `0.35` (low importance threshold): Replaced with `LOW_IMPORTANCE_THRESHOLD` + +Status: All complete. Constants extracted to `torch/src/services/memory/constants/`. diff --git a/torch/scripts/test-audit/run_python_flaky_check.py b/torch/scripts/test-audit/run_python_flaky_check.py index 9ddef432..3add8d15 100644 --- a/torch/scripts/test-audit/run_python_flaky_check.py +++ b/torch/scripts/test-audit/run_python_flaky_check.py @@ -11,6 +11,7 @@ TEST_PATTERN = "src/tests" RESULTS = {} RUN_DIAGNOSTICS = [] + def run_tests(run_index): """Run pytest and parse output for pass/fail counts.""" print(f"Run {run_index + 1}/{RUNS}...") @@ -19,11 +20,13 @@ def run_tests(run_index): # -q: quiet # --no-header --no-summary: minimal output cmd = [ - sys.executable, "-m", "pytest", + sys.executable, + "-m", + "pytest", TEST_PATTERN, "-q", "--no-header", - "--no-summary" + "--no-summary", ] start_time = time.time() @@ -33,7 +36,7 @@ def run_tests(run_index): cmd, capture_output=True, text=True, - env={"PYTHONPATH": "src"} # Ensure src is in path + env={"PYTHONPATH": "src"}, # Ensure src is in path ) output = result.stdout exit_code = result.returncode @@ -45,10 +48,10 @@ def run_tests(run_index): # Parse output character by character for status # . = pass, F = fail, E = error, s = skip, x = xfail, X = xpass - passed = output.count('.') - failed = output.count('F') - errors = output.count('E') - skipped = output.count('s') + passed = output.count(".") + failed = output.count("F") + errors = output.count("E") + skipped = output.count("s") # Store aggregate results (naive mapping, ideally we'd parse node ids) # Since we can't easily map node IDs without -v or --junitxml, we track totals for flakiness *rates* at suite level first. @@ -56,26 +59,36 @@ def run_tests(run_index): junit_path = OUTPUT_DIR / f"flaky-run-{run_index + 1}.xml" subprocess.run( - [sys.executable, "-m", "pytest", TEST_PATTERN, f"--junitxml={junit_path}", "-q"], - capture_output=True, # We only care about the XML file - env={"PYTHONPATH": "src"} + [ + sys.executable, + "-m", + "pytest", + TEST_PATTERN, + f"--junitxml={junit_path}", + "-q", + ], + capture_output=True, # We only care about the XML file + env={"PYTHONPATH": "src"}, + ) + + RUN_DIAGNOSTICS.append( + { + "run": run_index + 1, + "code": exit_code, + "duration": duration, + "passed": passed, + "failed": failed, + "errors": errors, + "skipped": skipped, + } ) - RUN_DIAGNOSTICS.append({ - "run": run_index + 1, - "code": exit_code, - "duration": duration, - "passed": passed, - "failed": failed, - "errors": errors, - "skipped": skipped - }) def parse_junit_xmls(): """Parse generated JUnit XML files to detect per-test flakiness.""" import xml.etree.ElementTree as ET - test_stats = {} # "test_id": {pass: 0, fail: 0, error: 0, skip: 0} + test_stats = {} # "test_id": {pass: 0, fail: 0, error: 0, skip: 0} for i in range(RUNS): xml_path = OUTPUT_DIR / f"flaky-run-{i + 1}.xml" @@ -108,6 +121,7 @@ def parse_junit_xmls(): return test_stats + def main(): OUTPUT_DIR.mkdir(parents=True, exist_ok=True) @@ -129,5 +143,6 @@ def main(): print(f"Flakiness matrix written to {OUTPUT_DIR / 'flakiness-matrix.json'}") + if __name__ == "__main__": main() diff --git a/torch/scripts/test-audit/run_python_static_analysis.py b/torch/scripts/test-audit/run_python_static_analysis.py index ec0116c4..a6aa78ca 100644 --- a/torch/scripts/test-audit/run_python_static_analysis.py +++ b/torch/scripts/test-audit/run_python_static_analysis.py @@ -20,29 +20,33 @@ PATTERNS = [ (r"# todo", "Found '# todo' comment"), ] + def scan_file(filepath): """Scan a single file for suspicious patterns.""" issues = [] try: content = filepath.read_text(encoding="utf-8") except Exception: - return # Skip non-text files + return # Skip non-text files for pattern, message in PATTERNS: if re.search(pattern, content): issues.append(message) # Check for lack of assertions (very naive) - if "test_" in filepath.name and "assert" not in content and "expect" not in content and "raise" not in content: + if ( + "test_" in filepath.name + and "assert" not in content + and "expect" not in content + and "raise" not in content + ): # Exclude conftest or helpers if "conftest" not in filepath.name and "helper" not in filepath.name: - issues.append("No obvious assertions found") + issues.append("No obvious assertions found") if issues: - SUSPICIOUS.append({ - "file": str(filepath), - "issues": issues - }) + SUSPICIOUS.append({"file": str(filepath), "issues": issues}) + def main(): OUTPUT_DIR.mkdir(parents=True, exist_ok=True) @@ -62,5 +66,6 @@ def main(): print(f"Found {len(SUSPICIOUS)} suspicious files.") print(f"Report written to {OUTPUT_DIR / 'suspicious-tests.json'}.") + if __name__ == "__main__": main() diff --git a/torch/src/services/memory/cache.js b/torch/src/services/memory/cache.js index 63170421..344d41d1 100644 --- a/torch/src/services/memory/cache.js +++ b/torch/src/services/memory/cache.js @@ -1,3 +1,5 @@ +import { MINUTE_MS } from './constants/time.js'; + const DEFAULT_MAX_NAMESPACES = 128; const DEFAULT_MAX_EVENTS_PER_NAMESPACE = 200; const DEFAULT_MAX_TOTAL_EVENTS = 5000; @@ -150,11 +152,11 @@ export function createMemoryCache(options = {}) { runtimeNamespaces.clear(); totalRuntimeEvents = 0; }, - setRuntimeEvent(scope, event, ttlMs = 60_000) { + setRuntimeEvent(scope, event, ttlMs = MINUTE_MS) { const now = Date.now(); removeExpired(now); - const safeTtlMs = Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : 60_000; + const safeTtlMs = Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : MINUTE_MS; const expiresAt = now + safeTtlMs; const namespaceKey = getNamespaceKey(scope); const bucket = runtimeNamespaces.get(namespaceKey) ?? { events: [] }; diff --git a/torch/src/services/memory/constants/scoring.js b/torch/src/services/memory/constants/scoring.js new file mode 100644 index 00000000..686f1345 --- /dev/null +++ b/torch/src/services/memory/constants/scoring.js @@ -0,0 +1,2 @@ +export const DEFAULT_IMPORTANCE_SCORE = 0.25; +export const LOW_IMPORTANCE_THRESHOLD = 0.35; diff --git a/torch/src/services/memory/constants/text.js b/torch/src/services/memory/constants/text.js new file mode 100644 index 00000000..538d4054 --- /dev/null +++ b/torch/src/services/memory/constants/text.js @@ -0,0 +1 @@ +export const MAX_SUMMARY_LENGTH = 280; diff --git a/torch/src/services/memory/constants/time.js b/torch/src/services/memory/constants/time.js new file mode 100644 index 00000000..446f9807 --- /dev/null +++ b/torch/src/services/memory/constants/time.js @@ -0,0 +1,6 @@ +export const SECOND_MS = 1000; +export const MINUTE_MS = 60 * SECOND_MS; +export const HOUR_MS = 60 * MINUTE_MS; +export const DAY_MS = 24 * HOUR_MS; +export const WEEK_MS = 7 * DAY_MS; +export const MONTH_MS = 30 * DAY_MS; diff --git a/torch/src/services/memory/index.js b/torch/src/services/memory/index.js index 1c9ec824..46b42822 100644 --- a/torch/src/services/memory/index.js +++ b/torch/src/services/memory/index.js @@ -42,7 +42,9 @@ let currentSavePromise = null; let pendingSavePromise = null; let pendingSaveResolve = null; let lastSaveTime = 0; -const MIN_SAVE_INTERVAL_MS = 1000; +import { SECOND_MS, MONTH_MS, MINUTE_MS } from './constants/time.js'; + +const MIN_SAVE_INTERVAL_MS = SECOND_MS; async function performSave(store) { try { @@ -326,7 +328,7 @@ export async function getRelevantMemories(params) { export async function runPruneCycle(options = {}) { const repository = options.repository ?? memoryRepository; const telemetry = buildTelemetryEmitter(options); - const retentionMs = options.retentionMs ?? (1000 * 60 * 60 * 24 * 30); + const retentionMs = options.retentionMs ?? MONTH_MS; const pruneMode = options.pruneMode ?? getMemoryPruneMode(options.env); const dbCandidates = await listPruneCandidates(repository, { retentionMs, @@ -492,7 +494,7 @@ export async function inspectMemory(id, options = {}) { */ export async function triggerPruneDryRun(options = {}) { const repository = options.repository ?? memoryRepository; - const retentionMs = options.retentionMs ?? (1000 * 60 * 60 * 24 * 30); + const retentionMs = options.retentionMs ?? MONTH_MS; const dbCandidates = await listPruneCandidates(repository, { retentionMs, now: options.now, @@ -532,7 +534,7 @@ export async function triggerPruneDryRun(options = {}) { export async function memoryStats(options = {}) { const repository = options.repository ?? memoryRepository; const now = options.now ?? Date.now(); - const windowMs = Number.isFinite(options.windowMs) ? Math.max(1, Number(options.windowMs)) : (60 * 60 * 1000); + const windowMs = Number.isFinite(options.windowMs) ? Math.max(1, Number(options.windowMs)) : (60 * MINUTE_MS); const memories = await listMemories({}, { repository }); const countsByType = memories.reduce((acc, memory) => { @@ -545,7 +547,7 @@ export async function memoryStats(options = {}) { const archivedRate = archivedCount / Math.max(1, memories.length); const ingestedInWindow = memoryStatsState.ingested.filter((timestamp) => timestamp >= (now - windowMs)).length; - const ingestThroughputPerMinute = ingestedInWindow / Math.max(1, (windowMs / 60_000)); + const ingestThroughputPerMinute = ingestedInWindow / Math.max(1, (windowMs / MINUTE_MS)); return { countsByType, diff --git a/torch/src/services/memory/ingestor.js b/torch/src/services/memory/ingestor.js index 5f9ac422..68ec7590 100644 --- a/torch/src/services/memory/ingestor.js +++ b/torch/src/services/memory/ingestor.js @@ -2,8 +2,10 @@ import { createHash, randomUUID } from 'node:crypto'; import { createMemoryRecord, normalizeEvent, validateMemoryItem } from './schema.js'; import { embedText, getDefaultEmbedderAdapter } from './embedder.js'; import { summarizeEvents } from './summarizer.js'; +import { MINUTE_MS } from './constants/time.js'; +import { MAX_SUMMARY_LENGTH } from './constants/text.js'; -const WINDOW_BUCKET_MS = 60_000; +const WINDOW_BUCKET_MS = MINUTE_MS; const MAX_DEDUPE_KEYS = 2000; const dedupeWindow = new Map(); @@ -89,7 +91,7 @@ async function rememberWindowFingerprint(repository, dedupeKey, payload) { } -function compactContextExcerpt(text, maxChars = 280) { +function compactContextExcerpt(text, maxChars = MAX_SUMMARY_LENGTH) { if (typeof text !== 'string' || text.length === 0) return ''; const compact = text.replace(/\s+/g, ' ').trim(); if (compact.length <= maxChars) return compact; diff --git a/torch/src/services/memory/pruner.js b/torch/src/services/memory/pruner.js index a224b892..89fad513 100644 --- a/torch/src/services/memory/pruner.js +++ b/torch/src/services/memory/pruner.js @@ -1,6 +1,7 @@ import { loadMemoryPromptTemplates } from './summarizer.js'; - -const DAY_MS = 24 * 60 * 60 * 1000; +import { DAY_MS } from './constants/time.js'; +import { MAX_SUMMARY_LENGTH } from './constants/text.js'; +import { LOW_IMPORTANCE_THRESHOLD } from './constants/scoring.js'; /** * @typedef {'keep' | 'archive' | 'delete'} LifecycleAction @@ -113,7 +114,7 @@ function groupNearDuplicates(memories, options) { */ async function condenseGroup(group, generateSummary, condenseTemplate) { const [primary, ...rest] = group; - const fallbackSummary = group.map((item) => item.summary || item.content).filter(Boolean).join(' ').slice(0, 280); + const fallbackSummary = group.map((item) => item.summary || item.content).filter(Boolean).join(' ').slice(0, MAX_SUMMARY_LENGTH); const fallback = { summary: fallbackSummary || primary.summary, importance: Math.max(...group.map((item) => item.importance)), @@ -181,7 +182,7 @@ export async function createLifecyclePlan(memories, options) { const policy = { now, retentionMs: options.retentionMs, - lowImportanceThreshold: options.lowImportanceThreshold ?? 0.35, + lowImportanceThreshold: options.lowImportanceThreshold ?? LOW_IMPORTANCE_THRESHOLD, deleteImportanceThreshold: options.deleteImportanceThreshold ?? 0.12, recentUsageMs: options.recentUsageMs ?? Math.max(Math.floor(options.retentionMs / 2), DAY_MS), duplicateWindowMs: options.duplicateWindowMs ?? (7 * DAY_MS), diff --git a/torch/src/services/memory/retriever.js b/torch/src/services/memory/retriever.js index 5a5e8fc9..24a12f23 100644 --- a/torch/src/services/memory/retriever.js +++ b/torch/src/services/memory/retriever.js @@ -1,11 +1,14 @@ +import { WEEK_MS } from './constants/time.js'; +import { DEFAULT_IMPORTANCE_SCORE } from './constants/scoring.js'; + const DEFAULT_WEIGHTS = { w_sim: 0.6, - w_imp: 0.25, + w_imp: DEFAULT_IMPORTANCE_SCORE, w_rec: 0.15, pinBoost: 0.15, }; -const DEFAULT_RECENCY_HALF_LIFE_MS = 1000 * 60 * 60 * 24 * 7; +const DEFAULT_RECENCY_HALF_LIFE_MS = WEEK_MS; function cosineSimilarity(left = [], right = []) { const size = Math.max(left.length, right.length); diff --git a/torch/src/services/memory/scheduler.js b/torch/src/services/memory/scheduler.js index 533734d9..6c6e451a 100644 --- a/torch/src/services/memory/scheduler.js +++ b/torch/src/services/memory/scheduler.js @@ -1,8 +1,5 @@ import { getMemoryPruneMode, isMemoryEnabled, isMemoryIngestEnabled } from './feature-flags.js'; -const MINUTE_MS = 60_000; -const HOUR_MS = 60 * MINUTE_MS; -const DAY_MS = 24 * HOUR_MS; -const WEEK_MS = 7 * DAY_MS; +import { MINUTE_MS, HOUR_MS, DAY_MS, WEEK_MS } from './constants/time.js'; const JOBS = { ingestRecentRuntimeEvents: { diff --git a/torch/src/services/memory/summarizer.js b/torch/src/services/memory/summarizer.js index 0f128509..d9188912 100644 --- a/torch/src/services/memory/summarizer.js +++ b/torch/src/services/memory/summarizer.js @@ -1,6 +1,8 @@ import { readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { MAX_SUMMARY_LENGTH } from './constants/text.js'; +import { DEFAULT_IMPORTANCE_SCORE, LOW_IMPORTANCE_THRESHOLD } from './constants/scoring.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -61,7 +63,7 @@ function parseSummaryPayload(rawResponse) { const rawImportance = Number(parsed.importance); const importance = Number.isFinite(rawImportance) ? Math.min(1, Math.max(0, rawImportance)) - : 0.25; + : DEFAULT_IMPORTANCE_SCORE; return { summary, importance }; } @@ -69,7 +71,7 @@ function parseSummaryPayload(rawResponse) { function deterministicFallback(events, maxSummaryLength) { const joined = events.map((event) => redactObviousPii(event.content)).join(' ').trim(); const bounded = joined.length > maxSummaryLength ? `${joined.slice(0, maxSummaryLength - 1)}…` : joined; - return { summary: bounded, importance: 0.25 }; + return { summary: bounded, importance: DEFAULT_IMPORTANCE_SCORE }; } function isLikelyFactual(summary, sourceText) { @@ -86,7 +88,7 @@ function isLikelyFactual(summary, sourceText) { if (!summaryTokens.length) return true; const overlap = summaryTokens.filter((token) => sourceTokens.has(token)).length; - return overlap / summaryTokens.length >= 0.35; + return overlap / summaryTokens.length >= LOW_IMPORTANCE_THRESHOLD; } function sanitizeResult(parsed, sourceText, maxSummaryLength, fallback) { @@ -113,7 +115,7 @@ function sanitizeResult(parsed, sourceText, maxSummaryLength, fallback) { * @returns {Promise<{ summary: string, importance: number }>} */ export async function summarizeEvents(events, options = {}) { - const maxSummaryLength = options.maxSummaryLength ?? 280; + const maxSummaryLength = options.maxSummaryLength ?? MAX_SUMMARY_LENGTH; const fallback = deterministicFallback(events, maxSummaryLength); if (!fallback.summary) return fallback; diff --git a/torch/task-logs/daily/2026-02-27T21-08-43Z__const-refactor-agent__completed.md b/torch/task-logs/daily/2026-02-27T21-08-43Z__const-refactor-agent__completed.md new file mode 100644 index 00000000..6ab9fedb --- /dev/null +++ b/torch/task-logs/daily/2026-02-27T21-08-43Z__const-refactor-agent__completed.md @@ -0,0 +1 @@ +completed