@librechat/agents 3.4.0 → 3.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +165 -57
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +81 -33
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs +14 -0
- package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs.map +1 -0
- package/dist/cjs/graphs/createGraph.cjs +15 -0
- package/dist/cjs/graphs/createGraph.cjs.map +1 -0
- package/dist/cjs/graphs/index.cjs +1 -0
- package/dist/cjs/hooks/HookRegistry.cjs +83 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +1 -1
- package/dist/cjs/hooks/executeHooks.cjs +60 -14
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
- package/dist/cjs/hooks/index.cjs.map +1 -1
- package/dist/cjs/hooks/types.cjs +2 -0
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +3 -3
- package/dist/cjs/llm/invoke.cjs +2 -2
- package/dist/cjs/main.cjs +34 -24
- package/dist/cjs/messages/format.cjs +40 -23
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/run.cjs +130 -53
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +37 -2
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/session/JsonlSessionStore.cjs +2 -2
- package/dist/cjs/stream.cjs +1 -1
- package/dist/cjs/summarization/node.cjs +6 -3
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
- package/dist/cjs/tools/SubagentTool.cjs +4 -4
- package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +321 -95
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +1 -1
- package/dist/cjs/tools/eagerEventExecution.cjs +1 -0
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +3 -3
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +2 -2
- package/dist/cjs/tools/search/rerankers.cjs +154 -1
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +4 -1
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +601 -0
- package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1114 -233
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs +177 -0
- package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -0
- package/dist/cjs/tools/subagent/childGraphConfig.cjs +278 -0
- package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -0
- package/dist/cjs/tools/subagent/index.cjs +1 -0
- package/dist/cjs/tools/subagent/runtimeLimits.cjs +5 -0
- package/dist/cjs/tools/subagent/runtimeLimits.cjs.map +1 -0
- package/dist/cjs/tools/toolOutputReferences.cjs +20 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +2 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +165 -57
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +81 -33
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/graphs/applyGraphRuntimeConfig.mjs +14 -0
- package/dist/esm/graphs/applyGraphRuntimeConfig.mjs.map +1 -0
- package/dist/esm/graphs/createGraph.mjs +15 -0
- package/dist/esm/graphs/createGraph.mjs.map +1 -0
- package/dist/esm/graphs/index.mjs +1 -0
- package/dist/esm/hooks/HookRegistry.mjs +83 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs +1 -1
- package/dist/esm/hooks/executeHooks.mjs +60 -14
- package/dist/esm/hooks/executeHooks.mjs.map +1 -1
- package/dist/esm/hooks/index.mjs.map +1 -1
- package/dist/esm/hooks/types.mjs +2 -1
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +2 -2
- package/dist/esm/main.mjs +21 -19
- package/dist/esm/messages/format.mjs +40 -23
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/run.mjs +131 -54
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +37 -2
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/session/JsonlSessionStore.mjs +2 -2
- package/dist/esm/stream.mjs +1 -1
- package/dist/esm/summarization/node.mjs +6 -3
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
- package/dist/esm/tools/SubagentTool.mjs +4 -4
- package/dist/esm/tools/SubagentTool.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +322 -96
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +1 -1
- package/dist/esm/tools/eagerEventExecution.mjs +1 -1
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +3 -3
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +2 -2
- package/dist/esm/tools/search/rerankers.mjs +154 -1
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +4 -1
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +597 -0
- package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +1114 -233
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs +170 -0
- package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -0
- package/dist/esm/tools/subagent/childGraphConfig.mjs +272 -0
- package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -0
- package/dist/esm/tools/subagent/index.mjs +1 -0
- package/dist/esm/tools/subagent/runtimeLimits.mjs +5 -0
- package/dist/esm/tools/subagent/runtimeLimits.mjs.map +1 -0
- package/dist/esm/tools/toolOutputReferences.mjs +20 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +2 -2
- package/dist/types/agents/AgentContext.d.ts +1 -1
- package/dist/types/graphs/Graph.d.ts +22 -2
- package/dist/types/graphs/MultiAgentGraph.d.ts +7 -1
- package/dist/types/graphs/applyGraphRuntimeConfig.d.ts +3 -0
- package/dist/types/graphs/createGraph.d.ts +13 -0
- package/dist/types/graphs/graphFactory.d.ts +13 -0
- package/dist/types/graphs/index.d.ts +2 -0
- package/dist/types/hooks/HookRegistry.d.ts +10 -1
- package/dist/types/hooks/executeHooks.d.ts +5 -1
- package/dist/types/hooks/index.d.ts +2 -2
- package/dist/types/hooks/types.d.ts +12 -0
- package/dist/types/run.d.ts +6 -3
- package/dist/types/session/AgentSession.d.ts +1 -0
- package/dist/types/tools/SubagentTool.d.ts +1 -1
- package/dist/types/tools/ToolNode.d.ts +23 -15
- package/dist/types/tools/search/rerankers.d.ts +28 -0
- package/dist/types/tools/search/types.d.ts +38 -1
- package/dist/types/tools/subagent/SubagentExecutionRegistry.d.ts +171 -0
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +70 -36
- package/dist/types/tools/subagent/SubagentReplay.d.ts +85 -0
- package/dist/types/tools/subagent/childGraphConfig.d.ts +37 -0
- package/dist/types/tools/subagent/index.d.ts +1 -1
- package/dist/types/tools/subagent/runtimeLimits.d.ts +4 -0
- package/dist/types/tools/toolOutputReferences.d.ts +12 -0
- package/dist/types/types/graph.d.ts +189 -24
- package/dist/types/types/hitl.d.ts +15 -0
- package/package.json +8 -2
- package/src/agents/AgentContext.ts +1 -1
- package/src/graphs/Graph.ts +349 -104
- package/src/graphs/MultiAgentGraph.ts +149 -47
- package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +48 -9
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1 -1
- package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +80 -0
- package/src/graphs/__tests__/MultiAgentGraph.test.ts +86 -0
- package/src/graphs/__tests__/composition.smoke.test.ts +152 -0
- package/src/graphs/__tests__/createGraph.test.ts +206 -0
- package/src/graphs/applyGraphRuntimeConfig.ts +25 -0
- package/src/graphs/createGraph.ts +32 -0
- package/src/graphs/graphFactory.ts +12 -0
- package/src/graphs/index.ts +2 -0
- package/src/hooks/HookRegistry.ts +151 -1
- package/src/hooks/__tests__/HookRegistry.test.ts +96 -0
- package/src/hooks/__tests__/executeHooks.test.ts +160 -0
- package/src/hooks/executeHooks.ts +133 -15
- package/src/hooks/index.ts +3 -1
- package/src/hooks/types.ts +16 -0
- package/src/messages/format.ts +54 -26
- package/src/messages/formatAgentMessages.reducer.test.ts +162 -0
- package/src/messages/formatAgentMessages.steer.test.ts +16 -11
- package/src/messages/formatAgentMessages.test.ts +7 -2
- package/src/run.ts +214 -56
- package/src/session/AgentSession.ts +52 -2
- package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
- package/src/specs/graph-subagent.live.test.ts +242 -0
- package/src/specs/graph-subagent.test.ts +322 -0
- package/src/specs/langfuse-routing.integration.test.ts +120 -1
- package/src/specs/subagent.test.ts +130 -4
- package/src/summarization/__tests__/node.test.ts +32 -14
- package/src/summarization/node.ts +15 -11
- package/src/tools/SubagentTool.ts +4 -4
- package/src/tools/ToolNode.ts +643 -157
- package/src/tools/__tests__/SubagentExecutor.test.ts +740 -15
- package/src/tools/__tests__/SubagentReplay.test.ts +329 -0
- package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
- package/src/tools/__tests__/directToolHITLResumeScope.test.ts +512 -2
- package/src/tools/__tests__/hitl.test.ts +234 -5
- package/src/tools/__tests__/subagentHooks.test.ts +1126 -61
- package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
- package/src/tools/search/rag-api-reranker.test.ts +891 -0
- package/src/tools/search/rerankers.ts +260 -0
- package/src/tools/search/tool.ts +6 -0
- package/src/tools/search/types.ts +45 -1
- package/src/tools/subagent/SubagentExecutionRegistry.ts +1159 -0
- package/src/tools/subagent/SubagentExecutor.ts +2232 -312
- package/src/tools/subagent/SubagentReplay.ts +584 -0
- package/src/tools/subagent/__tests__/SubagentExecutionRegistry.test.ts +637 -0
- package/src/tools/subagent/__tests__/SubagentExecutor.lazy.test.ts +3054 -0
- package/src/tools/subagent/__tests__/childGraphConfig.test.ts +521 -0
- package/src/tools/subagent/childGraphConfig.ts +694 -0
- package/src/tools/subagent/index.ts +6 -0
- package/src/tools/subagent/runtimeLimits.ts +7 -0
- package/src/tools/toolOutputReferences.ts +40 -1
- package/src/types/graph.ts +222 -24
- package/src/types/hitl.ts +16 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
import { describe, expect, it, jest } from '@jest/globals';
|
|
2
|
+
import {
|
|
3
|
+
SubagentExecutionRegistry,
|
|
4
|
+
SubagentExecutionInvalidatedError,
|
|
5
|
+
} from '@/tools/subagent/SubagentExecutionRegistry';
|
|
6
|
+
import {
|
|
7
|
+
SUBAGENT_PARENT_BATCH_CONFIG_KEY,
|
|
8
|
+
SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY,
|
|
9
|
+
} from '@/tools/subagent/SubagentReplay';
|
|
10
|
+
|
|
11
|
+
type Result = { content: string };
|
|
12
|
+
type ResolvedConfig = { agentId: string };
|
|
13
|
+
type ActiveRun = { graphId: string };
|
|
14
|
+
type SettledOutput = { content: string };
|
|
15
|
+
|
|
16
|
+
const createRegistry = (): SubagentExecutionRegistry<
|
|
17
|
+
Result,
|
|
18
|
+
ResolvedConfig,
|
|
19
|
+
ActiveRun,
|
|
20
|
+
SettledOutput
|
|
21
|
+
> =>
|
|
22
|
+
new SubagentExecutionRegistry({
|
|
23
|
+
parentRunId: 'parent-run',
|
|
24
|
+
parentAgentId: 'parent-agent',
|
|
25
|
+
durable: true,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const createInput = (
|
|
29
|
+
overrides: Record<string, unknown> = {}
|
|
30
|
+
): {
|
|
31
|
+
threadId: string;
|
|
32
|
+
parentToolCallId: string;
|
|
33
|
+
parentConfigurable: Record<string, unknown>;
|
|
34
|
+
} => ({
|
|
35
|
+
threadId: 'durable-thread',
|
|
36
|
+
parentToolCallId: 'call_shared',
|
|
37
|
+
parentConfigurable: {
|
|
38
|
+
checkpoint_id: 'fork-a',
|
|
39
|
+
[SUBAGENT_PARENT_BATCH_CONFIG_KEY]: 'batch-a',
|
|
40
|
+
[SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY]: 'attempt-a',
|
|
41
|
+
...overrides,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('SubagentExecutionRegistry', () => {
|
|
46
|
+
it('derives one canonical address and isolates forks, batches, and attempts', () => {
|
|
47
|
+
const registry = createRegistry();
|
|
48
|
+
const first = registry.open(createInput());
|
|
49
|
+
const duplicate = registry.open(createInput());
|
|
50
|
+
const fork = registry.open(createInput({ checkpoint_id: 'fork-b' }));
|
|
51
|
+
const batch = registry.open(
|
|
52
|
+
createInput({ [SUBAGENT_PARENT_BATCH_CONFIG_KEY]: 'batch-b' })
|
|
53
|
+
);
|
|
54
|
+
const attempt = registry.open(
|
|
55
|
+
createInput({ [SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY]: 'attempt-b' })
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(duplicate).toBe(first);
|
|
59
|
+
expect(
|
|
60
|
+
new Set([
|
|
61
|
+
first.address.key,
|
|
62
|
+
fork.address.key,
|
|
63
|
+
batch.address.key,
|
|
64
|
+
attempt.address.key,
|
|
65
|
+
]).size
|
|
66
|
+
).toBe(4);
|
|
67
|
+
expect(attempt.address.baseChildThreadId).toBe(
|
|
68
|
+
first.address.baseChildThreadId
|
|
69
|
+
);
|
|
70
|
+
expect(attempt.address.branchChildThreadId).not.toBe(
|
|
71
|
+
first.address.branchChildThreadId
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('keeps identity immutable and protects an effective definition binding', () => {
|
|
76
|
+
const record = createRegistry().open(createInput());
|
|
77
|
+
const identity = {
|
|
78
|
+
childRunId: 'child-run',
|
|
79
|
+
childThreadId: 'child-thread',
|
|
80
|
+
approvalExecutionScope: 'approval-scope',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
record.bindIdentity(identity);
|
|
84
|
+
record.bindDefinition(
|
|
85
|
+
{ subagentType: 'researcher', configId: 'researcher@v1' },
|
|
86
|
+
'provisional'
|
|
87
|
+
);
|
|
88
|
+
record.bindDefinition(
|
|
89
|
+
{ subagentType: 'coder', configId: 'coder@v2' },
|
|
90
|
+
'effective'
|
|
91
|
+
);
|
|
92
|
+
record.bindDefinition(
|
|
93
|
+
{ subagentType: 'researcher', configId: 'researcher@v1' },
|
|
94
|
+
'provisional'
|
|
95
|
+
);
|
|
96
|
+
expect(() =>
|
|
97
|
+
record.bindDefinition(
|
|
98
|
+
{ subagentType: 'coder', configId: 'coder@v2' },
|
|
99
|
+
'effective'
|
|
100
|
+
)
|
|
101
|
+
).not.toThrow();
|
|
102
|
+
|
|
103
|
+
expect(record.snapshot).toMatchObject({
|
|
104
|
+
identity,
|
|
105
|
+
binding: { subagentType: 'coder', configId: 'coder@v2' },
|
|
106
|
+
});
|
|
107
|
+
expect(() =>
|
|
108
|
+
record.bindIdentity({
|
|
109
|
+
...identity,
|
|
110
|
+
childThreadId: 'another-child-thread',
|
|
111
|
+
})
|
|
112
|
+
).toThrow('Subagent execution identity cannot change after binding.');
|
|
113
|
+
expect(() =>
|
|
114
|
+
record.bindDefinition(
|
|
115
|
+
{ subagentType: 'writer', configId: 'writer@v3' },
|
|
116
|
+
'effective'
|
|
117
|
+
)
|
|
118
|
+
).toThrow('Subagent effective definition binding cannot change.');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('coalesces resolution and isolates a stale completion after invalidation', async () => {
|
|
122
|
+
const registry = createRegistry();
|
|
123
|
+
const staleRecord = registry.open(createInput());
|
|
124
|
+
let finishStale = (_config: ResolvedConfig): void => undefined;
|
|
125
|
+
let finishCurrent = (_config: ResolvedConfig): void => undefined;
|
|
126
|
+
let markStaleStarted = (): void => undefined;
|
|
127
|
+
const staleStarted = new Promise<void>((resolve) => {
|
|
128
|
+
markStaleStarted = resolve;
|
|
129
|
+
});
|
|
130
|
+
const staleValue = new Promise<ResolvedConfig>((resolve) => {
|
|
131
|
+
finishStale = resolve;
|
|
132
|
+
});
|
|
133
|
+
const currentValue = new Promise<ResolvedConfig>((resolve) => {
|
|
134
|
+
finishCurrent = resolve;
|
|
135
|
+
});
|
|
136
|
+
const signal = new AbortController().signal;
|
|
137
|
+
const resolveStale = jest.fn(() => {
|
|
138
|
+
markStaleStarted();
|
|
139
|
+
return staleValue;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const stale = staleRecord.resolveConfig(resolveStale, signal);
|
|
143
|
+
const duplicate = staleRecord.resolveConfig(resolveStale, signal);
|
|
144
|
+
expect(duplicate).toBe(stale);
|
|
145
|
+
await staleStarted;
|
|
146
|
+
|
|
147
|
+
registry.clear();
|
|
148
|
+
const currentRecord = registry.open(createInput());
|
|
149
|
+
const current = currentRecord.resolveConfig(() => currentValue, signal);
|
|
150
|
+
|
|
151
|
+
finishStale({ agentId: 'stale-agent' });
|
|
152
|
+
await expect(stale).rejects.toBeInstanceOf(
|
|
153
|
+
SubagentExecutionInvalidatedError
|
|
154
|
+
);
|
|
155
|
+
expect(currentRecord.snapshot).toMatchObject({ resolving: true });
|
|
156
|
+
|
|
157
|
+
finishCurrent({ agentId: 'current-agent' });
|
|
158
|
+
await expect(current).resolves.toEqual({ agentId: 'current-agent' });
|
|
159
|
+
expect(currentRecord.snapshot).toMatchObject({
|
|
160
|
+
resolving: false,
|
|
161
|
+
resolvedConfig: { agentId: 'current-agent' },
|
|
162
|
+
});
|
|
163
|
+
expect(resolveStale).toHaveBeenCalledTimes(1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('coalesces identity resolution before binding the resolved identity', async () => {
|
|
167
|
+
const registry = createRegistry();
|
|
168
|
+
const record = registry.open(createInput());
|
|
169
|
+
let finish = (_identity: {
|
|
170
|
+
childRunId: string;
|
|
171
|
+
childThreadId: string;
|
|
172
|
+
approvalExecutionScope: string;
|
|
173
|
+
}): void => undefined;
|
|
174
|
+
const identity = new Promise<{
|
|
175
|
+
childRunId: string;
|
|
176
|
+
childThreadId: string;
|
|
177
|
+
approvalExecutionScope: string;
|
|
178
|
+
}>((resolve) => {
|
|
179
|
+
finish = resolve;
|
|
180
|
+
});
|
|
181
|
+
const resolveIdentity = jest.fn(async () => {
|
|
182
|
+
const resolvedIdentity = await identity;
|
|
183
|
+
return {
|
|
184
|
+
identity: resolvedIdentity,
|
|
185
|
+
lease: registry.beginIdentityPreparation(record, resolvedIdentity),
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const first = record.resolveIdentity(resolveIdentity);
|
|
190
|
+
const duplicate = record.resolveIdentity(resolveIdentity);
|
|
191
|
+
|
|
192
|
+
expect(duplicate).toBe(first);
|
|
193
|
+
expect(record.snapshot.resolvingIdentity).toBe(true);
|
|
194
|
+
finish({
|
|
195
|
+
childRunId: 'child-run',
|
|
196
|
+
childThreadId: 'child-thread',
|
|
197
|
+
approvalExecutionScope: 'approval-scope',
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await expect(first).resolves.toMatchObject({ childRunId: 'child-run' });
|
|
201
|
+
expect(resolveIdentity).toHaveBeenCalledTimes(1);
|
|
202
|
+
expect(record.snapshot).toMatchObject({
|
|
203
|
+
resolvingIdentity: false,
|
|
204
|
+
identity: { childThreadId: 'child-thread' },
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('rolls back prepared identity side effects after invalidation', async () => {
|
|
209
|
+
const registry = createRegistry();
|
|
210
|
+
const record = registry.open(createInput());
|
|
211
|
+
let finishPreparation = (): void => undefined;
|
|
212
|
+
let markPreparationStarted = (): void => undefined;
|
|
213
|
+
const preparationStarted = new Promise<void>((resolve) => {
|
|
214
|
+
markPreparationStarted = resolve;
|
|
215
|
+
});
|
|
216
|
+
const preparationGate = new Promise<void>((resolve) => {
|
|
217
|
+
finishPreparation = resolve;
|
|
218
|
+
});
|
|
219
|
+
const commit = jest.fn();
|
|
220
|
+
const rollback = jest.fn(async (): Promise<void> => undefined);
|
|
221
|
+
const staleIdentity = {
|
|
222
|
+
childRunId: 'stale-run',
|
|
223
|
+
childThreadId: 'stale-thread',
|
|
224
|
+
approvalExecutionScope: 'stale-scope',
|
|
225
|
+
};
|
|
226
|
+
const lease = registry.beginIdentityPreparation(record, staleIdentity);
|
|
227
|
+
const pending = record.resolveIdentity(async () => {
|
|
228
|
+
markPreparationStarted();
|
|
229
|
+
await preparationGate;
|
|
230
|
+
return {
|
|
231
|
+
identity: staleIdentity,
|
|
232
|
+
lease,
|
|
233
|
+
commit: () => commit(),
|
|
234
|
+
rollback: async () => rollback(),
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await preparationStarted;
|
|
239
|
+
registry.clear();
|
|
240
|
+
finishPreparation();
|
|
241
|
+
|
|
242
|
+
await expect(pending).rejects.toBeInstanceOf(
|
|
243
|
+
SubagentExecutionInvalidatedError
|
|
244
|
+
);
|
|
245
|
+
expect(commit).not.toHaveBeenCalled();
|
|
246
|
+
expect(rollback).toHaveBeenCalledTimes(1);
|
|
247
|
+
expect(record.snapshot.identity).toBeUndefined();
|
|
248
|
+
expect(registry.selectForResume()).toEqual([]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('keeps created-resource ownership exclusive through rollback', async () => {
|
|
252
|
+
const registry = createRegistry();
|
|
253
|
+
const staleRecord = registry.open(createInput());
|
|
254
|
+
const sharedIdentity = {
|
|
255
|
+
childRunId: 'shared-run',
|
|
256
|
+
childThreadId: 'shared-thread',
|
|
257
|
+
approvalExecutionScope: 'shared-scope',
|
|
258
|
+
};
|
|
259
|
+
const staleLease = registry.beginIdentityPreparation(
|
|
260
|
+
staleRecord,
|
|
261
|
+
sharedIdentity
|
|
262
|
+
);
|
|
263
|
+
const checkpointCleanup = jest.fn(async (): Promise<void> => undefined);
|
|
264
|
+
const approvalCleanup = jest.fn(async (): Promise<void> => undefined);
|
|
265
|
+
staleLease.markCheckpointCreated(checkpointCleanup);
|
|
266
|
+
staleLease.markApprovalScopeCreated(approvalCleanup);
|
|
267
|
+
registry.clear();
|
|
268
|
+
const currentRecord = registry.open(createInput());
|
|
269
|
+
const staleCause = new Error('stale preparation');
|
|
270
|
+
|
|
271
|
+
expect(() =>
|
|
272
|
+
registry.beginIdentityPreparation(currentRecord, sharedIdentity)
|
|
273
|
+
).toThrow(SubagentExecutionInvalidatedError);
|
|
274
|
+
|
|
275
|
+
await expect(staleLease.rollback(staleCause)).rejects.toBe(staleCause);
|
|
276
|
+
|
|
277
|
+
expect(staleLease.ownsCheckpoint).toBe(false);
|
|
278
|
+
expect(staleLease.ownsApprovalScope).toBe(false);
|
|
279
|
+
expect(checkpointCleanup).toHaveBeenCalledTimes(1);
|
|
280
|
+
expect(approvalCleanup).toHaveBeenCalledTimes(1);
|
|
281
|
+
const currentLease = registry.beginIdentityPreparation(
|
|
282
|
+
currentRecord,
|
|
283
|
+
sharedIdentity
|
|
284
|
+
);
|
|
285
|
+
expect(currentLease.ownsCreatedCheckpoint).toBe(false);
|
|
286
|
+
expect(currentLease.ownsCreatedApprovalScope).toBe(false);
|
|
287
|
+
currentLease.release();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('blocks a cleaning checkpoint without claiming an independent approval scope', async () => {
|
|
291
|
+
const registry = createRegistry();
|
|
292
|
+
const staleRecord = registry.open(createInput());
|
|
293
|
+
const staleIdentity = {
|
|
294
|
+
childRunId: 'stale-run',
|
|
295
|
+
childThreadId: 'shared-cleaning-thread',
|
|
296
|
+
approvalExecutionScope: 'stale-scope',
|
|
297
|
+
};
|
|
298
|
+
const staleLease = registry.beginIdentityPreparation(
|
|
299
|
+
staleRecord,
|
|
300
|
+
staleIdentity
|
|
301
|
+
);
|
|
302
|
+
let finishCleanup = (): void => undefined;
|
|
303
|
+
let markCleanupStarted = (): void => undefined;
|
|
304
|
+
const cleanupGate = new Promise<void>((resolve) => {
|
|
305
|
+
finishCleanup = resolve;
|
|
306
|
+
});
|
|
307
|
+
const cleanupStarted = new Promise<void>((resolve) => {
|
|
308
|
+
markCleanupStarted = resolve;
|
|
309
|
+
});
|
|
310
|
+
const cleanup = jest.fn(async (): Promise<void> => {
|
|
311
|
+
markCleanupStarted();
|
|
312
|
+
await cleanupGate;
|
|
313
|
+
});
|
|
314
|
+
staleLease.markCheckpointCreated(cleanup);
|
|
315
|
+
registry.clear();
|
|
316
|
+
const currentRecord = registry.open(createInput());
|
|
317
|
+
const replacementIdentity = {
|
|
318
|
+
childRunId: 'replacement-run',
|
|
319
|
+
childThreadId: staleIdentity.childThreadId,
|
|
320
|
+
approvalExecutionScope: 'replacement-scope',
|
|
321
|
+
};
|
|
322
|
+
const staleCause = new Error('stale preparation');
|
|
323
|
+
const rollback = staleLease.rollback(staleCause);
|
|
324
|
+
await cleanupStarted;
|
|
325
|
+
|
|
326
|
+
expect(() =>
|
|
327
|
+
registry.beginIdentityPreparation(currentRecord, replacementIdentity)
|
|
328
|
+
).toThrow(SubagentExecutionInvalidatedError);
|
|
329
|
+
const independentRecord = registry.open(
|
|
330
|
+
createInput({ checkpoint_id: 'independent-fork' })
|
|
331
|
+
);
|
|
332
|
+
const independentLease = registry.beginIdentityPreparation(
|
|
333
|
+
independentRecord,
|
|
334
|
+
{
|
|
335
|
+
childRunId: 'independent-run',
|
|
336
|
+
childThreadId: 'independent-thread',
|
|
337
|
+
approvalExecutionScope: replacementIdentity.approvalExecutionScope,
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
expect(independentLease.ownsCheckpoint).toBe(true);
|
|
341
|
+
expect(independentLease.ownsApprovalScope).toBe(true);
|
|
342
|
+
independentLease.release();
|
|
343
|
+
|
|
344
|
+
finishCleanup();
|
|
345
|
+
await expect(rollback).rejects.toBe(staleCause);
|
|
346
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
347
|
+
const retryLease = registry.beginIdentityPreparation(
|
|
348
|
+
currentRecord,
|
|
349
|
+
replacementIdentity
|
|
350
|
+
);
|
|
351
|
+
expect(retryLease.ownsCheckpoint).toBe(true);
|
|
352
|
+
expect(retryLease.ownsApprovalScope).toBe(true);
|
|
353
|
+
retryLease.release();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('preserves the primary failure when preparation rollback also fails', async () => {
|
|
357
|
+
const registry = createRegistry();
|
|
358
|
+
const record = registry.open(createInput());
|
|
359
|
+
const lease = registry.beginIdentityPreparation(record, {
|
|
360
|
+
childRunId: 'failed-run',
|
|
361
|
+
childThreadId: 'failed-thread',
|
|
362
|
+
approvalExecutionScope: 'failed-scope',
|
|
363
|
+
});
|
|
364
|
+
const cause = new Error('checkpoint fork failed');
|
|
365
|
+
const rollbackError = new Error('checkpoint cleanup failed');
|
|
366
|
+
|
|
367
|
+
await expect(
|
|
368
|
+
lease.rollback(cause, async () => {
|
|
369
|
+
throw rollbackError;
|
|
370
|
+
})
|
|
371
|
+
).rejects.toBe(cause);
|
|
372
|
+
|
|
373
|
+
expect((cause as Error & { rollbackError?: unknown }).rollbackError).toBe(
|
|
374
|
+
rollbackError
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('preserves a frozen primary failure and releases a failed lease', async () => {
|
|
379
|
+
const registry = createRegistry();
|
|
380
|
+
const record = registry.open(createInput());
|
|
381
|
+
const identity = {
|
|
382
|
+
childRunId: 'frozen-run',
|
|
383
|
+
childThreadId: 'frozen-thread',
|
|
384
|
+
approvalExecutionScope: 'frozen-scope',
|
|
385
|
+
};
|
|
386
|
+
const lease = registry.beginIdentityPreparation(record, identity);
|
|
387
|
+
const cause = Object.freeze(new Error('frozen preparation failure'));
|
|
388
|
+
lease.markCheckpointCreated(async () => {
|
|
389
|
+
throw new Error('cleanup failure');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
await expect(lease.rollback(cause)).rejects.toBe(cause);
|
|
393
|
+
|
|
394
|
+
expect(lease.ownsCheckpoint).toBe(false);
|
|
395
|
+
expect(lease.ownsApprovalScope).toBe(false);
|
|
396
|
+
const retryLease = registry.beginIdentityPreparation(record, identity);
|
|
397
|
+
expect(retryLease.ownsCheckpoint).toBe(true);
|
|
398
|
+
expect(retryLease.ownsApprovalScope).toBe(true);
|
|
399
|
+
retryLease.release();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('coalesces invocation and owns active, interrupted, and completed state', async () => {
|
|
403
|
+
const record = createRegistry().open(createInput());
|
|
404
|
+
const invocation = {
|
|
405
|
+
description: 'Run the child.',
|
|
406
|
+
subagentType: 'researcher',
|
|
407
|
+
configId: 'researcher@v1',
|
|
408
|
+
};
|
|
409
|
+
let finish = (_result: Result): void => undefined;
|
|
410
|
+
const result = new Promise<Result>((resolve) => {
|
|
411
|
+
finish = resolve;
|
|
412
|
+
});
|
|
413
|
+
const invoke = jest.fn(async (): Promise<Result> => {
|
|
414
|
+
record.activate({ graphId: 'graph-a' });
|
|
415
|
+
record.markStarted();
|
|
416
|
+
record.markInterrupted();
|
|
417
|
+
const completed = await result;
|
|
418
|
+
record.activate({ graphId: 'graph-a' });
|
|
419
|
+
record.markCompleted(completed);
|
|
420
|
+
return completed;
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const first = record.execute(invocation, invoke);
|
|
424
|
+
const duplicate = record.execute(invocation, invoke);
|
|
425
|
+
expect(duplicate).toBe(first);
|
|
426
|
+
expect(() =>
|
|
427
|
+
record.execute({ ...invocation, description: 'Changed request.' }, invoke)
|
|
428
|
+
).toThrow('Subagent invocation binding cannot change.');
|
|
429
|
+
expect(() =>
|
|
430
|
+
record.execute({ ...invocation, configId: 'researcher@v2' }, invoke)
|
|
431
|
+
).toThrow('Subagent effective definition binding cannot change.');
|
|
432
|
+
finish({ content: 'done' });
|
|
433
|
+
|
|
434
|
+
await expect(first).resolves.toEqual({ content: 'done' });
|
|
435
|
+
expect(invoke).toHaveBeenCalledTimes(1);
|
|
436
|
+
expect(record.snapshot).toMatchObject({
|
|
437
|
+
phase: 'completed',
|
|
438
|
+
activeRun: { graphId: 'graph-a' },
|
|
439
|
+
completedResult: { content: 'done' },
|
|
440
|
+
started: true,
|
|
441
|
+
completed: true,
|
|
442
|
+
executing: false,
|
|
443
|
+
});
|
|
444
|
+
expect(record.releaseActiveRun()).toEqual({ graphId: 'graph-a' });
|
|
445
|
+
expect(record.snapshot.activeRun).toBeUndefined();
|
|
446
|
+
expect(() => record.activate({ graphId: 'graph-b' })).toThrow(
|
|
447
|
+
'Cannot transition subagent execution from completed to active.'
|
|
448
|
+
);
|
|
449
|
+
expect(() => record.markFailed()).toThrow(
|
|
450
|
+
'Cannot transition subagent execution from completed to failed.'
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const retryable = createRegistry().open(createInput());
|
|
454
|
+
retryable.markFailed();
|
|
455
|
+
expect(retryable.snapshot.phase).toBe('failed');
|
|
456
|
+
expect(retryable.activate({ graphId: 'graph-retry' })).toEqual({
|
|
457
|
+
graphId: 'graph-retry',
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('coalesces settlement and retains only its minimal replay output', async () => {
|
|
462
|
+
const record = createRegistry().open(createInput());
|
|
463
|
+
const invocation = {
|
|
464
|
+
description: 'Run the child.',
|
|
465
|
+
subagentType: 'researcher',
|
|
466
|
+
configId: 'researcher@v1',
|
|
467
|
+
};
|
|
468
|
+
const settlement = {
|
|
469
|
+
definitionAuthority: 'effective' as const,
|
|
470
|
+
fingerprint: 'settlement-a',
|
|
471
|
+
invocation,
|
|
472
|
+
subagentType: invocation.subagentType,
|
|
473
|
+
configId: invocation.configId,
|
|
474
|
+
};
|
|
475
|
+
const settledOutput = { content: 'settled result' };
|
|
476
|
+
const persist = jest.fn(async (): Promise<void> => undefined);
|
|
477
|
+
await record.resolveConfig(
|
|
478
|
+
async () => ({ agentId: 'heavy-agent-config' }),
|
|
479
|
+
new AbortController().signal
|
|
480
|
+
);
|
|
481
|
+
record.activate({ graphId: 'heavy-graph' });
|
|
482
|
+
record.markStarted();
|
|
483
|
+
record.markCompleted({ content: 'heavy execution result' });
|
|
484
|
+
|
|
485
|
+
const first = record.settle(settlement, settledOutput, persist);
|
|
486
|
+
const duplicate = record.settle(settlement, settledOutput, persist);
|
|
487
|
+
|
|
488
|
+
expect(duplicate).toBe(first);
|
|
489
|
+
await first;
|
|
490
|
+
expect(persist).toHaveBeenCalledTimes(1);
|
|
491
|
+
expect(record.snapshot).toMatchObject({
|
|
492
|
+
phase: 'completed',
|
|
493
|
+
settled: true,
|
|
494
|
+
settling: false,
|
|
495
|
+
});
|
|
496
|
+
expect(record.settledOutput).toEqual(settledOutput);
|
|
497
|
+
expect(record.snapshot.activeRun).toBeUndefined();
|
|
498
|
+
expect(record.snapshot.completedResult).toBeUndefined();
|
|
499
|
+
expect(record.snapshot.resolvedConfig).toBeUndefined();
|
|
500
|
+
await record.settle(settlement, settledOutput, persist);
|
|
501
|
+
expect(persist).toHaveBeenCalledTimes(1);
|
|
502
|
+
expect(() =>
|
|
503
|
+
record.settle(
|
|
504
|
+
{ ...settlement, fingerprint: 'settlement-b' },
|
|
505
|
+
settledOutput,
|
|
506
|
+
persist
|
|
507
|
+
)
|
|
508
|
+
).toThrow('Subagent settlement binding cannot change.');
|
|
509
|
+
expect(() =>
|
|
510
|
+
record.settle(
|
|
511
|
+
{
|
|
512
|
+
...settlement,
|
|
513
|
+
invocation: { ...invocation, description: 'Changed request.' },
|
|
514
|
+
},
|
|
515
|
+
settledOutput,
|
|
516
|
+
persist
|
|
517
|
+
)
|
|
518
|
+
).toThrow('Subagent invocation binding cannot change.');
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('fails closed for ambiguous tool calls and selects an exact resume address', () => {
|
|
522
|
+
const registry = createRegistry();
|
|
523
|
+
const forkA = registry.open(createInput());
|
|
524
|
+
const forkB = registry.open(createInput({ checkpoint_id: 'fork-b' }));
|
|
525
|
+
const unrelated = registry.open({
|
|
526
|
+
...createInput(),
|
|
527
|
+
parentToolCallId: 'call_unrelated',
|
|
528
|
+
});
|
|
529
|
+
forkA.bindIdentity({
|
|
530
|
+
childRunId: 'run-a',
|
|
531
|
+
childThreadId: 'thread-a',
|
|
532
|
+
approvalExecutionScope: 'scope-a',
|
|
533
|
+
});
|
|
534
|
+
forkB.bindIdentity({
|
|
535
|
+
childRunId: 'run-b',
|
|
536
|
+
childThreadId: 'thread-b',
|
|
537
|
+
approvalExecutionScope: 'scope-b',
|
|
538
|
+
});
|
|
539
|
+
unrelated.bindIdentity({
|
|
540
|
+
childRunId: 'run-c',
|
|
541
|
+
childThreadId: 'thread-c',
|
|
542
|
+
approvalExecutionScope: 'scope-c',
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
expect(
|
|
546
|
+
registry.selectForResume({
|
|
547
|
+
parentToolCallIds: new Set(['call_shared']),
|
|
548
|
+
})
|
|
549
|
+
).toEqual([]);
|
|
550
|
+
expect(registry.selectForResume()).toEqual([unrelated]);
|
|
551
|
+
expect(
|
|
552
|
+
registry.selectForResume({
|
|
553
|
+
parentToolCallIds: new Set(['call_shared']),
|
|
554
|
+
config: {
|
|
555
|
+
configurable: {
|
|
556
|
+
...createInput().parentConfigurable,
|
|
557
|
+
thread_id: 'durable-thread',
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
})
|
|
561
|
+
).toEqual([forkA]);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('retires heavy state from superseded resume sources', () => {
|
|
565
|
+
const registry = createRegistry();
|
|
566
|
+
const attemptA = registry.open(createInput());
|
|
567
|
+
const attemptB = registry.open(
|
|
568
|
+
createInput({
|
|
569
|
+
checkpoint_id: 'fork-b',
|
|
570
|
+
[SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY]: 'attempt-b',
|
|
571
|
+
})
|
|
572
|
+
);
|
|
573
|
+
const attemptC = registry.open(
|
|
574
|
+
createInput({
|
|
575
|
+
checkpoint_id: 'fork-c',
|
|
576
|
+
[SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY]: 'attempt-c',
|
|
577
|
+
})
|
|
578
|
+
);
|
|
579
|
+
attemptA.bindIdentity({
|
|
580
|
+
childRunId: 'child-run',
|
|
581
|
+
childThreadId: 'thread-a',
|
|
582
|
+
approvalExecutionScope: 'scope-a',
|
|
583
|
+
});
|
|
584
|
+
attemptB.bindIdentity({
|
|
585
|
+
childRunId: 'child-run',
|
|
586
|
+
childThreadId: 'thread-b',
|
|
587
|
+
approvalExecutionScope: 'scope-b',
|
|
588
|
+
});
|
|
589
|
+
attemptC.bindIdentity({
|
|
590
|
+
childRunId: 'child-run',
|
|
591
|
+
childThreadId: 'thread-c',
|
|
592
|
+
approvalExecutionScope: 'scope-c',
|
|
593
|
+
});
|
|
594
|
+
attemptA.activate({ graphId: 'graph-a' });
|
|
595
|
+
attemptB.activate({ graphId: 'graph-b' });
|
|
596
|
+
const retired: string[] = [];
|
|
597
|
+
|
|
598
|
+
registry.retireResumeSources(attemptB, new Set(['thread-a']), (source) =>
|
|
599
|
+
retired.push(source.activeRun?.graphId ?? 'missing')
|
|
600
|
+
);
|
|
601
|
+
attemptC.activate({ graphId: 'graph-c' });
|
|
602
|
+
registry.retireResumeSources(attemptC, new Set(['thread-b']), (source) =>
|
|
603
|
+
retired.push(source.activeRun?.graphId ?? 'missing')
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
expect(retired).toEqual(['graph-a', 'graph-b']);
|
|
607
|
+
expect(attemptA.snapshot.phase).toBe('invalidated');
|
|
608
|
+
expect(attemptB.snapshot.phase).toBe('invalidated');
|
|
609
|
+
expect(attemptA.snapshot.identity).toBeUndefined();
|
|
610
|
+
expect(attemptB.snapshot.identity).toBeUndefined();
|
|
611
|
+
expect(attemptC.snapshot.activeRun).toEqual({ graphId: 'graph-c' });
|
|
612
|
+
expect(registry.selectForResume()).toEqual([attemptC]);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('invalidates records atomically while retaining checkpoint cleanup data', () => {
|
|
616
|
+
const registry = createRegistry();
|
|
617
|
+
const record = registry.open(createInput());
|
|
618
|
+
record.bindIdentity({
|
|
619
|
+
childRunId: 'child-run',
|
|
620
|
+
childThreadId: 'child-thread',
|
|
621
|
+
approvalExecutionScope: 'approval-scope',
|
|
622
|
+
});
|
|
623
|
+
record.activate({ graphId: 'graph-a' });
|
|
624
|
+
registry.rememberCheckpointThread('child-thread');
|
|
625
|
+
const disposed: string[] = [];
|
|
626
|
+
|
|
627
|
+
registry.clear((current) => {
|
|
628
|
+
disposed.push(current.activeRun?.graphId ?? 'missing');
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
expect(disposed).toEqual(['graph-a']);
|
|
632
|
+
expect(record.snapshot).toMatchObject({ phase: 'invalidated' });
|
|
633
|
+
expect(record.snapshot.identity).toBeUndefined();
|
|
634
|
+
expect(record.snapshot.activeRun).toBeUndefined();
|
|
635
|
+
expect(registry.getCheckpointThreadIds()).toEqual(['child-thread']);
|
|
636
|
+
});
|
|
637
|
+
});
|