authorgravatar for 123344143+AksharP5@users.noreply.github.comAkshar Patel <123344143+AksharP5@users.noreply.github.com> 2026-08-15 08:02:44-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-15 12:02:44+00:00
log135dc156e3f600a79a415b06e4c234677aa318d0
treec46387d27192b98db381ebb3480f4b20a6255d89
parentd79f975d0b39805c443e97accfcd4aa60786f13f
signature Signed by PGP key B5690EEEBB952194

fix(codex): keep background memory out of chats (#5468)


3 files changed, 226 insertions(+), 1 deletions(-)

apps/server/src/provider/Layers/CodexCollabRuntime.integration.test.ts+35-1
...@@ -24,6 +24,7 @@ import { makeCodexSessionRuntime } from "./CodexSessionRuntime.ts";...@@ -24,6 +24,7 @@ import { makeCodexSessionRuntime } from "./CodexSessionRuntime.ts";
2424
25const ROOT = wireFixture.rootThreadId;25const ROOT = wireFixture.rootThreadId;
26const [CHILD_A, CHILD_B] = wireFixture.childThreadIds as [string, string];26const [CHILD_A, CHILD_B] = wireFixture.childThreadIds as [string, string];
27const MEMORY = "memory-consolidation-thread";
2728
28/**29/**
29 * The captured sequence, extended with the shapes the live capture didn't30 * The captured sequence, extended with the shapes the live capture didn't
...@@ -174,15 +175,44 @@ describe("CodexSessionRuntime collab integration", () => {...@@ -174,15 +175,44 @@ describe("CodexSessionRuntime collab integration", () => {
174 const turnStartedB = byIndex.find((entry) => isTurnStarted(entry, CHILD_B));175 const turnStartedB = byIndex.find((entry) => isTurnStarted(entry, CHILD_B));
175 const registrationA = byIndex.find((entry) => isRegistration(entry, CHILD_A));176 const registrationA = byIndex.find((entry) => isRegistration(entry, CHILD_A));
176 const registrationB = byIndex.find((entry) => isRegistration(entry, CHILD_B));177 const registrationB = byIndex.find((entry) => isRegistration(entry, CHILD_B));
178 const rootThreadStarted = byIndex.find((entry) => entry.method === "thread/started");
177 assert.isDefined(turnStartedA);179 assert.isDefined(turnStartedA);
178 assert.isDefined(turnStartedB);180 assert.isDefined(turnStartedB);
179 assert.isDefined(registrationA);181 assert.isDefined(registrationA);
180 assert.isDefined(registrationB);182 assert.isDefined(registrationB);
183 assert.isDefined(rootThreadStarted);
184 const memoryThreadStarted = {
185 ...rootThreadStarted,
186 params: {
187 thread: {
188 ...rootThreadStarted.params.thread,
189 id: MEMORY,
190 sessionId: MEMORY,
191 source: "unknown",
192 threadSource: "memory_consolidation",
193 },
194 },
195 };
196 const memoryTurnStarted = {
197 ...turnStartedA,
198 params: {
199 ...turnStartedA.params,
200 threadId: MEMORY,
201 turn: { ...turnStartedA.params.turn, id: "memory-consolidation-turn" },
202 },
203 };
181 const script = {204 const script = {
182 rootThreadId: ROOT,205 rootThreadId: ROOT,
183 holdTurnOpen: true,206 holdTurnOpen: true,
184 hangInterruptFor: CHILD_A,207 hangInterruptFor: CHILD_A,
185 notifications: [turnStartedA, registrationA, registrationB, turnStartedB],208 notifications: [
209 turnStartedA,
210 registrationA,
211 memoryThreadStarted,
212 memoryTurnStarted,
213 registrationB,
214 turnStartedB,
215 ],
186 };216 };
187 // @effect-diagnostics-next-line preferSchemaOverJson:off217 // @effect-diagnostics-next-line preferSchemaOverJson:off
188 NodeFS.writeFileSync(scriptPath, JSON.stringify(script), "utf8");218 NodeFS.writeFileSync(scriptPath, JSON.stringify(script), "utf8");
...@@ -240,6 +270,10 @@ describe("CodexSessionRuntime collab integration", () => {...@@ -240,6 +270,10 @@ describe("CodexSessionRuntime collab integration", () => {
240 "pre-registration child A must still receive the interrupt RPC",270 "pre-registration child A must still receive the interrupt RPC",
241 );271 );
242 assert.isTrue(interruptedThreads.has(CHILD_B), "registered child B must be interrupted");272 assert.isTrue(interruptedThreads.has(CHILD_B), "registered child B must be interrupted");
273 assert.isTrue(
274 interruptedThreads.has(MEMORY),
275 "memory consolidation must be interrupted without appearing in chat",
276 );
243 assert.isTrue(interruptedThreads.has(ROOT), "parent turn must be interrupted last");277 assert.isTrue(interruptedThreads.has(ROOT), "parent turn must be interrupted last");
244278
245 yield* runtime.close;279 yield* runtime.close;
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts+140
...@@ -7,6 +7,7 @@ import { describe } from "vite-plus/test";...@@ -7,6 +7,7 @@ import { describe } from "vite-plus/test";
7import { DEFAULT_MODEL, ThreadId } from "@t3tools/contracts";7import { DEFAULT_MODEL, ThreadId } from "@t3tools/contracts";
8import * as CodexErrors from "effect-codex-app-server/errors";8import * as CodexErrors from "effect-codex-app-server/errors";
9import * as CodexRpc from "effect-codex-app-server/rpc";9import * as CodexRpc from "effect-codex-app-server/rpc";
10import * as EffectCodexSchema from "effect-codex-app-server/schema";
1011
11import {12import {
12 buildCodexDeveloperInstructions,13 buildCodexDeveloperInstructions,
...@@ -18,6 +19,7 @@ import {...@@ -18,6 +19,7 @@ import {
18 buildTurnStartParams,19 buildTurnStartParams,
19 hasConfiguredMcpServer,20 hasConfiguredMcpServer,
20 isRecoverableThreadResumeError,21 isRecoverableThreadResumeError,
22 makeMemoryConsolidationNotificationFilter,
21 openCodexThread,23 openCodexThread,
22} from "./CodexSessionRuntime.ts";24} from "./CodexSessionRuntime.ts";
23const isCodexAppServerRequestError = Schema.is(CodexErrors.CodexAppServerRequestError);25const isCodexAppServerRequestError = Schema.is(CodexErrors.CodexAppServerRequestError);
...@@ -318,6 +320,144 @@ describe("hasConfiguredMcpServer", () => {...@@ -318,6 +320,144 @@ describe("hasConfiguredMcpServer", () => {
318 });320 });
319});321});
320322
323function makeThreadStartedNotification(
324 threadId: string,
325 source: EffectCodexSchema.V2ThreadStartedNotification["thread"]["source"],
326 threadSource?: string,
327) {
328 return {
329 method: "thread/started" as const,
330 params: {
331 thread: {
332 cliVersion: "0.0.0",
333 createdAt: 0,
334 cwd: "/tmp/project",
335 ephemeral: true,
336 id: threadId,
337 modelProvider: "openai",
338 preview: "",
339 sessionId: threadId,
340 source,
341 status: { type: "idle" as const },
342 ...(threadSource ? { threadSource } : {}),
343 turns: [],
344 updatedAt: 0,
345 },
346 },
347 };
348}
349
350describe("makeMemoryConsolidationNotificationFilter", () => {
351 it("suppresses memory consolidation without hiding other Codex subagents", () => {
352 const shouldSuppress = makeMemoryConsolidationNotificationFilter();
353
354 NodeAssert.equal(
355 shouldSuppress(
356 makeThreadStartedNotification("memory-thread", "unknown", "memory_consolidation"),
357 ),
358 true,
359 );
360 NodeAssert.equal(
361 shouldSuppress({
362 method: "item/agentMessage/delta",
363 params: {
364 delta: "internal memory update",
365 itemId: "memory-message",
366 threadId: "memory-thread",
367 turnId: "memory-turn",
368 },
369 }),
370 true,
371 );
372 NodeAssert.equal(
373 shouldSuppress({
374 method: "serverRequest/resolved",
375 params: {
376 requestId: "memory-approval",
377 threadId: "memory-thread",
378 },
379 }),
380 false,
381 );
382 NodeAssert.equal(
383 shouldSuppress({
384 method: "warning",
385 params: {
386 message: "internal warning",
387 threadId: "memory-thread",
388 },
389 }),
390 true,
391 );
392 NodeAssert.equal(
393 shouldSuppress({
394 method: "item/agentMessage/delta",
395 params: {
396 delta: "normal reply",
397 itemId: "root-message",
398 threadId: "root-thread",
399 turnId: "root-turn",
400 },
401 }),
402 false,
403 );
404
405 NodeAssert.equal(
406 shouldSuppress(
407 makeThreadStartedNotification("legacy-memory-thread", {
408 subAgent: "memory_consolidation",
409 }),
410 ),
411 true,
412 );
413
414 for (const source of [
415 { subAgent: "review" as const },
416 { subAgent: "compact" as const },
417 {
418 subAgent: {
419 thread_spawn: {
420 depth: 1,
421 parent_thread_id: "root-thread",
422 },
423 },
424 },
425 ]) {
426 NodeAssert.equal(
427 shouldSuppress(makeThreadStartedNotification("visible-subagent", source)),
428 false,
429 );
430 }
431 });
432
433 it("forgets memory consolidation threads after they close", () => {
434 const shouldSuppress = makeMemoryConsolidationNotificationFilter();
435 shouldSuppress(
436 makeThreadStartedNotification("memory-thread", "unknown", "memory_consolidation"),
437 );
438
439 NodeAssert.equal(
440 shouldSuppress({
441 method: "thread/closed",
442 params: { threadId: "memory-thread" },
443 }),
444 true,
445 );
446 NodeAssert.equal(
447 shouldSuppress({
448 method: "item/agentMessage/delta",
449 params: {
450 delta: "later message",
451 itemId: "later-message",
452 threadId: "memory-thread",
453 turnId: "later-turn",
454 },
455 }),
456 false,
457 );
458 });
459});
460
321describe("codexSessionAppServerArgs", () => {461describe("codexSessionAppServerArgs", () => {
322 it("keeps the app-server subcommand when explicit args are provided", () => {462 it("keeps the app-server subcommand when explicit args are provided", () => {
323 NodeAssert.deepStrictEqual(codexSessionAppServerArgs(["-c", "model=gpt-5"], undefined), [463 NodeAssert.deepStrictEqual(codexSessionAppServerArgs(["-c", "model=gpt-5"], undefined), [
apps/server/src/provider/Layers/CodexSessionRuntime.ts+51
...@@ -541,6 +541,49 @@ function readNotificationThreadId(notification: CodexServerNotification): string...@@ -541,6 +541,49 @@ function readNotificationThreadId(notification: CodexServerNotification): string
541 }541 }
542}542}
543543
544export function makeMemoryConsolidationNotificationFilter(): (
545 notification: CodexServerNotification,
546) => boolean {
547 const threadIds = new Set<string>();
548
549 return (notification) => {
550 if (notification.method === "thread/started") {
551 const thread = notification.params.thread;
552 const source = thread.source;
553 if (
554 thread.threadSource === "memory_consolidation" ||
555 (typeof source === "object" &&
556 source !== null &&
557 "subAgent" in source &&
558 source.subAgent === "memory_consolidation")
559 ) {
560 threadIds.add(thread.id);
561 return true;
562 }
563 }
564
565 const params = notification.params;
566 const threadId =
567 notification.method === "thread/started"
568 ? notification.params.thread.id
569 : "threadId" in params && typeof params.threadId === "string"
570 ? params.threadId
571 : undefined;
572 if (!threadId || !threadIds.has(threadId)) {
573 return false;
574 }
575
576 if (notification.method === "serverRequest/resolved") {
577 return false;
578 }
579
580 if (notification.method === "thread/closed") {
581 threadIds.delete(threadId);
582 }
583 return true;
584 };
585}
586
544function readRouteFields(notification: CodexServerNotification): {587function readRouteFields(notification: CodexServerNotification): {
545 readonly turnId: TurnId | undefined;588 readonly turnId: TurnId | undefined;
546 readonly itemId: ProviderItemId | undefined;589 readonly itemId: ProviderItemId | undefined;
...@@ -857,6 +900,7 @@ export const makeCodexSessionRuntime = (...@@ -857,6 +900,7 @@ export const makeCodexSessionRuntime = (
857 const collabChildAgentsRef = yield* Ref.make(new Map<string, CollabChildAgentState>());900 const collabChildAgentsRef = yield* Ref.make(new Map<string, CollabChildAgentState>());
858 /** Child provider-thread id → its currently running provider turn id. */901 /** Child provider-thread id → its currently running provider turn id. */
859 const collabChildLiveTurnsRef = yield* Ref.make(new Map<string, string>());902 const collabChildLiveTurnsRef = yield* Ref.make(new Map<string, string>());
903 const suppressMemoryConsolidationNotification = makeMemoryConsolidationNotificationFilter();
860 const closedRef = yield* Ref.make(false);904 const closedRef = yield* Ref.make(false);
861905
862 // `~` is not shell-expanded when env vars are set via906 // `~` is not shell-expanded when env vars are set via
...@@ -1249,6 +1293,9 @@ export const makeCodexSessionRuntime = (...@@ -1249,6 +1293,9 @@ export const makeCodexSessionRuntime = (
12491293
1250 const handleRawNotification = (notification: CodexServerNotification) =>1294 const handleRawNotification = (notification: CodexServerNotification) =>
1251 Effect.gen(function* () {1295 Effect.gen(function* () {
1296 const isMemoryConsolidationNotification =
1297 suppressMemoryConsolidationNotification(notification);
1298
1252 const payload = notification.params;1299 const payload = notification.params;
1253 const route = readRouteFields(notification);1300 const route = readRouteFields(notification);
1254 const collabReceiverTurns = yield* Ref.get(collabReceiverTurnsRef);1301 const collabReceiverTurns = yield* Ref.get(collabReceiverTurnsRef);
...@@ -1326,6 +1373,10 @@ export const makeCodexSessionRuntime = (...@@ -1326,6 +1373,10 @@ export const makeCodexSessionRuntime = (
1326 return;1373 return;
1327 }1374 }
13281375
1376 if (isMemoryConsolidationNotification) {
1377 return;
1378 }
1379
1329 let requestId: ApprovalRequestId | undefined;1380 let requestId: ApprovalRequestId | undefined;
1330 let requestKind: ProviderRequestKind | undefined;1381 let requestKind: ProviderRequestKind | undefined;
1331 let turnId = childParentTurnId ?? route.turnId;1382 let turnId = childParentTurnId ?? route.turnId;