From c3428eef24409672a4901568a0b2a48883537a69 Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Wed, 26 Aug 2026 07:05:03 -0500 Subject: [PATCH 1/5] fix(stream): reclaim expired .prev generation before rotation rename An existing .prev older than AGENTMEMORY_LIVE_STREAM_PREV_TTL_DAYS days (default 30, parseInt fallback, 0 opts out) is unlinked best-effort before the rename so its bytes are reclaimed while the oversized current file still exists; fresh generations stay reserved for the atomic swap. --- src/state/live-stream-rotation.ts | 75 +++++++++++++++++-- test/live-stream-rotation.test.ts | 115 +++++++++++++++++++++++++++++- 2 files changed, 182 insertions(+), 8 deletions(-) diff --git a/src/state/live-stream-rotation.ts b/src/state/live-stream-rotation.ts index 1a2f75f78..7f0dec2f9 100644 --- a/src/state/live-stream-rotation.ts +++ b/src/state/live-stream-rotation.ts @@ -46,18 +46,60 @@ export interface LiveStreamRotationOptions { // subsequent publish would rename and rewrite ~cap bytes forever. One // rotation per cooldown window bounds disk without the storm. export const ROTATION_COOLDOWN_MS = 10 * 60_000; + let lastRotationAtMs = 0; +// How long a retired `.prev` generation is kept before the next rotation may +// discard it up front. Reclaiming an expired generation before the rename +// frees its bytes while the oversized current file is still in place; a fresh +// generation is left for the rename to replace atomically. +export const DEFAULT_LIVE_STREAM_PREV_TTL_DAYS = 30; +const DAY_MS = 24 * 60 * 60 * 1000; + +export function resolveLiveStreamPrevTtlDays( + env: NodeJS.ProcessEnv = process.env, +): number { + const parsed = Number.parseInt( + env.AGENTMEMORY_LIVE_STREAM_PREV_TTL_DAYS ?? "", + 10, + ); + // Mirrors resolveLiveStreamMaxBytes: unset, unparsable, or negative values + // fall back to the default; zero is an explicit opt-out that keeps every + // .prev generation until the next rename replaces it. + if (!Number.isFinite(parsed) || parsed < 0) { + return DEFAULT_LIVE_STREAM_PREV_TTL_DAYS; + } + return parsed; +} + +function isExpiredPreviousStream( + previousPath: string, + nowMs: number, + ttlDays: number, +): boolean { + if (ttlDays <= 0) return false; + try { + return statSync(previousPath).mtimeMs <= nowMs - ttlDays * DAY_MS; + } catch { + // An unstatable .prev (stat race) defers to the rename below, whose own + // catch reports any replacement failure. + return false; + } +} + // Rotates the viewer live stream when its persisted file has outgrown // AGENTMEMORY_LIVE_STREAM_MAX_BYTES: the current file is renamed to -// `.prev` (overwriting any previous generation) so the engine's next -// append starts a fresh file. The engine opens the store by path per append +// `.prev` (replacing any previous generation) so the engine's next +// append starts a fresh file. An existing .prev older than +// AGENTMEMORY_LIVE_STREAM_PREV_TTL_DAYS days (default 30, 0 opts out) is +// unlinked before the rename so its bytes are reclaimed while the oversized +// current file still exists; the engine opens the store by path per append // (no long-lived fd), so no engine restart or cooperation is needed. // -// Best-effort by contract: every failure — missing file, stat error, rename -// race — degrades to "keep appending to the oversized file" and at most one -// warn line, because blocking or failing an observation publish to save disk -// would be the wrong trade. +// Best-effort by contract: every failure — missing file, stat error, stuck +// .prev removal, rename race — degrades to "keep appending to the oversized +// file" and at most one warn line per failed step, because blocking or +// failing an observation publish to save disk would be the wrong trade. // // Under vitest the default path would point at the operator's real store // (tests share HOME and no engine owns the file there), so default-path @@ -93,7 +135,26 @@ export function rotateLiveStreamIfOversized( try { const previousPath = `${filePath}.prev`; - if (existsSync(previousPath)) rmSync(previousPath); + if ( + existsSync(previousPath) && + isExpiredPreviousStream( + previousPath, + nowMs, + resolveLiveStreamPrevTtlDays(options.env), + ) + ) { + try { + rmSync(previousPath); + } catch (error) { + // Best-effort: an unremovable stale generation (permissions, occupied + // directory) must not block the rotation; the rename below replaces + // the target on its own where the platform allows it. + logger.warn("expired live stream .prev could not be removed", { + previousPath, + error: error instanceof Error ? error.message : String(error), + }); + } + } renameSync(filePath, previousPath); lastRotationAtMs = nowMs; return true; diff --git a/test/live-stream-rotation.test.ts b/test/live-stream-rotation.test.ts index 91a32cc3a..ff8c9ea89 100644 --- a/test/live-stream-rotation.test.ts +++ b/test/live-stream-rotation.test.ts @@ -1,18 +1,22 @@ -import { describe, it, expect, afterEach, beforeEach } from "vitest"; +import { describe, it, expect, afterEach, beforeEach, vi } from "vitest"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, + utimesSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { logger } from "../src/logger.js"; import { DEFAULT_LIVE_STREAM_MAX_BYTES, + DEFAULT_LIVE_STREAM_PREV_TTL_DAYS, ROTATION_COOLDOWN_MS, resolveLiveStreamMaxBytes, + resolveLiveStreamPrevTtlDays, rotateLiveStreamIfOversized, viewerLiveStreamPath, } from "../src/state/live-stream-rotation.js"; @@ -29,6 +33,7 @@ function tempDataDir(): string { afterEach(() => { for (const done of cleanup) done(); cleanup = []; + vi.restoreAllMocks(); }); describe("DEFAULT_LIVE_STREAM_MAX_BYTES", () => { @@ -37,6 +42,31 @@ describe("DEFAULT_LIVE_STREAM_MAX_BYTES", () => { }); }); +describe("resolveLiveStreamPrevTtlDays", () => { + it("defaults to 30 days when unset", () => { + expect(DEFAULT_LIVE_STREAM_PREV_TTL_DAYS).toBe(30); + expect(resolveLiveStreamPrevTtlDays({})).toBe(DEFAULT_LIVE_STREAM_PREV_TTL_DAYS); + }); + + it("honors a positive override and truncates fractions", () => { + expect(resolveLiveStreamPrevTtlDays({ AGENTMEMORY_LIVE_STREAM_PREV_TTL_DAYS: "7" })).toBe(7); + expect(resolveLiveStreamPrevTtlDays({ AGENTMEMORY_LIVE_STREAM_PREV_TTL_DAYS: "1.9" })).toBe(1); + }); + + it("treats zero as an opt-out that keeps every generation", () => { + expect(resolveLiveStreamPrevTtlDays({ AGENTMEMORY_LIVE_STREAM_PREV_TTL_DAYS: "0" })).toBe(0); + }); + + it("falls back to the default for invalid values", () => { + expect(resolveLiveStreamPrevTtlDays({ AGENTMEMORY_LIVE_STREAM_PREV_TTL_DAYS: "soon" })).toBe( + DEFAULT_LIVE_STREAM_PREV_TTL_DAYS, + ); + expect(resolveLiveStreamPrevTtlDays({ AGENTMEMORY_LIVE_STREAM_PREV_TTL_DAYS: "-5" })).toBe( + DEFAULT_LIVE_STREAM_PREV_TTL_DAYS, + ); + }); +}); + describe("resolveLiveStreamMaxBytes", () => { it("defaults when unset", () => { expect(resolveLiveStreamMaxBytes({})).toBe(DEFAULT_LIVE_STREAM_MAX_BYTES); @@ -86,6 +116,15 @@ describe("rotateLiveStreamIfOversized", () => { return filePath; } + // Seeds a stale generation at .prev with a backdated mtime so the + // injected nowMs clock decides its TTL age deterministically. + function seedPrevious(filePath: string, bytes: number, ageDays: number): void { + const previousPath = `${filePath}.prev`; + writeFileSync(previousPath, Buffer.alloc(bytes, 0x01)); + const stamped = new Date(clock - ageDays * 24 * 60 * 60 * 1000); + utimesSync(previousPath, stamped, stamped); + } + it("rotates once to .prev when the injected cap is tiny", () => { const dataDir = tempDataDir(); const filePath = seedStream(dataDir, 64); @@ -193,4 +232,78 @@ describe("rotateLiveStreamIfOversized", () => { expect(existsSync(filePath)).toBe(false); expect(readFileSync(`${filePath}.prev`).length).toBe(64); }); + + it("removes an expired .prev before renaming current into place", () => { + const dataDir = tempDataDir(); + const filePath = seedStream(dataDir, 64); + seedPrevious(filePath, 4, 40); // older than the 30-day TTL + + const rotated = rotateLiveStreamIfOversized({ + dataDir, + maxBytes: 32, + nowMs: clock, + env: { AGENTMEMORY_LIVE_STREAM_PREV_TTL_DAYS: "30" }, + }); + + expect(rotated).toBe(true); + const previous = readFileSync(`${filePath}.prev`); + expect(previous.length).toBe(64); // former current content, not the stale marker + }); + + it("leaves a fresh .prev alone until the rename replaces it", () => { + const dataDir = tempDataDir(); + const filePath = seedStream(dataDir, 64); + seedPrevious(filePath, 4, 1); // well inside the TTL window + + const rotated = rotateLiveStreamIfOversized({ + dataDir, + maxBytes: 32, + nowMs: clock, + env: { AGENTMEMORY_LIVE_STREAM_PREV_TTL_DAYS: "30" }, + }); + + expect(rotated).toBe(true); + expect(readFileSync(`${filePath}.prev`).length).toBe(64); + }); + + it("still rotates from scratch when no .prev exists yet", () => { + const dataDir = tempDataDir(); + const filePath = seedStream(dataDir, 64); + + expect(existsSync(`${filePath}.prev`)).toBe(false); + expect( + rotateLiveStreamIfOversized({ + dataDir, + maxBytes: 32, + nowMs: clock, + env: { AGENTMEMORY_LIVE_STREAM_PREV_TTL_DAYS: "30" }, + }), + ).toBe(true); + expect(readFileSync(`${filePath}.prev`).length).toBe(64); + }); + + it("warns and keeps the oversized stream when an expired .prev cannot be removed", () => { + const dataDir = tempDataDir(); + const filePath = seedStream(dataDir, 64); + // A non-empty directory at .prev defeats rmSync without recursive. + mkdirSync(`${filePath}.prev`); + writeFileSync(join(`${filePath}.prev`, "occupied"), "x"); + const stamped = new Date(clock - 40 * 24 * 60 * 60 * 1000); + utimesSync(`${filePath}.prev`, stamped, stamped); + const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {}); + + expect( + rotateLiveStreamIfOversized({ + dataDir, + maxBytes: 32, + nowMs: clock, + env: { AGENTMEMORY_LIVE_STREAM_PREV_TTL_DAYS: "30" }, + }), + ).toBe(false); + + expect(warnSpy.mock.calls.some(([message]) => String(message).includes("could not be removed"))).toBe( + true, + ); + expect(existsSync(filePath)).toBe(true); + }); }); From eac06001fc4dffac6e71c19a7d6d858fd83e375d Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Wed, 26 Aug 2026 07:05:38 -0500 Subject: [PATCH 2/5] docs(sync): release-dir pruning rule in deploy recipe Keep the 3 newest release dirs plus the one 'current' references; delete the rest after the fresh release soaks. --- docs/upstream-sync.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/upstream-sync.md b/docs/upstream-sync.md index 4f5153a35..391d3d48f 100644 --- a/docs/upstream-sync.md +++ b/docs/upstream-sync.md @@ -231,3 +231,19 @@ main sha. 7. Update the tier-1 decision record (`decisions` table via `dsh-sor.sh`) so the deployment is queryable truth, not session folklore. + +8. **Prune release dirs after soak.** Once the new release has soaked (health + endpoint and hook round-trip green), delete old release directories under + `releases/` keeping the 3 newest plus any directory the `current` symlink + references: + + ```sh + cd "$HOME/Library/Application Support/Agentmemory/releases" || exit 1 + KEEP_CURRENT="$(basename "$(readlink ../current)")" + ls -t | grep -v -x -e "$KEEP_CURRENT" | tail -n +4 | while IFS= read -r dir; do + rm -rf "$dir" + done + ``` + + Without this, one directory per train accumulates indefinitely (23 were + on disk before the rule existed). From 78a3d8c81af48c8e6a3eef0cb9297f1167774c9b Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Wed, 26 Aug 2026 07:08:36 -0500 Subject: [PATCH 3/5] chore(release): 0.9.30-chronode.6 version stamps + changelog Ledger closure train: .prev TTL reclamation, watchdog RSS sampling, auth-log pass results, release-dir pruning policy. --- CHANGELOG.md | 18 ++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- packages/mcp/package.json | 4 ++-- plugin/.claude-plugin/plugin.json | 2 +- plugin/.codex-plugin/plugin.json | 2 +- plugin/plugin.json | 2 +- src/version.ts | 2 +- 8 files changed, 27 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4432e4c25..380ce4a62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +## [0.9.30-chronode.6] — 2026-08-26 + +Checkup-and-ledger closure release: TTL-bounded `.prev` retention, watchdog RSS sampling, and tier-1 ledger closure of the sync-wave follow-ups. + +### Added + +- **Expired `.prev` reclamation before live-stream rotation** (`src/state/live-stream-rotation.ts`). Before renaming an oversized current stream onto `.prev`, an existing generation whose mtime is older than `AGENTMEMORY_LIVE_STREAM_PREV_TTL_DAYS` days is unlinked best-effort — one warn line on failure — so its bytes are reclaimed while the oversized current file still exists. The TTL defaults to `30`; unparsable and negative values fall back to the default via `parseInt`, and `0` opts out entirely. Fresh generations stay untouched until the rename replaces them atomically. Tests cover expired-removed-before-rename, fresh-preserved, rotation with no `.prev` present, and the stuck-generation warn path. +- **Release-dir pruning rule** in the deploy recipe (`docs/upstream-sync.md` §5 step 8): after a fresh release soaks, keep the 3 newest dirs under `releases/` plus whatever `current` references, delete the rest — one directory per train had accumulated indefinitely (23 on disk before the rule). + +### Deployment (not repo code) + +- **Watchdog RSS sampling** (`~/.agentmemory/bin/watchdog.sh`): every run now appends ` rss_kb=` for the PID in `~/.agentmemory/iii.pid` to `~/Library/Logs/Agentmemory/watchdog.log`, giving the restart history a memory-growth series alongside the viewer-down and engine-restarted lines. + +### Ledger + +- Six tier-1 `decisions` rows recorded under project `chronodeai/agentmemory`: the embedding-dimension premise closed as resolved (newest vector shard decodes to 2560-dim float32 ×145 samples, `.env` pins `OPENAI_EMBEDDING_DIMENSIONS=2560` against omlx `:8002`, which honors the Matryoshka `dimensions` param at 2560 and 1024); unbounded `.prev` retention superseded by this train's TTL fix; the release-pruning policy; the `/lesson` adoption gap recorded visibility-only (tool-driven pipeline, no auto-create upstream); the weekly `scripts/sync/upstream-status.sh` habit; and the auth-failure log pass. +- **Auth-failure log pass** (read-only): `legacy_authentication_disabled`, `401`, and `project_binding_mismatch` occur zero times across `~/Library/Logs/Agentmemory/*.log` and `/tmp/am-worker-9032348.log`, in both pre-sync (< 2026-08-24T21:02:26Z, activation of `0.9.30-chronode.1`) and post-sync buckets; only near-misses exist (legacy data-dir notices, one empty OTel WebSocket error line), with a retained-logs-only caveat in the ledger row. + ## [0.9.30-chronode.5] — 2026-08-25 Backlog-closure release: live-stream disk bounding, deployment watchdog v2, and a written upstream-sync process. diff --git a/package-lock.json b/package-lock.json index 5bd4f02e5..a1e94a1ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@agentmemory/agentmemory", - "version": "0.9.30-chronode.5", + "version": "0.9.30-chronode.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agentmemory/agentmemory", - "version": "0.9.30-chronode.5", + "version": "0.9.30-chronode.6", "license": "Apache-2.0", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.142", diff --git a/package.json b/package.json index 33c894e34..472f6eb4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agentmemory/agentmemory", - "version": "0.9.30-chronode.5", + "version": "0.9.30-chronode.6", "description": "Persistent memory for AI coding agents, powered by iii-engine's three primitives", "type": "module", "main": "dist/index.mjs", diff --git a/packages/mcp/package.json b/packages/mcp/package.json index ff919b619..afa6980e0 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@agentmemory/mcp", - "version": "0.9.30-chronode.5", + "version": "0.9.30-chronode.6", "description": "Standalone MCP server for agentmemory — thin shim that re-exposes @agentmemory/agentmemory's MCP entrypoint", "type": "module", "bin": { @@ -28,7 +28,7 @@ "homepage": "https://github.com/rohitg00/agentmemory#readme", "bugs": "https://github.com/rohitg00/agentmemory/issues", "dependencies": { - "@agentmemory/agentmemory": "0.9.30-chronode.5" + "@agentmemory/agentmemory": "0.9.30-chronode.6" }, "publishConfig": { "access": "public", diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 39dec430f..56b01c506 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agentmemory", - "version": "0.9.30-chronode.5", + "version": "0.9.30-chronode.6", "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 60 MCP tools, 17 skills, real-time viewer.", "author": { "name": "Rohit Ghumare", diff --git a/plugin/.codex-plugin/plugin.json b/plugin/.codex-plugin/plugin.json index 1bf014c32..5a18a8043 100644 --- a/plugin/.codex-plugin/plugin.json +++ b/plugin/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agentmemory", - "version": "0.9.30-chronode.5", + "version": "0.9.30-chronode.6", "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 11 hooks, 60 MCP tools, 17 skills, real-time viewer.", "author": { "name": "Rohit Ghumare", diff --git a/plugin/plugin.json b/plugin/plugin.json index 3130d3d8d..7f0b44cd9 100644 --- a/plugin/plugin.json +++ b/plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agentmemory", - "version": "0.9.30-chronode.5", + "version": "0.9.30-chronode.6", "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 60 MCP tools, 17 skills, real-time viewer.", "author": { "name": "Rohit Ghumare", diff --git a/src/version.ts b/src/version.ts index a372c1e2b..07b94fe84 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,4 +1,4 @@ -export const VERSION = "0.9.30-chronode.5"; +export const VERSION = "0.9.30-chronode.6"; export const EXPORT_FORMAT_VERSION = "0.9.28" as const; export const API_CONTRACT_VERSION = 1; export const BACKEND_BUILD_ID = From e25ab1dd3884301b307a62f278cd38966fb043f9 Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Wed, 26 Aug 2026 07:09:49 -0500 Subject: [PATCH 4/5] chore(bundles): rebuild plugin sidecars for chronode.6 --- plugin/scripts/standalone.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/scripts/standalone.mjs b/plugin/scripts/standalone.mjs index 5a7a2cb84..998618776 100755 --- a/plugin/scripts/standalone.mjs +++ b/plugin/scripts/standalone.mjs @@ -1761,7 +1761,7 @@ function getAllTools() { } //#endregion //#region src/version.ts -const VERSION = "0.9.30-chronode.5"; +const VERSION = "0.9.30-chronode.6"; process.env["AGENTMEMORY_BUILD_ID"]; process.env["AGENTMEMORY_VIEWER_BUILD_ID"]; //#endregion From 453236fadef87b070704ef2a0713cf69a6bb8f91 Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Wed, 26 Aug 2026 07:11:08 -0500 Subject: [PATCH 5/5] chore(evidence): refresh inventory, skills reference, r13 manifest Manifest restamped for the live-stream-rotation test additions (180 tracked test files); interface inventory regenerated at chronode.6. --- .aiwg/reports/g-icm-01-interface-inventory.json | 6 +++--- ci/r13-test-manifest.json | 2 +- plugin/skills/agentmemory-config/REFERENCE.md | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.aiwg/reports/g-icm-01-interface-inventory.json b/.aiwg/reports/g-icm-01-interface-inventory.json index 50a052798..6b48173fe 100644 --- a/.aiwg/reports/g-icm-01-interface-inventory.json +++ b/.aiwg/reports/g-icm-01-interface-inventory.json @@ -3,9 +3,9 @@ "control_id": "G-ICM-01", "project_id": "github.com/chronodeai/agentmemory", "source_identity": { - "commit_sha": "b244872f43e6c98363d864305f5fcd594d9d1e31", - "commit_tree_sha": "4a0008a80fb82dc38ad719f30b452c969b09acb6", - "inventory_input_sha256": "3e1807639308f135d8310debd30c1836fc8b33b25e377199752884e9b051645c" + "commit_sha": "e25ab1dd3884301b307a62f278cd38966fb043f9", + "commit_tree_sha": "70d799994621397b125eb394d3c9bad4be75bc5c", + "inventory_input_sha256": "7054c53f2295f18ea1edf6ad6930e8c66ab96c6d8a194bd856a8340a6576bed9" }, "public_route_allowlist": [ "GET /agentmemory/livez" diff --git a/ci/r13-test-manifest.json b/ci/r13-test-manifest.json index 003010a3f..dd103c4bf 100644 --- a/ci/r13-test-manifest.json +++ b/ci/r13-test-manifest.json @@ -1,5 +1,5 @@ { "count": 180, "sha256": "e8489acf3396ad149ca9342214e4865124021fd1e05d37ff548e63dcf2f6a242", - "content_sha256": "d814f7af2764ee51260a113cdce86f535b3bc7ca1074bafe1b9cdebc9bef7596" + "content_sha256": "1a221bd79fe25417ada1a4d3628eac17bc3c92eaf01b023aa41148dc7f4d4d25" } diff --git a/plugin/skills/agentmemory-config/REFERENCE.md b/plugin/skills/agentmemory-config/REFERENCE.md index 4a51bd0b8..d40cbb45a 100644 --- a/plugin/skills/agentmemory-config/REFERENCE.md +++ b/plugin/skills/agentmemory-config/REFERENCE.md @@ -3,7 +3,7 @@ Generated by scanning `src/` for `AGENTMEMORY_*` usage. Do not edit the block below by hand; run `npm run skills:gen` after adding or removing a variable. Internal markers ending in two underscores are excluded. -Configuration is read from the environment and from `~/.agentmemory/.env` (no `export` prefix). 68 recognized variables: +Configuration is read from the environment and from `~/.agentmemory/.env` (no `export` prefix). 69 recognized variables: - `AGENTMEMORY_ADMIN_SECRET` - `AGENTMEMORY_ADMIN_SECRET_FILE` @@ -38,6 +38,7 @@ Configuration is read from the environment and from `~/.agentmemory/.env` (no `e - `AGENTMEMORY_INJECT_CONTEXT` - `AGENTMEMORY_LAUNCHD_LABELS` - `AGENTMEMORY_LIVE_STREAM_MAX_BYTES` +- `AGENTMEMORY_LIVE_STREAM_PREV_TTL_DAYS` - `AGENTMEMORY_LLM_TIMEOUT_MS` - `AGENTMEMORY_LOCAL_PROCESSING` - `AGENTMEMORY_MCP_BLOCK`