diff --git a/extensions/qa-lab/src/scenario-runtime-api.test.ts b/extensions/qa-lab/src/scenario-runtime-api.test.ts index 3ac4e60aebe..9f704f1eaa5 100644 --- a/extensions/qa-lab/src/scenario-runtime-api.test.ts +++ b/extensions/qa-lab/src/scenario-runtime-api.test.ts @@ -48,6 +48,7 @@ function createDeps(overrides?: Partial): QaScenarioRunti readEffectiveTools: fn, readSkillStatus: fn, readRawQaSessionEntries: fn, + setQaActiveMemorySessionDisabled: fn, seedQaSessionTranscript: fn, runQaCli: fn, extractMediaPathFromText: fn, diff --git a/extensions/qa-lab/src/scenario-runtime-api.ts b/extensions/qa-lab/src/scenario-runtime-api.ts index 533e9ce8725..f91bac8a0a2 100644 --- a/extensions/qa-lab/src/scenario-runtime-api.ts +++ b/extensions/qa-lab/src/scenario-runtime-api.ts @@ -60,6 +60,7 @@ export type QaScenarioRuntimeDeps = { readEffectiveTools: QaScenarioRuntimeFunction; readSkillStatus: QaScenarioRuntimeFunction; readRawQaSessionEntries: QaScenarioRuntimeFunction; + setQaActiveMemorySessionDisabled: QaScenarioRuntimeFunction; seedQaSessionTranscript: QaScenarioRuntimeFunction; runQaCli: QaScenarioRuntimeFunction; extractMediaPathFromText: QaScenarioRuntimeFunction; @@ -145,6 +146,7 @@ type QaScenarioRuntimeApi< readEffectiveTools: TDeps["readEffectiveTools"]; readSkillStatus: TDeps["readSkillStatus"]; readRawQaSessionEntries: TDeps["readRawQaSessionEntries"]; + setQaActiveMemorySessionDisabled: TDeps["setQaActiveMemorySessionDisabled"]; seedQaSessionTranscript: TDeps["seedQaSessionTranscript"]; runQaCli: TDeps["runQaCli"]; extractMediaPathFromText: TDeps["extractMediaPathFromText"]; @@ -245,6 +247,7 @@ export function createQaScenarioRuntimeApi< readEffectiveTools: params.deps.readEffectiveTools, readSkillStatus: params.deps.readSkillStatus, readRawQaSessionEntries: params.deps.readRawQaSessionEntries, + setQaActiveMemorySessionDisabled: params.deps.setQaActiveMemorySessionDisabled, seedQaSessionTranscript: params.deps.seedQaSessionTranscript, runQaCli: params.deps.runQaCli, extractMediaPathFromText: params.deps.extractMediaPathFromText, diff --git a/extensions/qa-lab/src/suite-runtime-agent-session.ts b/extensions/qa-lab/src/suite-runtime-agent-session.ts index 10c963684cd..cb8a7fae09b 100644 --- a/extensions/qa-lab/src/suite-runtime-agent-session.ts +++ b/extensions/qa-lab/src/suite-runtime-agent-session.ts @@ -4,6 +4,7 @@ import { replaceSqliteSessionTranscriptEvents, } from "openclaw/plugin-sdk/agent-harness-runtime"; import { upsertSessionEntry } from "openclaw/plugin-sdk/config-runtime"; +import { createPluginStateKeyedStore } from "openclaw/plugin-sdk/plugin-state-runtime"; import { liveTurnTimeoutMs } from "./suite-runtime-agent-common.js"; import type { QaRawSessionEntry, @@ -11,6 +12,20 @@ import type { QaSuiteRuntimeEnv, } from "./suite-runtime-types.js"; +type ActiveMemorySessionToggleEntry = { + version: 1; + disabled: true; + updatedAt: number; +}; + +function createActiveMemorySessionToggleStore(env: Pick) { + return createPluginStateKeyedStore("active-memory", { + namespace: "session-toggles", + maxEntries: 50_000, + env: env.gateway.runtimeEnv, + }); +} + async function createSession( env: Pick, label: string, @@ -120,6 +135,27 @@ async function seedQaSessionTranscript( return { agentId, sessionId, sessionKey, sessionFile }; } +async function setQaActiveMemorySessionDisabled( + env: Pick, + params: { sessionKey: string; disabled: boolean; now?: number }, +) { + const sessionKey = params.sessionKey.trim(); + if (!sessionKey) { + throw new Error("setQaActiveMemorySessionDisabled requires sessionKey"); + } + const toggleStore = createActiveMemorySessionToggleStore(env); + if (params.disabled) { + await toggleStore.register(sessionKey, { + version: 1, + disabled: true, + updatedAt: params.now ?? Date.now(), + }); + return { sessionKey, disabled: true }; + } + await toggleStore.delete(sessionKey); + return { sessionKey, disabled: false }; +} + async function readEffectiveTools( env: Pick, sessionKey: string, @@ -213,5 +249,6 @@ export { readEffectiveTools, readRawQaSessionEntries, readSkillStatus, + setQaActiveMemorySessionDisabled, seedQaSessionTranscript, }; diff --git a/extensions/qa-lab/src/suite-runtime-agent.ts b/extensions/qa-lab/src/suite-runtime-agent.ts index 6e65cd011b4..11e37d4d2c4 100644 --- a/extensions/qa-lab/src/suite-runtime-agent.ts +++ b/extensions/qa-lab/src/suite-runtime-agent.ts @@ -3,6 +3,7 @@ export { readEffectiveTools, readRawQaSessionEntries, readSkillStatus, + setQaActiveMemorySessionDisabled, seedQaSessionTranscript, } from "./suite-runtime-agent-session.js"; export { diff --git a/extensions/qa-lab/src/suite-runtime-flow.test.ts b/extensions/qa-lab/src/suite-runtime-flow.test.ts index c3bf570bbd5..f17ac29e960 100644 --- a/extensions/qa-lab/src/suite-runtime-flow.test.ts +++ b/extensions/qa-lab/src/suite-runtime-flow.test.ts @@ -22,6 +22,7 @@ const createSession = vi.hoisted(() => vi.fn()); const readEffectiveTools = vi.hoisted(() => vi.fn()); const readSkillStatus = vi.hoisted(() => vi.fn()); const readRawQaSessionEntries = vi.hoisted(() => vi.fn()); +const setQaActiveMemorySessionDisabled = vi.hoisted(() => vi.fn()); const seedQaSessionTranscript = vi.hoisted(() => vi.fn()); const runQaCli = vi.hoisted(() => vi.fn()); const extractMediaPathFromText = vi.hoisted(() => vi.fn()); @@ -88,6 +89,7 @@ vi.mock("./suite-runtime-agent.js", () => ({ readEffectiveTools, readSkillStatus, readRawQaSessionEntries, + setQaActiveMemorySessionDisabled, seedQaSessionTranscript, runQaCli, extractMediaPathFromText, diff --git a/extensions/qa-lab/src/suite-runtime-flow.ts b/extensions/qa-lab/src/suite-runtime-flow.ts index 87b35aa1882..08a425aaefc 100644 --- a/extensions/qa-lab/src/suite-runtime-flow.ts +++ b/extensions/qa-lab/src/suite-runtime-flow.ts @@ -40,6 +40,7 @@ import { resolveGeneratedImagePath, runAgentPrompt, runQaCli, + setQaActiveMemorySessionDisabled, seedQaSessionTranscript, startAgentRun, waitForAgentRun, @@ -163,6 +164,7 @@ function createQaSuiteScenarioDeps(params: QaSuiteScenarioDepsParams) { readEffectiveTools, readSkillStatus, readRawQaSessionEntries, + setQaActiveMemorySessionDisabled, seedQaSessionTranscript, runQaCli, extractMediaPathFromText, diff --git a/qa/scenarios/memory/active-memory-preprompt-recall.md b/qa/scenarios/memory/active-memory-preprompt-recall.md index b924f88219a..150b6b3e1df 100644 --- a/qa/scenarios/memory/active-memory-preprompt-recall.md +++ b/qa/scenarios/memory/active-memory-preprompt-recall.md @@ -85,30 +85,12 @@ steps: - set: activeSessionKey value: expr: "'agent:qa:qa-channel:direct:active-memory-on'" - - set: transcriptRoot - value: - expr: "path.join(env.gateway.tempRoot, 'state', 'plugins', 'active-memory', 'transcripts', 'agents', 'qa', config.transcriptDir)" - - set: toggleStorePath - value: - expr: "path.join(env.gateway.tempRoot, 'state', 'plugins', 'active-memory', 'session-toggles.json')" - - call: fs.rm + - call: setQaActiveMemorySessionDisabled args: - - ref: transcriptRoot - - recursive: true - force: true - - call: fs.rm - args: - - ref: toggleStorePath - - force: true - - call: fs.mkdir - args: - - expr: "path.dirname(toggleStorePath)" - - recursive: true - - call: fs.writeFile - args: - - ref: toggleStorePath - - expr: "`${JSON.stringify({ sessions: { [baselineSessionKey]: { disabled: true, updatedAt: Date.now() } } }, null, 2)}\\n`" - - utf8 + - ref: env + - sessionKey: + ref: baselineSessionKey + disabled: true - set: requestCountBeforeBaseline value: expr: "env.mock ? (await fetchJson(`${env.mock.baseUrl}/debug/requests`)).length : 0" @@ -152,11 +134,12 @@ steps: - set: requestCountBeforeActive value: expr: "env.mock ? (await fetchJson(`${env.mock.baseUrl}/debug/requests`)).length : 0" - - call: fs.writeFile + - call: setQaActiveMemorySessionDisabled args: - - ref: toggleStorePath - - expr: "'{}\\n'" - - utf8 + - ref: env + - sessionKey: + ref: activeSessionKey + disabled: false - set: activeStartIndex value: expr: "state.getSnapshot().messages.length" @@ -189,24 +172,6 @@ steps: expr: "activeLower.includes(normalizeLowercaseStringOrEmpty(config.expectedNeedle))" message: expr: "`active memory reply missed the hidden preference: ${activeOutbound.text}`" - - call: waitForCondition - saveAs: transcriptPath - args: - - lambda: - async: true - expr: "await (async () => { const entries = (await fs.readdir(transcriptRoot).catch(() => [])).filter((entry) => entry.endsWith('.jsonl')).toSorted(); return entries.length > 0 ? path.join(transcriptRoot, entries.at(-1)) : undefined; })()" - - 10000 - - call: fs.readFile - saveAs: transcriptText - args: - - ref: transcriptPath - - utf8 - - assert: - expr: "transcriptText.includes('memory_search')" - message: active memory transcript missing memory_search - - assert: - expr: "transcriptText.includes('memory_get')" - message: active memory transcript missing memory_get - call: waitForCondition saveAs: activeSessionEntry args: @@ -226,5 +191,5 @@ steps: - assert: expr: "mockRequests.some((request) => request.allInputText.includes('You are a memory search agent.') && request.plannedToolName === 'memory_get')" message: expected mock Active Memory memory_get request - detailsExpr: "`${activeOutbound.text}\\n\\ntranscript=${transcriptPath}`" + detailsExpr: "`${activeOutbound.text}\\n\\nactiveSession=${JSON.stringify(activeSessionEntry)}`" ``` diff --git a/src/plugin-state/plugin-state-store.sqlite.ts b/src/plugin-state/plugin-state-store.sqlite.ts index f160bdf307d..49b26ab8c22 100644 --- a/src/plugin-state/plugin-state-store.sqlite.ts +++ b/src/plugin-state/plugin-state-store.sqlite.ts @@ -9,6 +9,7 @@ import { requireNodeSqlite } from "../infra/node-sqlite.js"; import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js"; import { openOpenClawStateDatabase, + type OpenClawStateDatabaseOptions, runOpenClawStateWriteTransaction, } from "../state/openclaw-state-db.js"; import { resolvePluginStateSqlitePath } from "./plugin-state-store.paths.js"; @@ -293,8 +294,10 @@ function sweepExpiredPluginStateEntriesFromDatabase(db: DatabaseSync, now: numbe function openPluginStateDatabase( operation: PluginStateStoreOperation = "open", + options: OpenClawStateDatabaseOptions = {}, ): PluginStateDatabase { - const pathname = resolvePluginStateSqlitePath(process.env); + const env = options.env ?? process.env; + const pathname = resolvePluginStateSqlitePath(env); if (cachedDatabase && cachedDatabase.path === pathname && cachedDatabase.db.isOpen) { return cachedDatabase; } @@ -303,7 +306,7 @@ function openPluginStateDatabase( } try { - const database = openOpenClawStateDatabase(); + const database = openOpenClawStateDatabase(options); cachedDatabase = { db: database.db, path: database.path, @@ -335,15 +338,20 @@ function countRow(row: CountRow | undefined): number { return typeof raw === "bigint" ? Number(raw) : raw; } +function envOptions(env?: NodeJS.ProcessEnv): OpenClawStateDatabaseOptions { + return env ? { env } : {}; +} + function runWriteTransaction( operation: PluginStateStoreOperation, write: (store: PluginStateDatabase) => T, + options: OpenClawStateDatabaseOptions = {}, ): T { return runOpenClawStateWriteTransaction(() => { - const store = openPluginStateDatabase(operation); + const store = openPluginStateDatabase(operation, options); const result = write(store); return result; - }); + }, options); } function enforcePostRegisterLimits(params: { @@ -377,36 +385,41 @@ export function pluginStateRegister(params: { valueJson: string; maxEntries: number; ttlMs?: number; + env?: NodeJS.ProcessEnv; }): void { try { - runWriteTransaction("register", (store) => { - const now = Date.now(); - const expiresAt = params.ttlMs == null ? null : now + params.ttlMs; - deleteExpiredPluginStateNamespaceEntries(store.db, { - pluginId: params.pluginId, - namespace: params.namespace, - now, - }); - upsertPluginStateEntry( - store.db, - bindPluginStateEntry({ + runWriteTransaction( + "register", + (store) => { + const now = Date.now(); + const expiresAt = params.ttlMs == null ? null : now + params.ttlMs; + deleteExpiredPluginStateNamespaceEntries(store.db, { pluginId: params.pluginId, namespace: params.namespace, - key: params.key, - valueJson: params.valueJson, - createdAt: now, - expiresAt, - }), - ); - enforcePostRegisterLimits({ - store, - pluginId: params.pluginId, - namespace: params.namespace, - maxEntries: params.maxEntries, - now, - protectedKey: params.key, - }); - }); + now, + }); + upsertPluginStateEntry( + store.db, + bindPluginStateEntry({ + pluginId: params.pluginId, + namespace: params.namespace, + key: params.key, + valueJson: params.valueJson, + createdAt: now, + expiresAt, + }), + ); + enforcePostRegisterLimits({ + store, + pluginId: params.pluginId, + namespace: params.namespace, + maxEntries: params.maxEntries, + now, + protectedKey: params.key, + }); + }, + envOptions(params.env), + ); } catch (error) { throw wrapPluginStateError( error, @@ -424,40 +437,45 @@ export function pluginStateRegisterIfAbsent(params: { valueJson: string; maxEntries: number; ttlMs?: number; + env?: NodeJS.ProcessEnv; }): boolean { try { - return runWriteTransaction("register", (store) => { - const now = Date.now(); - const expiresAt = params.ttlMs == null ? null : now + params.ttlMs; - deleteExpiredPluginStateNamespaceEntries(store.db, { - pluginId: params.pluginId, - namespace: params.namespace, - now, - }); - const inserted = insertPluginStateEntryIfAbsent( - store.db, - bindPluginStateEntry({ + return runWriteTransaction( + "register", + (store) => { + const now = Date.now(); + const expiresAt = params.ttlMs == null ? null : now + params.ttlMs; + deleteExpiredPluginStateNamespaceEntries(store.db, { pluginId: params.pluginId, namespace: params.namespace, - key: params.key, - valueJson: params.valueJson, - createdAt: now, - expiresAt, - }), - ); - if (!inserted) { - return false; - } - enforcePostRegisterLimits({ - store, - pluginId: params.pluginId, - namespace: params.namespace, - maxEntries: params.maxEntries, - now, - protectedKey: params.key, - }); - return true; - }); + now, + }); + const inserted = insertPluginStateEntryIfAbsent( + store.db, + bindPluginStateEntry({ + pluginId: params.pluginId, + namespace: params.namespace, + key: params.key, + valueJson: params.valueJson, + createdAt: now, + expiresAt, + }), + ); + if (!inserted) { + return false; + } + enforcePostRegisterLimits({ + store, + pluginId: params.pluginId, + namespace: params.namespace, + maxEntries: params.maxEntries, + now, + protectedKey: params.key, + }); + return true; + }, + envOptions(params.env), + ); } catch (error) { throw wrapPluginStateError( error, @@ -472,9 +490,10 @@ export function pluginStateLookup(params: { pluginId: string; namespace: string; key: string; + env?: NodeJS.ProcessEnv; }): unknown { try { - const { db } = openPluginStateDatabase("lookup"); + const { db } = openPluginStateDatabase("lookup", envOptions(params.env)); const row = selectPluginStateEntry(db, { pluginId: params.pluginId, namespace: params.namespace, @@ -496,21 +515,26 @@ export function pluginStateConsume(params: { pluginId: string; namespace: string; key: string; + env?: NodeJS.ProcessEnv; }): unknown { try { - return runWriteTransaction("consume", (store) => { - const row = selectPluginStateEntry(store.db, { - pluginId: params.pluginId, - namespace: params.namespace, - key: params.key, - now: Date.now(), - }); - if (!row) { - return undefined; - } - deletePluginStateEntry(store.db, params); - return parseStoredJson(row.value_json, "consume"); - }); + return runWriteTransaction( + "consume", + (store) => { + const row = selectPluginStateEntry(store.db, { + pluginId: params.pluginId, + namespace: params.namespace, + key: params.key, + now: Date.now(), + }); + if (!row) { + return undefined; + } + deletePluginStateEntry(store.db, params); + return parseStoredJson(row.value_json, "consume"); + }, + envOptions(params.env), + ); } catch (error) { throw wrapPluginStateError( error, @@ -525,11 +549,16 @@ export function pluginStateDelete(params: { pluginId: string; namespace: string; key: string; + env?: NodeJS.ProcessEnv; }): boolean { try { - return runWriteTransaction("delete", ({ db }) => { - return deletePluginStateEntry(db, params) > 0; - }); + return runWriteTransaction( + "delete", + ({ db }) => { + return deletePluginStateEntry(db, params) > 0; + }, + envOptions(params.env), + ); } catch (error) { throw wrapPluginStateError( error, @@ -543,9 +572,10 @@ export function pluginStateDelete(params: { export function pluginStateEntries(params: { pluginId: string; namespace: string; + env?: NodeJS.ProcessEnv; }): PluginStateEntry[] { try { - const { db } = openPluginStateDatabase("entries"); + const { db } = openPluginStateDatabase("entries", envOptions(params.env)); const rows = selectPluginStateEntries(db, { pluginId: params.pluginId, namespace: params.namespace, @@ -562,17 +592,25 @@ export function pluginStateEntries(params: { } } -export function pluginStateClear(params: { pluginId: string; namespace: string }): void { +export function pluginStateClear(params: { + pluginId: string; + namespace: string; + env?: NodeJS.ProcessEnv; +}): void { try { - runWriteTransaction("clear", ({ db }) => { - executeSqliteQuerySync( - db, - getPluginStateKysely(db) - .deleteFrom("plugin_state_entries") - .where("plugin_id", "=", params.pluginId) - .where("namespace", "=", params.namespace), - ); - }); + runWriteTransaction( + "clear", + ({ db }) => { + executeSqliteQuerySync( + db, + getPluginStateKysely(db) + .deleteFrom("plugin_state_entries") + .where("plugin_id", "=", params.pluginId) + .where("namespace", "=", params.namespace), + ); + }, + envOptions(params.env), + ); } catch (error) { throw wrapPluginStateError( error, diff --git a/src/plugin-state/plugin-state-store.test.ts b/src/plugin-state/plugin-state-store.test.ts index 53c4da20ef3..6e9dcf58391 100644 --- a/src/plugin-state/plugin-state-store.test.ts +++ b/src/plugin-state/plugin-state-store.test.ts @@ -79,6 +79,38 @@ describe("plugin state keyed store", () => { }); }); + it("honors explicit store env without mutating process state", async () => { + await withOpenClawTestState( + { label: "plugin-state-explicit-env-a", applyEnv: false }, + async (stateA) => { + await withOpenClawTestState( + { label: "plugin-state-explicit-env-b", applyEnv: false }, + async (stateB) => { + const storeA = createPluginStateKeyedStore<{ owner: string }>("discord", { + namespace: "explicit-env", + maxEntries: 10, + env: stateA.env, + }); + const storeB = createPluginStateKeyedStore<{ owner: string }>("discord", { + namespace: "explicit-env", + maxEntries: 10, + env: stateB.env, + }); + + await storeA.register("shared", { owner: "a" }); + await storeB.register("shared", { owner: "b" }); + + await expect(storeA.lookup("shared")).resolves.toEqual({ owner: "a" }); + await expect(storeB.lookup("shared")).resolves.toEqual({ owner: "b" }); + expect(resolvePluginStateSqlitePath(stateA.env)).not.toBe( + resolvePluginStateSqlitePath(stateB.env), + ); + }, + ); + }, + ); + }); + it("upserts values and refreshes deterministic entry ordering", async () => { await withPluginStateTestState(async () => { vi.useFakeTimers(); diff --git a/src/plugin-state/plugin-state-store.ts b/src/plugin-state/plugin-state-store.ts index 83849eca285..8332294d6b0 100644 --- a/src/plugin-state/plugin-state-store.ts +++ b/src/plugin-state/plugin-state-store.ts @@ -244,6 +244,7 @@ function createKeyedStoreForPluginId( const namespace = validateNamespace(options.namespace); const maxEntries = validateMaxEntries(options.maxEntries); const defaultTtlMs = validateOptionalTtlMs(options.defaultTtlMs); + const env = options.env; assertConsistentOptions(pluginId, namespace, { maxEntries, defaultTtlMs }); return { @@ -255,6 +256,7 @@ function createKeyedStoreForPluginId( key: params.key, valueJson: params.valueJson, maxEntries, + ...(env ? { env } : {}), ...(params.ttlMs != null ? { ttlMs: params.ttlMs } : {}), }); }, @@ -266,26 +268,46 @@ function createKeyedStoreForPluginId( key: params.key, valueJson: params.valueJson, maxEntries, + ...(env ? { env } : {}), ...(params.ttlMs != null ? { ttlMs: params.ttlMs } : {}), }); }, async lookup(key) { const normalizedKey = validateKey(key, "lookup"); - return pluginStateLookup({ pluginId, namespace, key: normalizedKey }) as T | undefined; + return pluginStateLookup({ + pluginId, + namespace, + key: normalizedKey, + ...(env ? { env } : {}), + }) as T | undefined; }, async consume(key) { const normalizedKey = validateKey(key, "consume"); - return pluginStateConsume({ pluginId, namespace, key: normalizedKey }) as T | undefined; + return pluginStateConsume({ + pluginId, + namespace, + key: normalizedKey, + ...(env ? { env } : {}), + }) as T | undefined; }, async delete(key) { const normalizedKey = validateKey(key, "delete"); - return pluginStateDelete({ pluginId, namespace, key: normalizedKey }); + return pluginStateDelete({ + pluginId, + namespace, + key: normalizedKey, + ...(env ? { env } : {}), + }); }, async entries() { - return pluginStateEntries({ pluginId, namespace }) as PluginStateEntry[]; + return pluginStateEntries({ + pluginId, + namespace, + ...(env ? { env } : {}), + }) as PluginStateEntry[]; }, async clear() { - pluginStateClear({ pluginId, namespace }); + pluginStateClear({ pluginId, namespace, ...(env ? { env } : {}) }); }, }; } @@ -297,6 +319,7 @@ function createSyncKeyedStoreForPluginId( const namespace = validateNamespace(options.namespace); const maxEntries = validateMaxEntries(options.maxEntries); const defaultTtlMs = validateOptionalTtlMs(options.defaultTtlMs); + const env = options.env; assertConsistentOptions(pluginId, namespace, { maxEntries, defaultTtlMs }); return { @@ -308,6 +331,7 @@ function createSyncKeyedStoreForPluginId( key: params.key, valueJson: params.valueJson, maxEntries, + ...(env ? { env } : {}), ...(params.ttlMs != null ? { ttlMs: params.ttlMs } : {}), }); }, @@ -319,26 +343,46 @@ function createSyncKeyedStoreForPluginId( key: params.key, valueJson: params.valueJson, maxEntries, + ...(env ? { env } : {}), ...(params.ttlMs != null ? { ttlMs: params.ttlMs } : {}), }); }, lookup(key) { const normalizedKey = validateKey(key, "lookup"); - return pluginStateLookup({ pluginId, namespace, key: normalizedKey }) as T | undefined; + return pluginStateLookup({ + pluginId, + namespace, + key: normalizedKey, + ...(env ? { env } : {}), + }) as T | undefined; }, consume(key) { const normalizedKey = validateKey(key, "consume"); - return pluginStateConsume({ pluginId, namespace, key: normalizedKey }) as T | undefined; + return pluginStateConsume({ + pluginId, + namespace, + key: normalizedKey, + ...(env ? { env } : {}), + }) as T | undefined; }, delete(key) { const normalizedKey = validateKey(key, "delete"); - return pluginStateDelete({ pluginId, namespace, key: normalizedKey }); + return pluginStateDelete({ + pluginId, + namespace, + key: normalizedKey, + ...(env ? { env } : {}), + }); }, entries() { - return pluginStateEntries({ pluginId, namespace }) as PluginStateEntry[]; + return pluginStateEntries({ + pluginId, + namespace, + ...(env ? { env } : {}), + }) as PluginStateEntry[]; }, clear() { - pluginStateClear({ pluginId, namespace }); + pluginStateClear({ pluginId, namespace, ...(env ? { env } : {}) }); }, }; } diff --git a/src/plugin-state/plugin-state-store.types.ts b/src/plugin-state/plugin-state-store.types.ts index 17597eee812..79613aa2148 100644 --- a/src/plugin-state/plugin-state-store.types.ts +++ b/src/plugin-state/plugin-state-store.types.ts @@ -29,6 +29,7 @@ export type OpenKeyedStoreOptions = { namespace: string; maxEntries: number; defaultTtlMs?: number; + env?: NodeJS.ProcessEnv; }; export type PluginStateStoreErrorCode =