Merge pull request #943 from PR0M3TH3AN/agents/daily/const-refactor-agent-6148832155416611801

[constants] extract memory service numeric literals to canonical modules
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-28 22:36:42 -05:00
commit 09a21a4b13
23 files changed
+216 -23

No files matched your search

@@ -1,3 +1,6 @@
### 1. [2026-02-27] (scheduler, daily, retrieve)
Memory retrieval seed for daily :: Required startup + artifacts + memory + issue capture
### 1. [2026-02-28] (scheduler, daily, retrieve)
# Memory Update (2026-02-25)
+16
View File
@@ -513,6 +513,12 @@
}
],
[
"094aab2a-4a25-4322-a198-1638752acc77",
{
"schema_version": 1,
"id": "094aab2a-4a25-4322-a198-1638752acc77",
"agent_id": "const-refactor-agent",
"session_id": "session-mm5d7amf",
"c60afd2d-1325-4096-849a-77e11f2b5e39",
{
"schema_version": 1,
@@ -547,6 +553,8 @@
],
"importance": 0.4,
"embedding_id": null,
"created_at": 1772225327752,
"last_seen": 1772225327764,
"created_at": 1772221041979,
"last_seen": 1772221041993,
"created_at": 1772218140227,
@@ -560,6 +568,12 @@
}
],
[
"cef905d0-7a66-459c-8937-41310afc934f",
{
"schema_version": 1,
"id": "cef905d0-7a66-459c-8937-41310afc934f",
"agent_id": "const-refactor-agent",
"session_id": "session-mm5dpefy",
"9c49ca58-c6e5-4751-a3de-ab48bf7a4da1",
{
"schema_version": 1,
@@ -592,6 +606,8 @@
],
"importance": 0.8,
"embedding_id": null,
"created_at": 1772226172512,
"last_seen": 1772226172512,
"created_at": 1772222021666,
"last_seen": 1772222021666,
"created_at": 1772218432277,
@@ -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"
}
@@ -0,0 +1 @@
MEMORY_RETRIEVED
@@ -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"
}
@@ -0,0 +1 @@
MEMORY_STORED
@@ -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,"}
]
}
]
@@ -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;
+2
View File
@@ -0,0 +1,2 @@
Fri Feb 27 20:49:17 UTC 2026
Exploring duplicated constants in js/
+7
View File
@@ -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`.
+1
View File
@@ -0,0 +1 @@
Ran `npm run --prefix torch lint` and verified that there are no linter errors.
+10
View File
@@ -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/`.
+4 -2
View File
@@ -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: [] };
@@ -0,0 +1,2 @@
export const DEFAULT_IMPORTANCE_SCORE = 0.25;
export const LOW_IMPORTANCE_THRESHOLD = 0.35;
@@ -0,0 +1 @@
export const MAX_SUMMARY_LENGTH = 280;
@@ -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;
+7 -5
View File
@@ -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,
+4 -2
View File
@@ -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;
+5 -4
View File
@@ -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),
+5 -2
View File
@@ -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);
+1 -4
View File
@@ -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: {
+6 -4
View File
@@ -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;