mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-06 12:51:54 +00:00
Add memory rollout feature flags and docs
This commit is contained in:
1 parent
ab680f8b7b
commit
d1586228be
10 files changed
+274
-27
No files matched your search
@@ -67,6 +67,10 @@ You can override configuration using environment variables:
|
|||||||
- `NOSTR_LOCK_WEEKLY_ROSTER` (comma-separated list of agents)
|
- `NOSTR_LOCK_WEEKLY_ROSTER` (comma-separated list of agents)
|
||||||
- `TORCH_CONFIG_PATH` (path to config file)
|
- `TORCH_CONFIG_PATH` (path to config file)
|
||||||
- `AGENT_PLATFORM` (platform identifier, e.g. `codex`)
|
- `AGENT_PLATFORM` (platform identifier, e.g. `codex`)
|
||||||
|
- `TORCH_MEMORY_ENABLED` (`true`/`false`; global memory kill switch, defaults to enabled)
|
||||||
|
- `TORCH_MEMORY_INGEST_ENABLED` (`true`/`false` or comma-separated canary `agent_id` allow list)
|
||||||
|
- `TORCH_MEMORY_RETRIEVAL_ENABLED` (`true`/`false` or comma-separated canary `agent_id` allow list)
|
||||||
|
- `TORCH_MEMORY_PRUNE_ENABLED` (`true`, `false`, or `dry-run`)
|
||||||
|
|
||||||
## Roster precedence
|
## Roster precedence
|
||||||
|
|
||||||
@@ -98,3 +102,19 @@ If you are developing `torch-lock` itself:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Memory rollout plan
|
||||||
|
|
||||||
|
1. Deploy memory schema changes with scheduler jobs disabled (`TORCH_MEMORY_ENABLED=false` or subsystem flags set to `false`).
|
||||||
|
2. Enable ingest for one canary `agent_id` via `TORCH_MEMORY_INGEST_ENABLED=<agent_id>`.
|
||||||
|
3. Validate retrieval quality and storage growth metrics before expanding scope.
|
||||||
|
4. Enable broader retrieval (`TORCH_MEMORY_RETRIEVAL_ENABLED=<allow-list>` then `true`).
|
||||||
|
5. Enable pruning in `dry-run` mode first, then switch to active pruning after revalidation.
|
||||||
|
|
||||||
|
## Memory rollback plan
|
||||||
|
|
||||||
|
1. Disable memory flags (`TORCH_MEMORY_ENABLED=false` and/or set ingest/retrieval/prune flags to `false`).
|
||||||
|
2. Stop memory maintenance scheduler processes.
|
||||||
|
3. Preserve database state for post-incident analysis; do not drop or rewrite memory tables during rollback.
|
||||||
|
4. Keep prune actions in `dry-run` (or disabled) until lifecycle policy and data integrity are revalidated.
|
||||||
@@ -36,6 +36,10 @@ node src/nostr-lock.mjs list
|
|||||||
- `NOSTR_LOCK_WEEKLY_ROSTER`
|
- `NOSTR_LOCK_WEEKLY_ROSTER`
|
||||||
- `TORCH_CONFIG_PATH`
|
- `TORCH_CONFIG_PATH`
|
||||||
- `AGENT_PLATFORM`
|
- `AGENT_PLATFORM`
|
||||||
|
- `TORCH_MEMORY_ENABLED` (`true`/`false`; global memory kill switch, defaults to enabled)
|
||||||
|
- `TORCH_MEMORY_INGEST_ENABLED` (`true`/`false` or comma-separated canary `agent_id` allow list)
|
||||||
|
- `TORCH_MEMORY_RETRIEVAL_ENABLED` (`true`/`false` or comma-separated canary `agent_id` allow list)
|
||||||
|
- `TORCH_MEMORY_PRUNE_ENABLED` (`true`, `false`, or `dry-run`)
|
||||||
|
|
||||||
## torch-config.json
|
## torch-config.json
|
||||||
|
|
||||||
@@ -75,3 +79,19 @@ The lock CLI resolves roster names in this order:
|
|||||||
- `1`: usage error
|
- `1`: usage error
|
||||||
- `2`: relay/network error
|
- `2`: relay/network error
|
||||||
- `3`: lock denied (already locked or race lost)
|
- `3`: lock denied (already locked or race lost)
|
||||||
|
|
||||||
|
|
||||||
|
## Memory rollout plan
|
||||||
|
|
||||||
|
1. Deploy memory schema changes with scheduler jobs disabled (`TORCH_MEMORY_ENABLED=false` or subsystem flags set to `false`).
|
||||||
|
2. Enable ingest for one canary `agent_id` via `TORCH_MEMORY_INGEST_ENABLED=<agent_id>`.
|
||||||
|
3. Validate retrieval quality and storage growth metrics before expanding scope.
|
||||||
|
4. Enable broader retrieval (`TORCH_MEMORY_RETRIEVAL_ENABLED=<allow-list>` then `true`).
|
||||||
|
5. Enable pruning in `dry-run` mode first, then switch to active pruning after revalidation.
|
||||||
|
|
||||||
|
## Memory rollback plan
|
||||||
|
|
||||||
|
1. Disable memory flags (`TORCH_MEMORY_ENABLED=false` and/or set ingest/retrieval/prune flags to `false`).
|
||||||
|
2. Stop memory maintenance scheduler processes.
|
||||||
|
3. Preserve database state for post-incident analysis; do not drop or rewrite memory tables during rollback.
|
||||||
|
4. Keep prune actions in `dry-run` (or disabled) until lifecycle policy and data integrity are revalidated.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
const FALSEY = new Set(['0', 'false', 'off', 'no', 'disabled']);
|
||||||
|
const TRUTHY = new Set(['1', 'true', 'on', 'yes', 'enabled', 'all']);
|
||||||
|
|
||||||
|
function normalize(value) {
|
||||||
|
return String(value ?? '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAgentToggle(value) {
|
||||||
|
const raw = normalize(value);
|
||||||
|
if (!raw) return { kind: 'unset' };
|
||||||
|
|
||||||
|
const lower = raw.toLowerCase();
|
||||||
|
if (FALSEY.has(lower)) return { kind: 'off' };
|
||||||
|
if (TRUTHY.has(lower)) return { kind: 'all' };
|
||||||
|
|
||||||
|
const allowList = raw
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return allowList.length > 0 ? { kind: 'allow-list', allowList } : { kind: 'off' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isToggleEnabledForAgent(value, agentId, fallbackWhenUnset) {
|
||||||
|
const parsed = parseAgentToggle(value);
|
||||||
|
if (parsed.kind === 'unset') return fallbackWhenUnset;
|
||||||
|
if (parsed.kind === 'off') return false;
|
||||||
|
if (parsed.kind === 'all') return true;
|
||||||
|
if (!agentId) return false;
|
||||||
|
return parsed.allowList.includes(String(agentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMemoryEnabled(env = process.env) {
|
||||||
|
return isToggleEnabledForAgent(env.TORCH_MEMORY_ENABLED, undefined, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMemoryIngestEnabled(agentId, env = process.env) {
|
||||||
|
if (!isMemoryEnabled(env)) return false;
|
||||||
|
return isToggleEnabledForAgent(env.TORCH_MEMORY_INGEST_ENABLED, agentId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMemoryRetrievalEnabled(agentId, env = process.env) {
|
||||||
|
if (!isMemoryEnabled(env)) return false;
|
||||||
|
return isToggleEnabledForAgent(env.TORCH_MEMORY_RETRIEVAL_ENABLED, agentId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMemoryPruneMode(env = process.env) {
|
||||||
|
if (!isMemoryEnabled(env)) return 'off';
|
||||||
|
|
||||||
|
const raw = normalize(env.TORCH_MEMORY_PRUNE_ENABLED);
|
||||||
|
if (!raw) return 'active';
|
||||||
|
|
||||||
|
const lower = raw.toLowerCase();
|
||||||
|
if (FALSEY.has(lower)) return 'off';
|
||||||
|
if (['dry-run', 'dryrun', 'report', 'audit'].includes(lower)) return 'dry-run';
|
||||||
|
return 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMemoryPruneEnabled(env = process.env) {
|
||||||
|
return getMemoryPruneMode(env) === 'active';
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isMemoryRetrievalEnabled } from './feature-flags.js';
|
||||||
|
export { isMemoryRetrievalEnabled } from './feature-flags.js';
|
||||||
const DEFAULT_TOKEN_BUDGET = 1000;
|
const DEFAULT_TOKEN_BUDGET = 1000;
|
||||||
const DEFAULT_MAX_SUMMARY_CHARS = 240;
|
const DEFAULT_MAX_SUMMARY_CHARS = 240;
|
||||||
const DEFAULT_MAX_CONTEXT_CHARS = 320;
|
const DEFAULT_MAX_CONTEXT_CHARS = 320;
|
||||||
@@ -130,32 +132,6 @@ function toContextString(agentContext) {
|
|||||||
return String(agentContext).trim();
|
return String(agentContext).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Supports safe rollout and canarying with one env var:
|
|
||||||
* - true/1/on/all => enabled for all agents
|
|
||||||
* - false/0/off/no/empty => disabled
|
|
||||||
* - comma-separated agent ids => enabled only for matching ids
|
|
||||||
*
|
|
||||||
* @param {string} agentId
|
|
||||||
* @param {Record<string, string | undefined>} [env]
|
|
||||||
*/
|
|
||||||
export function isMemoryRetrievalEnabled(agentId, env = process.env) {
|
|
||||||
const raw = String(env.TORCH_MEMORY_RETRIEVAL_ENABLED ?? '').trim();
|
|
||||||
if (!raw) return false;
|
|
||||||
|
|
||||||
const normalized = raw.toLowerCase();
|
|
||||||
if (['0', 'false', 'off', 'no', 'disabled'].includes(normalized)) return false;
|
|
||||||
if (['1', 'true', 'on', 'yes', 'enabled', 'all'].includes(normalized)) return true;
|
|
||||||
|
|
||||||
const allowList = raw
|
|
||||||
.split(',')
|
|
||||||
.map((value) => value.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
if (allowList.length === 0) return false;
|
|
||||||
return allowList.includes(agentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a final prompt with retrieved memory context inserted ahead of the base prompt.
|
* Builds a final prompt with retrieved memory context inserted ahead of the base prompt.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ingestMemoryWindow } from './ingestor.js';
|
|||||||
import { listPruneCandidates, selectPrunableMemories } from './pruner.js';
|
import { listPruneCandidates, selectPrunableMemories } from './pruner.js';
|
||||||
import { filterAndRankMemories, updateMemoryUsage } from './retriever.js';
|
import { filterAndRankMemories, updateMemoryUsage } from './retriever.js';
|
||||||
import { startScheduler } from './scheduler.js';
|
import { startScheduler } from './scheduler.js';
|
||||||
|
import { getMemoryPruneMode, isMemoryIngestEnabled } from './feature-flags.js';
|
||||||
|
|
||||||
const memoryStore = new Map();
|
const memoryStore = new Map();
|
||||||
const cache = createMemoryCache();
|
const cache = createMemoryCache();
|
||||||
@@ -133,6 +134,10 @@ function recordThroughput(samples, now) {
|
|||||||
export async function ingestEvents(events, options = {}) {
|
export async function ingestEvents(events, options = {}) {
|
||||||
const repository = options.repository ?? memoryRepository;
|
const repository = options.repository ?? memoryRepository;
|
||||||
const telemetry = buildTelemetryEmitter(options);
|
const telemetry = buildTelemetryEmitter(options);
|
||||||
|
if (!isMemoryIngestEnabled(options.agent_id, options.env)) {
|
||||||
|
telemetry('memory:ingest_skipped', { reason: 'flag_disabled', agent_id: options.agent_id ?? null });
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const records = await ingestMemoryWindow({ events }, {
|
const records = await ingestMemoryWindow({ events }, {
|
||||||
...options,
|
...options,
|
||||||
repository,
|
repository,
|
||||||
@@ -201,6 +206,7 @@ export async function runPruneCycle(options = {}) {
|
|||||||
const repository = options.repository ?? memoryRepository;
|
const repository = options.repository ?? memoryRepository;
|
||||||
const telemetry = buildTelemetryEmitter(options);
|
const telemetry = buildTelemetryEmitter(options);
|
||||||
const retentionMs = options.retentionMs ?? (1000 * 60 * 60 * 24 * 30);
|
const retentionMs = options.retentionMs ?? (1000 * 60 * 60 * 24 * 30);
|
||||||
|
const pruneMode = options.pruneMode ?? getMemoryPruneMode(options.env);
|
||||||
const dbCandidates = await listPruneCandidates(repository, {
|
const dbCandidates = await listPruneCandidates(repository, {
|
||||||
retentionMs,
|
retentionMs,
|
||||||
now: options.now,
|
now: options.now,
|
||||||
@@ -213,6 +219,21 @@ export async function runPruneCycle(options = {}) {
|
|||||||
now: options.now,
|
now: options.now,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (pruneMode === 'off') {
|
||||||
|
telemetry('memory:prune_skipped', { reason: 'flag_disabled', retention_ms: retentionMs });
|
||||||
|
return { pruned: [], mode: 'off' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pruneMode === 'dry-run') {
|
||||||
|
telemetry('memory:pruned', {
|
||||||
|
pruned_count: prunable.length,
|
||||||
|
retention_ms: retentionMs,
|
||||||
|
dry_run: true,
|
||||||
|
});
|
||||||
|
emitMetric(options, 'memory_pruned_total', { count: 0, retention_ms: retentionMs, dry_run: true });
|
||||||
|
return { pruned: [], candidates: prunable, mode: 'dry-run' };
|
||||||
|
}
|
||||||
|
|
||||||
for (const memory of prunable) {
|
for (const memory of prunable) {
|
||||||
memoryStore.delete(memory.id);
|
memoryStore.delete(memory.id);
|
||||||
}
|
}
|
||||||
@@ -229,7 +250,7 @@ export async function runPruneCycle(options = {}) {
|
|||||||
|
|
||||||
const result = { pruned: prunable };
|
const result = { pruned: prunable };
|
||||||
if (Number.isFinite(options.scheduleEveryMs) && options.scheduleEveryMs > 0) {
|
if (Number.isFinite(options.scheduleEveryMs) && options.scheduleEveryMs > 0) {
|
||||||
result.scheduler = startScheduler(() => runPruneCycle({ retentionMs, repository }), {
|
result.scheduler = startScheduler(() => runPruneCycle({ retentionMs, repository, env: options.env, pruneMode: options.pruneMode }), {
|
||||||
intervalMs: options.scheduleEveryMs,
|
intervalMs: options.scheduleEveryMs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getMemoryPruneMode, isMemoryEnabled, isMemoryIngestEnabled } from './feature-flags.js';
|
||||||
const MINUTE_MS = 60_000;
|
const MINUTE_MS = 60_000;
|
||||||
const HOUR_MS = 60 * MINUTE_MS;
|
const HOUR_MS = 60 * MINUTE_MS;
|
||||||
const DAY_MS = 24 * HOUR_MS;
|
const DAY_MS = 24 * HOUR_MS;
|
||||||
@@ -182,6 +183,21 @@ async function runScheduledJob(options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldRunJob(jobName, options) {
|
||||||
|
const env = options.env ?? process.env;
|
||||||
|
if (!isMemoryEnabled(env)) return false;
|
||||||
|
|
||||||
|
if (jobName === 'ingestRecentRuntimeEvents') {
|
||||||
|
return isMemoryIngestEnabled(options.ingestAgentId, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jobName === 'pruningCycle') {
|
||||||
|
return getMemoryPruneMode(env) !== 'off';
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{
|
* @param {{
|
||||||
* handlers: {
|
* handlers: {
|
||||||
@@ -218,6 +234,17 @@ export function startMemoryMaintenanceScheduler(options) {
|
|||||||
|
|
||||||
const startFixedIntervalJob = (jobName, definition, handler) => {
|
const startFixedIntervalJob = (jobName, definition, handler) => {
|
||||||
const run = () => {
|
const run = () => {
|
||||||
|
if (!shouldRunJob(jobName, options)) {
|
||||||
|
options.emitMetric?.('memory_scheduler_job', {
|
||||||
|
job: jobName,
|
||||||
|
status: 'skipped_flag_disabled',
|
||||||
|
durationMs: 0,
|
||||||
|
itemCount: 0,
|
||||||
|
failures: 0,
|
||||||
|
retries: 0,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
void runJob(jobName, definition, handler);
|
void runJob(jobName, definition, handler);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -232,6 +259,18 @@ export function startMemoryMaintenanceScheduler(options) {
|
|||||||
const delayMs = definition.minIntervalMs + Math.floor((definition.maxIntervalMs - definition.minIntervalMs) * jitterRatio);
|
const delayMs = definition.minIntervalMs + Math.floor((definition.maxIntervalMs - definition.minIntervalMs) * jitterRatio);
|
||||||
const timeout = setTimeout(async () => {
|
const timeout = setTimeout(async () => {
|
||||||
timeouts.delete(timeout);
|
timeouts.delete(timeout);
|
||||||
|
if (!shouldRunJob(jobName, options)) {
|
||||||
|
options.emitMetric?.('memory_scheduler_job', {
|
||||||
|
job: jobName,
|
||||||
|
status: 'skipped_flag_disabled',
|
||||||
|
durationMs: 0,
|
||||||
|
itemCount: 0,
|
||||||
|
failures: 0,
|
||||||
|
retries: 0,
|
||||||
|
});
|
||||||
|
scheduleNext();
|
||||||
|
return;
|
||||||
|
}
|
||||||
await runJob(jobName, definition, handler);
|
await runJob(jobName, definition, handler);
|
||||||
scheduleNext();
|
scheduleNext();
|
||||||
}, delayMs);
|
}, delayMs);
|
||||||
@@ -239,6 +278,18 @@ export function startMemoryMaintenanceScheduler(options) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (options.runImmediately !== false) {
|
if (options.runImmediately !== false) {
|
||||||
|
if (!shouldRunJob(jobName, options)) {
|
||||||
|
options.emitMetric?.('memory_scheduler_job', {
|
||||||
|
job: jobName,
|
||||||
|
status: 'skipped_flag_disabled',
|
||||||
|
durationMs: 0,
|
||||||
|
itemCount: 0,
|
||||||
|
failures: 0,
|
||||||
|
retries: 0,
|
||||||
|
});
|
||||||
|
scheduleNext();
|
||||||
|
return;
|
||||||
|
}
|
||||||
void runJob(jobName, definition, handler).then(() => {
|
void runJob(jobName, definition, handler).then(() => {
|
||||||
scheduleNext();
|
scheduleNext();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
listMemories,
|
listMemories,
|
||||||
memoryStats,
|
memoryStats,
|
||||||
pinMemory,
|
pinMemory,
|
||||||
|
runPruneCycle,
|
||||||
triggerPruneDryRun,
|
triggerPruneDryRun,
|
||||||
unpinMemory,
|
unpinMemory,
|
||||||
} from '../src/services/memory/index.js';
|
} from '../src/services/memory/index.js';
|
||||||
@@ -104,3 +105,31 @@ test('memoryStats reports counts/rates and retrieval telemetry remains redaction
|
|||||||
assert.ok(stats.indexSizeEstimateBytes >= 0);
|
assert.ok(stats.indexSizeEstimateBytes >= 0);
|
||||||
assert.ok(Number.isFinite(stats.ingestThroughputPerMinute));
|
assert.ok(Number.isFinite(stats.ingestThroughputPerMinute));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('runPruneCycle respects prune feature-flag modes', async () => {
|
||||||
|
const repository = {
|
||||||
|
async listPruneCandidates() {
|
||||||
|
return [
|
||||||
|
{ id: 'old-1', agent_id: 'agent-z', type: 'event', pinned: false, last_seen: 1, merged_into: null },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const dryRunResult = await runPruneCycle({
|
||||||
|
repository,
|
||||||
|
env: { TORCH_MEMORY_PRUNE_ENABLED: 'dry-run' },
|
||||||
|
retentionMs: 1000,
|
||||||
|
});
|
||||||
|
assert.equal(dryRunResult.mode, 'dry-run');
|
||||||
|
assert.equal(dryRunResult.pruned.length, 0);
|
||||||
|
assert.equal(dryRunResult.candidates.length, 1);
|
||||||
|
|
||||||
|
const offResult = await runPruneCycle({
|
||||||
|
repository,
|
||||||
|
env: { TORCH_MEMORY_PRUNE_ENABLED: 'false' },
|
||||||
|
retentionMs: 1000,
|
||||||
|
});
|
||||||
|
assert.equal(offResult.mode, 'off');
|
||||||
|
assert.equal(offResult.pruned.length, 0);
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getMemoryPruneMode,
|
||||||
|
isMemoryEnabled,
|
||||||
|
isMemoryIngestEnabled,
|
||||||
|
isMemoryPruneEnabled,
|
||||||
|
} from '../src/services/memory/feature-flags.js';
|
||||||
|
|
||||||
|
test('memory feature flags parse defaults and canary allow-lists', () => {
|
||||||
|
assert.equal(isMemoryEnabled({}), true);
|
||||||
|
assert.equal(isMemoryEnabled({ TORCH_MEMORY_ENABLED: 'false' }), false);
|
||||||
|
|
||||||
|
assert.equal(isMemoryIngestEnabled('agent-a', {}), true);
|
||||||
|
assert.equal(isMemoryIngestEnabled('agent-a', { TORCH_MEMORY_INGEST_ENABLED: 'false' }), false);
|
||||||
|
assert.equal(isMemoryIngestEnabled('agent-a', { TORCH_MEMORY_INGEST_ENABLED: 'agent-a,agent-b' }), true);
|
||||||
|
assert.equal(isMemoryIngestEnabled('agent-c', { TORCH_MEMORY_INGEST_ENABLED: 'agent-a,agent-b' }), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('memory prune flag supports off, dry-run, and active modes', () => {
|
||||||
|
assert.equal(getMemoryPruneMode({}), 'active');
|
||||||
|
assert.equal(getMemoryPruneMode({ TORCH_MEMORY_PRUNE_ENABLED: 'false' }), 'off');
|
||||||
|
assert.equal(getMemoryPruneMode({ TORCH_MEMORY_PRUNE_ENABLED: 'dry-run' }), 'dry-run');
|
||||||
|
assert.equal(getMemoryPruneMode({ TORCH_MEMORY_PRUNE_ENABLED: 'true' }), 'active');
|
||||||
|
assert.equal(isMemoryPruneEnabled({ TORCH_MEMORY_PRUNE_ENABLED: 'dry-run' }), false);
|
||||||
|
assert.equal(isMemoryPruneEnabled({ TORCH_MEMORY_PRUNE_ENABLED: 'true' }), true);
|
||||||
|
});
|
||||||
@@ -143,6 +143,13 @@ test('isMemoryRetrievalEnabled supports global toggle and agent canary list', ()
|
|||||||
isMemoryRetrievalEnabled('agent-c', { TORCH_MEMORY_RETRIEVAL_ENABLED: 'agent-a,agent-b' }),
|
isMemoryRetrievalEnabled('agent-c', { TORCH_MEMORY_RETRIEVAL_ENABLED: 'agent-a,agent-b' }),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
isMemoryRetrievalEnabled('agent-a', {
|
||||||
|
TORCH_MEMORY_ENABLED: 'false',
|
||||||
|
TORCH_MEMORY_RETRIEVAL_ENABLED: 'true',
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renderPromptWithMemoryContext injects bounded memory block before base prompt', async () => {
|
test('renderPromptWithMemoryContext injects bounded memory block before base prompt', async () => {
|
||||||
|
|||||||
@@ -90,3 +90,37 @@ test('startMemoryMaintenanceScheduler runs all jobs and emits metrics including
|
|||||||
assert.equal(byJob.pruningCycle[0].payload.status, 'success');
|
assert.equal(byJob.pruningCycle[0].payload.status, 'success');
|
||||||
assert.equal(byJob.deepMergeArchivalMaintenance[0].payload.status, 'success');
|
assert.equal(byJob.deepMergeArchivalMaintenance[0].payload.status, 'success');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('startMemoryMaintenanceScheduler skips jobs when feature flags disable memory', async () => {
|
||||||
|
const metrics = [];
|
||||||
|
const called = [];
|
||||||
|
|
||||||
|
const scheduler = startMemoryMaintenanceScheduler({
|
||||||
|
runImmediately: true,
|
||||||
|
env: { TORCH_MEMORY_ENABLED: 'false' },
|
||||||
|
emitMetric(name, payload) {
|
||||||
|
metrics.push({ name, payload });
|
||||||
|
},
|
||||||
|
handlers: {
|
||||||
|
async ingestRecentRuntimeEvents() {
|
||||||
|
called.push('ingest');
|
||||||
|
},
|
||||||
|
async consolidateObservations() {
|
||||||
|
called.push('consolidate');
|
||||||
|
},
|
||||||
|
async pruningCycle() {
|
||||||
|
called.push('prune');
|
||||||
|
},
|
||||||
|
async deepMergeArchivalMaintenance() {
|
||||||
|
called.push('merge');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(10);
|
||||||
|
scheduler.stop();
|
||||||
|
|
||||||
|
assert.deepEqual(called, []);
|
||||||
|
assert.ok(metrics.some((entry) => entry.payload.status === 'skipped_flag_disabled'));
|
||||||
|
});
|
||||||
Reference in new issue
Block a user