From 355a9fa44b00a15b92a3b3d108d28ceb4005331c Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 11:58:24 -0500 Subject: [PATCH 1/8] feat(config): hydrate ~/.agentmemory/.env into process.env at boot Port the 6cc9b9f env-hydration subset. hydrateProcessEnvFromFile() copies ~/.agentmemory/.env into process.env fill-missing-only, so a real environment value always wins, and is wired before the first config read in both entries (src/cli.ts after the --version/--help exits, src/index.ts at the top of main()). loadEnvFile() is memoized for the process lifetime with a __resetEnvFileCache() test hook. loadAgentmemoryEnvironment() now delegates hydration to that single implementation instead of re-parsing the file with dotenv, giving one precedence story everywhere: real environment > ~/.agentmemory/.env > project manifest overrides > defaults. --- ci/r13-test-manifest.json | 6 +-- src/cli.ts | 7 +++ src/config.ts | 39 +++++++++++++- src/index.ts | 6 +++ src/project-config.ts | 22 +++----- test/env-hydration.test.ts | 106 +++++++++++++++++++++++++++++++++++++ 6 files changed, 167 insertions(+), 19 deletions(-) create mode 100644 test/env-hydration.test.ts diff --git a/ci/r13-test-manifest.json b/ci/r13-test-manifest.json index 853e6e20f..86d394b76 100644 --- a/ci/r13-test-manifest.json +++ b/ci/r13-test-manifest.json @@ -1,5 +1,5 @@ { - "count": 159, - "sha256": "987f48c8beda0e67f96c4b16a60c5637cca072d2bb054493561a74fbf4e984b7", - "content_sha256": "5b4e61aba030ee6139c86f29cf7c2eccda3923cb959578b1aada8e67efee6184" + "count": 160, + "sha256": "8cea0d0ab2866d5ca9d93a9d50088af3878aeecc9d0d5a1f18f4eb184b2cf578", + "content_sha256": "3f6b3fc21c4480e46aa86b0bde86085d9e3925d53882911139f72b02ccaf07b8" } diff --git a/src/cli.ts b/src/cli.ts index 733648e95..ce34c617e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -60,6 +60,7 @@ import { renderSplash } from "./cli/splash.js"; import { isFirstRun, readPrefs, resetPrefs, writePrefs } from "./cli/preferences.js"; import { runOnboarding } from "./cli/onboarding.js"; import { setBootVerbose } from "./logger.js"; +import { hydrateProcessEnvFromFile } from "./config.js"; import { VERSION } from "./version.js"; import { getAllTools, ESSENTIAL_TOOLS } from "./mcp/tools-registry.js"; import { knownAgents } from "./cli/connect/index.js"; @@ -113,6 +114,12 @@ if (args.includes("--version") || args.includes("-V")) { process.exit(0); } +// Fold ~/.agentmemory/.env into process.env before anything reads config +// from the environment (the iii version pin, --tools/--port/--instance +// handling, engine boot). Fill-missing-only: a real process.env value — +// including one set by a CLI flag below — always wins over the file. +hydrateProcessEnvFromFile(); + // Pinned iii-engine version. The unpinned `install.iii.dev/iii/main/install.sh` // script tracks `latest`, which made every fresh agentmemory install pull // engine 0.11.6 — and 0.11.6 introduces a new sandbox-everything-via- diff --git a/src/config.ts b/src/config.ts index 49079a263..465559c5a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -22,8 +22,20 @@ const ENV_FILE = join(DATA_DIR, ".env"); let warnPremiumModelShown = false; +// Parsed ~/.agentmemory/.env, memoized for the process lifetime. getMergedEnv() +// runs on every config getter, so without this cache a single request would +// readFileSync + reparse the file dozens of times. The file is boot-static, so +// read it from disk once and reuse the result. Tests that mutate the file +// between cases reset the module (clearing this via reload) or call +// __resetEnvFileCache(). +let envFileCache: Record | undefined; + function loadEnvFile(): Record { - if (!existsSync(ENV_FILE)) return {}; + if (envFileCache) return envFileCache; + if (!existsSync(ENV_FILE)) { + envFileCache = {}; + return envFileCache; + } const content = readFileSync(ENV_FILE, "utf-8"); const vars: Record = {}; for (const line of content.split("\n")) { @@ -43,7 +55,30 @@ function loadEnvFile(): Record { } vars[key] = val; } - return vars; + envFileCache = vars; + return envFileCache; +} + +// Test hook: clears the memoized .env so the next loadEnvFile() re-reads disk +// within the same module instance. vi.resetModules() reloads this module and +// resets the cache on its own; this exists for tests that mutate the file +// without a module reload. +export function __resetEnvFileCache(): void { + envFileCache = undefined; +} + +// Hydrate ~/.agentmemory/.env into process.env at boot. loadEnvFile() is +// otherwise only consumed via getMergedEnv(), which the many modules that read +// raw process.env["X"] never call — so .env-only values were silently ignored +// by them. Copy the file's vars into process.env, but only when the key is +// currently unset so a real process.env value still wins (this preserves the +// {...fileEnv, ...process.env} precedence getMergedEnv uses). This is the +// single boot-time hydration for the whole precedence chain: +// real environment > ~/.agentmemory/.env > project manifest overrides > defaults +export function hydrateProcessEnvFromFile(): void { + for (const [k, v] of Object.entries(loadEnvFile())) { + if (process.env[k] === undefined) process.env[k] = v; + } } function hasRealValue(v: string | undefined): v is string { diff --git a/src/index.ts b/src/index.ts index 94b1457bf..5f7850211 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import { registerWorker } from "iii-sdk"; import { + hydrateProcessEnvFromFile, loadConfig, getEnvVar, loadEmbeddingConfig, @@ -184,6 +185,11 @@ process.on("unhandledRejection", (reason) => { }); async function main() { + // Fold ~/.agentmemory/.env into process.env before anything reads config + // or raw process.env. Fill-missing-only, so a real process.env value + // always wins over the file. + hydrateProcessEnvFromFile(); + const config = loadConfig(); const embeddingConfig = loadEmbeddingConfig(); const fallbackConfig = loadFallbackConfig(); diff --git a/src/project-config.ts b/src/project-config.ts index 2344d434e..0df6b7f8e 100644 --- a/src/project-config.ts +++ b/src/project-config.ts @@ -7,8 +7,8 @@ import { } from "node:fs"; import { homedir } from "node:os"; import { basename, isAbsolute, join, relative, resolve } from "node:path"; -import { parse as parseDotenv } from "dotenv"; import { parse as parseYaml } from "yaml"; +import { hydrateProcessEnvFromFile } from "./config.js"; export type ProjectPrivacy = "standard" | "private" | "strict"; export type CaptureProfile = "minimal" | "balanced" | "full"; @@ -257,18 +257,12 @@ export function getUserProjectConfigPath(root: string): string { } export function loadAgentmemoryEnvironment(): Record { - const envPath = join(userHome(), ".agentmemory", ".env"); - let fileEnv: Record = {}; - if (existsSync(envPath)) { - try { - fileEnv = parseDotenv(readFileSync(envPath)); - } catch { - fileEnv = {}; - } - } - for (const [key, value] of Object.entries(fileEnv)) { - if (process.env[key] === undefined) process.env[key] = value; - } + // Single precedence story across the codebase: + // real environment > ~/.agentmemory/.env > project manifest overrides > defaults + // hydrateProcessEnvFromFile() (config.js) owns the fill-missing-only + // copy from ~/.agentmemory/.env into process.env; nothing here may + // overwrite an existing process.env entry. + hydrateProcessEnvFromFile(); if (!asString(process.env["AGENTMEMORY_SECRET"])) { const secretFile = asString(process.env["AGENTMEMORY_SECRET_FILE"]); @@ -281,7 +275,7 @@ export function loadAgentmemoryEnvironment(): Record { } } } - return { ...fileEnv, ...process.env } as Record; + return { ...process.env } as Record; } function envLayer(env: Record): ConfigLayer { diff --git a/test/env-hydration.test.ts b/test/env-hydration.test.ts new file mode 100644 index 000000000..5fe3f9867 --- /dev/null +++ b/test/env-hydration.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ORIGINAL_HOME = process.env["HOME"]; +const ORIGINAL_USERPROFILE = process.env["USERPROFILE"]; + +let sandboxHome: string; +const hydratedKeys = [ + "AM_HYD_MISSING", + "AM_HYD_TAKEN", + "AM_HYD_EMPTY", + "AM_HYD_QUOTED", +]; + +async function freshConfig() { + vi.resetModules(); + return await import("../src/config.js"); +} + +function writeEnv(contents: string) { + const dir = join(sandboxHome, ".agentmemory"); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, ".env"), contents); +} + +describe("hydrateProcessEnvFromFile", () => { + beforeEach(() => { + sandboxHome = mkdtempSync(join(tmpdir(), "agentmemory-hydrate-")); + process.env["HOME"] = sandboxHome; + process.env["USERPROFILE"] = sandboxHome; + for (const key of hydratedKeys) delete process.env[key]; + }); + + afterEach(() => { + if (ORIGINAL_HOME === undefined) delete process.env["HOME"]; + else process.env["HOME"] = ORIGINAL_HOME; + if (ORIGINAL_USERPROFILE === undefined) delete process.env["USERPROFILE"]; + else process.env["USERPROFILE"] = ORIGINAL_USERPROFILE; + for (const key of hydratedKeys) delete process.env[key]; + rmSync(sandboxHome, { recursive: true, force: true }); + }); + + it("sets process.env entries for names not already present", async () => { + writeEnv("AM_HYD_MISSING=from-file\n"); + const cfg = await freshConfig(); + cfg.hydrateProcessEnvFromFile(); + expect(process.env["AM_HYD_MISSING"]).toBe("from-file"); + }); + + it("leaves names that already exist in the real environment untouched", async () => { + writeEnv("AM_HYD_TAKEN=from-file\n"); + process.env["AM_HYD_TAKEN"] = "from-process"; + const cfg = await freshConfig(); + cfg.hydrateProcessEnvFromFile(); + expect(process.env["AM_HYD_TAKEN"]).toBe("from-process"); + }); + + it("treats an empty-string value as present and does not overwrite it", async () => { + writeEnv("AM_HYD_EMPTY=from-file\n"); + process.env["AM_HYD_EMPTY"] = ""; + const cfg = await freshConfig(); + cfg.hydrateProcessEnvFromFile(); + expect(process.env["AM_HYD_EMPTY"]).toBe(""); + }); + + it("ignores malformed lines without setting anything", async () => { + writeEnv( + [ + "# a full-line comment", + "", + " ", + "NO_EQUALS_SIGN_HERE", + "=value-without-key", + "AM_HYD_MISSING=valid-after-malformed", + ].join("\n"), + ); + const cfg = await freshConfig(); + cfg.hydrateProcessEnvFromFile(); + expect(process.env["AM_HYD_MISSING"]).toBe("valid-after-malformed"); + expect(process.env["NO_EQUALS_SIGN_HERE"]).toBeUndefined(); + }); + + it("hydrates a missing quoted value with quotes unwrapped", async () => { + writeEnv('AM_HYD_QUOTED="quoted # value"\n'); + const cfg = await freshConfig(); + cfg.hydrateProcessEnvFromFile(); + expect(process.env["AM_HYD_QUOTED"]).toBe("quoted # value"); + }); + + it("keeps getEnvVar precedence on the real environment after hydration", async () => { + writeEnv("AM_HYD_TAKEN=from-file\n"); + process.env["AM_HYD_TAKEN"] = "from-process"; + const cfg = await freshConfig(); + cfg.hydrateProcessEnvFromFile(); + expect(cfg.getEnvVar("AM_HYD_TAKEN")).toBe("from-process"); + }); + + it("is a no-op for config reads when ~/.agentmemory/.env does not exist", async () => { + const before = { ...process.env }; + const cfg = await freshConfig(); + expect(() => cfg.hydrateProcessEnvFromFile()).not.toThrow(); + expect({ ...process.env }).toEqual(before); + }); +}); From 8aa2c0957374c4cfa2178eca77a11c7725a2d137 Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 12:00:17 -0500 Subject: [PATCH 2/8] feat(providers): retry 429/503 honoring Retry-After under a total-elapsed budget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the 6cc9b9f _fetch.ts subset. fetchWithTimeout now retries 429/503 responses (max 3 total attempts) with the Retry-After delay — integer-seconds or HTTP-date form, clamped to a 5s per-delay cap and falling back to exponential backoff when absent. Retries are bounded by the caller's TOTAL timeout budget, hard-capped at 170s so attempts + sleeps can never approach the iii 180s invocation timeout; the first attempt honors the capped budget too, and each late attempt gets only the remaining time. Discarded response bodies are cancelled before retrying so connections return to the pool. Brings the bounded-retry regression cases from upstream's test/fetch-timeout.test.ts: first-attempt cap, single retry, hostile / oversized / HTTP-date Retry-After, small-budget bail-outs, and the persistent-503 attempt cap. --- ci/r13-test-manifest.json | 2 +- src/providers/_fetch.ts | 101 +++++++++++++++-- test/fetch-timeout.test.ts | 225 +++++++++++++++++++++++++++++++++++++ 3 files changed, 320 insertions(+), 8 deletions(-) diff --git a/ci/r13-test-manifest.json b/ci/r13-test-manifest.json index 86d394b76..0296e2c4f 100644 --- a/ci/r13-test-manifest.json +++ b/ci/r13-test-manifest.json @@ -1,5 +1,5 @@ { "count": 160, "sha256": "8cea0d0ab2866d5ca9d93a9d50088af3878aeecc9d0d5a1f18f4eb184b2cf578", - "content_sha256": "3f6b3fc21c4480e46aa86b0bde86085d9e3925d53882911139f72b02ccaf07b8" + "content_sha256": "118a1d22b467dc9a226b9c0fe7ff77483711d9a2fc964c98fe09aa36b498b318" } diff --git a/src/providers/_fetch.ts b/src/providers/_fetch.ts index ed9b5e896..3b02d9352 100644 --- a/src/providers/_fetch.ts +++ b/src/providers/_fetch.ts @@ -1,15 +1,52 @@ import { getEnvVar } from "../config.js"; -export function fetchWithTimeout( +// Bounded retry for transient rate-limit / unavailable responses. Attempts is +// total tries (initial + retries). Retries are bounded by a TOTAL elapsed +// deadline — not per-attempt — so the worst case never blows past the caller's +// timeout budget or the iii invocation timeout. A single retry delay is capped +// low so a hostile Retry-After header can't dominate the budget. +const MAX_ATTEMPTS = 3; +const MAX_RETRY_DELAY_MS = 5000; +// Absolute ceiling on the total budget, kept well under the iii 180s invocation +// timeout so retries + sleeps + per-attempt timeouts can never overrun it. +const HARD_BUDGET_CAP_MS = 170000; +// A retry only makes sense if there's room for at least a token attempt after +// the sleep; without this floor we'd sleep, fire, and get instantly cut off. +const MIN_ATTEMPT_FLOOR_MS = 100; +const RETRY_STATUS = new Set([429, 503]); + +const sleep = (ms: number): Promise => + new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * Parse a Retry-After header into a delay in milliseconds. Supports both the + * integer-seconds form and the HTTP-date form. Returns undefined when absent + * or unparseable, so the caller falls back to exponential backoff. Negative + * or past values clamp to 0. + */ +function parseRetryAfter(header: string | null): number | undefined { + if (!header) return undefined; + const trimmed = header.trim(); + if (trimmed === "") return undefined; + + const seconds = Number(trimmed); + if (Number.isFinite(seconds)) { + return Math.max(0, seconds * 1000); + } + + const date = Date.parse(trimmed); + if (Number.isFinite(date)) { + return Math.max(0, date - Date.now()); + } + + return undefined; +} + +async function fetchOnce( url: string, init: RequestInit, - timeoutMs?: number, + ms: number, ): Promise { - const parsed = - timeoutMs ?? - Number.parseInt(getEnvVar("AGENTMEMORY_LLM_TIMEOUT_MS") ?? "60000", 10); - const ms = Number.isFinite(parsed) && parsed > 0 ? parsed : 60000; - const ctl = new AbortController(); const signal = init.signal ? AbortSignal.any([init.signal, ctl.signal]) @@ -17,3 +54,53 @@ export function fetchWithTimeout( const t = setTimeout(() => ctl.abort(), ms); return fetch(url, { ...init, signal }).finally(() => clearTimeout(t)); } + +export async function fetchWithTimeout( + url: string, + init: RequestInit, + timeoutMs?: number, +): Promise { + const parsed = + timeoutMs ?? + Number.parseInt(getEnvVar("AGENTMEMORY_LLM_TIMEOUT_MS") ?? "60000", 10); + const ms = Number.isFinite(parsed) && parsed > 0 ? parsed : 60000; + + // The caller's timeout is the TOTAL budget for all attempts + sleeps, hard + // capped so we never approach the iii invocation timeout. + const budgetMs = Math.min(ms, HARD_BUDGET_CAP_MS); + const start = Date.now(); + + // The first attempt must honor the capped budget too — passing raw `ms` + // here would let a large caller timeout hang past HARD_BUDGET_CAP_MS (and + // the iii 180s invocation timeout) before any retry logic runs. + let response: Response = await fetchOnce(url, init, budgetMs); + for (let attempt = 1; attempt < MAX_ATTEMPTS; attempt++) { + if (!RETRY_STATUS.has(response.status)) return response; + + const retryAfter = parseRetryAfter(response.headers.get("Retry-After")); + // Exponential backoff fallback when no Retry-After: 500ms, 1000ms, ... + const backoff = 500 * 2 ** (attempt - 1); + const delay = Math.min(retryAfter ?? backoff, MAX_RETRY_DELAY_MS); + + // Stop retrying if the sleep plus a minimal attempt would overrun the total + // budget — a hostile Retry-After that alone exceeds the remaining budget + // returns the last response instead of stalling the caller. + const elapsed = Date.now() - start; + const remaining = budgetMs - elapsed; + if (delay + MIN_ATTEMPT_FLOOR_MS > remaining) return response; + + // This response is being discarded for a retry; release its body so the + // underlying connection is returned to the pool instead of leaking. + await response.body?.cancel().catch(() => {}); + await sleep(delay); + + // Cap the per-attempt timeout to whatever budget is left so a late attempt + // can't push total elapsed past the deadline. + const attemptMs = Math.max( + MIN_ATTEMPT_FLOOR_MS, + Math.min(ms, budgetMs - (Date.now() - start)), + ); + response = await fetchOnce(url, init, attemptMs); + } + return response; +} diff --git a/test/fetch-timeout.test.ts b/test/fetch-timeout.test.ts index 5b2cd7c9c..5038ee51d 100644 --- a/test/fetch-timeout.test.ts +++ b/test/fetch-timeout.test.ts @@ -74,6 +74,231 @@ describe("fetchWithTimeout", () => { }); }); +// ───────────────────────────────────────────────────────────── +// Bounded-retry total-deadline tests +// +// The retry wrapper must bound TOTAL elapsed time across every +// attempt + sleep — not per-attempt — so worst case never blows +// past the caller's budget or the iii 180s invocation timeout. +// ───────────────────────────────────────────────────────────── +describe("fetchWithTimeout bounded retry (total deadline)", () => { + // Builds a fetch mock that replays a queue of {status, headers} in order, + // repeating the last entry once the queue is exhausted. Records how many + // times it was invoked so we can assert attempt counts. + function queuedFetch( + responses: Array<{ status: number; headers?: Record }>, + ): { fetch: typeof fetch; calls: () => number } { + let i = 0; + const impl = (async () => { + const spec = responses[Math.min(i, responses.length - 1)]; + i++; + return new Response(null, { + status: spec.status, + headers: spec.headers, + }); + }) as typeof fetch; + return { fetch: impl, calls: () => i }; + } + + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + delete process.env["AGENTMEMORY_LLM_TIMEOUT_MS"]; + }); + + // The FIRST attempt must honor the hard budget cap, not the raw caller + // timeout — otherwise a large caller timeout could hang past + // HARD_BUDGET_CAP_MS before any retry logic runs. + it("caps the first attempt at the hard budget, not the raw caller timeout", async () => { + const signals: AbortSignal[] = []; + const capturing = ((_url: string, init?: RequestInit) => { + const signal = init!.signal as AbortSignal; + signals.push(signal); + return new Promise((_resolve, reject) => { + signal.addEventListener("abort", () => + reject(new DOMException("AbortError", "AbortError")), + ); + }); + }) as typeof fetch; + vi.spyOn(globalThis, "fetch").mockImplementation(capturing); + vi.useFakeTimers(); + + // 300s caller timeout — far above the 170s hard budget cap. + const p = fetchWithTimeout("https://example.com", {}, 300000); + p.catch(() => {}); // observe rejection so it isn't flagged unhandled + + expect(signals).toHaveLength(1); + // Just before the cap the first attempt is still alive. + await vi.advanceTimersByTimeAsync(169999); + expect(signals[0].aborted).toBe(false); + // At the cap it must abort. + await vi.advanceTimersByTimeAsync(2); + expect(signals[0].aborted).toBe(true); + + await expect(p).rejects.toThrow(); + }); + + // 429 then 200 → retried exactly once, resolves 200. + it("retries once on 429 then resolves the follow-up 200", async () => { + const q = queuedFetch([{ status: 429 }, { status: 200 }]); + vi.spyOn(globalThis, "fetch").mockImplementation(q.fetch); + vi.useFakeTimers(); + + const p = fetchWithTimeout("https://example.com", {}, 60000); + // Drain the backoff sleep (500ms default) so the retry fires. + await vi.advanceTimersByTimeAsync(500); + const res = await p; + + expect(res.status).toBe(200); + expect(q.calls()).toBe(2); + }); + + // A hostile Retry-After must NOT be honored literally — the delay collapses + // to the low per-delay cap so total elapsed stays bounded. + it("does not honour a hostile Retry-After literally — caps it and stays within budget", async () => { + const q = queuedFetch([ + { status: 429, headers: { "Retry-After": "100000" } }, // 100000s + { status: 200 }, + ]); + vi.spyOn(globalThis, "fetch").mockImplementation(q.fetch); + vi.useFakeTimers(); + const start = Date.now(); + + const p = fetchWithTimeout("https://example.com", {}, 60000); + // runAllTimers drains every scheduled sleep; if the code honored 100000s + // literally this would advance 100_000_000ms of simulated time. + await vi.runAllTimersAsync(); + const res = await p; + const elapsed = Date.now() - start; + + expect(res.status).toBe(200); + expect(q.calls()).toBe(2); + // The only sleep was the capped 5000ms delay. + expect(elapsed).toBeLessThanOrEqual(5000); + expect(elapsed).toBeLessThan(60000); + }); + + // When even the capped delay cannot fit inside a small budget, do NOT + // retry — return the last 429 within bound. + it("returns the last 429 without retrying when even the capped delay overruns a small budget", async () => { + const q = queuedFetch([ + { status: 429, headers: { "Retry-After": "100000" } }, + { status: 200 }, + ]); + vi.spyOn(globalThis, "fetch").mockImplementation(q.fetch); + vi.useFakeTimers(); + const start = Date.now(); + + // Budget 1000ms: capped delay 5000ms + floor 100ms > remaining, no retry. + const p = fetchWithTimeout("https://example.com", {}, 1000); + await vi.runAllTimersAsync(); + const res = await p; + const elapsed = Date.now() - start; + + expect(res.status).toBe(429); + expect(q.calls()).toBe(1); + expect(elapsed).toBeLessThan(1000); + }); + + // Retry-After larger than the per-delay cap collapses to MAX_RETRY_DELAY_MS, + // still bounded, still retried when budget allows. + it("caps an oversized Retry-After to the max delay and still retries within budget", async () => { + const q = queuedFetch([ + { status: 503, headers: { "Retry-After": "60" } }, // 60s requested + { status: 200 }, + ]); + vi.spyOn(globalThis, "fetch").mockImplementation(q.fetch); + vi.useFakeTimers(); + + const p = fetchWithTimeout("https://example.com", {}, 60000); + // Requested 60s, but the honored delay is capped at 5000ms. + await vi.advanceTimersByTimeAsync(5000); + const res = await p; + + expect(res.status).toBe(200); + expect(q.calls()).toBe(2); + }); + + // Persistent 503 → stops after the bounded attempts AND within the total + // deadline (initial + 2 retries = MAX_ATTEMPTS). + it("stops persistent 503 at the attempt cap and within the deadline", async () => { + const q = queuedFetch([{ status: 503 }]); + vi.spyOn(globalThis, "fetch").mockImplementation(q.fetch); + vi.useFakeTimers(); + const start = Date.now(); + + const p = fetchWithTimeout("https://example.com", {}, 60000); + // Two retries with 500ms + 1000ms exponential backoff. + await vi.advanceTimersByTimeAsync(500); + await vi.advanceTimersByTimeAsync(1000); + const res = await p; + const elapsed = Date.now() - start; + + expect(res.status).toBe(503); + expect(q.calls()).toBe(3); + expect(elapsed).toBeLessThan(60000); + // Total simulated sleep was only the two backoffs. + expect(elapsed).toBeLessThanOrEqual(1500); + }); + + // A tiny budget forces a bail before even the first retry sleep. + it("does not retry when the budget is too small to fit a retry", async () => { + const q = queuedFetch([{ status: 503 }, { status: 200 }]); + vi.spyOn(globalThis, "fetch").mockImplementation(q.fetch); + vi.useFakeTimers(); + + // Budget of 200ms: backoff 500ms + floor 100ms > remaining, so no retry. + const p = fetchWithTimeout("https://example.com", {}, 200); + await vi.runAllTimersAsync(); + const res = await p; + + expect(res.status).toBe(503); + expect(q.calls()).toBe(1); + }); + + // Retry-After as an HTTP-date is parsed and the retry fires within budget. + it("parses an HTTP-date Retry-After and honors it when within budget", async () => { + // 2s in the future — under the max delay cap, so honored as-is. + const future = new Date(Date.now() + 2000).toUTCString(); + const q = queuedFetch([ + { status: 429, headers: { "Retry-After": future } }, + { status: 200 }, + ]); + vi.spyOn(globalThis, "fetch").mockImplementation(q.fetch); + vi.useFakeTimers(); + + const p = fetchWithTimeout("https://example.com", {}, 60000); + await vi.advanceTimersByTimeAsync(2000); + const res = await p; + + expect(res.status).toBe(200); + expect(q.calls()).toBe(2); + }); + + // A far-future HTTP-date collapses to the capped delay (5000ms), which still + // exceeds a small budget → no retry, last 503 returned in bound. + it("does not retry on a far-future HTTP-date Retry-After that overruns a small budget", async () => { + const farFuture = new Date(Date.now() + 3600_000).toUTCString(); // 1h out + const q = queuedFetch([ + { status: 503, headers: { "Retry-After": farFuture } }, + { status: 200 }, + ]); + vi.spyOn(globalThis, "fetch").mockImplementation(q.fetch); + vi.useFakeTimers(); + const start = Date.now(); + + // Budget 1000ms: capped delay 5000ms + floor > remaining, so no retry. + const p = fetchWithTimeout("https://example.com", {}, 1000); + await vi.runAllTimersAsync(); + const res = await p; + const elapsed = Date.now() - start; + + expect(res.status).toBe(503); + expect(q.calls()).toBe(1); + expect(elapsed).toBeLessThan(1000); + }); +}); + // ───────────────────────────────────────────────────────────── // Provider hang regression tests // Each provider must call fetchWithTimeout, which honours the From 7a96c1ba9cb0c0e60536e432b21fac05f72202b3 Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 12:27:59 -0500 Subject: [PATCH 3/8] feat(events): run consolidation + crystallization on session stop with a cooldown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the 6cc9b9f consolidation-lifecycle subset, reconciled with the fork's persistent background-pipeline state. After the stop pipeline reaches a successful terminal state — inside the existing withKeyedLock('background-pipeline:'+sessionId) — the handler fires mem::consolidate-pipeline {tier:'all',force:true} and mem::auto-crystallize {olderThanDays:0}, both scoped to the session's project (fork project-scope rules), gated on isConsolidationEnabled() so keyless installs never fire no-op LLM work. Debounce: a consolidation:lastRun marker in KV.config bounds corpus consolidation to once per AGENTMEMORY_CONSOLIDATION_COOLDOWN_MS (default 5 min, 0 disables). The read-check-write is serialized through an in-process chain so concurrent stops cannot both pass. The gate sits after every resume/supersede/terminal early-return, so retried runs consume the cooldown only when they actually complete, stages the resume machinery marks complete are never re-run, and failed pipelines leave the marker untouched. getConsolidationCooldownMs() added to config. New test/consolidation-lifecycle.test.ts covers fire-once payloads, cooldown suppression and expiry, debounce-disabled mode, stage-resume interplay, failure paths not consuming the window, keyless gating, and concurrent stop serialization; session-end-triggers-graph pins CONSOLIDATION_ ENABLED=false to keep its exact fan-out assertions hermetic. --- ci/r13-test-manifest.json | 6 +- src/config.ts | 15 + src/triggers/events.ts | 89 ++++++ test/consolidation-lifecycle.test.ts | 348 ++++++++++++++++++++++++ test/session-end-triggers-graph.test.ts | 12 +- 5 files changed, 466 insertions(+), 4 deletions(-) create mode 100644 test/consolidation-lifecycle.test.ts diff --git a/ci/r13-test-manifest.json b/ci/r13-test-manifest.json index 0296e2c4f..c80b76932 100644 --- a/ci/r13-test-manifest.json +++ b/ci/r13-test-manifest.json @@ -1,5 +1,5 @@ { - "count": 160, - "sha256": "8cea0d0ab2866d5ca9d93a9d50088af3878aeecc9d0d5a1f18f4eb184b2cf578", - "content_sha256": "118a1d22b467dc9a226b9c0fe7ff77483711d9a2fc964c98fe09aa36b498b318" + "count": 161, + "sha256": "e3501eaad4d4aa7572d13b79c024ca42a1243b215db89ef2de8a571f3deea90d", + "content_sha256": "9934851993c215609b52af4a0a177a86f964a8451f68d6592357ee2357baf174" } diff --git a/src/config.ts b/src/config.ts index 465559c5a..4c3c4b650 100644 --- a/src/config.ts +++ b/src/config.ts @@ -476,6 +476,21 @@ export function getConsolidationDecayDays(): number { return safeParseInt(getMergedEnv()["CONSOLIDATION_DECAY_DAYS"], 30); } +// Cooldown between corpus consolidations triggered by session stop. The Stop +// hook fires per agent turn and posts /session/end, so without this every +// turn would kick full-corpus LLM consolidation + crystallization. Debounced +// to at most once per window via the consolidation:lastRun marker; set to 0 +// to disable the debounce (consolidate on every stop). Default 5 minutes. +const CONSOLIDATION_COOLDOWN_DEFAULT_MS = 300000; + +export function getConsolidationCooldownMs(): number { + const raw = safeParseInt( + getMergedEnv()["AGENTMEMORY_CONSOLIDATION_COOLDOWN_MS"], + CONSOLIDATION_COOLDOWN_DEFAULT_MS, + ); + return raw >= 0 ? raw : CONSOLIDATION_COOLDOWN_DEFAULT_MS; +} + export function isStandaloneMcp(): boolean { return getMergedEnv()["STANDALONE_MCP"] === "true"; } diff --git a/src/triggers/events.ts b/src/triggers/events.ts index db89cd1af..d829ffd03 100644 --- a/src/triggers/events.ts +++ b/src/triggers/events.ts @@ -5,7 +5,9 @@ import { StateKV } from "../state/kv.js"; import { isReflectEnabled } from "../functions/slots.js"; import { getAgentId, + getConsolidationCooldownMs, getEnvVar, + isConsolidationEnabled, isGraphExtractionEnabled, } from "../config.js"; import { logger } from "../logger.js"; @@ -27,6 +29,36 @@ import { const MAX_BACKGROUND_PIPELINE_ATTEMPTS = 3; +// Global marker recording when corpus consolidation last ran, used to debounce +// the per-turn session-stop fan-out. +const CONSOLIDATION_MARKER_KEY = "consolidation:lastRun"; + +async function consolidationDueUnserialized(kv: StateKV): Promise { + const cooldownMs = getConsolidationCooldownMs(); + if (cooldownMs <= 0) return true; // debounce disabled + const now = Date.now(); + const marker = await kv + .get<{ at?: number }>(KV.config, CONSOLIDATION_MARKER_KEY) + .catch(() => null); + const lastAt = typeof marker?.at === "number" ? marker.at : 0; + if (now - lastAt < cooldownMs) return false; + await kv.set(KV.config, CONSOLIDATION_MARKER_KEY, { at: now }).catch(() => {}); + return true; +} + +// Concurrent session-stop events would otherwise interleave the marker +// read-check-write above and both pass the cooldown. Serialize the whole +// check through an in-process chain so exactly one concurrent caller wins. +let consolidationCheckChain: Promise = Promise.resolve(); + +function consolidationDue(kv: StateKV): Promise { + const result = consolidationCheckChain.then(() => + consolidationDueUnserialized(kv), + ); + consolidationCheckChain = result.catch(() => false); + return result; +} + function successful(result: unknown): result is Record & { success: true; } { @@ -812,6 +844,63 @@ export function registerEventTriggers(sdk: ISdk, kv: StateKV): void { }); } } + // Crystals + lessons corpus consolidation. Fires only after the + // background pipeline reached a successful terminal state inside the + // background-pipeline lock — the resume machinery above owns which + // stages run, and superseded / already-processed / terminal-failed + // completions never reach this point or consume the cooldown. + // + // Debounce: /session/end is posted by the per-turn Stop hook, so this + // handler fires on every agent turn. mem::consolidate-pipeline + + // mem::auto-crystallize are full-corpus LLM work with no internal + // "nothing changed" guard, so firing them every turn is a cost/latency + // storm. Bound the global corpus consolidation to once per cooldown + // window; AGENTMEMORY_CONSOLIDATION_COOLDOWN_MS=0 disables the debounce. + if (isConsolidationEnabled()) { + const due = await consolidationDue(kv); + if (due) { + // Same dispatch discipline as the slot-reflect / graph-extract + // fan-outs above: tolerate synchronous throws and non-promise + // returns from sdk.trigger, log async rejections without failing + // the stop lifecycle. + const fireVoid = ( + function_id: string, + payload: Record, + ) => { + try { + const dispatched = sdk.trigger({ + function_id, + payload, + action: TriggerAction.Void(), + }); + Promise.resolve(dispatched).catch((err: unknown) => + logger.warn(function_id + " trigger failed", { + sessionId: data.sessionId, + project: data.project, + pipelineRunId, + error: err instanceof Error ? err.message : String(err), + }), + ); + } catch (err) { + logger.warn(function_id + " trigger failed", { + sessionId: data.sessionId, + project: data.project, + pipelineRunId, + error: err instanceof Error ? err.message : String(err), + }); + } + }; + fireVoid("mem::consolidate-pipeline", { + tier: "all", + force: true, + project: data.project, + }); + fireVoid("mem::auto-crystallize", { + olderThanDays: 0, + project: data.project, + }); + } + } return successful(summary) ? { ...summary, pipelineRunId, promotion } : { diff --git a/test/consolidation-lifecycle.test.ts b/test/consolidation-lifecycle.test.ts new file mode 100644 index 000000000..e01169010 --- /dev/null +++ b/test/consolidation-lifecycle.test.ts @@ -0,0 +1,348 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { InMemoryKV } from "../src/mcp/in-memory-kv.js"; +import { KV } from "../src/state/schema.js"; +import { registerEventTriggers } from "../src/triggers/events.js"; +import type { Session } from "../src/types.js"; +import { mockSdk } from "./helpers/mocks.js"; + +const PROJECT = "github.com/example/consolidation-lifecycle"; + +const ORIGINAL_HOME = process.env["HOME"]; +const ORIGINAL_USERPROFILE = process.env["USERPROFILE"]; + +type UpdateOperation = { + type: "set"; + path: string; + value: unknown; +}; + +class TestKV extends InMemoryKV { + override async update( + scope: string, + key: string, + operations: UpdateOperation[], + ): Promise { + const current = (await this.get>(scope, key)) ?? {}; + const next = structuredClone(current); + for (const operation of operations) { + const segments = operation.path.split(".").filter(Boolean); + let target: Record = next; + for (const segment of segments.slice(0, -1)) { + const child = target[segment]; + if (!child || typeof child !== "object" || Array.isArray(child)) { + target[segment] = {}; + } + target = target[segment] as Record; + } + target[segments.at(-1)!] = operation.value; + } + return this.set(scope, key, next as T); + } +} + +function sessionRow(id: string, overrides: Partial = {}): Session { + return { + id, + project: PROJECT, + cwd: "/tmp/consolidation-lifecycle", + startedAt: "2026-08-11T00:00:00.000Z", + status: "active", + observationCount: 1, + ...overrides, + } as Session; +} + +const ENV_KEYS = [ + "CONSOLIDATION_ENABLED", + "AGENTMEMORY_PROVIDER", + "AGENTMEMORY_CONSOLIDATION_COOLDOWN_MS", + "AGENTMEMORY_REFLECT", + "GRAPH_EXTRACTION_ENABLED", +] as const; + +interface RecordedCall { + functionId: string; + payload: Record; +} + +async function flush(): Promise { + for (let i = 0; i < 5; i++) { + await new Promise((resolve) => setImmediate(resolve)); + } +} + +describe("session-stop consolidation lifecycle", () => { + let kv: TestKV; + let sdk: ReturnType; + let calls: RecordedCall[]; + let savedEnv: Record; + let promotionShouldFail: boolean; + let sandboxHome: string; + + function registerRuntime(): void { + calls = []; + promotionShouldFail = false; + const record = (functionId: string, payload: unknown) => { + calls.push({ + functionId, + payload: payload as Record, + }); + return { success: true }; + }; + sdk.registerFunction("mem::consolidate-pipeline", async (payload) => + record("mem::consolidate-pipeline", payload), + ); + sdk.registerFunction("mem::auto-crystallize", async (payload) => + record("mem::auto-crystallize", payload), + ); + let summarizeCalls = 0; + sdk.registerFunction("mem::summarize", async () => { + summarizeCalls += 1; + return { success: true, summary: "synthetic" }; + }); + (sdk as unknown as { __summarizeCalls: () => number }).__summarizeCalls = + () => summarizeCalls; + sdk.registerFunction("mem::promotion-generate", async () => { + if (promotionShouldFail) { + promotionShouldFail = false; + return { success: false, error: "SIMULATED_PROMOTION_FAILURE" }; + } + return { success: true, candidates: [], promoted: 0 }; + }); + registerEventTriggers(sdk as never, kv as never); + } + + async function stop(input: { + sessionId: string; + project?: string; + pipelineRunId?: string; + }): Promise> { + return (await sdk.trigger("event::session::stopped", { + sessionId: input.sessionId, + project: input.project ?? PROJECT, + ...(input.pipelineRunId ? { pipelineRunId: input.pipelineRunId } : {}), + })) as Record; + } + + function consolidationCalls(): RecordedCall[] { + return calls.filter( + (call) => call.functionId === "mem::consolidate-pipeline", + ); + } + + function crystallizeCalls(): RecordedCall[] { + return calls.filter((call) => call.functionId === "mem::auto-crystallize"); + } + + async function marker(): Promise<{ at?: number } | null> { + return kv.get<{ at?: number }>(KV.config, "consolidation:lastRun"); + } + + beforeEach(() => { + // Isolate the config layer from the real operator ~/.agentmemory/.env: + // getMergedEnv() folds file values under process.env, so a real file + // could otherwise flip the consolidation gate or the cooldown. + sandboxHome = mkdtempSync(join(tmpdir(), "agentmemory-consolidation-")); + process.env["HOME"] = sandboxHome; + process.env["USERPROFILE"] = sandboxHome; + savedEnv = {}; + for (const key of ENV_KEYS) { + savedEnv[key] = process.env[key]; + delete process.env[key]; + } + process.env["CONSOLIDATION_ENABLED"] = "true"; + // Pin the default window explicitly: process.env beats the operator's + // ~/.agentmemory/.env, so a real file value can't skew the debounce. + process.env["AGENTMEMORY_CONSOLIDATION_COOLDOWN_MS"] = "300000"; + process.env["AGENTMEMORY_REFLECT"] = "false"; + process.env["GRAPH_EXTRACTION_ENABLED"] = "false"; + kv = new TestKV(); + sdk = mockSdk(); + }); + + afterEach(async () => { + for (const key of ENV_KEYS) { + if (savedEnv[key] === undefined) delete process.env[key]; + else process.env[key] = savedEnv[key]; + } + if (ORIGINAL_HOME === undefined) delete process.env["HOME"]; + else process.env["HOME"] = ORIGINAL_HOME; + if (ORIGINAL_USERPROFILE === undefined) delete process.env["USERPROFILE"]; + else process.env["USERPROFILE"] = ORIGINAL_USERPROFILE; + rmSync(sandboxHome, { recursive: true, force: true }); + }); + + it("fires corpus consolidation and crystallization once per eligible stop", async () => { + await kv.set(KV.sessions, "s1", sessionRow("s1")); + registerRuntime(); + + const result = await stop({ sessionId: "s1" }); + expect(result).toMatchObject({ success: true }); + await flush(); + + expect(consolidationCalls()).toEqual([ + { + functionId: "mem::consolidate-pipeline", + payload: { tier: "all", force: true, project: PROJECT }, + }, + ]); + expect(crystallizeCalls()).toEqual([ + { + functionId: "mem::auto-crystallize", + payload: { olderThanDays: 0, project: PROJECT }, + }, + ]); + const written = await marker(); + expect(typeof written?.at).toBe("number"); + }); + + it("suppresses consolidation for a later stop inside the cooldown window", async () => { + await kv.set(KV.sessions, "s1", sessionRow("s1")); + await kv.set(KV.sessions, "s2", sessionRow("s2")); + registerRuntime(); + + await stop({ sessionId: "s1" }); + await flush(); + await stop({ sessionId: "s2" }); + await flush(); + + expect(consolidationCalls()).toHaveLength(1); + expect(crystallizeCalls()).toHaveLength(1); + }); + + it("fires again once the cooldown window has elapsed", async () => { + await kv.set(KV.sessions, "s1", sessionRow("s1")); + await kv.set(KV.sessions, "s2", sessionRow("s2")); + registerRuntime(); + + await stop({ sessionId: "s1" }); + await flush(); + const stale = (await marker())!; + await kv.set(KV.config, "consolidation:lastRun", { + at: (stale.at ?? Date.now()) - 300_001, + }); + + await stop({ sessionId: "s2" }); + await flush(); + + expect(consolidationCalls()).toHaveLength(2); + }); + + it("treats AGENTMEMORY_CONSOLIDATION_COOLDOWN_MS=0 as debounce disabled", async () => { + process.env["AGENTMEMORY_CONSOLIDATION_COOLDOWN_MS"] = "0"; + await kv.set(KV.sessions, "s1", sessionRow("s1")); + await kv.set(KV.sessions, "s2", sessionRow("s2")); + registerRuntime(); + + await stop({ sessionId: "s1" }); + await flush(); + await stop({ sessionId: "s2" }); + await flush(); + + expect(consolidationCalls()).toHaveLength(2); + }); + + it("resumes completed stages without rerunning them and still consolidates once", async () => { + await kv.set( + KV.sessions, + "s-resume", + sessionRow("s-resume", { + backgroundPipelineRunId: "pipeline-resume-1", + backgroundPipelineStatus: "failed", + backgroundPipelineStage: "promotion", + backgroundPipelineSummaryStatus: "succeeded", + backgroundPipelineAttempts: 1, + }), + ); + registerRuntime(); + + const first = await stop({ + sessionId: "s-resume", + pipelineRunId: "pipeline-resume-1", + }); + await flush(); + + // Summary stage was already complete — the resume machinery must not + // re-run mem::summarize; only promotion ran. + const summarizeCalls = ( + sdk as unknown as { __summarizeCalls: () => number } + ).__summarizeCalls(); + expect(summarizeCalls).toBe(0); + expect(first).toMatchObject({ success: true }); + expect(await marker()).not.toBeNull(); + + // A repeat stop on the now-succeeded pipeline short-circuits before the + // consolidation gate: no stage rerun and no cooldown consumption. + const second = await stop({ + sessionId: "s-resume", + pipelineRunId: "pipeline-resume-1", + }); + await flush(); + expect(second).toMatchObject({ success: true, alreadyProcessed: true }); + expect(consolidationCalls()).toHaveLength(1); + expect(crystallizeCalls()).toHaveLength(1); + }); + + it("does not consume the cooldown when the pipeline fails", async () => { + await kv.set(KV.sessions, "s-fail", sessionRow("s-fail")); + registerRuntime(); + promotionShouldFail = true; + + const failed = await stop({ + sessionId: "s-fail", + pipelineRunId: "pipeline-fail-1", + }); + await flush(); + expect(failed).toMatchObject({ success: false, error: "promotion_failed" }); + expect(await marker()).toBeNull(); + expect(consolidationCalls()).toHaveLength(0); + + // Retry under the same runId succeeds — the gate is reached with an + // empty marker and fires. + const retried = await stop({ + sessionId: "s-fail", + pipelineRunId: "pipeline-fail-1", + }); + await flush(); + expect(retried).toMatchObject({ success: true }); + expect(consolidationCalls()).toHaveLength(1); + }); + + it("never fires or consumes the cooldown when consolidation is disabled", async () => { + // Explicit env values beat ~/.agentmemory/.env in getMergedEnv + // precedence, so this gate is deterministic regardless of the real + // operator file on the test machine. + process.env["AGENTMEMORY_PROVIDER"] = "noop"; + process.env["CONSOLIDATION_ENABLED"] = "false"; + await kv.set(KV.sessions, "s-keyless", sessionRow("s-keyless")); + registerRuntime(); + + const result = await stop({ sessionId: "s-keyless" }); + await flush(); + + expect(result).toMatchObject({ success: true }); + expect(consolidationCalls()).toHaveLength(0); + expect(crystallizeCalls()).toHaveLength(0); + expect(await marker()).toBeNull(); + }); + + it("serializes concurrent stops so only one passes the cooldown check", async () => { + await kv.set(KV.sessions, "s-a", sessionRow("s-a")); + await kv.set(KV.sessions, "s-b", sessionRow("s-b")); + await kv.set(KV.sessions, "s-c", sessionRow("s-c")); + registerRuntime(); + + await Promise.all([ + stop({ sessionId: "s-a" }), + stop({ sessionId: "s-b" }), + stop({ sessionId: "s-c" }), + ]); + await flush(); + + expect(consolidationCalls()).toHaveLength(1); + expect(crystallizeCalls()).toHaveLength(1); + }); +}); diff --git a/test/session-end-triggers-graph.test.ts b/test/session-end-triggers-graph.test.ts index 5b95f3185..cb5f1463c 100644 --- a/test/session-end-triggers-graph.test.ts +++ b/test/session-end-triggers-graph.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, it, expect } from "vitest"; +import { afterEach, beforeEach, describe, it, expect } from "vitest"; import { readFileSync } from "node:fs"; import { registerApiTriggers } from "../src/triggers/api.js"; import { @@ -69,9 +69,19 @@ describe("api::session::end → event::session::stopped (#666)", () => { beforeEach(() => { process.env["AGENTMEMORY_REFLECT"] = "false"; process.env["GRAPH_EXTRACTION_ENABLED"] = "false"; + // These tests pin the exact post-stop fan-out list. Explicitly disable + // the corpus-consolidation debounce gate (added by the session-stop + // consolidation lifecycle) so the expectations don't depend on the + // operator's real ~/.agentmemory/.env or LLM keys; that behavior has + // dedicated coverage in test/consolidation-lifecycle.test.ts. + process.env["CONSOLIDATION_ENABLED"] = "false"; resetBackgroundPipelineHealthForTests(); }); + afterEach(() => { + delete process.env["CONSOLIDATION_ENABLED"]; + }); + it("closes quickly, records dispatch acceptance, and is idempotent", async () => { const calls: Array<{ function_id: string; payload?: Record }> = []; const pending = new Promise(() => undefined); From dd23e0c43f837274ad70e185f2df0a9100c3ef0a Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 12:36:55 -0500 Subject: [PATCH 4/8] feat(config): unified data-dir resolver with --data-dir flag and legacy ./data warning Port the d8b5267/e04ba88 core with fork semantics replacing upstream's: src/data-dir.ts exports resolveDataDir() resolving --data-dir (separated or = form) > AGENTMEMORY_DATA_DIR > ~/.agentmemory, with ~/ expansion and cwd-relative resolution. Upstream's platform-default dirs and its automatic adoption/copying of a legacy cwd ./data store are NOT ported; instead, when no explicit data dir is configured and a ./data directory holding an agentmemory store marker exists in cwd, boot logs a warning pointing the operator at an explicit --data-dir. config.ts drops the module-load DATA_DIR constant: the .env path, loadConfig().dataDir, snapshot dir, and standalone persist path now resolve lazily through the resolver; migrate.ts validates migration dbPaths against allowedDirs() from it. cli.ts parses --data-dir before anything reads the environment (folding the flag over any pre-set AGENTMEMORY_DATA_DIR so flag > env holds for spawned processes), emits the legacy-store warning once at boot, and documents the flag in --help. --- ci/r13-test-manifest.json | 6 +- src/cli.ts | 19 +++ src/config.ts | 21 +-- src/data-dir.ts | 115 +++++++++++++++ src/functions/migrate.ts | 9 +- test/data-dir.test.ts | 294 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 449 insertions(+), 15 deletions(-) create mode 100644 src/data-dir.ts create mode 100644 test/data-dir.test.ts diff --git a/ci/r13-test-manifest.json b/ci/r13-test-manifest.json index c80b76932..2be21e881 100644 --- a/ci/r13-test-manifest.json +++ b/ci/r13-test-manifest.json @@ -1,5 +1,5 @@ { - "count": 161, - "sha256": "e3501eaad4d4aa7572d13b79c024ca42a1243b215db89ef2de8a571f3deea90d", - "content_sha256": "9934851993c215609b52af4a0a177a86f964a8451f68d6592357ee2357baf174" + "count": 162, + "sha256": "76148dfe1d533bed066bc8ab709e3c526696d409782b6dd6adf22f3a8d3c2d43", + "content_sha256": "db2d31c696d808814410f893a0f6385cb4a80aad838d67f3edba92bdc9a6826c" } diff --git a/src/cli.ts b/src/cli.ts index ce34c617e..e72470e5e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -61,6 +61,7 @@ import { isFirstRun, readPrefs, resetPrefs, writePrefs } from "./cli/preferences import { runOnboarding } from "./cli/onboarding.js"; import { setBootVerbose } from "./logger.js"; import { hydrateProcessEnvFromFile } from "./config.js"; +import { readDataDirFlag, warnOnLegacyDataDir } from "./data-dir.js"; import { VERSION } from "./version.js"; import { getAllTools, ESSENTIAL_TOOLS } from "./mcp/tools-registry.js"; import { knownAgents } from "./cli/connect/index.js"; @@ -222,6 +223,11 @@ Options: --reset Wipe ~/.agentmemory/preferences.json and re-run onboarding --tools all|core Tool visibility (default: all = ${ALL_TOOLS_COUNT} tools; core = ${CORE_TOOLS_COUNT} essentials) --no-engine Skip auto-starting iii-engine + --data-dir Relocate the data directory (default: ~/.agentmemory). + Wins over the AGENTMEMORY_DATA_DIR environment variable. + Accepts ~ expansion. A legacy ./data store in the current + directory is NOT adopted automatically — pass this flag + explicitly to use one. --port Override REST port (default: 3111). Streams (N+1), viewer (N+2), and iii engine (N+46023) auto-derive from N so a single flag relocates the whole quartet. @@ -284,6 +290,19 @@ if (instanceIdx !== -1 && args[instanceIdx + 1]) { } } +// --data-dir relocates the agentmemory data dir (default ~/.agentmemory). +// The flag wins over a pre-set AGENTMEMORY_DATA_DIR, so folding it into the +// environment here keeps one precedence story — flag > env > default — for +// everything downstream of this process (worker, engine, hooks inherit env). +const dataDirFlagValue = readDataDirFlag(args); +if (dataDirFlagValue !== undefined && dataDirFlagValue.trim().length > 0) { + process.env["AGENTMEMORY_DATA_DIR"] = dataDirFlagValue.trim(); +} +// Upstream adopts a legacy cwd ./data store automatically; this fork does +// not. When state would silently land somewhere different from an existing +// local ./data store, tell the operator how to opt in explicitly. +warnOnLegacyDataDir(); + const skipEngine = args.includes("--no-engine"); function getRestPort(): number { diff --git a/src/config.ts b/src/config.ts index 4c3c4b650..ec0c0d09b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,6 +10,7 @@ import type { ClaudeBridgeConfig, TeamConfig, } from "./types.js"; +import { resolveDataDir } from "./data-dir.js"; function safeParseInt(value: string | undefined, fallback: number): number { if (!value) return fallback; @@ -17,12 +18,9 @@ function safeParseInt(value: string | undefined, fallback: number): number { return Number.isNaN(parsed) ? fallback : parsed; } -const DATA_DIR = join(homedir(), ".agentmemory"); -const ENV_FILE = join(DATA_DIR, ".env"); - let warnPremiumModelShown = false; -// Parsed ~/.agentmemory/.env, memoized for the process lifetime. getMergedEnv() +// Parsed /.env, memoized for the process lifetime. getMergedEnv() // runs on every config getter, so without this cache a single request would // readFileSync + reparse the file dozens of times. The file is boot-static, so // read it from disk once and reuse the result. Tests that mutate the file @@ -30,13 +28,18 @@ let warnPremiumModelShown = false; // __resetEnvFileCache(). let envFileCache: Record | undefined; +function envFilePath(): string { + return join(resolveDataDir(), ".env"); +} + function loadEnvFile(): Record { if (envFileCache) return envFileCache; - if (!existsSync(ENV_FILE)) { + const envFile = envFilePath(); + if (!existsSync(envFile)) { envFileCache = {}; return envFileCache; } - const content = readFileSync(ENV_FILE, "utf-8"); + const content = readFileSync(envFile, "utf-8"); const vars: Record = {}; for (const line of content.split("\n")) { const trimmed = line.trim(); @@ -232,7 +235,7 @@ export function loadConfig(): AgentMemoryConfig { tokenBudget: safeParseInt(env["TOKEN_BUDGET"], 2000), maxObservationsPerSession: safeParseInt(env["MAX_OBS_PER_SESSION"], 500), compressionModel: provider.model, - dataDir: DATA_DIR, + dataDir: resolveDataDir(), }; } @@ -397,7 +400,7 @@ export function loadSnapshotConfig(): { return { enabled: env["SNAPSHOT_ENABLED"] === "true", interval: safeParseInt(env["SNAPSHOT_INTERVAL"], 3600), - dir: env["SNAPSHOT_DIR"] || join(homedir(), ".agentmemory", "snapshots"), + dir: env["SNAPSHOT_DIR"] || join(resolveDataDir(), "snapshots"), }; } @@ -499,7 +502,7 @@ export function getStandalonePersistPath(): string { const env = getMergedEnv(); return ( env["STANDALONE_PERSIST_PATH"] || - join(homedir(), ".agentmemory", "standalone.json") + join(resolveDataDir(), "standalone.json") ); } diff --git a/src/data-dir.ts b/src/data-dir.ts new file mode 100644 index 000000000..56f494257 --- /dev/null +++ b/src/data-dir.ts @@ -0,0 +1,115 @@ +import { existsSync } from "node:fs"; +import { homedir } from "node:os"; +import { isAbsolute, join, resolve } from "node:path"; + +export const DATA_DIR_FLAG = "--data-dir"; +export const DATA_DIR_ENV = "AGENTMEMORY_DATA_DIR"; + +export type DataDirSource = "flag" | "env" | "default"; + +export interface ResolvedDataDir { + dir: string; + source: DataDirSource; +} + +export interface ResolveDataDirOptions { + argv?: string[]; + env?: NodeJS.ProcessEnv; + cwd?: string; + home?: string; +} + +// Read a value from argv in both "--data-dir " and +// "--data-dir=" forms. Returns undefined when the flag is absent or +// has no following token. +export function readDataDirFlag(argv: string[]): string | undefined { + const equalsPrefix = `${DATA_DIR_FLAG}=`; + for (const arg of argv) { + if (arg.startsWith(equalsPrefix)) return arg.slice(equalsPrefix.length); + } + const idx = argv.indexOf(DATA_DIR_FLAG); + if (idx !== -1) return argv[idx + 1]; + return undefined; +} + +// Expand a leading ~ / ~/ to the given home directory; other values pass +// through unchanged. +export function expandHomePath(pathValue: string, home: string): string { + if (pathValue === "~") return home; + if (pathValue.startsWith("~/") || pathValue.startsWith("~\\")) { + return join(home, pathValue.slice(2)); + } + return pathValue; +} + +// Fork-compat default data directory. Unlike upstream, platform-specific +// directories are intentionally NOT adopted. +export function defaultDataDir(home: string = homedir()): string { + return join(home, ".agentmemory"); +} + +function toAbsoluteDataDir(raw: string, cwd: string, home: string): string { + const expanded = expandHomePath(raw.trim(), home); + return isAbsolute(expanded) ? expanded : resolve(cwd, expanded); +} + +// Single precedence chain for the agentmemory data dir: +// --data-dir CLI flag > AGENTMEMORY_DATA_DIR > ~/.agentmemory +// Resolution reads only the real environment and CLI args — never the +// hydrated .env file — so boot stays deterministic (the .env file itself is +// loaded from the resolved data dir afterwards). +export function resolveDataDirDetailed( + options: ResolveDataDirOptions = {}, +): ResolvedDataDir { + const argv = options.argv ?? process.argv.slice(2); + const env = options.env ?? process.env; + const cwd = options.cwd ?? process.cwd(); + const home = options.home ?? homedir(); + + const flagValue = readDataDirFlag(argv); + if (flagValue !== undefined && flagValue.trim().length > 0) { + return { dir: toAbsoluteDataDir(flagValue, cwd, home), source: "flag" }; + } + + const envValue = env[DATA_DIR_ENV]; + if (envValue !== undefined && envValue.trim().length > 0) { + return { dir: toAbsoluteDataDir(envValue, cwd, home), source: "env" }; + } + + return { dir: defaultDataDir(home), source: "default" }; +} + +export function resolveDataDir(options: ResolveDataDirOptions = {}): string { + return resolveDataDirDetailed(options).dir; +} + +// A legacy cwd-local ./data counts only when it actually holds an +// agentmemory store marker, so unrelated projects that happen to have a +// data/ folder don't trigger the relocation warning. +export function legacyDataDirInCwd(cwd: string = process.cwd()): boolean { + const legacyDir = resolve(cwd, "data"); + return ( + existsSync(join(legacyDir, "state_store.db")) || + existsSync(join(legacyDir, "iii-config.yaml")) + ); +} + +// Upstream's d8b5267 adopted a legacy cwd ./data store automatically; this +// fork deliberately does NOT. When state would silently land somewhere +// different from a pre-existing local store, tell the operator to opt in +// with an explicit --data-dir instead of copying anything. +export function warnOnLegacyDataDir( + options: ResolveDataDirOptions & { write?: (message: string) => void } = {}, +): boolean { + const resolved = resolveDataDirDetailed(options); + if (resolved.source !== "default") return false; + const cwd = options.cwd ?? process.cwd(); + if (!legacyDataDirInCwd(cwd)) return false; + const write = + options.write ?? ((message: string) => process.stderr.write(message)); + write( + `[agentmemory] Found a legacy ./data store directory in ${cwd}, but this install keeps its state in ${resolved.dir}. ` + + `Nothing was copied or moved — pass --data-dir ${join(cwd, "data")} (or set ${DATA_DIR_ENV}) to use the legacy location explicitly.\n`, + ); + return true; +} diff --git a/src/functions/migrate.ts b/src/functions/migrate.ts index d98898abc..a9a94d8ae 100644 --- a/src/functions/migrate.ts +++ b/src/functions/migrate.ts @@ -17,8 +17,11 @@ import type { } from "../types.js"; import { logger } from "../logger.js"; import { withKeyedLock } from "../state/keyed-mutex.js"; +import { resolveDataDir } from "../data-dir.js"; -const ALLOWED_DIRS = [resolve(homedir(), ".agentmemory")]; +function allowedDirs(): string[] { + return [resolve(resolveDataDir())]; +} type ProjectRecord = Record & { project?: string }; @@ -350,7 +353,7 @@ export async function normalizeProjectScopes( function isAllowedPath(dbPath: string): boolean { const resolved = resolve(dbPath); - return ALLOWED_DIRS.some((dir) => resolved.startsWith(dir + "/")); + return allowedDirs().some((dir) => resolved.startsWith(dir + "/")); } // Infer memory project from the majority project of its associated sessions. @@ -1015,7 +1018,7 @@ export function registerMigrateFunction(sdk: ISdk, kv: StateKV): void { if (!isAllowedPath(data.dbPath)) { return { success: false, - error: `Path not allowed. Must be under: ${ALLOWED_DIRS.join(", ")}`, + error: `Path not allowed. Must be under: ${allowedDirs().join(", ")}`, }; } diff --git a/test/data-dir.test.ts b/test/data-dir.test.ts new file mode 100644 index 000000000..476c592e6 --- /dev/null +++ b/test/data-dir.test.ts @@ -0,0 +1,294 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { + mkdirSync, + mkdtempSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { homedir, tmpdir } from "node:os"; +import { join } from "node:path"; +import { + DATA_DIR_ENV, + defaultDataDir, + expandHomePath, + legacyDataDirInCwd, + readDataDirFlag, + resolveDataDir, + resolveDataDirDetailed, + warnOnLegacyDataDir, +} from "../src/data-dir.js"; + +const ORIGINAL_HOME = process.env["HOME"]; +const ORIGINAL_USERPROFILE = process.env["USERPROFILE"]; + +let sandboxHome: string; +let sandboxCwd: string; + +function seedLegacyStore(): string { + const dataDir = join(sandboxCwd, "data"); + mkdirSync(dataDir, { recursive: true }); + writeFileSync(join(dataDir, "state_store.db"), ""); + return dataDir; +} + +describe("readDataDirFlag", () => { + it("reads the separated form", () => { + expect(readDataDirFlag(["--data-dir", "/tmp/am"])).toBe("/tmp/am"); + }); + + it("reads the = form", () => { + expect(readDataDirFlag(["--data-dir=/tmp/am"])).toBe("/tmp/am"); + }); + + it("returns undefined when absent or dangling", () => { + expect(readDataDirFlag([])).toBeUndefined(); + expect(readDataDirFlag(["--verbose"])).toBeUndefined(); + expect(readDataDirFlag(["--data-dir"])).toBeUndefined(); + }); +}); + +describe("expandHomePath", () => { + it("expands ~ and ~/ to the given home", () => { + const home = "/home/operator"; + expect(expandHomePath("~", home)).toBe(home); + expect(expandHomePath("~/notes", home)).toBe(join(home, "notes")); + expect(expandHomePath("/abs/path", home)).toBe("/abs/path"); + expect(expandHomePath("relative/path", home)).toBe("relative/path"); + }); +}); + +describe("resolveDataDir precedence", () => { + beforeEach(() => { + sandboxHome = mkdtempSync(join(tmpdir(), "agentmemory-dataroot-home-")); + sandboxCwd = mkdtempSync(join(tmpdir(), "agentmemory-dataroot-cwd-")); + }); + + afterEach(() => { + if (ORIGINAL_HOME === undefined) delete process.env["HOME"]; + else process.env["HOME"] = ORIGINAL_HOME; + if (ORIGINAL_USERPROFILE === undefined) + delete process.env["USERPROFILE"]; + else process.env["USERPROFILE"] = ORIGINAL_USERPROFILE; + delete process.env[DATA_DIR_ENV]; + rmSync(sandboxHome, { recursive: true, force: true }); + rmSync(sandboxCwd, { recursive: true, force: true }); + }); + + it("defaults to ~/.agentmemory (fork-compatible), not a platform dir", () => { + const resolved = resolveDataDirDetailed({ + argv: [], + env: {}, + cwd: sandboxCwd, + home: sandboxHome, + }); + expect(resolved).toEqual({ + dir: join(sandboxHome, ".agentmemory"), + source: "default", + }); + expect(defaultDataDir(sandboxHome)).toBe(join(sandboxHome, ".agentmemory")); + }); + + it("prefers AGENTMEMORY_DATA_DIR over the default and resolves relative to cwd", () => { + const resolved = resolveDataDirDetailed({ + argv: [], + env: { [DATA_DIR_ENV]: "state/memory" }, + cwd: sandboxCwd, + home: sandboxHome, + }); + expect(resolved).toEqual({ + dir: join(sandboxCwd, "state/memory"), + source: "env", + }); + }); + + it("prefers the --data-dir flag over both the environment and the default", () => { + const resolved = resolveDataDirDetailed({ + argv: ["--data-dir", "/explicit/path"], + env: { [DATA_DIR_ENV]: "/from/env" }, + cwd: sandboxCwd, + home: sandboxHome, + }); + expect(resolved).toEqual({ dir: "/explicit/path", source: "flag" }); + }); + + it("supports tilde expansion for the flag and env values", () => { + const flagged = resolveDataDirDetailed({ + argv: ["--data-dir=~/memories"], + env: {}, + cwd: sandboxCwd, + home: sandboxHome, + }); + expect(flagged.dir).toBe(join(sandboxHome, "memories")); + + const fromEnv = resolveDataDirDetailed({ + argv: [], + env: { [DATA_DIR_ENV]: "~" }, + cwd: sandboxCwd, + home: sandboxHome, + }); + expect(fromEnv.dir).toBe(sandboxHome); + }); + + it("ignores blank flag/env values and falls through to the default", () => { + const resolved = resolveDataDirDetailed({ + argv: ["--data-dir", " "], + env: { [DATA_DIR_ENV]: "" }, + cwd: sandboxCwd, + home: sandboxHome, + }); + expect(resolved.source).toBe("default"); + }); + + it("resolveDataDir returns just the directory", () => { + expect( + resolveDataDir({ argv: [], env: {}, cwd: sandboxCwd, home: sandboxHome }), + ).toBe(join(sandboxHome, ".agentmemory")); + }); +}); + +describe("legacy ./data handling", () => { + beforeEach(() => { + sandboxHome = mkdtempSync(join(tmpdir(), "agentmemory-legacy-home-")); + sandboxCwd = mkdtempSync(join(tmpdir(), "agentmemory-legacy-cwd-")); + process.env["HOME"] = sandboxHome; + process.env["USERPROFILE"] = sandboxHome; + delete process.env[DATA_DIR_ENV]; + }); + + afterEach(() => { + if (ORIGINAL_HOME === undefined) delete process.env["HOME"]; + else process.env["HOME"] = ORIGINAL_HOME; + if (ORIGINAL_USERPROFILE === undefined) + delete process.env["USERPROFILE"]; + else process.env["USERPROFILE"] = ORIGINAL_USERPROFILE; + delete process.env[DATA_DIR_ENV]; + rmSync(sandboxHome, { recursive: true, force: true }); + rmSync(sandboxCwd, { recursive: true, force: true }); + }); + + it("detects a legacy store only via agentmemory markers", () => { + expect(legacyDataDirInCwd(sandboxCwd)).toBe(false); + const dataDir = seedLegacyStore(); + expect(legacyDataDirInCwd(sandboxCwd)).toBe(true); + expect(dataDir).toBe(join(sandboxCwd, "data")); + }); + + it("warns when a legacy store exists and no explicit data dir was configured", () => { + seedLegacyStore(); + const messages: string[] = []; + const warned = warnOnLegacyDataDir({ + argv: [], + env: {}, + cwd: sandboxCwd, + home: sandboxHome, + write: (message) => messages.push(message), + }); + expect(warned).toBe(true); + expect(messages).toHaveLength(1); + expect(messages[0]).toContain("--data-dir"); + expect(messages[0]).toContain(DATA_DIR_ENV); + expect(messages[0]).toContain(join(sandboxCwd, "data")); + // Log-and-warn only: the legacy store must be untouched. + expect(legacyDataDirInCwd(sandboxCwd)).toBe(true); + }); + + it("stays silent once an explicit data dir was configured", () => { + seedLegacyStore(); + const messages: string[] = []; + const viaFlag = warnOnLegacyDataDir({ + argv: ["--data-dir", "/elsewhere"], + env: {}, + cwd: sandboxCwd, + home: sandboxHome, + write: (message) => messages.push(message), + }); + const viaEnv = warnOnLegacyDataDir({ + argv: [], + env: { [DATA_DIR_ENV]: "/elsewhere" }, + cwd: sandboxCwd, + home: sandboxHome, + write: (message) => messages.push(message), + }); + expect(viaFlag).toBe(false); + expect(viaEnv).toBe(false); + expect(messages).toHaveLength(0); + }); + + it("stays silent when no legacy store exists", () => { + const messages: string[] = []; + const warned = warnOnLegacyDataDir({ + argv: [], + env: {}, + cwd: sandboxCwd, + home: sandboxHome, + write: (message) => messages.push(message), + }); + expect(warned).toBe(false); + expect(messages).toHaveLength(0); + }); +}); + +describe("config integration", () => { + beforeEach(() => { + sandboxHome = mkdtempSync(join(tmpdir(), "agentmemory-dataroot-cfg-")); + process.env["HOME"] = sandboxHome; + process.env["USERPROFILE"] = sandboxHome; + delete process.env[DATA_DIR_ENV]; + }); + + afterEach(() => { + if (ORIGINAL_HOME === undefined) delete process.env["HOME"]; + else process.env["HOME"] = ORIGINAL_HOME; + if (ORIGINAL_USERPROFILE === undefined) + delete process.env["USERPROFILE"]; + else process.env["USERPROFILE"] = ORIGINAL_USERPROFILE; + delete process.env[DATA_DIR_ENV]; + rmSync(sandboxHome, { recursive: true, force: true }); + }); + + async function freshConfig() { + vi.resetModules(); + return await import("../src/config.js"); + } + + it("loadConfig().dataDir follows AGENTMEMORY_DATA_DIR over the default", async () => { + const cfg = await freshConfig(); + expect(cfg.loadConfig().dataDir).toBe(join(homedir(), ".agentmemory")); + + process.env[DATA_DIR_ENV] = join(sandboxHome, "relocated"); + const relocated = await freshConfig(); + expect(relocated.loadConfig().dataDir).toBe( + join(sandboxHome, "relocated"), + ); + }); + + it("hydrates .env from the resolved data dir", async () => { + const relocated = join(sandboxHome, "relocated"); + mkdirSync(relocated, { recursive: true }); + writeFileSync(join(relocated, ".env"), "AM_DATAROOT_PROBE=from-relocated\n"); + + process.env[DATA_DIR_ENV] = relocated; + const cfg = await freshConfig(); + cfg.hydrateProcessEnvFromFile(); + expect(process.env["AM_DATAROOT_PROBE"]).toBe("from-relocated"); + delete process.env["AM_DATAROOT_PROBE"]; + + // The default location has no .env — nothing else may be hydrated from + // the old path once the data dir moved. + delete process.env[DATA_DIR_ENV]; + const backToDefault = await freshConfig(); + backToDefault.hydrateProcessEnvFromFile(); + expect(process.env["AM_DATAROOT_PROBE"]).toBeUndefined(); + }); + + it("snapshot and standalone defaults anchor under the resolved data dir", async () => { + process.env[DATA_DIR_ENV] = join(sandboxHome, "relocated"); + const cfg = await freshConfig(); + expect(cfg.loadSnapshotConfig().dir).toBe( + join(join(sandboxHome, "relocated"), "snapshots"), + ); + expect(cfg.getStandalonePersistPath()).toBe( + join(join(sandboxHome, "relocated"), "standalone.json"), + ); + }); +}); From 8f1142e7678291bf154a0603952cd0fc0ebdf718 Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 12:43:00 -0500 Subject: [PATCH 5/8] chore(build): regenerate hook bundles after phase 2 source changes Shared chunk hashes moved with the hydration/data-dir/retry changes; CI rebuilds these committed outputs and a stale tree fails the R13 dirty-worktree check. --- ...{_auth-r09nwS46.mjs => _auth-C5rlVU3b.mjs} | 373 ++++-------------- ...ure-CalTsfGN.mjs => _capture-BSb-Fcto.mjs} | 2 +- ...ry--9SDKTY7.mjs => _delivery-D3R3VQcM.mjs} | 2 +- ...llS.mjs => _observe-delivery-DZP2ns6D.mjs} | 2 +- ...ect-BNYA1N7W.mjs => _project-VJwrNfCx.mjs} | 2 +- plugin/scripts/notification.mjs | 4 +- plugin/scripts/post-commit.mjs | 6 +- plugin/scripts/post-tool-failure.mjs | 6 +- plugin/scripts/post-tool-use.mjs | 6 +- plugin/scripts/pre-compact.mjs | 4 +- plugin/scripts/pre-tool-use.mjs | 4 +- plugin/scripts/prompt-submit.mjs | 4 +- plugin/scripts/session-end.mjs | 4 +- plugin/scripts/session-start.mjs | 6 +- plugin/scripts/stop.mjs | 4 +- plugin/scripts/subagent-start.mjs | 4 +- plugin/scripts/subagent-stop.mjs | 4 +- plugin/scripts/task-completed.mjs | 4 +- 18 files changed, 121 insertions(+), 320 deletions(-) rename plugin/scripts/{_auth-r09nwS46.mjs => _auth-C5rlVU3b.mjs} (95%) rename plugin/scripts/{_capture-CalTsfGN.mjs => _capture-BSb-Fcto.mjs} (99%) rename plugin/scripts/{_delivery--9SDKTY7.mjs => _delivery-D3R3VQcM.mjs} (97%) rename plugin/scripts/{_observe-delivery-Dp1TkllS.mjs => _observe-delivery-DZP2ns6D.mjs} (97%) rename plugin/scripts/{_project-BNYA1N7W.mjs => _project-VJwrNfCx.mjs} (89%) diff --git a/plugin/scripts/_auth-r09nwS46.mjs b/plugin/scripts/_auth-C5rlVU3b.mjs similarity index 95% rename from plugin/scripts/_auth-r09nwS46.mjs rename to plugin/scripts/_auth-C5rlVU3b.mjs index 5f9fc1490..3294465bc 100644 --- a/plugin/scripts/_auth-r09nwS46.mjs +++ b/plugin/scripts/_auth-C5rlVU3b.mjs @@ -5,279 +5,11 @@ import { isAbsolute, join, relative, resolve } from "node:path"; import { createHash } from "node:crypto"; import { homedir } from "node:os"; import { execFileSync } from "node:child_process"; +import "picocolors"; //#region \0rolldown/runtime.js var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports); var __require = /* @__PURE__ */ createRequire(import.meta.url); //#endregion -//#region node_modules/dotenv/lib/main.js -var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => { - const fs = __require("fs"); - const path = __require("path"); - const os = __require("os"); - const crypto = __require("crypto"); - const TIPS = [ - "◈ encrypted .env [www.dotenvx.com]", - "◈ secrets for agents [www.dotenvx.com]", - "⌁ auth for agents [www.vestauth.com]", - "⌘ custom filepath { path: '/custom/path/.env' }", - "⌘ enable debugging { debug: true }", - "⌘ override existing { override: true }", - "⌘ suppress logs { quiet: true }", - "⌘ multiple files { path: ['.env.local', '.env'] }" - ]; - function _getRandomTip() { - return TIPS[Math.floor(Math.random() * TIPS.length)]; - } - function parseBoolean(value) { - if (typeof value === "string") return ![ - "false", - "0", - "no", - "off", - "" - ].includes(value.toLowerCase()); - return Boolean(value); - } - function supportsAnsi() { - return process.stdout.isTTY; - } - function dim(text) { - return supportsAnsi() ? `\x1b[2m${text}\x1b[0m` : text; - } - const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm; - function parse(src) { - const obj = {}; - let lines = src.toString(); - lines = lines.replace(/\r\n?/gm, "\n"); - let match; - while ((match = LINE.exec(lines)) != null) { - const key = match[1]; - let value = match[2] || ""; - value = value.trim(); - const maybeQuote = value[0]; - value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2"); - if (maybeQuote === "\"") { - value = value.replace(/\\n/g, "\n"); - value = value.replace(/\\r/g, "\r"); - } - obj[key] = value; - } - return obj; - } - function _parseVault(options) { - options = options || {}; - const vaultPath = _vaultPath(options); - options.path = vaultPath; - const result = DotenvModule.configDotenv(options); - if (!result.parsed) { - const err = /* @__PURE__ */ new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`); - err.code = "MISSING_DATA"; - throw err; - } - const keys = _dotenvKey(options).split(","); - const length = keys.length; - let decrypted; - for (let i = 0; i < length; i++) try { - const attrs = _instructions(result, keys[i].trim()); - decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key); - break; - } catch (error) { - if (i + 1 >= length) throw error; - } - return DotenvModule.parse(decrypted); - } - function _warn(message) { - console.error(`⚠ ${message}`); - } - function _debug(message) { - console.log(`┆ ${message}`); - } - function _log(message) { - console.log(`◇ ${message}`); - } - function _dotenvKey(options) { - if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) return options.DOTENV_KEY; - if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) return process.env.DOTENV_KEY; - return ""; - } - function _instructions(result, dotenvKey) { - let uri; - try { - uri = new URL(dotenvKey); - } catch (error) { - if (error.code === "ERR_INVALID_URL") { - const err = /* @__PURE__ */ new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development"); - err.code = "INVALID_DOTENV_KEY"; - throw err; - } - throw error; - } - const key = uri.password; - if (!key) { - const err = /* @__PURE__ */ new Error("INVALID_DOTENV_KEY: Missing key part"); - err.code = "INVALID_DOTENV_KEY"; - throw err; - } - const environment = uri.searchParams.get("environment"); - if (!environment) { - const err = /* @__PURE__ */ new Error("INVALID_DOTENV_KEY: Missing environment part"); - err.code = "INVALID_DOTENV_KEY"; - throw err; - } - const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`; - const ciphertext = result.parsed[environmentKey]; - if (!ciphertext) { - const err = /* @__PURE__ */ new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`); - err.code = "NOT_FOUND_DOTENV_ENVIRONMENT"; - throw err; - } - return { - ciphertext, - key - }; - } - function _vaultPath(options) { - let possibleVaultPath = null; - if (options && options.path && options.path.length > 0) if (Array.isArray(options.path)) { - for (const filepath of options.path) if (fs.existsSync(filepath)) possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`; - } else possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`; - else possibleVaultPath = path.resolve(process.cwd(), ".env.vault"); - if (fs.existsSync(possibleVaultPath)) return possibleVaultPath; - return null; - } - function _resolveHome(envPath) { - return envPath[0] === "~" ? path.join(os.homedir(), envPath.slice(1)) : envPath; - } - function _configVault(options) { - const debug = parseBoolean(process.env.DOTENV_CONFIG_DEBUG || options && options.debug); - const quiet = parseBoolean(process.env.DOTENV_CONFIG_QUIET || options && options.quiet); - if (debug || !quiet) _log("loading env from encrypted .env.vault"); - const parsed = DotenvModule._parseVault(options); - let processEnv = process.env; - if (options && options.processEnv != null) processEnv = options.processEnv; - DotenvModule.populate(processEnv, parsed, options); - return { parsed }; - } - function configDotenv(options) { - const dotenvPath = path.resolve(process.cwd(), ".env"); - let encoding = "utf8"; - let processEnv = process.env; - if (options && options.processEnv != null) processEnv = options.processEnv; - let debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || options && options.debug); - let quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || options && options.quiet); - if (options && options.encoding) encoding = options.encoding; - else if (debug) _debug("no encoding is specified (UTF-8 is used by default)"); - let optionPaths = [dotenvPath]; - if (options && options.path) if (!Array.isArray(options.path)) optionPaths = [_resolveHome(options.path)]; - else { - optionPaths = []; - for (const filepath of options.path) optionPaths.push(_resolveHome(filepath)); - } - let lastError; - const parsedAll = {}; - for (const path of optionPaths) try { - const parsed = DotenvModule.parse(fs.readFileSync(path, { encoding })); - DotenvModule.populate(parsedAll, parsed, options); - } catch (e) { - if (debug) _debug(`failed to load ${path} ${e.message}`); - lastError = e; - } - const populated = DotenvModule.populate(processEnv, parsedAll, options); - debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || debug); - quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || quiet); - if (debug || !quiet) { - const keysCount = Object.keys(populated).length; - const shortPaths = []; - for (const filePath of optionPaths) try { - const relative = path.relative(process.cwd(), filePath); - shortPaths.push(relative); - } catch (e) { - if (debug) _debug(`failed to load ${filePath} ${e.message}`); - lastError = e; - } - _log(`injected env (${keysCount}) from ${shortPaths.join(",")} ${dim(`// tip: ${_getRandomTip()}`)}`); - } - if (lastError) return { - parsed: parsedAll, - error: lastError - }; - else return { parsed: parsedAll }; - } - function config(options) { - if (_dotenvKey(options).length === 0) return DotenvModule.configDotenv(options); - const vaultPath = _vaultPath(options); - if (!vaultPath) { - _warn(`you set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}`); - return DotenvModule.configDotenv(options); - } - return DotenvModule._configVault(options); - } - function decrypt(encrypted, keyStr) { - const key = Buffer.from(keyStr.slice(-64), "hex"); - let ciphertext = Buffer.from(encrypted, "base64"); - const nonce = ciphertext.subarray(0, 12); - const authTag = ciphertext.subarray(-16); - ciphertext = ciphertext.subarray(12, -16); - try { - const aesgcm = crypto.createDecipheriv("aes-256-gcm", key, nonce); - aesgcm.setAuthTag(authTag); - return `${aesgcm.update(ciphertext)}${aesgcm.final()}`; - } catch (error) { - const isRange = error instanceof RangeError; - const invalidKeyLength = error.message === "Invalid key length"; - const decryptionFailed = error.message === "Unsupported state or unable to authenticate data"; - if (isRange || invalidKeyLength) { - const err = /* @__PURE__ */ new Error("INVALID_DOTENV_KEY: It must be 64 characters long (or more)"); - err.code = "INVALID_DOTENV_KEY"; - throw err; - } else if (decryptionFailed) { - const err = /* @__PURE__ */ new Error("DECRYPTION_FAILED: Please check your DOTENV_KEY"); - err.code = "DECRYPTION_FAILED"; - throw err; - } else throw error; - } - } - function populate(processEnv, parsed, options = {}) { - const debug = Boolean(options && options.debug); - const override = Boolean(options && options.override); - const populated = {}; - if (typeof parsed !== "object") { - const err = /* @__PURE__ */ new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate"); - err.code = "OBJECT_REQUIRED"; - throw err; - } - for (const key of Object.keys(parsed)) if (Object.prototype.hasOwnProperty.call(processEnv, key)) { - if (override === true) { - processEnv[key] = parsed[key]; - populated[key] = parsed[key]; - } - if (debug) if (override === true) _debug(`"${key}" is already defined and WAS overwritten`); - else _debug(`"${key}" is already defined and was NOT overwritten`); - } else { - processEnv[key] = parsed[key]; - populated[key] = parsed[key]; - } - return populated; - } - const DotenvModule = { - configDotenv, - _configVault, - _parseVault, - config, - decrypt, - parse, - populate - }; - module.exports.configDotenv = DotenvModule.configDotenv; - module.exports._configVault = DotenvModule._configVault; - module.exports._parseVault = DotenvModule._parseVault; - module.exports.config = DotenvModule.config; - module.exports.decrypt = DotenvModule.decrypt; - module.exports.parse = DotenvModule.parse; - module.exports.populate = DotenvModule.populate; - module.exports = DotenvModule; -})); -//#endregion //#region node_modules/yaml/dist/nodes/identity.js var require_identity = /* @__PURE__ */ __commonJSMin(((exports) => { const ALIAS = Symbol.for("yaml.alias"); @@ -6843,8 +6575,8 @@ var require_public_api = /* @__PURE__ */ __commonJSMin(((exports) => { exports.stringify = stringify; })); //#endregion -//#region node_modules/yaml/dist/index.js -var require_dist = /* @__PURE__ */ __commonJSMin(((exports) => { +//#region src/data-dir.ts +var import_dist = (/* @__PURE__ */ __commonJSMin(((exports) => { var composer = require_composer(); var Document = require_Document(); var Schema = require_Schema(); @@ -6889,11 +6621,90 @@ var require_dist = /* @__PURE__ */ __commonJSMin(((exports) => { exports.stringify = publicApi.stringify; exports.visit = visit.visit; exports.visitAsync = visit.visitAsync; -})); +})))(); +const DATA_DIR_FLAG = "--data-dir"; +const DATA_DIR_ENV = "AGENTMEMORY_DATA_DIR"; +function readDataDirFlag(argv) { + const equalsPrefix = `${DATA_DIR_FLAG}=`; + for (const arg of argv) if (arg.startsWith(equalsPrefix)) return arg.slice(equalsPrefix.length); + const idx = argv.indexOf(DATA_DIR_FLAG); + if (idx !== -1) return argv[idx + 1]; +} +function expandHomePath(pathValue, home) { + if (pathValue === "~") return home; + if (pathValue.startsWith("~/") || pathValue.startsWith("~\\")) return join(home, pathValue.slice(2)); + return pathValue; +} +function defaultDataDir(home = homedir()) { + return join(home, ".agentmemory"); +} +function toAbsoluteDataDir(raw, cwd, home) { + const expanded = expandHomePath(raw.trim(), home); + return isAbsolute(expanded) ? expanded : resolve(cwd, expanded); +} +function resolveDataDirDetailed(options = {}) { + const argv = options.argv ?? process.argv.slice(2); + const env = options.env ?? process.env; + const cwd = options.cwd ?? process.cwd(); + const home = options.home ?? homedir(); + const flagValue = readDataDirFlag(argv); + if (flagValue !== void 0 && flagValue.trim().length > 0) return { + dir: toAbsoluteDataDir(flagValue, cwd, home), + source: "flag" + }; + const envValue = env[DATA_DIR_ENV]; + if (envValue !== void 0 && envValue.trim().length > 0) return { + dir: toAbsoluteDataDir(envValue, cwd, home), + source: "env" + }; + return { + dir: defaultDataDir(home), + source: "default" + }; +} +function resolveDataDir(options = {}) { + return resolveDataDirDetailed(options).dir; +} +//#endregion +//#region src/config.ts +let envFileCache; +function envFilePath() { + return join(resolveDataDir(), ".env"); +} +function loadEnvFile() { + if (envFileCache) return envFileCache; + const envFile = envFilePath(); + if (!existsSync(envFile)) { + envFileCache = {}; + return envFileCache; + } + const content = readFileSync(envFile, "utf-8"); + const vars = {}; + for (const line of content.split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + const quoteChar = val[0] === "\"" || val[0] === "'" ? val[0] : ""; + if (quoteChar) { + const closeIdx = val.indexOf(quoteChar, 1); + if (closeIdx !== -1) val = val.slice(1, closeIdx); + } else { + const hashIdx = val.indexOf(" #"); + if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); + } + vars[key] = val; + } + envFileCache = vars; + return envFileCache; +} +function hydrateProcessEnvFromFile() { + for (const [k, v] of Object.entries(loadEnvFile())) if (process.env[k] === void 0) process.env[k] = v; +} //#endregion //#region src/project-config.ts -var import_main = require_main(); -var import_dist = require_dist(); const PRIVACY_ORDER = { standard: 0, private: 1, @@ -7055,14 +6866,7 @@ function getUserProjectConfigPath(root) { return join(userHome(), ".agentmemory", "projects", `${projectPathHash(root)}.yaml`); } function loadAgentmemoryEnvironment() { - const envPath = join(userHome(), ".agentmemory", ".env"); - let fileEnv = {}; - if (existsSync(envPath)) try { - fileEnv = (0, import_main.parse)(readFileSync(envPath)); - } catch { - fileEnv = {}; - } - for (const [key, value] of Object.entries(fileEnv)) if (process.env[key] === void 0) process.env[key] = value; + hydrateProcessEnvFromFile(); if (!asString(process.env["AGENTMEMORY_SECRET"])) { const secretFile = asString(process.env["AGENTMEMORY_SECRET_FILE"]); if (secretFile) try { @@ -7070,10 +6874,7 @@ function loadAgentmemoryEnvironment() { if (secret) process.env["AGENTMEMORY_SECRET"] = secret; } catch {} } - return { - ...fileEnv, - ...process.env - }; + return { ...process.env }; } function envLayer(env) { return { diff --git a/plugin/scripts/_capture-CalTsfGN.mjs b/plugin/scripts/_capture-BSb-Fcto.mjs similarity index 99% rename from plugin/scripts/_capture-CalTsfGN.mjs rename to plugin/scripts/_capture-BSb-Fcto.mjs index 2f2c18dcb..1058fb374 100644 --- a/plugin/scripts/_capture-CalTsfGN.mjs +++ b/plugin/scripts/_capture-BSb-Fcto.mjs @@ -1,4 +1,4 @@ -import { a as normalizedProjectPath, r as isProjectPathExcluded } from "./_auth-r09nwS46.mjs"; +import { a as normalizedProjectPath, r as isProjectPathExcluded } from "./_auth-C5rlVU3b.mjs"; import { resolve } from "node:path"; import { createHash } from "node:crypto"; import { execFileSync } from "node:child_process"; diff --git a/plugin/scripts/_delivery--9SDKTY7.mjs b/plugin/scripts/_delivery-D3R3VQcM.mjs similarity index 97% rename from plugin/scripts/_delivery--9SDKTY7.mjs rename to plugin/scripts/_delivery-D3R3VQcM.mjs index 9865974e1..f60aa8cef 100644 --- a/plugin/scripts/_delivery--9SDKTY7.mjs +++ b/plugin/scripts/_delivery-D3R3VQcM.mjs @@ -1,4 +1,4 @@ -import { n as projectAuthHeaders } from "./_auth-r09nwS46.mjs"; +import { n as projectAuthHeaders } from "./_auth-C5rlVU3b.mjs"; //#region src/hooks/_delivery.ts var HookDeliveryError = class extends Error { retryable; diff --git a/plugin/scripts/_observe-delivery-Dp1TkllS.mjs b/plugin/scripts/_observe-delivery-DZP2ns6D.mjs similarity index 97% rename from plugin/scripts/_observe-delivery-Dp1TkllS.mjs rename to plugin/scripts/_observe-delivery-DZP2ns6D.mjs index 276e07389..2fd711fdc 100644 --- a/plugin/scripts/_observe-delivery-Dp1TkllS.mjs +++ b/plugin/scripts/_observe-delivery-DZP2ns6D.mjs @@ -1,4 +1,4 @@ -import { n as projectAuthHeaders } from "./_auth-r09nwS46.mjs"; +import { n as projectAuthHeaders } from "./_auth-C5rlVU3b.mjs"; //#region src/hooks/_observe-delivery.ts const MAX_ATTEMPTS = 2; const REQUEST_TIMEOUT_MS = 250; diff --git a/plugin/scripts/_project-BNYA1N7W.mjs b/plugin/scripts/_project-VJwrNfCx.mjs similarity index 89% rename from plugin/scripts/_project-BNYA1N7W.mjs rename to plugin/scripts/_project-VJwrNfCx.mjs index 48db13530..8c36eaa1b 100644 --- a/plugin/scripts/_project-BNYA1N7W.mjs +++ b/plugin/scripts/_project-VJwrNfCx.mjs @@ -1,4 +1,4 @@ -import { i as loadAgentmemoryEnvironment, o as resolveProjectConfig } from "./_auth-r09nwS46.mjs"; +import { i as loadAgentmemoryEnvironment, o as resolveProjectConfig } from "./_auth-C5rlVU3b.mjs"; //#region src/hooks/_project.ts loadAgentmemoryEnvironment(); function resolveProject(cwd) { diff --git a/plugin/scripts/notification.mjs b/plugin/scripts/notification.mjs index 951566f89..4c2d1993c 100755 --- a/plugin/scripts/notification.mjs +++ b/plugin/scripts/notification.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-BNYA1N7W.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-Dp1TkllS.mjs"; +import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-DZP2ns6D.mjs"; //#region src/hooks/notification.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/post-commit.mjs b/plugin/scripts/post-commit.mjs index d4171ff35..1b40f102e 100755 --- a/plugin/scripts/post-commit.mjs +++ b/plugin/scripts/post-commit.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-BNYA1N7W.mjs"; -import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery--9SDKTY7.mjs"; -import { n as credentialFreeWorktreeId, r as parseCommitTransitions } from "./_capture-CalTsfGN.mjs"; +import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; +import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-D3R3VQcM.mjs"; +import { n as credentialFreeWorktreeId, r as parseCommitTransitions } from "./_capture-BSb-Fcto.mjs"; import { resolve } from "node:path"; import { execFile } from "node:child_process"; import { pathToFileURL } from "node:url"; diff --git a/plugin/scripts/post-tool-failure.mjs b/plugin/scripts/post-tool-failure.mjs index ece1f1026..94ff91d66 100755 --- a/plugin/scripts/post-tool-failure.mjs +++ b/plugin/scripts/post-tool-failure.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { o as resolveProjectConfig } from "./_auth-r09nwS46.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-Dp1TkllS.mjs"; -import { t as captureToolEvent } from "./_capture-CalTsfGN.mjs"; +import { o as resolveProjectConfig } from "./_auth-C5rlVU3b.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-DZP2ns6D.mjs"; +import { t as captureToolEvent } from "./_capture-BSb-Fcto.mjs"; //#region src/hooks/post-tool-failure.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/post-tool-use.mjs b/plugin/scripts/post-tool-use.mjs index 879d8b5f6..094e0c3e9 100755 --- a/plugin/scripts/post-tool-use.mjs +++ b/plugin/scripts/post-tool-use.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { o as resolveProjectConfig } from "./_auth-r09nwS46.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-Dp1TkllS.mjs"; -import { t as captureToolEvent } from "./_capture-CalTsfGN.mjs"; +import { o as resolveProjectConfig } from "./_auth-C5rlVU3b.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-DZP2ns6D.mjs"; +import { t as captureToolEvent } from "./_capture-BSb-Fcto.mjs"; //#region src/hooks/post-tool-use.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/pre-compact.mjs b/plugin/scripts/pre-compact.mjs index fd70a17a3..1ef5dbba9 100755 --- a/plugin/scripts/pre-compact.mjs +++ b/plugin/scripts/pre-compact.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { i as loadAgentmemoryEnvironment, n as projectAuthHeaders, t as contextAcknowledgementSecret } from "./_auth-r09nwS46.mjs"; -import { t as resolveProject } from "./_project-BNYA1N7W.mjs"; +import { i as loadAgentmemoryEnvironment, n as projectAuthHeaders, t as contextAcknowledgementSecret } from "./_auth-C5rlVU3b.mjs"; +import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; import { createHash, createHmac, randomUUID } from "node:crypto"; //#region src/hooks/pre-compact.ts function isSdkChildContext(payload) { diff --git a/plugin/scripts/pre-tool-use.mjs b/plugin/scripts/pre-tool-use.mjs index b14b1eddb..ad927744e 100755 --- a/plugin/scripts/pre-tool-use.mjs +++ b/plugin/scripts/pre-tool-use.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { i as loadAgentmemoryEnvironment, n as projectAuthHeaders } from "./_auth-r09nwS46.mjs"; -import { t as resolveProject } from "./_project-BNYA1N7W.mjs"; +import { i as loadAgentmemoryEnvironment, n as projectAuthHeaders } from "./_auth-C5rlVU3b.mjs"; +import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; //#region src/hooks/pre-tool-use.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/prompt-submit.mjs b/plugin/scripts/prompt-submit.mjs index 67af703b5..befbd3122 100755 --- a/plugin/scripts/prompt-submit.mjs +++ b/plugin/scripts/prompt-submit.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-BNYA1N7W.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-Dp1TkllS.mjs"; +import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-DZP2ns6D.mjs"; //#region src/hooks/prompt-submit.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/session-end.mjs b/plugin/scripts/session-end.mjs index 10ed11e67..eb34b1901 100755 --- a/plugin/scripts/session-end.mjs +++ b/plugin/scripts/session-end.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-BNYA1N7W.mjs"; -import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery--9SDKTY7.mjs"; +import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; +import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-D3R3VQcM.mjs"; //#region src/hooks/session-end.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/session-start.mjs b/plugin/scripts/session-start.mjs index 659b7ed3b..c06ace2cd 100755 --- a/plugin/scripts/session-start.mjs +++ b/plugin/scripts/session-start.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { o as resolveProjectConfig } from "./_auth-r09nwS46.mjs"; -import "./_project-BNYA1N7W.mjs"; -import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery--9SDKTY7.mjs"; +import { o as resolveProjectConfig } from "./_auth-C5rlVU3b.mjs"; +import "./_project-VJwrNfCx.mjs"; +import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-D3R3VQcM.mjs"; //#region src/hooks/session-start.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/stop.mjs b/plugin/scripts/stop.mjs index 6af6298ab..350d61427 100755 --- a/plugin/scripts/stop.mjs +++ b/plugin/scripts/stop.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-BNYA1N7W.mjs"; -import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery--9SDKTY7.mjs"; +import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; +import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-D3R3VQcM.mjs"; //#region src/hooks/stop.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/subagent-start.mjs b/plugin/scripts/subagent-start.mjs index 7bab3c28d..d5c066d0b 100755 --- a/plugin/scripts/subagent-start.mjs +++ b/plugin/scripts/subagent-start.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-BNYA1N7W.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-Dp1TkllS.mjs"; +import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-DZP2ns6D.mjs"; //#region src/hooks/subagent-start.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/subagent-stop.mjs b/plugin/scripts/subagent-stop.mjs index 01329f2b1..498267de0 100755 --- a/plugin/scripts/subagent-stop.mjs +++ b/plugin/scripts/subagent-stop.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-BNYA1N7W.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-Dp1TkllS.mjs"; +import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-DZP2ns6D.mjs"; //#region src/hooks/subagent-stop.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/task-completed.mjs b/plugin/scripts/task-completed.mjs index 8f6f412ba..5c1885902 100755 --- a/plugin/scripts/task-completed.mjs +++ b/plugin/scripts/task-completed.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-BNYA1N7W.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-Dp1TkllS.mjs"; +import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-DZP2ns6D.mjs"; //#region src/hooks/task-completed.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; From 08eb08c3d65ac15aa7ebb0a58f7a7bf85d6d4fa7 Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 12:45:25 -0500 Subject: [PATCH 6/8] fix(build): bundle picocolors into hook chunks The hydration wiring made config.ts's provider hint reachable from the _auth chunk, leaving an external picocolors import in committed bundles and breaking the isolated-plugin-tree invariant. Bundle it like dotenv and yaml so hooks stay dependency-free. --- plugin/scripts/{_auth-C5rlVU3b.mjs => _auth-dmt9vymH.mjs} | 1 - .../{_capture-BSb-Fcto.mjs => _capture-KP8LxcSz.mjs} | 2 +- .../{_delivery-D3R3VQcM.mjs => _delivery-BtSVOGKV.mjs} | 2 +- ...delivery-DZP2ns6D.mjs => _observe-delivery-Bt5vTi-C.mjs} | 2 +- .../{_project-VJwrNfCx.mjs => _project-CXCTta9T.mjs} | 2 +- plugin/scripts/notification.mjs | 4 ++-- plugin/scripts/post-commit.mjs | 6 +++--- plugin/scripts/post-tool-failure.mjs | 6 +++--- plugin/scripts/post-tool-use.mjs | 6 +++--- plugin/scripts/pre-compact.mjs | 4 ++-- plugin/scripts/pre-tool-use.mjs | 4 ++-- plugin/scripts/prompt-submit.mjs | 4 ++-- plugin/scripts/session-end.mjs | 4 ++-- plugin/scripts/session-start.mjs | 6 +++--- plugin/scripts/stop.mjs | 4 ++-- plugin/scripts/subagent-start.mjs | 4 ++-- plugin/scripts/subagent-stop.mjs | 4 ++-- plugin/scripts/task-completed.mjs | 4 ++-- tsdown.config.ts | 5 +++-- 19 files changed, 37 insertions(+), 37 deletions(-) rename plugin/scripts/{_auth-C5rlVU3b.mjs => _auth-dmt9vymH.mjs} (99%) rename plugin/scripts/{_capture-BSb-Fcto.mjs => _capture-KP8LxcSz.mjs} (99%) rename plugin/scripts/{_delivery-D3R3VQcM.mjs => _delivery-BtSVOGKV.mjs} (97%) rename plugin/scripts/{_observe-delivery-DZP2ns6D.mjs => _observe-delivery-Bt5vTi-C.mjs} (97%) rename plugin/scripts/{_project-VJwrNfCx.mjs => _project-CXCTta9T.mjs} (89%) diff --git a/plugin/scripts/_auth-C5rlVU3b.mjs b/plugin/scripts/_auth-dmt9vymH.mjs similarity index 99% rename from plugin/scripts/_auth-C5rlVU3b.mjs rename to plugin/scripts/_auth-dmt9vymH.mjs index 3294465bc..1d4238f55 100644 --- a/plugin/scripts/_auth-C5rlVU3b.mjs +++ b/plugin/scripts/_auth-dmt9vymH.mjs @@ -5,7 +5,6 @@ import { isAbsolute, join, relative, resolve } from "node:path"; import { createHash } from "node:crypto"; import { homedir } from "node:os"; import { execFileSync } from "node:child_process"; -import "picocolors"; //#region \0rolldown/runtime.js var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports); var __require = /* @__PURE__ */ createRequire(import.meta.url); diff --git a/plugin/scripts/_capture-BSb-Fcto.mjs b/plugin/scripts/_capture-KP8LxcSz.mjs similarity index 99% rename from plugin/scripts/_capture-BSb-Fcto.mjs rename to plugin/scripts/_capture-KP8LxcSz.mjs index 1058fb374..1447e9b62 100644 --- a/plugin/scripts/_capture-BSb-Fcto.mjs +++ b/plugin/scripts/_capture-KP8LxcSz.mjs @@ -1,4 +1,4 @@ -import { a as normalizedProjectPath, r as isProjectPathExcluded } from "./_auth-C5rlVU3b.mjs"; +import { a as normalizedProjectPath, r as isProjectPathExcluded } from "./_auth-dmt9vymH.mjs"; import { resolve } from "node:path"; import { createHash } from "node:crypto"; import { execFileSync } from "node:child_process"; diff --git a/plugin/scripts/_delivery-D3R3VQcM.mjs b/plugin/scripts/_delivery-BtSVOGKV.mjs similarity index 97% rename from plugin/scripts/_delivery-D3R3VQcM.mjs rename to plugin/scripts/_delivery-BtSVOGKV.mjs index f60aa8cef..41e07573a 100644 --- a/plugin/scripts/_delivery-D3R3VQcM.mjs +++ b/plugin/scripts/_delivery-BtSVOGKV.mjs @@ -1,4 +1,4 @@ -import { n as projectAuthHeaders } from "./_auth-C5rlVU3b.mjs"; +import { n as projectAuthHeaders } from "./_auth-dmt9vymH.mjs"; //#region src/hooks/_delivery.ts var HookDeliveryError = class extends Error { retryable; diff --git a/plugin/scripts/_observe-delivery-DZP2ns6D.mjs b/plugin/scripts/_observe-delivery-Bt5vTi-C.mjs similarity index 97% rename from plugin/scripts/_observe-delivery-DZP2ns6D.mjs rename to plugin/scripts/_observe-delivery-Bt5vTi-C.mjs index 2fd711fdc..c69a5c72c 100644 --- a/plugin/scripts/_observe-delivery-DZP2ns6D.mjs +++ b/plugin/scripts/_observe-delivery-Bt5vTi-C.mjs @@ -1,4 +1,4 @@ -import { n as projectAuthHeaders } from "./_auth-C5rlVU3b.mjs"; +import { n as projectAuthHeaders } from "./_auth-dmt9vymH.mjs"; //#region src/hooks/_observe-delivery.ts const MAX_ATTEMPTS = 2; const REQUEST_TIMEOUT_MS = 250; diff --git a/plugin/scripts/_project-VJwrNfCx.mjs b/plugin/scripts/_project-CXCTta9T.mjs similarity index 89% rename from plugin/scripts/_project-VJwrNfCx.mjs rename to plugin/scripts/_project-CXCTta9T.mjs index 8c36eaa1b..f2c5e16a7 100644 --- a/plugin/scripts/_project-VJwrNfCx.mjs +++ b/plugin/scripts/_project-CXCTta9T.mjs @@ -1,4 +1,4 @@ -import { i as loadAgentmemoryEnvironment, o as resolveProjectConfig } from "./_auth-C5rlVU3b.mjs"; +import { i as loadAgentmemoryEnvironment, o as resolveProjectConfig } from "./_auth-dmt9vymH.mjs"; //#region src/hooks/_project.ts loadAgentmemoryEnvironment(); function resolveProject(cwd) { diff --git a/plugin/scripts/notification.mjs b/plugin/scripts/notification.mjs index 4c2d1993c..3b86034b9 100755 --- a/plugin/scripts/notification.mjs +++ b/plugin/scripts/notification.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-DZP2ns6D.mjs"; +import { t as resolveProject } from "./_project-CXCTta9T.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-Bt5vTi-C.mjs"; //#region src/hooks/notification.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/post-commit.mjs b/plugin/scripts/post-commit.mjs index 1b40f102e..95848464f 100755 --- a/plugin/scripts/post-commit.mjs +++ b/plugin/scripts/post-commit.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; -import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-D3R3VQcM.mjs"; -import { n as credentialFreeWorktreeId, r as parseCommitTransitions } from "./_capture-BSb-Fcto.mjs"; +import { t as resolveProject } from "./_project-CXCTta9T.mjs"; +import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-BtSVOGKV.mjs"; +import { n as credentialFreeWorktreeId, r as parseCommitTransitions } from "./_capture-KP8LxcSz.mjs"; import { resolve } from "node:path"; import { execFile } from "node:child_process"; import { pathToFileURL } from "node:url"; diff --git a/plugin/scripts/post-tool-failure.mjs b/plugin/scripts/post-tool-failure.mjs index 94ff91d66..80ef49d35 100755 --- a/plugin/scripts/post-tool-failure.mjs +++ b/plugin/scripts/post-tool-failure.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { o as resolveProjectConfig } from "./_auth-C5rlVU3b.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-DZP2ns6D.mjs"; -import { t as captureToolEvent } from "./_capture-BSb-Fcto.mjs"; +import { o as resolveProjectConfig } from "./_auth-dmt9vymH.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-Bt5vTi-C.mjs"; +import { t as captureToolEvent } from "./_capture-KP8LxcSz.mjs"; //#region src/hooks/post-tool-failure.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/post-tool-use.mjs b/plugin/scripts/post-tool-use.mjs index 094e0c3e9..6d0371a9f 100755 --- a/plugin/scripts/post-tool-use.mjs +++ b/plugin/scripts/post-tool-use.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { o as resolveProjectConfig } from "./_auth-C5rlVU3b.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-DZP2ns6D.mjs"; -import { t as captureToolEvent } from "./_capture-BSb-Fcto.mjs"; +import { o as resolveProjectConfig } from "./_auth-dmt9vymH.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-Bt5vTi-C.mjs"; +import { t as captureToolEvent } from "./_capture-KP8LxcSz.mjs"; //#region src/hooks/post-tool-use.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/pre-compact.mjs b/plugin/scripts/pre-compact.mjs index 1ef5dbba9..fd2500d97 100755 --- a/plugin/scripts/pre-compact.mjs +++ b/plugin/scripts/pre-compact.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { i as loadAgentmemoryEnvironment, n as projectAuthHeaders, t as contextAcknowledgementSecret } from "./_auth-C5rlVU3b.mjs"; -import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; +import { i as loadAgentmemoryEnvironment, n as projectAuthHeaders, t as contextAcknowledgementSecret } from "./_auth-dmt9vymH.mjs"; +import { t as resolveProject } from "./_project-CXCTta9T.mjs"; import { createHash, createHmac, randomUUID } from "node:crypto"; //#region src/hooks/pre-compact.ts function isSdkChildContext(payload) { diff --git a/plugin/scripts/pre-tool-use.mjs b/plugin/scripts/pre-tool-use.mjs index ad927744e..384a1df60 100755 --- a/plugin/scripts/pre-tool-use.mjs +++ b/plugin/scripts/pre-tool-use.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { i as loadAgentmemoryEnvironment, n as projectAuthHeaders } from "./_auth-C5rlVU3b.mjs"; -import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; +import { i as loadAgentmemoryEnvironment, n as projectAuthHeaders } from "./_auth-dmt9vymH.mjs"; +import { t as resolveProject } from "./_project-CXCTta9T.mjs"; //#region src/hooks/pre-tool-use.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/prompt-submit.mjs b/plugin/scripts/prompt-submit.mjs index befbd3122..1dcbced85 100755 --- a/plugin/scripts/prompt-submit.mjs +++ b/plugin/scripts/prompt-submit.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-DZP2ns6D.mjs"; +import { t as resolveProject } from "./_project-CXCTta9T.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-Bt5vTi-C.mjs"; //#region src/hooks/prompt-submit.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/session-end.mjs b/plugin/scripts/session-end.mjs index eb34b1901..7eeacb6d5 100755 --- a/plugin/scripts/session-end.mjs +++ b/plugin/scripts/session-end.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; -import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-D3R3VQcM.mjs"; +import { t as resolveProject } from "./_project-CXCTta9T.mjs"; +import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-BtSVOGKV.mjs"; //#region src/hooks/session-end.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/session-start.mjs b/plugin/scripts/session-start.mjs index c06ace2cd..a7be025dd 100755 --- a/plugin/scripts/session-start.mjs +++ b/plugin/scripts/session-start.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { o as resolveProjectConfig } from "./_auth-C5rlVU3b.mjs"; -import "./_project-VJwrNfCx.mjs"; -import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-D3R3VQcM.mjs"; +import { o as resolveProjectConfig } from "./_auth-dmt9vymH.mjs"; +import "./_project-CXCTta9T.mjs"; +import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-BtSVOGKV.mjs"; //#region src/hooks/session-start.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/stop.mjs b/plugin/scripts/stop.mjs index 350d61427..3ffe5fcbb 100755 --- a/plugin/scripts/stop.mjs +++ b/plugin/scripts/stop.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; -import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-D3R3VQcM.mjs"; +import { t as resolveProject } from "./_project-CXCTta9T.mjs"; +import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-BtSVOGKV.mjs"; //#region src/hooks/stop.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/subagent-start.mjs b/plugin/scripts/subagent-start.mjs index d5c066d0b..5295268cc 100755 --- a/plugin/scripts/subagent-start.mjs +++ b/plugin/scripts/subagent-start.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-DZP2ns6D.mjs"; +import { t as resolveProject } from "./_project-CXCTta9T.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-Bt5vTi-C.mjs"; //#region src/hooks/subagent-start.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/subagent-stop.mjs b/plugin/scripts/subagent-stop.mjs index 498267de0..8f6244809 100755 --- a/plugin/scripts/subagent-stop.mjs +++ b/plugin/scripts/subagent-stop.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-DZP2ns6D.mjs"; +import { t as resolveProject } from "./_project-CXCTta9T.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-Bt5vTi-C.mjs"; //#region src/hooks/subagent-stop.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/task-completed.mjs b/plugin/scripts/task-completed.mjs index 5c1885902..f2c22a82e 100755 --- a/plugin/scripts/task-completed.mjs +++ b/plugin/scripts/task-completed.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-VJwrNfCx.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-DZP2ns6D.mjs"; +import { t as resolveProject } from "./_project-CXCTta9T.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-Bt5vTi-C.mjs"; //#region src/hooks/task-completed.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/tsdown.config.ts b/tsdown.config.ts index f0f8d323b..6cdc44e19 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -46,12 +46,13 @@ const shared = { // Provider plugin caches contain only the published plugin tree. Bundle the // small config parsers used by hooks so those entrypoints never depend on a -// repository or package-level node_modules directory at runtime. +// repository or package-level node_modules directory at runtime. picocolors +// rides in too: config.ts colorizes a provider hint that hook chunks reach. const pluginShared = { ...shared, deps: { ...shared.deps, - alwaysBundle: ["dotenv", "yaml"], + alwaysBundle: ["dotenv", "yaml", "picocolors"], }, }; From 267378bf5eb344110ff5c969be64ffdb34c4ab58 Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 12:59:28 -0500 Subject: [PATCH 7/8] docs(skills): regenerate env reference for AGENTMEMORY_DATA_DIR Phase 2's data-dir resolver added the env var without refreshing the autogen environment block; skills:check drift gate caught it. --- plugin/skills/agentmemory-config/REFERENCE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin/skills/agentmemory-config/REFERENCE.md b/plugin/skills/agentmemory-config/REFERENCE.md index 11ee1daaf..1282d44e2 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). 64 recognized variables: +Configuration is read from the environment and from `~/.agentmemory/.env` (no `export` prefix). 66 recognized variables: - `AGENTMEMORY_ADMIN_SECRET` - `AGENTMEMORY_ADMIN_SECRET_FILE` @@ -14,10 +14,12 @@ Configuration is read from the environment and from `~/.agentmemory/.env` (no `e - `AGENTMEMORY_BUILD_ID` - `AGENTMEMORY_CAPTURE_PROFILE` - `AGENTMEMORY_COMMIT_SHA` +- `AGENTMEMORY_CONSOLIDATION_COOLDOWN_MS` - `AGENTMEMORY_CONTEXT_ACK_SECRET` - `AGENTMEMORY_CONTEXT_ACK_SECRET_FILE` - `AGENTMEMORY_COPILOT_MCP_BLOCK` - `AGENTMEMORY_CWD` +- `AGENTMEMORY_DATA_DIR` - `AGENTMEMORY_DEBUG` - `AGENTMEMORY_DECISION_ROOTS` - `AGENTMEMORY_DROP_STALE_INDEX` From 2658c22e069160e16cbe105308d006fa723ad892 Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 13:02:51 -0500 Subject: [PATCH 8/8] chore(evidence): refresh interface inventory for phase 2 surfaces input_sha256 tracks the registered-function/route/env input set, which the data-dir resolver and hydration wiring changed. --- .aiwg/reports/g-icm-01-interface-inventory.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.aiwg/reports/g-icm-01-interface-inventory.json b/.aiwg/reports/g-icm-01-interface-inventory.json index a7f1ccf1b..c6e4068e5 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": "f4d09d665ce361469d656d33c58fb7c3b9501c10", - "commit_tree_sha": "4030db14ef5d20729acb7ab5d3af6f89d2e88e9a", - "inventory_input_sha256": "a37239e57e45775e39143e539e5e620e5545f4a9c51e49bb4f5f6197ddf3e874" + "commit_sha": "267378bf5eb344110ff5c969be64ffdb34c4ab58", + "commit_tree_sha": "6fa7b440e32dcbc6e466ccce3b2bd09a5fc0a726", + "inventory_input_sha256": "376432da417f43d8ec0d9d29e59d77bab437c6e08066f5b1987ef4653c90b338" }, "public_route_allowlist": [ "GET /agentmemory/livez"