Skip to content

Commit 4256584

Browse files
ericjutacodex
andcommitted
Fix retrieval fallback under Docker state pressure
Co-authored-by: Codex <noreply@openai.com>
1 parent 751fc93 commit 4256584

4 files changed

Lines changed: 184 additions & 7 deletions

File tree

src/functions/retrieval-blocks.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,71 @@ export async function collectRetrievalBlocksFromState(
875875
return [...blocks.values()];
876876
}
877877

878+
export async function collectLightweightRetrievalBlocksFromState(
879+
kv: StateKV,
880+
options?: {
881+
project?: string;
882+
sessionId?: string;
883+
},
884+
): Promise<RetrievalBlock[]> {
885+
const memories = await kv.list<Memory>(KV.memories).catch(() => []);
886+
const semantic = await kv.list<SemanticMemory>(KV.semantic).catch(() => []);
887+
const procedural = await kv.list<ProceduralMemory>(KV.procedural).catch(() => []);
888+
const summaries = await kv.list<SessionSummary>(KV.summaries).catch(() => []);
889+
const handoffs = await kv.list<HandoffPacket>(KV.handoffPackets).catch(() => []);
890+
const branchOverlays = await kv.list<BranchOverlay>(KV.branchOverlays).catch(() => []);
891+
const guardrails = await kv.list<GuardrailMemory>(KV.guardrails).catch(() => []);
892+
const decisions = await kv.list<DecisionMemory>(KV.decisions).catch(() => []);
893+
const dossiers = await kv.list<ComponentDossier>(KV.componentDossiers).catch(() => []);
894+
const profiles = await kv.list<ProjectProfile>(KV.profiles).catch(() => []);
895+
const observations = options?.sessionId
896+
? await kv.list<CompressedObservation>(KV.observations(options.sessionId)).catch(() => [])
897+
: [];
898+
899+
const blocks = new Map<string, RetrievalBlock>();
900+
const put = (block: RetrievalBlock | null) => {
901+
if (!block) return;
902+
if (
903+
options?.project &&
904+
block.project !== options.project &&
905+
block.project !== "global"
906+
) {
907+
return;
908+
}
909+
blocks.set(block.id, block);
910+
};
911+
912+
for (const memory of memories.filter((item) => item.isLatest)) {
913+
put(buildMemoryRetrievalBlock(memory));
914+
}
915+
for (const item of semantic) put(buildSemanticRetrievalBlock(item));
916+
for (const item of procedural) put(buildProceduralRetrievalBlock(item));
917+
for (const summary of summaries) put(buildSessionSummaryRetrievalBlock(summary));
918+
for (const packet of handoffs) put(buildHandoffRetrievalBlock(packet));
919+
for (const overlay of branchOverlays.filter((item) => item.status === "active")) {
920+
put(buildBranchOverlayRetrievalBlock(overlay));
921+
}
922+
for (const guardrail of guardrails.filter((item) => item.status === "active")) {
923+
put(buildGuardrailRetrievalBlock(guardrail));
924+
}
925+
for (const decision of decisions.filter((item) => item.status === "active")) {
926+
put(buildDecisionRetrievalBlock(decision));
927+
}
928+
for (const dossier of dossiers) put(buildDossierRetrievalBlock(dossier));
929+
for (const profile of profiles) put(buildProfileRetrievalBlock(profile));
930+
for (const observation of observations) {
931+
if (
932+
observation.importance >= 6 ||
933+
observation.type === "error" ||
934+
observation.type === "decision"
935+
) {
936+
put(buildObservationRetrievalBlock(observation, options?.project || "global"));
937+
}
938+
}
939+
940+
return [...blocks.values()];
941+
}
942+
878943
export async function refreshRetrievalBlocksFromState(
879944
kv: StateKV,
880945
): Promise<number> {

src/functions/retrieval-engine.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,22 @@ import { SearchIndex } from "../state/search-index.js";
1919
import { GraphRetrieval } from "./graph-retrieval.js";
2020
import { extractEntitiesFromQuery } from "./query-expansion.js";
2121
import {
22+
collectLightweightRetrievalBlocksFromState,
2223
collectRetrievalBlocksFromState,
2324
} from "./retrieval-blocks.js";
2425

2526
const QUERY_EMBEDDING_CACHE_MAX_ENTRIES = 128;
2627
const QUERY_EMBEDDING_CACHE_TTL_MS = 5 * 60_000;
2728
const QUERY_EMBEDDING_TIMEOUT_MS = 2500;
29+
const RETRIEVAL_BLOCK_SCOPE_COOLDOWN_MS = 60_000;
2830

2931
type CachedQueryEmbedding = {
3032
embedding: Float32Array;
3133
cachedAt: number;
3234
};
3335

3436
const queryEmbeddingCache = new Map<string, CachedQueryEmbedding>();
37+
let retrievalBlockScopeUnavailableUntil = 0;
3538

3639
function estimateTokens(text: string): number {
3740
return Math.ceil(text.length / 3);
@@ -301,12 +304,32 @@ export async function retrieveRelevantBlocks(
301304
blocks.some(
302305
(block) => block.project === query.project || block.project === "global",
303306
);
304-
let allBlocks = await kv.list<RetrievalBlock>(KV.retrievalBlocks).catch(() => []);
307+
let allBlocks: RetrievalBlock[] = [];
308+
let usingStateFallbackBlocks = false;
309+
const canReadStoredBlocks = Date.now() >= retrievalBlockScopeUnavailableUntil;
310+
if (canReadStoredBlocks) {
311+
try {
312+
allBlocks = await kv.list<RetrievalBlock>(KV.retrievalBlocks);
313+
retrievalBlockScopeUnavailableUntil = 0;
314+
} catch {
315+
retrievalBlockScopeUnavailableUntil = Date.now() + RETRIEVAL_BLOCK_SCOPE_COOLDOWN_MS;
316+
}
317+
}
305318
const shouldRefreshBlocks =
306319
allBlocks.length === 0 ||
307320
(Boolean(query.project) && !hasProjectCoverage(allBlocks));
308321
if (shouldRefreshBlocks) {
309-
allBlocks = await collectRetrievalBlocksFromState(kv).catch(() => []);
322+
const lightweightBlocks = await collectLightweightRetrievalBlocksFromState(kv, {
323+
project: query.project,
324+
sessionId: query.sessionId,
325+
}).catch(() => []);
326+
if (lightweightBlocks.length > 0) {
327+
allBlocks = lightweightBlocks;
328+
usingStateFallbackBlocks = true;
329+
} else {
330+
allBlocks = await collectRetrievalBlocksFromState(kv).catch(() => []);
331+
usingStateFallbackBlocks = allBlocks.length > 0;
332+
}
310333
}
311334
const blocks = allBlocks
312335
.filter((block) =>
@@ -344,8 +367,10 @@ export async function retrieveRelevantBlocks(
344367
]).join(" ");
345368
const lexicalScores = new Map<string, number>();
346369
if (lexicalQuery.trim()) {
347-
let lexicalResults = getRetrievalSearchIndex().searchDocuments(lexicalQuery, 120);
348-
if (blocks.length > 0 && lexicalResults.length === 0) {
370+
let lexicalResults = usingStateFallbackBlocks
371+
? []
372+
: getRetrievalSearchIndex().searchDocuments(lexicalQuery, 120);
373+
if (blocks.length > 0 && (usingStateFallbackBlocks || lexicalResults.length === 0)) {
349374
const fallbackIndex = new SearchIndex();
350375
for (const block of blocks) {
351376
fallbackIndex.addDocument(

src/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,12 @@ async function main() {
161161
});
162162

163163
const kv = new StateKV(sdk);
164+
const persistenceKv = new StateKV(sdk, {
165+
timeoutMs: Math.max(
166+
Number.parseInt(getEnvVar("STATE_KV_TIMEOUT_MS") || "5000", 10) || 5000,
167+
20000,
168+
),
169+
});
164170
const secret = getEnvVar("AGENTMEMORY_SECRET");
165171
const metricsStore = new MetricsStore(kv);
166172
const dedupMap = new DedupMap();
@@ -341,9 +347,9 @@ async function main() {
341347
};
342348
});
343349

344-
indexPersistence = new IndexPersistence(kv, bm25Index, vectorIndex);
350+
indexPersistence = new IndexPersistence(persistenceKv, bm25Index, vectorIndex);
345351
retrievalIndexPersistence = new IndexPersistence(
346-
kv,
352+
persistenceKv,
347353
getRetrievalSearchIndex(),
348354
retrievalVectorIndex,
349355
KV.retrievalBlockIndex,

test/retrieval-engine.test.ts

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
22

3-
const { collectRetrievalBlocksFromStateMock } = vi.hoisted(() => ({
3+
const {
4+
collectRetrievalBlocksFromStateMock,
5+
collectLightweightRetrievalBlocksFromStateMock,
6+
} = vi.hoisted(() => ({
47
collectRetrievalBlocksFromStateMock: vi.fn(async () => []),
8+
collectLightweightRetrievalBlocksFromStateMock: vi.fn(async () => []),
59
}));
610

711
vi.mock("../src/functions/retrieval-blocks.js", async () => {
@@ -11,6 +15,8 @@ vi.mock("../src/functions/retrieval-blocks.js", async () => {
1115
return {
1216
...actual,
1317
collectRetrievalBlocksFromState: collectRetrievalBlocksFromStateMock,
18+
collectLightweightRetrievalBlocksFromState:
19+
collectLightweightRetrievalBlocksFromStateMock,
1420
};
1521
});
1622

@@ -27,6 +33,7 @@ import type { RetrievalBlock } from "../src/types.js";
2733
describe("retrieveRelevantBlocks", () => {
2834
beforeEach(() => {
2935
collectRetrievalBlocksFromStateMock.mockClear();
36+
collectLightweightRetrievalBlocksFromStateMock.mockClear();
3037
getRetrievalSearchIndex().clear();
3138
configureRetrievalBlockIndexingRuntime({
3239
embeddingProvider: null,
@@ -77,4 +84,78 @@ describe("retrieveRelevantBlocks", () => {
7784
expect(result.searchResults).toHaveLength(1);
7885
expect(result.searchResults[0]?.block.id).toBe(block.id);
7986
});
87+
88+
it("falls back to lightweight state collection when the retrieval block scope is unavailable", async () => {
89+
const kv = mockKV();
90+
const staleBlock: RetrievalBlock = {
91+
id: "rblk_stale",
92+
sourceType: "semantic_memory",
93+
sourceId: "sem_stale",
94+
project: "global",
95+
scope: "global",
96+
freshnessLane: "cold",
97+
canonicalText: "Unrelated stale retrieval memory",
98+
title: "Stale memory",
99+
files: [],
100+
concepts: ["stale"],
101+
entities: ["stale"],
102+
sourceObservationIds: [],
103+
hadFailure: false,
104+
hadDecision: false,
105+
hadAssistantConclusion: true,
106+
isResumeArtifact: false,
107+
importance: 7,
108+
createdAt: "2026-01-01T00:00:00Z",
109+
updatedAt: "2026-01-01T00:00:00Z",
110+
eventAt: "2026-01-01T00:00:00Z",
111+
};
112+
const block: RetrievalBlock = {
113+
id: "rblk_theta",
114+
sourceType: "memory",
115+
sourceId: "mem_theta",
116+
project: "global",
117+
scope: "global",
118+
freshnessLane: "cold",
119+
canonicalText: "Codex durable retrieval probe theta retrieval memory",
120+
title: "Theta memory",
121+
files: ["/tmp/codex-theta.txt"],
122+
concepts: ["theta sentinel"],
123+
entities: ["theta", "sentinel"],
124+
sourceObservationIds: [],
125+
hadFailure: false,
126+
hadDecision: false,
127+
hadAssistantConclusion: true,
128+
isResumeArtifact: false,
129+
importance: 7,
130+
createdAt: "2026-01-01T00:00:00Z",
131+
updatedAt: "2026-01-01T00:00:00Z",
132+
eventAt: "2026-01-01T00:00:00Z",
133+
};
134+
135+
getRetrievalSearchIndex().addDocument(
136+
staleBlock.id,
137+
staleBlock.project,
138+
buildRetrievalBlockLexicalText(staleBlock),
139+
);
140+
141+
const listError = new Error("retrieval block scope timeout");
142+
const rawList = kv.list.bind(kv);
143+
kv.list = (async <T>(scope: string): Promise<T[]> => {
144+
if (scope === KV.retrievalBlocks) throw listError;
145+
return rawList(scope);
146+
}) as typeof kv.list;
147+
148+
collectLightweightRetrievalBlocksFromStateMock.mockResolvedValue([block]);
149+
150+
const result = await retrieveRelevantBlocks(kv as never, {
151+
query: "theta sentinel",
152+
budget: 300,
153+
purpose: "smart-search",
154+
});
155+
156+
expect(collectLightweightRetrievalBlocksFromStateMock).toHaveBeenCalled();
157+
expect(collectRetrievalBlocksFromStateMock).not.toHaveBeenCalled();
158+
expect(result.searchResults).toHaveLength(1);
159+
expect(result.searchResults[0]?.block.id).toBe(block.id);
160+
});
80161
});

0 commit comments

Comments
 (0)