Skip to content

Commit bd9fe01

Browse files
marthakellyclaude
andcommitted
fix(core): guard thread-store auto-unregister against initial empty agents
CopilotKitCore subscribes to onAgentsChanged and unregisters thread stores for any agentId not in the new agents map. For published cores, core.agents is asynchronously populated, so the FIRST onAgentsChanged({ agents: {} }) notification fires BEFORE published agents are merged in. Without a guard, that empty notification rips out a thread store that a consumer (e.g. useThreads) just registered. Track previousAgentIds and only unregister an agentId that was present in the previous snapshot AND missing from the new one. The first empty-agents notification (where the agentId was never previously present) becomes a no-op. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent daf52a2 commit bd9fe01

1 file changed

Lines changed: 30 additions & 14 deletions

File tree

packages/core/src/core/core.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
import {
2-
AbstractAgent,
3-
type AgentSubscriber,
4-
Context,
5-
State,
6-
} from "@ag-ui/client";
1+
import type { AbstractAgent, Context, State } from "@ag-ui/client";
2+
import { type AgentSubscriber } from "@ag-ui/client";
73
import { Throttler } from "@tanstack/pacer";
8-
import {
4+
import type {
95
FrontendTool,
106
SuggestionsConfig,
117
Suggestion,
@@ -14,21 +10,22 @@ import {
1410
RuntimeLicenseStatus,
1511
IntelligenceRuntimeInfo,
1612
} from "../types";
17-
import { AgentRegistry, CopilotKitCoreAddAgentParams } from "./agent-registry";
13+
import type { CopilotKitCoreAddAgentParams } from "./agent-registry";
14+
import { AgentRegistry } from "./agent-registry";
1815
import { ContextStore } from "./context-store";
1916
import { SuggestionEngine } from "./suggestion-engine";
20-
import {
21-
RunHandler,
17+
import type {
2218
CopilotKitCoreRunAgentParams,
2319
CopilotKitCoreConnectAgentParams,
2420
CopilotKitCoreGetToolParams,
2521
CopilotKitCoreRunToolParams,
2622
CopilotKitCoreRunToolResult,
2723
} from "./run-handler";
28-
import { DebugConfig } from "@copilotkit/shared";
24+
import { RunHandler } from "./run-handler";
25+
import type { DebugConfig } from "@copilotkit/shared";
2926
import { StateManager } from "./state-manager";
3027
import { ThreadStoreRegistry } from "./thread-store-registry";
31-
import { type ɵThreadStore } from "../threads";
28+
import type { ɵThreadStore } from "../threads";
3229

3330
/** Configuration options for `CopilotKitCore`. */
3431
export interface CopilotKitCoreConfig {
@@ -349,6 +346,13 @@ export class CopilotKitCore {
349346
private runHandler: RunHandler;
350347
private stateManager: StateManager;
351348
private threadStoreRegistry: ThreadStoreRegistry;
349+
/**
350+
* Tracks the agent IDs from the most recent `onAgentsChanged` notification.
351+
* Used to gate thread-store auto-unregister so the FIRST empty-agents
352+
* notification (before published agents are merged in) does not rip out a
353+
* store that was registered prior to that initial notification.
354+
*/
355+
private previousAgentIds: Set<string> = new Set();
352356

353357
constructor({
354358
runtimeUrl,
@@ -392,13 +396,25 @@ export class CopilotKitCore {
392396
}
393397
});
394398

395-
// Unregister thread stores for agents that are no longer present
399+
// Unregister thread stores for agents that have been removed.
400+
//
401+
// Critically, only unregister an agentId that was present in the
402+
// PREVIOUS agents snapshot AND is missing from the new one. Without
403+
// the "previously had" guard, the FIRST `onAgentsChanged({ agents: {} })`
404+
// delivered to a freshly-published core would tear out a thread store
405+
// that a consumer (e.g. useThreads) just registered — `core.agents`
406+
// is asynchronously populated and the empty-map notification fires
407+
// before the published agents are merged in.
396408
const currentAgentIds = new Set(Object.keys(agents));
397409
for (const agentId of Object.keys(this.threadStoreRegistry.getAll())) {
398-
if (!currentAgentIds.has(agentId)) {
410+
if (
411+
this.previousAgentIds.has(agentId) &&
412+
!currentAgentIds.has(agentId)
413+
) {
399414
this.threadStoreRegistry.unregister(agentId);
400415
}
401416
}
417+
this.previousAgentIds = currentAgentIds;
402418
},
403419
});
404420
}

0 commit comments

Comments
 (0)